diff options
| author | Shulhan <ms@kilabit.info> | 2019-02-01 11:32:34 +0700 |
|---|---|---|
| committer | Shulhan <ms@kilabit.info> | 2019-02-05 07:48:52 +0700 |
| commit | 930b8e7bdeb7b4909905f3185647d8d64d89ebbf (patch) | |
| tree | 623956d90d0e33b85bc23f97ccc93bf15b1950e9 /lib | |
| parent | 45a23e05d85eac33e1a4b44c0888be5d91a73cdb (diff) | |
| download | pakakeh.go-930b8e7bdeb7b4909905f3185647d8d64d89ebbf.tar.xz | |
lib/email: new package for working with Internet Message Format
This package provide library for parsing email message format as
specified in RFC 5322.
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/email/body.go | 23 | ||||
| -rw-r--r-- | lib/email/doc.go | 9 | ||||
| -rw-r--r-- | lib/email/email.go | 33 | ||||
| -rw-r--r-- | lib/email/field.go | 363 | ||||
| -rw-r--r-- | lib/email/field_test.go | 255 | ||||
| -rw-r--r-- | lib/email/fieldtype.go | 12 | ||||
| -rw-r--r-- | lib/email/header.go | 79 | ||||
| -rw-r--r-- | lib/email/header_test.go | 72 | ||||
| -rw-r--r-- | lib/email/mime.go | 16 | ||||
| -rw-r--r-- | lib/io/reader.go | 66 | ||||
| -rw-r--r-- | lib/io/reader_test.go | 69 | ||||
| -rw-r--r-- | lib/time/time.go | 29 |
12 files changed, 1026 insertions, 0 deletions
diff --git a/lib/email/body.go b/lib/email/body.go new file mode 100644 index 00000000..c1e3d31f --- /dev/null +++ b/lib/email/body.go @@ -0,0 +1,23 @@ +// Copyright 2019, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package email + +// +// Body represent multiple message body. +// +type Body struct { + // + // We are not using map here it to prevent the body parts being reordeded when + // packing the message back into raw format. + // + mimes []*MIME // nolint: structcheck,unused +} + +// +// Unpack the message's body using boundary. +// +func (body *Body) Unpack(raw, boundary []byte) ([]byte, error) { + return raw, nil +} diff --git a/lib/email/doc.go b/lib/email/doc.go new file mode 100644 index 00000000..34b959a8 --- /dev/null +++ b/lib/email/doc.go @@ -0,0 +1,9 @@ +// Copyright 2019, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +// Package email provide a library for working with Internet Message Format as +// defined by RFC 5322. +// +package email diff --git a/lib/email/email.go b/lib/email/email.go new file mode 100644 index 00000000..64bf4e16 --- /dev/null +++ b/lib/email/email.go @@ -0,0 +1,33 @@ +// Copyright 2019, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package email + +var ( // nolint: gochecknoglobals + crlf = []byte{'\r', '\n'} +) + +// +// Email represent an internet message. +// +type Email struct { + Header Header + Body Body +} + +// +// Unpack the raw message header and body. +// +func (email *Email) Unpack(raw []byte) ([]byte, error) { + var err error + + raw, err = email.Header.Unpack(raw) + if err != nil { + return raw, err + } + + raw, err = email.Body.Unpack(raw, nil) + + return raw, err +} diff --git a/lib/email/field.go b/lib/email/field.go new file mode 100644 index 00000000..cdf7b90a --- /dev/null +++ b/lib/email/field.go @@ -0,0 +1,363 @@ +// Copyright 2019, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package email + +import ( + "bytes" + "fmt" + "time" + + libbytes "github.com/shuLhan/share/lib/bytes" + libio "github.com/shuLhan/share/lib/io" + libtime "github.com/shuLhan/share/lib/time" +) + +var ( + FieldNameDate = []byte("date") +) + +// +// Field represent field name and value in header. +// +type Field struct { + // Type of field, the numeric representation of field name. + Type FieldType + + // Name contains "relaxed" canonicalization of field name. + Name []byte + // Value contains "relaxed" canonicalization of field value. + Value []byte + + // oriName contains "simple" canonicalization of field name. + oriName []byte + // oriValue contains "simple" canonicalization of field value. + oriValue []byte + + date *time.Time +} + +// +// ParseField create and initialize Field by parsing a single line message +// header field from raw input. +// +// If raw input contains multiple lines, the rest of lines will be returned. +// +// On error, it will return nil Field, and rest will contains the beginning of +// invalid input. +// +func ParseField(raw []byte) (field *Field, rest []byte, err error) { // nolint: gocyclo + if len(raw) == 0 { + return nil, nil, nil + } + + field = &Field{} + isFolded := false + start := 0 + + // Get field's name. + // Valid values: %d33-57 / %d59-126 . + x := 0 + for ; x < len(raw); x++ { + if raw[x] == ' ' || raw[x] == ':' { + break + } + if raw[x] < 33 || raw[x] > 126 { + goto invalid + } + } + if len(raw) == x { + goto invalid + } + + // Skip spaces before ':'. + for ; x < len(raw) && raw[x] == ' '; x++ { + } + if len(raw) == x { + goto invalid + } + if raw[x] != ':' { + goto invalid + } + + field.SetName(raw[:x]) + x++ + start = x + + // Skip WSP after ':'. + for ; x < len(raw) && (raw[x] == '\t' || raw[x] == ' '); x++ { + } + if len(raw) == x { + goto invalid + } + + // Get field's value. + // Valid values: WSP / %d33-126 . + for ; x < len(raw); x++ { + for ; x < len(raw); x++ { + if raw[x] == '\t' || raw[x] == ' ' { + continue + } + if raw[x] == '\r' { + x++ + break + } + if raw[x] < 33 || raw[x] > 126 { + goto invalid + } + } + if x == len(raw) || raw[x] != '\n' { + goto invalid + } + if x++; x == len(raw) { + break + } + + // Unfolding ... + if raw[x] == '\t' || raw[x] == ' ' { + isFolded = true + continue + } + break + } + if !isFolded && x > 1000 { + err = fmt.Errorf("ParseField: line greater than 998 characters") + return nil, nil, err + } + + field.SetValue(raw[start:x]) + + if len(field.Value) == 0 { + goto invalid + } + + if len(raw) > x { + rest = raw[x:] + } + + return field, rest, nil + +invalid: + if x < len(raw) { + err = fmt.Errorf("ParseField: invalid character at index %d", x) + rest = raw[x:] + } else { + err = fmt.Errorf("ParseField: invalid input") + } + return nil, rest, err +} + +// +// SetName set field Name by canonicalizing raw field name using "simple" and +// "relaxed" algorithms. +//. +// "simple" algorithm store raw field name as is. +// +// "relaxed" algorithm convert field name to lowercase and removing trailing +// whitespaces. +// +func (field *Field) SetName(raw []byte) { + field.oriName = raw + field.Name = make([]byte, 0, len(raw)) + for x := 0; x < len(raw); x++ { + if raw[x] == ' ' || raw[x] < 33 || raw[x] > 126 { + break + } + if raw[x] >= 'A' && raw[x] <= 'Z' { + field.Name = append(field.Name, raw[x]+32) + } else { + field.Name = append(field.Name, raw[x]) + } + } + field.updateType() +} + +// +// SetValue set the field Value by canonicalizing raw input using "simple" and +// "relaxed" algorithms. +// +// "simple" algorithm store raw field value as is. +// +// "relaxed" algorithm remove leading and trailing WSP, replacing all +// CFWS with single space, but not removing CRLF at end. +// +func (field *Field) SetValue(raw []byte) { + field.oriValue = raw + field.Value = make([]byte, 0, len(raw)) + + x := 0 + // Skip leading spaces. + for ; x < len(raw); x++ { + if !libbytes.IsSpace(raw[x]) { + break + } + } + + spaces := 0 + for ; x < len(raw); x++ { + if libbytes.IsSpace(raw[x]) { + spaces++ + continue + } + if spaces > 0 { + field.Value = append(field.Value, ' ') + spaces = 0 + } + field.Value = append(field.Value, raw[x]) + } + if len(field.Value) > 0 { + field.Value = append(field.Value, crlf...) + } +} + +// +// String return the relaxed canonicalization of field name and value +// separated by colon. +// +func (field *Field) String() string { + return string(field.Name) + ":" + string(field.Value) +} + +// +// Unpack the field Value based on field Name. +// +func (field *Field) Unpack() (err error) { + switch field.Type { + case FieldTypeDate: + err = field.unpackDate() + } + + return err +} + +// +// updateType update the field type based on field name. +// +func (field *Field) updateType() { + switch { + case bytes.Equal(FieldNameDate, field.Name): + field.Type = FieldTypeDate + default: + field.Type = FieldTypeOptional + } +} + +// +// unpackDate from field value into time.Time. +// +// Format, +// +// [day-of-week ","] day month year hour ":" minute [ ":" second ] zone +// +// day-of-week = "Mon" / ... / "Sun" +// day = 1*2DIGIT +// month = "Jan" / ... / "Dec" +// year = 4*DIGIT +// hour = 2DIGIT +// minute = 2DIGIT +// second = 2DIGIT +// zone = ("+" / "-") 4DIGIT +// +// +// +func (field *Field) unpackDate() (err error) { + var ( + v []byte + ok bool + c byte + space = []byte{' ', '\r', '\n'} + day, year int64 + hour, min, sec int64 + off int64 + month time.Month + loc *time.Location = time.UTC + ) + + if len(field.Value) == 0 { + return fmt.Errorf("unpackDate: empty date") + } + + r := &libio.Reader{} + r.InitBytes(field.Value) + + c = r.SkipSpace() + if !libbytes.IsDigit(c) { + v, _, c = r.ReadUntil([]byte{','}, nil) + if len(v) == 0 || c != ',' { + return fmt.Errorf("unpackDate: invalid date format") + } + if c = r.SkipSpace(); c == 0 { + return fmt.Errorf("unpackDate: invalid date format") + } + } + + // Get day .... + if day, c = r.ScanInt64(); c == 0 || c != ' ' { + return fmt.Errorf("unpackDate: missing month") + } + // Get month ... + r.SkipSpace() + v, _, c = r.ReadUntil(space, nil) + month, ok = libtime.ShortMonths[string(v)] + if !ok { + return fmt.Errorf("unpackDate: invalid month: '%s'", v) + } + + // Get year ... + r.SkipSpace() + if year, c = r.ScanInt64(); c == 0 || c != ' ' { + return fmt.Errorf("unpackDate: invalid year") + } + + // Obsolete year allow two or three digits. + switch { + case year < 50: + year += 2000 + case year >= 50 && year < 1000: + year += 1900 + } + + // Get hour ... + if hour, c = r.ScanInt64(); c == 0 || c != ':' { + return fmt.Errorf("unpackDate: invalid hour") + } + if hour < 0 || hour > 23 { + return fmt.Errorf("unpackDate: invalid hour: %d", hour) + } + + // Get minute ... + r.SkipN(1) + min, c = r.ScanInt64() + if min < 0 || min > 59 { + return fmt.Errorf("unpackDate: invalid minute: %d", min) + } + + // Get second ... + if c == ':' { + r.SkipN(1) + sec, c = r.ScanInt64() + if sec < 0 || sec > 59 { + return fmt.Errorf("unpackDate: invalid second: %d", sec) + } + } + + // Get zone offset ... + c = r.SkipSpace() + if c == 0 { + return fmt.Errorf("unpackDate: missing zone") + } + off, c = r.ScanInt64() + + loc = time.FixedZone("UTC", computeOffSeconds(off)) + td := time.Date(int(year), month, int(day), int(hour), int(min), int(sec), 0, loc) + field.date = &td + + return err +} + +func computeOffSeconds(off int64) int { + hour := int(off / 100) + min := int(off) - (hour * 100) + return ((hour * 60) + min) * 60 +} diff --git a/lib/email/field_test.go b/lib/email/field_test.go new file mode 100644 index 00000000..4e9b65ac --- /dev/null +++ b/lib/email/field_test.go @@ -0,0 +1,255 @@ +// Copyright 2019, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package email + +import ( + "testing" + "time" + + libbytes "github.com/shuLhan/share/lib/bytes" + "github.com/shuLhan/share/lib/test" +) + +func TestParseField(t *testing.T) { + longValue := string(libbytes.Random([]byte(libbytes.ASCIILetters), 994)) + + cases := []struct { + desc string + raw []byte + expErr string + exp *Field + expRest []byte + }{{ + desc: "With empty input", + }, { + desc: "With long line", + raw: []byte("name:" + longValue + "\r\n"), + expErr: "ParseField: line greater than 998 characters", + }, { + desc: "With only whitespaces", + raw: []byte(" "), + expErr: "ParseField: invalid input", + }, { + desc: "With only CRLF", + raw: []byte("\r\n"), + expErr: "ParseField: invalid character at index 0", + }, { + desc: "Without separator and CRLF", + raw: []byte("name"), + expErr: "ParseField: invalid input", + }, { + desc: "Without separator", + raw: []byte("name\r\n"), + expErr: "ParseField: invalid character at index 4", + }, { + desc: "With space on name", + raw: []byte("na me\r\n"), + expErr: "ParseField: invalid character at index 3", + }, { + desc: "Without value and CRLF", + raw: []byte("name:"), + expErr: "ParseField: invalid input", + }, { + desc: "Without value and CRLF", + raw: []byte("name: "), + expErr: "ParseField: invalid input", + }, { + desc: "Without value", + raw: []byte("name:\r\n"), + expErr: "ParseField: invalid input", + }, { + desc: "Without value", + raw: []byte("name: \r\n"), + expErr: "ParseField: invalid input", + }, { + desc: "Without CRLF", + raw: []byte("name:value"), + expErr: "ParseField: invalid input", + }, { + desc: "Without CR", + raw: []byte("name:value\n"), + expErr: "ParseField: invalid character at index 10", + }, { + desc: "Without LF", + raw: []byte("name:value\r"), + expErr: "ParseField: invalid input", + }, { + desc: "With CR inside value", + raw: []byte("name:valu\re"), + expErr: "ParseField: invalid character at index 10", + }, { + desc: "With valid input", + raw: []byte("NAME : VALUE\r\n"), + exp: &Field{ + Name: []byte("name"), + Value: []byte("VALUE\r\n"), + oriName: []byte("NAME "), + oriValue: []byte(" VALUE\r\n"), + }, + }, { + desc: "With single folding", + raw: []byte("Name : \r\n \t Value\r\n"), + exp: &Field{ + Name: []byte("name"), + Value: []byte("Value\r\n"), + oriName: []byte("Name "), + oriValue: []byte(" \r\n \t Value\r\n"), + }, + }, { + desc: "With multiple folding between value", + raw: []byte("namE : This\r\n is\r\n\ta\r\n \tvalue\r\n"), + exp: &Field{ + Name: []byte("name"), + Value: []byte("This is a value\r\n"), + oriName: []byte("namE "), + oriValue: []byte(" This\r\n is\r\n\ta\r\n \tvalue\r\n"), + }, + }, { + desc: "With multiple fields", + raw: []byte("a : 1\r\nb : 2\r\n"), + exp: &Field{ + Name: []byte("a"), + Value: []byte("1\r\n"), + oriName: []byte("a "), + oriValue: []byte(" 1\r\n"), + }, + expRest: []byte("b : 2\r\n"), + }} + + for _, c := range cases { + t.Log(c.desc) + + got, rest, err := ParseField(c.raw) + if err != nil { + test.Assert(t, "error", c.expErr, err.Error(), true) + continue + } + if got == nil { + test.Assert(t, "Field", c.exp, got, true) + continue + } + + test.Assert(t, "Field.oriName", c.exp.oriName, got.oriName, true) + test.Assert(t, "Field.oriValue", c.exp.oriValue, got.oriValue, true) + test.Assert(t, "Field.Name", c.exp.Name, got.Name, true) + test.Assert(t, "Field.Value", c.exp.Value, got.Value, true) + + test.Assert(t, "rest", c.expRest, rest, true) + } +} + +func TestUnpackDate(t *testing.T) { + cases := []struct { + desc string + value []byte + exp time.Time + expErr string + }{{ + desc: "With empty value", + expErr: "unpackDate: empty date", + }, { + desc: "With only spaces", + value: []byte(" "), + expErr: "unpackDate: empty date", + }, { + desc: "With invalid date format", + value: []byte("Sat"), + expErr: "unpackDate: invalid date format", + }, { + desc: "With invalid date format", + value: []byte("Sat,"), + expErr: "unpackDate: invalid date format", + }, { + desc: "With missing month", + value: []byte("Sat, 2"), + expErr: "unpackDate: missing month", + }, { + desc: "With missing month", + value: []byte("Sat, 2 "), + expErr: "unpackDate: missing month", + }, { + desc: "With invalid month", + value: []byte("Sat, 2 X 2019"), + expErr: "unpackDate: invalid month: 'X'", + }, { + desc: "With missing year", + value: []byte("Sat, 2 Feb"), + expErr: "unpackDate: invalid year", + }, { + desc: "With invalid year", + value: []byte("Sat, 2 Feb 2019"), + expErr: "unpackDate: invalid year", + }, { + desc: "With invalid hour", + value: []byte("Sat, 2 Feb 2019 00"), + expErr: "unpackDate: invalid hour", + }, { + desc: "With invalid hour", + value: []byte("Sat, 2 Feb 2019 24:55:16 +0000"), + expErr: "unpackDate: invalid hour: 24", + }, { + desc: "With invalid minute", + value: []byte("Sat, 2 Feb 2019 00:60:16 +0000"), + expErr: "unpackDate: invalid minute: 60", + }, { + desc: "Without second and missing zone", + value: []byte("Sat, 2 Feb 2019 00:55"), + expErr: "unpackDate: missing zone", + }, { + desc: "With invalid second", + value: []byte("Sat, 2 Feb 2019 00:55:60 +0000"), + expErr: "unpackDate: invalid second: 60", + }, { + desc: "With missing zone", + value: []byte("Sat, 2 Feb 2019 00:55:16"), + expErr: "unpackDate: missing zone", + }, { + desc: "With zone", + value: []byte("Sat, 2 Feb 2019 00:55:16 UTC"), + exp: time.Date(2019, time.February, 2, 0, 55, 16, 0, time.UTC), + }, { + desc: "With +0800", + value: []byte("Sat, 2 Feb 2019 00:55:16 +0800"), + exp: time.Date(2019, time.February, 2, 0, 55, 16, 0, time.FixedZone("UTC", 8*60*60)), + }, { + desc: "Without week day", + value: []byte("2 Feb 2019 00:55:16 UTC"), + exp: time.Date(2019, time.February, 2, 0, 55, 16, 0, time.UTC), + }, { + desc: "Without second", + value: []byte("Sat, 2 Feb 2019 00:55 UTC"), + exp: time.Date(2019, time.February, 2, 0, 55, 0, 0, time.UTC), + }, { + desc: "Without week-day and second", + value: []byte("2 Feb 2019 00:55 UTC"), + exp: time.Date(2019, time.February, 2, 0, 55, 0, 0, time.UTC), + }, { + desc: "With obsolete year 2 digits", + value: []byte("2 Feb 19 00:55 UTC"), + exp: time.Date(2019, time.February, 2, 0, 55, 0, 0, time.UTC), + }, { + desc: "With obsolete year 3 digits", + value: []byte("2 Feb 89 00:55 UTC"), + exp: time.Date(1989, time.February, 2, 0, 55, 0, 0, time.UTC), + }} + + field := &Field{ + Type: FieldTypeDate, + } + + for _, c := range cases { + t.Log(c.desc) + + field.SetValue(c.value) + + err := field.Unpack() + if err != nil { + test.Assert(t, "error", c.expErr, err.Error(), true) + continue + } + + test.Assert(t, "date", c.exp.String(), field.date.String(), true) + } +} diff --git a/lib/email/fieldtype.go b/lib/email/fieldtype.go new file mode 100644 index 00000000..c419bdad --- /dev/null +++ b/lib/email/fieldtype.go @@ -0,0 +1,12 @@ +// Copyright 2019, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package email + +type FieldType int + +const ( + FieldTypeOptional FieldType = 0 + FieldTypeDate FieldType = 1 << iota +) diff --git a/lib/email/header.go b/lib/email/header.go new file mode 100644 index 00000000..54d92f07 --- /dev/null +++ b/lib/email/header.go @@ -0,0 +1,79 @@ +// Copyright 2019, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package email + +import ( + "fmt" + "strings" +) + +// +// Header represent list of field. +// +// We are not using map here it to prevent the header being reordeded when +// packing the message back into raw format. +// +type Header struct { + fields []*Field +} + +// +// Unpack the raw header from top to bottom. +// +// The raw header may end with optional CRLF, an empty line that separate +// header from body of message. +// +// On success it will return the rest of raw input (possible message's body) +// without leading CRLF. +// +func (hdr *Header) Unpack(raw []byte) ([]byte, error) { + var ( + field *Field + err error + ) + + for len(raw) > 2 { + field, raw, err = ParseField(raw) + if err != nil { + return raw, err + } + hdr.fields = append(hdr.fields, field) + if len(raw) > 2 { + if raw[0] == crlf[0] && raw[1] == crlf[1] { + break + } + } + } + + switch len(raw) { + case 0: + case 1: + err = fmt.Errorf("Header.Unpack: invalid end of header: '%s'", raw) + case 2: + if raw[0] != crlf[0] || raw[1] != crlf[1] { + err = fmt.Errorf("Header.Unpack: invalid end of header: '%s'", raw) + } else { + raw = raw[2:] + } + default: + raw = raw[2:] + } + + return raw, err +} + +// +// String return the text representation of header, by concatenating all +// sanitized fields with CRLF. +// +func (hdr *Header) String() string { + var sb strings.Builder + + for _, f := range hdr.fields { + sb.WriteString(f.String()) + } + + return sb.String() +} diff --git a/lib/email/header_test.go b/lib/email/header_test.go new file mode 100644 index 00000000..29cb93a1 --- /dev/null +++ b/lib/email/header_test.go @@ -0,0 +1,72 @@ +// Copyright 2019, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package email + +import ( + "testing" + + "github.com/shuLhan/share/lib/test" +) + +func TestHeaderUnpack(t *testing.T) { + cases := []struct { + desc string + raw []byte + expErr string + exp string + expRest []byte + }{{ + desc: "With empty input", + }, { + desc: "With whitespaces only", + raw: []byte(" \t"), + expErr: "Header.Unpack: invalid end of header: ' \t'", + }, { + desc: "With CRLF only", + raw: crlf, + expRest: []byte{}, + }, { + desc: "With invalid end", + raw: []byte("a: 1\r\nx"), + expErr: "Header.Unpack: invalid end of header: 'x'", + }, { + desc: "With invalid field: missing value", + raw: []byte("a:\r\n\t"), + expErr: "ParseField: invalid input", + }, { + desc: "With single field", + raw: []byte("a:1\r\n"), + exp: "a:1\r\n", + }, { + desc: "With multiple fields", + raw: []byte("a:1\r\nb : 2\r\n"), + exp: "a:1\r\nb:2\r\n", + }, { + desc: "With empty line at the end", + raw: []byte("a:1\r\nb : 2\r\n\r\n"), + exp: "a:1\r\nb:2\r\n", + expRest: []byte{}, + }, { + desc: "With body", + raw: []byte("a:1\r\nb : 2\r\n\r\nBody."), + exp: "a:1\r\nb:2\r\n", + expRest: []byte("Body."), + }} + + for _, c := range cases { + t.Log(c.desc) + + header := &Header{} + + rest, err := header.Unpack(c.raw) + if err != nil { + test.Assert(t, "error", c.expErr, err.Error(), true) + continue + } + + test.Assert(t, "Header.String", c.exp, header.String(), true) + test.Assert(t, "rest", c.expRest, rest, true) + } +} diff --git a/lib/email/mime.go b/lib/email/mime.go new file mode 100644 index 00000000..8e4102a5 --- /dev/null +++ b/lib/email/mime.go @@ -0,0 +1,16 @@ +// Copyright 2019, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package email + +// +// MIME represent part of message body with id, content type, encoding, +// description, and content. +// +type MIME struct { + ID []byte + Type []byte + Description []byte + Content []byte +} diff --git a/lib/io/reader.go b/lib/io/reader.go index 5a8896f3..43b0c504 100644 --- a/lib/io/reader.go +++ b/lib/io/reader.go @@ -43,6 +43,14 @@ func (r *Reader) Init(src string) { } // +// InitBytes initialize reader buffer from slice of byte. +// +func (r *Reader) InitBytes(src []byte) { + r.p = 0 + r.v = src +} + +// // ReadUntil read the content of file until one of separator found, or until // it reach the terminator character, or until EOF. // The content will be returned along the status of termination. @@ -73,6 +81,64 @@ func (r *Reader) ReadUntil(seps []byte, terms []byte) (b []byte, isTerm bool, c } // +// ScanInt64 convert textual representation of number into int64 and return +// it. +// Any spaces before actual reading of text will be ignored. +// The number may prefixed with '-' or '+', if its '-', the returned value +// must be negative. +// +// On success, c is non digit character that terminate scan, if its 0, its +// mean EOF. +// +func (r *Reader) ScanInt64() (n int64, c byte) { + var min int64 = 1 + if len(r.v) == r.p { + return + } + + for ; r.p < len(r.v); r.p++ { + c = r.v[r.p] + if !libbytes.IsSpace(c) { + break + } + } + if c == '-' { + min = -1 + r.p++ + } else if c == '+' { + r.p++ + } + for r.p < len(r.v) { + c = r.v[r.p] + if !libbytes.IsDigit(c) { + break + } + c = c - '0' + n *= 10 + n += int64(c) + r.p++ + } + n *= min + if r.p == len(r.v) { + return n, 0 + } + + return n, c +} + +// +// SkipN skip reading n bytes from buffer and return true if EOF. +// +func (r *Reader) SkipN(n int) bool { + r.p += n + if r.p >= len(r.v) { + r.p = len(r.v) + return true + } + return false +} + +// // SkipSpace read until no white spaces found and return the first byte that // is not white spaces. // On EOF, it will return 0. diff --git a/lib/io/reader_test.go b/lib/io/reader_test.go new file mode 100644 index 00000000..bae919d0 --- /dev/null +++ b/lib/io/reader_test.go @@ -0,0 +1,69 @@ +// Copyright 2019, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package io + +import ( + "testing" + + "github.com/shuLhan/share/lib/test" +) + +func TestReaderScanInt64(t *testing.T) { + cases := []struct { + desc string + src []byte + exp int64 + expc byte + }{{ + desc: "With empty input", + }, { + desc: "With non digit", + src: []byte("a -1"), + expc: 'a', + }, { + desc: "With leading spaces", + src: []byte(" +1"), + exp: 1, + }, { + desc: "With -1", + src: []byte("-1"), + exp: -1, + }, { + desc: "With -1", + src: []byte("-1x"), + exp: -1, + expc: 'x', + }, { + desc: "With +1", + src: []byte("+1"), + exp: 1, + }, { + desc: "With 1000", + src: []byte("1000"), + exp: 1000, + }, { + desc: "With 9876543210 1", + src: []byte("9876543210 1"), + exp: 9876543210, + expc: ' ', + }, { + desc: "With leading zero 001", + src: []byte("-001"), + exp: -1, + }} + + r := &Reader{} + + for _, c := range cases { + t.Log(c.desc) + + r.InitBytes(c.src) + + got, gotc := r.ScanInt64() + + test.Assert(t, "n", c.exp, got, true) + test.Assert(t, "c", c.expc, gotc, true) + } +} diff --git a/lib/time/time.go b/lib/time/time.go index 63d359b8..4a88f828 100644 --- a/lib/time/time.go +++ b/lib/time/time.go @@ -4,3 +4,32 @@ // Package time provide a library for working with time. package time + +import ( + "time" +) + +var ( + ShortDayNames = []string{ + "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", + } + + // + // ShortMonths provide mapping between text of month, in English, + // short format to their time.Month value + // + ShortMonths = map[string]time.Month{ + "Jan": time.January, + "Feb": time.February, + "Mar": time.March, + "Apr": time.April, + "May": time.May, + "Jun": time.June, + "Jul": time.July, + "Aug": time.August, + "Sep": time.September, + "Oct": time.October, + "Nov": time.November, + "Dec": time.December, + } +) |
