aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2019-02-01 11:32:34 +0700
committerShulhan <ms@kilabit.info>2019-02-05 07:48:52 +0700
commit930b8e7bdeb7b4909905f3185647d8d64d89ebbf (patch)
tree623956d90d0e33b85bc23f97ccc93bf15b1950e9 /lib
parent45a23e05d85eac33e1a4b44c0888be5d91a73cdb (diff)
downloadpakakeh.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.go23
-rw-r--r--lib/email/doc.go9
-rw-r--r--lib/email/email.go33
-rw-r--r--lib/email/field.go363
-rw-r--r--lib/email/field_test.go255
-rw-r--r--lib/email/fieldtype.go12
-rw-r--r--lib/email/header.go79
-rw-r--r--lib/email/header_test.go72
-rw-r--r--lib/email/mime.go16
-rw-r--r--lib/io/reader.go66
-rw-r--r--lib/io/reader_test.go69
-rw-r--r--lib/time/time.go29
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,
+ }
+)