Added files.

This commit is contained in:
2025-12-08 06:42:29 +02:00
commit a65a31fdac
109 changed files with 16539 additions and 0 deletions

13
internal/acl.go Normal file
View File

@@ -0,0 +1,13 @@
package internal
import (
"github.com/emersion/go-imap/v2"
)
func FormatRights(rm imap.RightModification, rs imap.RightSet) string {
s := ""
if rm != imap.RightModificationReplace {
s = string(rm)
}
return s + string(rs)
}

306
internal/imapnum/numset.go Normal file
View File

@@ -0,0 +1,306 @@
package imapnum
import (
"fmt"
"strconv"
"strings"
)
// Range represents a single seq-number or seq-range value (RFC 3501 ABNF). Values
// may be static (e.g. "1", "2:4") or dynamic (e.g. "*", "1:*"). A seq-number is
// represented by setting Start = Stop. Zero is used to represent "*", which is
// safe because seq-number uses nz-number rule. The order of values is always
// Start <= Stop, except when representing "n:*", where Start = n and Stop = 0.
type Range struct {
Start, Stop uint32
}
// Contains returns true if the seq-number q is contained in range value s.
// The dynamic value "*" contains only other "*" values, the dynamic range "n:*"
// contains "*" and all numbers >= n.
func (s Range) Contains(q uint32) bool {
if q == 0 {
return s.Stop == 0 // "*" is contained only in "*" and "n:*"
}
return s.Start != 0 && s.Start <= q && (q <= s.Stop || s.Stop == 0)
}
// Less returns true if s precedes and does not contain seq-number q.
func (s Range) Less(q uint32) bool {
return (s.Stop < q || q == 0) && s.Stop != 0
}
// Merge combines range values s and t into a single union if the two
// intersect or one is a superset of the other. The order of s and t does not
// matter. If the values cannot be merged, s is returned unmodified and ok is
// set to false.
func (s Range) Merge(t Range) (union Range, ok bool) {
union = s
if s == t {
return s, true
}
if s.Start != 0 && t.Start != 0 {
// s and t are any combination of "n", "n:m", or "n:*"
if s.Start > t.Start {
s, t = t, s
}
// s starts at or before t, check where it ends
if (s.Stop >= t.Stop && t.Stop != 0) || s.Stop == 0 {
return s, true // s is a superset of t
}
// s is "n" or "n:m", if m == ^uint32(0) then t is "n:*"
if s.Stop+1 >= t.Start || s.Stop == ^uint32(0) {
return Range{s.Start, t.Stop}, true // s intersects or touches t
}
return union, false
}
// exactly one of s and t is "*"
if s.Start == 0 {
if t.Stop == 0 {
return t, true // s is "*", t is "n:*"
}
} else if s.Stop == 0 {
return s, true // s is "n:*", t is "*"
}
return union, false
}
// String returns range value s as a seq-number or seq-range string.
func (s Range) String() string {
if s.Start == s.Stop {
if s.Start == 0 {
return "*"
}
return strconv.FormatUint(uint64(s.Start), 10)
}
b := strconv.AppendUint(make([]byte, 0, 24), uint64(s.Start), 10)
if s.Stop == 0 {
return string(append(b, ':', '*'))
}
return string(strconv.AppendUint(append(b, ':'), uint64(s.Stop), 10))
}
func (s Range) append(nums []uint32) (out []uint32, ok bool) {
if s.Start == 0 || s.Stop == 0 {
return nil, false
}
for n := s.Start; n <= s.Stop; n++ {
nums = append(nums, n)
}
return nums, true
}
// Set is used to represent a set of message sequence numbers or UIDs (see
// sequence-set ABNF rule). The zero value is an empty set.
type Set []Range
// AddNum inserts new numbers into the set. The value 0 represents "*".
func (s *Set) AddNum(q ...uint32) {
for _, v := range q {
s.insert(Range{v, v})
}
}
// AddRange inserts a new range into the set.
func (s *Set) AddRange(start, stop uint32) {
if (stop < start && stop != 0) || start == 0 {
s.insert(Range{stop, start})
} else {
s.insert(Range{start, stop})
}
}
// AddSet inserts all values from t into s.
func (s *Set) AddSet(t Set) {
for _, v := range t {
s.insert(v)
}
}
// Dynamic returns true if the set contains "*" or "n:*" values.
func (s Set) Dynamic() bool {
return len(s) > 0 && s[len(s)-1].Stop == 0
}
// Contains returns true if the non-zero sequence number or UID q is contained
// in the set. The dynamic range "n:*" contains all q >= n. It is the caller's
// responsibility to handle the special case where q is the maximum UID in the
// mailbox and q < n (i.e. the set cannot match UIDs against "*:n" or "*" since
// it doesn't know what the maximum value is).
func (s Set) Contains(q uint32) bool {
if _, ok := s.search(q); ok {
return q != 0
}
return false
}
// Nums returns a slice of all numbers contained in the set.
func (s Set) Nums() (nums []uint32, ok bool) {
for _, v := range s {
nums, ok = v.append(nums)
if !ok {
return nil, false
}
}
return nums, true
}
// String returns a sorted representation of all contained number values.
func (s Set) String() string {
if len(s) == 0 {
return ""
}
b := make([]byte, 0, 64)
for _, v := range s {
b = append(b, ',')
if v.Start == 0 {
b = append(b, '*')
continue
}
b = strconv.AppendUint(b, uint64(v.Start), 10)
if v.Start != v.Stop {
if v.Stop == 0 {
b = append(b, ':', '*')
continue
}
b = strconv.AppendUint(append(b, ':'), uint64(v.Stop), 10)
}
}
return string(b[1:])
}
// insert adds range value v to the set.
func (ptr *Set) insert(v Range) {
s := *ptr
defer func() {
*ptr = s
}()
i, _ := s.search(v.Start)
merged := false
if i > 0 {
// try merging with the preceding entry (e.g. "1,4".insert(2), i == 1)
s[i-1], merged = s[i-1].Merge(v)
}
if i == len(s) {
// v was either merged with the last entry or needs to be appended
if !merged {
s.insertAt(i, v)
}
return
} else if merged {
i--
} else if s[i], merged = s[i].Merge(v); !merged {
s.insertAt(i, v) // insert in the middle (e.g. "1,5".insert(3), i == 1)
return
}
// v was merged with s[i], continue trying to merge until the end
for j := i + 1; j < len(s); j++ {
if s[i], merged = s[i].Merge(s[j]); !merged {
if j > i+1 {
// cut out all entries between i and j that were merged
s = append(s[:i+1], s[j:]...)
}
return
}
}
// everything after s[i] was merged
s = s[:i+1]
}
// insertAt inserts a new range value v at index i, resizing s.Set as needed.
func (ptr *Set) insertAt(i int, v Range) {
s := *ptr
defer func() {
*ptr = s
}()
if n := len(s); i == n {
// insert at the end
s = append(s, v)
return
} else if n < cap(s) {
// enough space, shift everything at and after i to the right
s = s[:n+1]
copy(s[i+1:], s[i:])
} else {
// allocate new slice and copy everything, n is at least 1
set := make([]Range, n+1, n*2)
copy(set, s[:i])
copy(set[i+1:], s[i:])
s = set
}
s[i] = v
}
// search attempts to find the index of the range set value that contains q.
// If no values contain q, the returned index is the position where q should be
// inserted and ok is set to false.
func (s Set) search(q uint32) (i int, ok bool) {
min, max := 0, len(s)-1
for min < max {
if mid := (min + max) >> 1; s[mid].Less(q) {
min = mid + 1
} else {
max = mid
}
}
if max < 0 || s[min].Less(q) {
return len(s), false // q is the new largest value
}
return min, s[min].Contains(q)
}
// errBadNumSet is used to report problems with the format of a number set
// value.
type errBadNumSet string
func (err errBadNumSet) Error() string {
return fmt.Sprintf("imap: bad number set value %q", string(err))
}
// parseNum parses a single seq-number value (non-zero uint32 or "*").
func parseNum(v string) (uint32, error) {
if n, err := strconv.ParseUint(v, 10, 32); err == nil && v[0] != '0' {
return uint32(n), nil
} else if v == "*" {
return 0, nil
}
return 0, errBadNumSet(v)
}
// parseNumRange creates a new seq instance by parsing strings in the format
// "n" or "n:m", where n and/or m may be "*". An error is returned for invalid
// values.
func parseNumRange(v string) (Range, error) {
var (
r Range
err error
)
if sep := strings.IndexRune(v, ':'); sep < 0 {
r.Start, err = parseNum(v)
r.Stop = r.Start
return r, err
} else if r.Start, err = parseNum(v[:sep]); err == nil {
if r.Stop, err = parseNum(v[sep+1:]); err == nil {
if (r.Stop < r.Start && r.Stop != 0) || r.Start == 0 {
r.Start, r.Stop = r.Stop, r.Start
}
return r, nil
}
}
return r, errBadNumSet(v)
}
// ParseSet returns a new Set after parsing the set string.
func ParseSet(set string) (Set, error) {
var s Set
for _, sv := range strings.Split(set, ",") {
r, err := parseNumRange(sv)
if err != nil {
return s, err
}
s.AddRange(r.Start, r.Stop)
}
return s, nil
}

View File

@@ -0,0 +1,724 @@
package imapnum
import (
"math/rand"
"strings"
"testing"
)
const max = ^uint32(0)
func TestParseNumRange(t *testing.T) {
tests := []struct {
in string
out Range
ok bool
}{
// Invalid number
{"", Range{}, false},
{" ", Range{}, false},
{"A", Range{}, false},
{"0", Range{}, false},
{" 1", Range{}, false},
{"1 ", Range{}, false},
{"*1", Range{}, false},
{"1*", Range{}, false},
{"-1", Range{}, false},
{"01", Range{}, false},
{"0x1", Range{}, false},
{"1 2", Range{}, false},
{"1,2", Range{}, false},
{"1.2", Range{}, false},
{"4294967296", Range{}, false},
// Valid number
{"*", Range{0, 0}, true},
{"1", Range{1, 1}, true},
{"42", Range{42, 42}, true},
{"1000", Range{1000, 1000}, true},
{"4294967295", Range{max, max}, true},
// Invalid range
{":", Range{}, false},
{"*:", Range{}, false},
{":*", Range{}, false},
{"1:", Range{}, false},
{":1", Range{}, false},
{"0:0", Range{}, false},
{"0:*", Range{}, false},
{"0:1", Range{}, false},
{"1:0", Range{}, false},
{"1:2 ", Range{}, false},
{"1: 2", Range{}, false},
{"1:2:", Range{}, false},
{"1:2,", Range{}, false},
{"1:2:3", Range{}, false},
{"1:2,3", Range{}, false},
{"*:4294967296", Range{}, false},
{"0:4294967295", Range{}, false},
{"1:4294967296", Range{}, false},
{"4294967296:*", Range{}, false},
{"4294967295:0", Range{}, false},
{"4294967296:1", Range{}, false},
{"4294967295:4294967296", Range{}, false},
// Valid range
{"*:*", Range{0, 0}, true},
{"1:*", Range{1, 0}, true},
{"*:1", Range{1, 0}, true},
{"2:2", Range{2, 2}, true},
{"2:42", Range{2, 42}, true},
{"42:2", Range{2, 42}, true},
{"*:4294967294", Range{max - 1, 0}, true},
{"*:4294967295", Range{max, 0}, true},
{"4294967294:*", Range{max - 1, 0}, true},
{"4294967295:*", Range{max, 0}, true},
{"1:4294967294", Range{1, max - 1}, true},
{"1:4294967295", Range{1, max}, true},
{"4294967295:1000", Range{1000, max}, true},
{"4294967294:4294967295", Range{max - 1, max}, true},
{"4294967295:4294967295", Range{max, max}, true},
}
for _, test := range tests {
out, err := parseNumRange(test.in)
if !test.ok {
if err == nil {
t.Errorf("parseSeq(%q) expected error; got %q", test.in, out)
}
} else if err != nil {
t.Errorf("parseSeq(%q) expected %q; got %v", test.in, test.out, err)
} else if out != test.out {
t.Errorf("parseSeq(%q) expected %q; got %q", test.in, test.out, out)
}
}
}
func TestNumRangeContainsLess(t *testing.T) {
tests := []struct {
s string
q uint32
contains bool
less bool
}{
{"2", 0, false, true},
{"2", 1, false, false},
{"2", 2, true, false},
{"2", 3, false, true},
{"2", max, false, true},
{"*", 0, true, false},
{"*", 1, false, false},
{"*", 2, false, false},
{"*", 3, false, false},
{"*", max, false, false},
{"2:3", 0, false, true},
{"2:3", 1, false, false},
{"2:3", 2, true, false},
{"2:3", 3, true, false},
{"2:3", 4, false, true},
{"2:3", 5, false, true},
{"2:4", 0, false, true},
{"2:4", 1, false, false},
{"2:4", 2, true, false},
{"2:4", 3, true, false},
{"2:4", 4, true, false},
{"2:4", 5, false, true},
{"4:4294967295", 0, false, true},
{"4:4294967295", 1, false, false},
{"4:4294967295", 2, false, false},
{"4:4294967295", 3, false, false},
{"4:4294967295", 4, true, false},
{"4:4294967295", 5, true, false},
{"4:4294967295", max, true, false},
{"4:*", 0, true, false},
{"4:*", 1, false, false},
{"4:*", 2, false, false},
{"4:*", 3, false, false},
{"4:*", 4, true, false},
{"4:*", 5, true, false},
{"4:*", max, true, false},
}
for _, test := range tests {
s, err := parseNumRange(test.s)
if err != nil {
t.Errorf("parseSeq(%q) unexpected error; %v", test.s, err)
continue
}
if s.Contains(test.q) != test.contains {
t.Errorf("%q.Contains(%d) expected %v", test.s, test.q, test.contains)
}
if s.Less(test.q) != test.less {
t.Errorf("%q.Less(%d) expected %v", test.s, test.q, test.less)
}
}
}
func TestNumRangeMerge(T *testing.T) {
tests := []struct {
s, t, out string
}{
// Number with number
{"1", "1", "1"},
{"1", "2", "1:2"},
{"1", "3", ""},
{"1", "4294967295", ""},
{"1", "*", ""},
{"4", "1", ""},
{"4", "2", ""},
{"4", "3", "3:4"},
{"4", "4", "4"},
{"4", "5", "4:5"},
{"4", "6", ""},
{"4294967295", "4294967293", ""},
{"4294967295", "4294967294", "4294967294:4294967295"},
{"4294967295", "4294967295", "4294967295"},
{"4294967295", "*", ""},
{"*", "1", ""},
{"*", "2", ""},
{"*", "4294967294", ""},
{"*", "4294967295", ""},
{"*", "*", "*"},
// Range with number
{"1:3", "1", "1:3"},
{"1:3", "2", "1:3"},
{"1:3", "3", "1:3"},
{"1:3", "4", "1:4"},
{"1:3", "5", ""},
{"1:3", "*", ""},
{"3:4", "1", ""},
{"3:4", "2", "2:4"},
{"3:4", "3", "3:4"},
{"3:4", "4", "3:4"},
{"3:4", "5", "3:5"},
{"3:4", "6", ""},
{"3:4", "*", ""},
{"2:3", "5", ""},
{"2:4", "5", "2:5"},
{"2:5", "5", "2:5"},
{"2:6", "5", "2:6"},
{"2:7", "5", "2:7"},
{"2:*", "5", "2:*"},
{"3:4", "5", "3:5"},
{"3:5", "5", "3:5"},
{"3:6", "5", "3:6"},
{"3:7", "5", "3:7"},
{"3:*", "5", "3:*"},
{"4:5", "5", "4:5"},
{"4:6", "5", "4:6"},
{"4:7", "5", "4:7"},
{"4:*", "5", "4:*"},
{"5:6", "5", "5:6"},
{"5:7", "5", "5:7"},
{"5:*", "5", "5:*"},
{"6:7", "5", "5:7"},
{"6:*", "5", "5:*"},
{"7:8", "5", ""},
{"7:*", "5", ""},
{"3:4294967294", "1", ""},
{"3:4294967294", "2", "2:4294967294"},
{"3:4294967294", "3", "3:4294967294"},
{"3:4294967294", "4", "3:4294967294"},
{"3:4294967294", "4294967293", "3:4294967294"},
{"3:4294967294", "4294967294", "3:4294967294"},
{"3:4294967294", "4294967295", "3:4294967295"},
{"3:4294967294", "*", ""},
{"3:4294967295", "1", ""},
{"3:4294967295", "2", "2:4294967295"},
{"3:4294967295", "3", "3:4294967295"},
{"3:4294967295", "4", "3:4294967295"},
{"3:4294967295", "4294967294", "3:4294967295"},
{"3:4294967295", "4294967295", "3:4294967295"},
{"3:4294967295", "*", ""},
{"1:4294967295", "1", "1:4294967295"},
{"1:4294967295", "4294967295", "1:4294967295"},
{"1:4294967295", "*", ""},
{"1:*", "1", "1:*"},
{"1:*", "2", "1:*"},
{"1:*", "4294967294", "1:*"},
{"1:*", "4294967295", "1:*"},
{"1:*", "*", "1:*"},
// Range with range
{"5:8", "1:2", ""},
{"5:8", "1:3", ""},
{"5:8", "1:4", "1:8"},
{"5:8", "1:5", "1:8"},
{"5:8", "1:6", "1:8"},
{"5:8", "1:7", "1:8"},
{"5:8", "1:8", "1:8"},
{"5:8", "1:9", "1:9"},
{"5:8", "1:10", "1:10"},
{"5:8", "1:11", "1:11"},
{"5:8", "1:*", "1:*"},
{"5:8", "2:3", ""},
{"5:8", "2:4", "2:8"},
{"5:8", "2:5", "2:8"},
{"5:8", "2:6", "2:8"},
{"5:8", "2:7", "2:8"},
{"5:8", "2:8", "2:8"},
{"5:8", "2:9", "2:9"},
{"5:8", "2:10", "2:10"},
{"5:8", "2:11", "2:11"},
{"5:8", "2:*", "2:*"},
{"5:8", "3:4", "3:8"},
{"5:8", "3:5", "3:8"},
{"5:8", "3:6", "3:8"},
{"5:8", "3:7", "3:8"},
{"5:8", "3:8", "3:8"},
{"5:8", "3:9", "3:9"},
{"5:8", "3:10", "3:10"},
{"5:8", "3:11", "3:11"},
{"5:8", "3:*", "3:*"},
{"5:8", "4:5", "4:8"},
{"5:8", "4:6", "4:8"},
{"5:8", "4:7", "4:8"},
{"5:8", "4:8", "4:8"},
{"5:8", "4:9", "4:9"},
{"5:8", "4:10", "4:10"},
{"5:8", "4:11", "4:11"},
{"5:8", "4:*", "4:*"},
{"5:8", "5:6", "5:8"},
{"5:8", "5:7", "5:8"},
{"5:8", "5:8", "5:8"},
{"5:8", "5:9", "5:9"},
{"5:8", "5:10", "5:10"},
{"5:8", "5:11", "5:11"},
{"5:8", "5:*", "5:*"},
{"5:8", "6:7", "5:8"},
{"5:8", "6:8", "5:8"},
{"5:8", "6:9", "5:9"},
{"5:8", "6:10", "5:10"},
{"5:8", "6:11", "5:11"},
{"5:8", "6:*", "5:*"},
{"5:8", "7:8", "5:8"},
{"5:8", "7:9", "5:9"},
{"5:8", "7:10", "5:10"},
{"5:8", "7:11", "5:11"},
{"5:8", "7:*", "5:*"},
{"5:8", "8:9", "5:9"},
{"5:8", "8:10", "5:10"},
{"5:8", "8:11", "5:11"},
{"5:8", "8:*", "5:*"},
{"5:8", "9:10", "5:10"},
{"5:8", "9:11", "5:11"},
{"5:8", "9:*", "5:*"},
{"5:8", "10:11", ""},
{"5:8", "10:*", ""},
{"1:*", "1:*", "1:*"},
{"1:*", "2:*", "1:*"},
{"1:*", "1:4294967294", "1:*"},
{"1:*", "1:4294967295", "1:*"},
{"1:*", "2:4294967295", "1:*"},
{"1:4294967295", "1:4294967294", "1:4294967295"},
{"1:4294967295", "1:4294967295", "1:4294967295"},
{"1:4294967295", "2:4294967295", "1:4294967295"},
{"1:4294967295", "2:*", "1:*"},
}
for _, test := range tests {
s, err := parseNumRange(test.s)
if err != nil {
T.Errorf("parseSeq(%q) unexpected error; %v", test.s, err)
continue
}
t, err := parseNumRange(test.t)
if err != nil {
T.Errorf("parseSeq(%q) unexpected error; %v", test.t, err)
continue
}
testOK := test.out != ""
for i := 0; i < 2; i++ {
if !testOK {
test.out = test.s
}
out, ok := s.Merge(t)
if out.String() != test.out || ok != testOK {
T.Errorf("%q.Merge(%q) expected %q; got %q", test.s, test.t, test.out, out)
}
// Swap s & t, result should be identical
test.s, test.t = test.t, test.s
s, t = t, s
}
}
}
func checkNumSet(s Set, t *testing.T) {
n := len(s)
for i, v := range s {
if v.Start == 0 {
if v.Stop != 0 {
t.Errorf(`NumSet(%q) index %d: "*:n" range`, s, i)
} else if i != n-1 {
t.Errorf(`NumSet(%q) index %d: "*" not at the end`, s, i)
}
continue
}
if i > 0 && s[i-1].Stop >= v.Start-1 {
t.Errorf(`NumSet(%q) index %d: overlap`, s, i)
}
if v.Stop < v.Start {
if v.Stop != 0 {
t.Errorf(`NumSet(%q) index %d: reversed range`, s, i)
} else if i != n-1 {
t.Errorf(`NumSet(%q) index %d: "n:*" not at the end`, s, i)
}
}
}
}
func TestNumSetInfo(t *testing.T) {
tests := []struct {
s string
q uint32
contains bool
}{
{"", 0, false},
{"", 1, false},
{"", 2, false},
{"", 3, false},
{"", max, false},
{"2", 0, false},
{"2", 1, false},
{"2", 2, true},
{"2", 3, false},
{"2", max, false},
{"*", 0, false}, // Contains("*") is always false, use Dynamic() instead
{"*", 1, false},
{"*", 2, false},
{"*", 3, false},
{"*", max, false},
{"1:*", 0, false},
{"1:*", 1, true},
{"1:*", max, true},
{"2:4", 0, false},
{"2:4", 1, false},
{"2:4", 2, true},
{"2:4", 3, true},
{"2:4", 4, true},
{"2:4", 5, false},
{"2:4", max, false},
{"2,4", 0, false},
{"2,4", 1, false},
{"2,4", 2, true},
{"2,4", 3, false},
{"2,4", 4, true},
{"2,4", 5, false},
{"2,4", max, false},
{"2:4,6", 0, false},
{"2:4,6", 1, false},
{"2:4,6", 2, true},
{"2:4,6", 3, true},
{"2:4,6", 4, true},
{"2:4,6", 5, false},
{"2:4,6", 6, true},
{"2:4,6", 7, false},
{"2,4:6", 0, false},
{"2,4:6", 1, false},
{"2,4:6", 2, true},
{"2,4:6", 3, false},
{"2,4:6", 4, true},
{"2,4:6", 5, true},
{"2,4:6", 6, true},
{"2,4:6", 7, false},
{"2,4,6", 0, false},
{"2,4,6", 1, false},
{"2,4,6", 2, true},
{"2,4,6", 3, false},
{"2,4,6", 4, true},
{"2,4,6", 5, false},
{"2,4,6", 6, true},
{"2,4,6", 7, false},
{"1,3:5,7,9:*", 0, false},
{"1,3:5,7,9:*", 1, true},
{"1,3:5,7,9:*", 2, false},
{"1,3:5,7,9:*", 3, true},
{"1,3:5,7,9:*", 4, true},
{"1,3:5,7,9:*", 5, true},
{"1,3:5,7,9:*", 6, false},
{"1,3:5,7,9:*", 7, true},
{"1,3:5,7,9:*", 8, false},
{"1,3:5,7,9:*", 9, true},
{"1,3:5,7,9:*", 10, true},
{"1,3:5,7,9:*", max, true},
{"1,3:5,7,9,42", 0, false},
{"1,3:5,7,9,42", 1, true},
{"1,3:5,7,9,42", 2, false},
{"1,3:5,7,9,42", 3, true},
{"1,3:5,7,9,42", 4, true},
{"1,3:5,7,9,42", 5, true},
{"1,3:5,7,9,42", 6, false},
{"1,3:5,7,9,42", 7, true},
{"1,3:5,7,9,42", 8, false},
{"1,3:5,7,9,42", 9, true},
{"1,3:5,7,9,42", 10, false},
{"1,3:5,7,9,42", 41, false},
{"1,3:5,7,9,42", 42, true},
{"1,3:5,7,9,42", 43, false},
{"1,3:5,7,9,42", max, false},
{"1,3:5,7,9,42,*", 0, false},
{"1,3:5,7,9,42,*", 1, true},
{"1,3:5,7,9,42,*", 2, false},
{"1,3:5,7,9,42,*", 3, true},
{"1,3:5,7,9,42,*", 4, true},
{"1,3:5,7,9,42,*", 5, true},
{"1,3:5,7,9,42,*", 6, false},
{"1,3:5,7,9,42,*", 7, true},
{"1,3:5,7,9,42,*", 8, false},
{"1,3:5,7,9,42,*", 9, true},
{"1,3:5,7,9,42,*", 10, false},
{"1,3:5,7,9,42,*", 41, false},
{"1,3:5,7,9,42,*", 42, true},
{"1,3:5,7,9,42,*", 43, false},
{"1,3:5,7,9,42,*", max, false},
{"1,3:5,7,9,42,60:70,100:*", 0, false},
{"1,3:5,7,9,42,60:70,100:*", 1, true},
{"1,3:5,7,9,42,60:70,100:*", 2, false},
{"1,3:5,7,9,42,60:70,100:*", 3, true},
{"1,3:5,7,9,42,60:70,100:*", 4, true},
{"1,3:5,7,9,42,60:70,100:*", 5, true},
{"1,3:5,7,9,42,60:70,100:*", 6, false},
{"1,3:5,7,9,42,60:70,100:*", 7, true},
{"1,3:5,7,9,42,60:70,100:*", 8, false},
{"1,3:5,7,9,42,60:70,100:*", 9, true},
{"1,3:5,7,9,42,60:70,100:*", 10, false},
{"1,3:5,7,9,42,60:70,100:*", 41, false},
{"1,3:5,7,9,42,60:70,100:*", 42, true},
{"1,3:5,7,9,42,60:70,100:*", 43, false},
{"1,3:5,7,9,42,60:70,100:*", 59, false},
{"1,3:5,7,9,42,60:70,100:*", 60, true},
{"1,3:5,7,9,42,60:70,100:*", 65, true},
{"1,3:5,7,9,42,60:70,100:*", 70, true},
{"1,3:5,7,9,42,60:70,100:*", 71, false},
{"1,3:5,7,9,42,60:70,100:*", 99, false},
{"1,3:5,7,9,42,60:70,100:*", 100, true},
{"1,3:5,7,9,42,60:70,100:*", 1000, true},
{"1,3:5,7,9,42,60:70,100:*", max, true},
}
for _, test := range tests {
s, _ := ParseSet(test.s)
checkNumSet(s, t)
if s.Contains(test.q) != test.contains {
t.Errorf("%q.Contains(%v) expected %v", test.s, test.q, test.contains)
}
if str := s.String(); str != test.s {
t.Errorf("%q.String() expected %q; got %q", test.s, test.s, str)
}
testEmpty := len(test.s) == 0
if (len(s) == 0) != testEmpty {
t.Errorf("%q.Empty() expected %v", test.s, testEmpty)
}
testDynamic := !testEmpty && test.s[len(test.s)-1] == '*'
if s.Dynamic() != testDynamic {
t.Errorf("%q.Dynamic() expected %v", test.s, testDynamic)
}
}
}
func TestParseNumSet(t *testing.T) {
tests := []struct {
in string
out string
}{
{"1,1", "1"},
{"1,2", "1:2"},
{"1,3", "1,3"},
{"1,*", "1,*"},
{"1,1,1", "1"},
{"1,1,2", "1:2"},
{"1,1:2", "1:2"},
{"1,1,3", "1,3"},
{"1,1:3", "1:3"},
{"1,2,2", "1:2"},
{"1,2,3", "1:3"},
{"1,2:3", "1:3"},
{"1,2,4", "1:2,4"},
{"1,3,3", "1,3"},
{"1,3,4", "1,3:4"},
{"1,3:4", "1,3:4"},
{"1,3,5", "1,3,5"},
{"1,3:5", "1,3:5"},
{"1:3,5", "1:3,5"},
{"1:5,3", "1:5"},
{"1,2,3,4", "1:4"},
{"1,2,4,5", "1:2,4:5"},
{"1,2,4:5", "1:2,4:5"},
{"1:2,4:5", "1:2,4:5"},
{"1,2,3,4,5", "1:5"},
{"1,2:3,4:5", "1:5"},
{"1,2,4,5,7,9", "1:2,4:5,7,9"},
{"1,2,4,5,7:9", "1:2,4:5,7:9"},
{"1:2,4:5,7:9", "1:2,4:5,7:9"},
{"1,2,4,5,7,8,9", "1:2,4:5,7:9"},
{"1:2,4:5,7,8,9", "1:2,4:5,7:9"},
{"3,5:10,15:20", "3,5:10,15:20"},
{"4,5:10,15:20", "4:10,15:20"},
{"5,5:10,15:20", "5:10,15:20"},
{"7,5:10,15:20", "5:10,15:20"},
{"10,5:10,15:20", "5:10,15:20"},
{"11,5:10,15:20", "5:11,15:20"},
{"12,5:10,15:20", "5:10,12,15:20"},
{"14,5:10,15:20", "5:10,14:20"},
{"17,5:10,15:20", "5:10,15:20"},
{"21,5:10,15:20", "5:10,15:21"},
{"22,5:10,15:20", "5:10,15:20,22"},
{"*,5:10,15:20", "5:10,15:20,*"},
{"1:3,5:10,15:20", "1:3,5:10,15:20"},
{"1:4,5:10,15:20", "1:10,15:20"},
{"1:8,5:10,15:20", "1:10,15:20"},
{"1:13,5:10,15:20", "1:13,15:20"},
{"1:14,5:10,15:20", "1:20"},
{"7:17,5:10,15:20", "5:20"},
{"11:14,5:10,15:20", "5:20"},
{"12,13,5:10,15:20", "5:10,12:13,15:20"},
{"12:13,5:10,15:20", "5:10,12:13,15:20"},
{"12:14,5:10,15:20", "5:10,12:20"},
{"11:13,5:10,15:20", "5:13,15:20"},
{"11,12,13,14,5:10,15:20", "5:20"},
{"1:*,5:10,15:20", "1:*"},
{"4:*,5:10,15:20", "4:*"},
{"6:*,5:10,15:20", "5:*"},
{"12:*,5:10,15:20", "5:10,12:*"},
{"19:*,5:10,15:20", "5:10,15:*"},
{"5:8,6,7:10,15,16,17,18:20,19,21:*", "5:10,15:*"},
{"4:13,1,5,10,15,20", "1,4:13,15,20"},
{"4:14,1,5,10,15,20", "1,4:15,20"},
{"4:15,1,5,10,15,20", "1,4:15,20"},
{"4:16,1,5,10,15,20", "1,4:16,20"},
{"4:17,1,5,10,15,20", "1,4:17,20"},
{"4:18,1,5,10,15,20", "1,4:18,20"},
{"4:19,1,5,10,15,20", "1,4:20"},
{"4:20,1,5,10,15,20", "1,4:20"},
{"4:21,1,5,10,15,20", "1,4:21"},
{"4:*,1,5,10,15,20", "1,4:*"},
{"1,3,5,7,9,11,13,15,17,19", "1,3,5,7,9,11,13,15,17,19"},
{"1,3,5,7,9,11:13,15,17,19", "1,3,5,7,9,11:13,15,17,19"},
{"1,3,5,7,9:11,13:15,17,19", "1,3,5,7,9:11,13:15,17,19"},
{"1,3,5,7:9,11:13,15:17,19", "1,3,5,7:9,11:13,15:17,19"},
{"1,3,5,7,9,11,13,15,17,19,*", "1,3,5,7,9,11,13,15,17,19,*"},
{"1,3,5,7,9,11,13,15,17,19:*", "1,3,5,7,9,11,13,15,17,19:*"},
{"1:20,3,5,7,9,11,13,15,17,19,*", "1:20,*"},
{"1:20,3,5,7,9,11,13,15,17,19:*", "1:*"},
{"4294967295,*", "4294967295,*"},
{"1,4294967295,*", "1,4294967295,*"},
{"1:4294967295,*", "1:4294967295,*"},
{"1,4294967295:*", "1,4294967295:*"},
{"1:*,4294967295", "1:*"},
{"1:*,4294967295:*", "1:*"},
{"1:4294967295,4294967295:*", "1:*"},
}
prng := rand.New(rand.NewSource(19860201))
done := make(map[string]bool)
permute := func(in string) string {
v := strings.Split(in, ",")
r := make([]string, len(v))
// Try to find a permutation that hasn't been checked already
for i := 0; i < 50; i++ {
for i, j := range prng.Perm(len(v)) {
r[i] = v[j]
}
if s := strings.Join(r, ","); !done[s] {
done[s] = true
return s
}
}
return ""
}
for _, test := range tests {
for i := 0; i < 100 && test.in != ""; i++ {
s, err := ParseSet(test.in)
if err != nil {
t.Errorf("Add(%q) unexpected error; %v", test.in, err)
i = 100
}
checkNumSet(s, t)
if out := s.String(); out != test.out {
t.Errorf("%q.String() expected %q; got %q", test.in, test.out, out)
i = 100
}
test.in = permute(test.in)
}
}
}
func TestNumSetAddNumRangeSet(t *testing.T) {
type num []uint32
tests := []struct {
num num
rng Range
set string
out string
}{
{num{5}, Range{1, 3}, "1:2,5,7:13,15,17:*", "1:3,5,7:13,15,17:*"},
{num{5}, Range{3, 1}, "2:3,7:13,15,17:*", "1:3,5,7:13,15,17:*"},
{num{15}, Range{17, 0}, "1:3,5,7:13", "1:3,5,7:13,15,17:*"},
{num{15}, Range{0, 17}, "1:3,5,7:13", "1:3,5,7:13,15,17:*"},
{num{1, 3, 5, 7, 9, 11, 0}, Range{8, 13}, "2,15,17:*", "1:3,5,7:13,15,17:*"},
{num{5, 1, 7, 3, 9, 0, 11}, Range{8, 13}, "2,15,17:*", "1:3,5,7:13,15,17:*"},
{num{5, 1, 7, 3, 9, 0, 11}, Range{13, 8}, "2,15,17:*", "1:3,5,7:13,15,17:*"},
}
for _, test := range tests {
other, _ := ParseSet(test.set)
var s Set
s.AddNum(test.num...)
checkNumSet(s, t)
s.AddRange(test.rng.Start, test.rng.Stop)
checkNumSet(s, t)
s.AddSet(other)
checkNumSet(s, t)
if out := s.String(); out != test.out {
t.Errorf("(%v + %v + %q).String() expected %q; got %q", test.num, test.rng, test.set, test.out, out)
}
}
}

View File

@@ -0,0 +1,654 @@
package imapwire
import (
"bufio"
"fmt"
"io"
"strconv"
"strings"
"unicode"
"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/internal/imapnum"
"github.com/emersion/go-imap/v2/internal/utf7"
)
// This limits the max list nesting depth to prevent stack overflow.
const maxListDepth = 1000
// IsAtomChar returns true if ch is an ATOM-CHAR.
func IsAtomChar(ch byte) bool {
switch ch {
case '(', ')', '{', ' ', '%', '*', '"', '\\', ']':
return false
default:
return !unicode.IsControl(rune(ch))
}
}
// Is non-empty char
func isAStringChar(ch byte) bool {
return IsAtomChar(ch) || ch == ']'
}
// DecoderExpectError is an error due to the Decoder.Expect family of methods.
type DecoderExpectError struct {
Message string
}
func (err *DecoderExpectError) Error() string {
return fmt.Sprintf("imapwire: %v", err.Message)
}
// A Decoder reads IMAP data.
//
// There are multiple families of methods:
//
// - Methods directly named after IMAP grammar elements attempt to decode
// said element, and return false if it's another element.
// - "Expect" methods do the same, but set the decoder error (see Err) on
// failure.
type Decoder struct {
// CheckBufferedLiteralFunc is called when a literal is about to be decoded
// and needs to be fully buffered in memory.
CheckBufferedLiteralFunc func(size int64, nonSync bool) error
// MaxSize defines a maximum number of bytes to be read from the input.
// Literals are ignored.
MaxSize int64
r *bufio.Reader
side ConnSide
err error
literal bool
crlf bool
listDepth int
readBytes int64
}
// NewDecoder creates a new decoder.
func NewDecoder(r *bufio.Reader, side ConnSide) *Decoder {
return &Decoder{r: r, side: side}
}
func (dec *Decoder) mustUnreadByte() {
if err := dec.r.UnreadByte(); err != nil {
panic(fmt.Errorf("imapwire: failed to unread byte: %v", err))
}
dec.readBytes--
}
// Err returns the decoder error, if any.
func (dec *Decoder) Err() error {
return dec.err
}
func (dec *Decoder) returnErr(err error) bool {
if err == nil {
return true
}
if dec.err == nil {
dec.err = err
}
return false
}
func (dec *Decoder) readByte() (byte, bool) {
if dec.MaxSize > 0 && dec.readBytes > dec.MaxSize {
return 0, dec.returnErr(fmt.Errorf("imapwire: max size exceeded"))
}
dec.crlf = false
if dec.literal {
return 0, dec.returnErr(fmt.Errorf("imapwire: cannot decode while a literal is open"))
}
b, err := dec.r.ReadByte()
if err != nil {
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
return b, dec.returnErr(err)
}
dec.readBytes++
return b, true
}
func (dec *Decoder) acceptByte(want byte) bool {
got, ok := dec.readByte()
if !ok {
return false
} else if got != want {
dec.mustUnreadByte()
return false
}
return true
}
// EOF returns true if end-of-file is reached.
func (dec *Decoder) EOF() bool {
_, err := dec.r.ReadByte()
if err == io.EOF {
return true
} else if err != nil {
return dec.returnErr(err)
}
dec.mustUnreadByte()
return false
}
// Expect sets the decoder error if ok is false.
func (dec *Decoder) Expect(ok bool, name string) bool {
if !ok {
msg := fmt.Sprintf("expected %v", name)
if dec.r.Buffered() > 0 {
b, _ := dec.r.Peek(1)
msg += fmt.Sprintf(", got %q", b)
}
return dec.returnErr(&DecoderExpectError{Message: msg})
}
return true
}
func (dec *Decoder) SP() bool {
if dec.acceptByte(' ') {
// https://github.com/emersion/go-imap/issues/571
b, ok := dec.readByte()
if !ok {
return false
}
dec.mustUnreadByte()
return b != '\r' && b != '\n'
}
// Special case: SP is optional if the next field is a parenthesized list
b, ok := dec.readByte()
if !ok {
return false
}
dec.mustUnreadByte()
return b == '('
}
func (dec *Decoder) ExpectSP() bool {
return dec.Expect(dec.SP(), "SP")
}
func (dec *Decoder) CRLF() bool {
dec.acceptByte(' ') // https://github.com/emersion/go-imap/issues/540
dec.acceptByte('\r') // be liberal in what we receive and accept lone LF
if !dec.acceptByte('\n') {
return false
}
dec.crlf = true
return true
}
func (dec *Decoder) ExpectCRLF() bool {
return dec.Expect(dec.CRLF(), "CRLF")
}
func (dec *Decoder) Func(ptr *string, valid func(ch byte) bool) bool {
var sb strings.Builder
for {
b, ok := dec.readByte()
if !ok {
return false
}
if !valid(b) {
dec.mustUnreadByte()
break
}
sb.WriteByte(b)
}
if sb.Len() == 0 {
return false
}
*ptr = sb.String()
return true
}
func (dec *Decoder) Atom(ptr *string) bool {
return dec.Func(ptr, IsAtomChar)
}
func (dec *Decoder) ExpectAtom(ptr *string) bool {
return dec.Expect(dec.Atom(ptr), "atom")
}
func (dec *Decoder) ExpectNIL() bool {
var s string
return dec.ExpectAtom(&s) && dec.Expect(s == "NIL", "NIL")
}
func (dec *Decoder) Special(b byte) bool {
return dec.acceptByte(b)
}
func (dec *Decoder) ExpectSpecial(b byte) bool {
return dec.Expect(dec.Special(b), fmt.Sprintf("'%v'", string(b)))
}
func (dec *Decoder) Text(ptr *string) bool {
var sb strings.Builder
for {
b, ok := dec.readByte()
if !ok {
return false
} else if b == '\r' || b == '\n' {
dec.mustUnreadByte()
break
}
sb.WriteByte(b)
}
if sb.Len() == 0 {
return false
}
*ptr = sb.String()
return true
}
func (dec *Decoder) ExpectText(ptr *string) bool {
return dec.Expect(dec.Text(ptr), "text")
}
func (dec *Decoder) DiscardUntilByte(untilCh byte) {
for {
ch, ok := dec.readByte()
if !ok {
return
} else if ch == untilCh {
dec.mustUnreadByte()
return
}
}
}
func (dec *Decoder) DiscardLine() {
if dec.crlf {
return
}
var text string
dec.Text(&text)
dec.CRLF()
}
func (dec *Decoder) DiscardValue() bool {
var s string
if dec.String(&s) {
return true
}
isList, err := dec.List(func() error {
if !dec.DiscardValue() {
return dec.Err()
}
return nil
})
if err != nil {
return false
} else if isList {
return true
}
if dec.Atom(&s) {
return true
}
dec.Expect(false, "value")
return false
}
func (dec *Decoder) numberStr() (s string, ok bool) {
var sb strings.Builder
for {
ch, ok := dec.readByte()
if !ok {
return "", false
} else if ch < '0' || ch > '9' {
dec.mustUnreadByte()
break
}
sb.WriteByte(ch)
}
if sb.Len() == 0 {
return "", false
}
return sb.String(), true
}
func (dec *Decoder) Number(ptr *uint32) bool {
s, ok := dec.numberStr()
if !ok {
return false
}
v64, err := strconv.ParseUint(s, 10, 32)
if err != nil {
return false // can happen on overflow
}
*ptr = uint32(v64)
return true
}
func (dec *Decoder) ExpectNumber(ptr *uint32) bool {
return dec.Expect(dec.Number(ptr), "number")
}
func (dec *Decoder) ExpectBodyFldOctets(ptr *uint32) bool {
// Workaround: some servers incorrectly return "-1" for the body structure
// size. See:
// https://github.com/emersion/go-imap/issues/534
if dec.acceptByte('-') {
*ptr = 0
return dec.Expect(dec.acceptByte('1'), "-1 (body-fld-octets workaround)")
}
return dec.ExpectNumber(ptr)
}
func (dec *Decoder) Number64(ptr *int64) bool {
s, ok := dec.numberStr()
if !ok {
return false
}
v, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return false // can happen on overflow
}
*ptr = v
return true
}
func (dec *Decoder) ExpectNumber64(ptr *int64) bool {
return dec.Expect(dec.Number64(ptr), "number64")
}
func (dec *Decoder) ModSeq(ptr *uint64) bool {
s, ok := dec.numberStr()
if !ok {
return false
}
v, err := strconv.ParseUint(s, 10, 64)
if err != nil {
return false // can happen on overflow
}
*ptr = v
return true
}
func (dec *Decoder) ExpectModSeq(ptr *uint64) bool {
return dec.Expect(dec.ModSeq(ptr), "mod-sequence-value")
}
func (dec *Decoder) Quoted(ptr *string) bool {
if !dec.Special('"') {
return false
}
var sb strings.Builder
for {
ch, ok := dec.readByte()
if !ok {
return false
}
if ch == '"' {
break
}
if ch == '\\' {
ch, ok = dec.readByte()
if !ok {
return false
}
}
sb.WriteByte(ch)
}
*ptr = sb.String()
return true
}
func (dec *Decoder) ExpectAString(ptr *string) bool {
if dec.Quoted(ptr) {
return true
}
if dec.Literal(ptr) {
return true
}
// We cannot do dec.Atom(ptr) here because sometimes mailbox names are unquoted,
// and they can contain special characters like `]`.
return dec.Expect(dec.Func(ptr, isAStringChar), "ASTRING-CHAR")
}
func (dec *Decoder) String(ptr *string) bool {
return dec.Quoted(ptr) || dec.Literal(ptr)
}
func (dec *Decoder) ExpectString(ptr *string) bool {
return dec.Expect(dec.String(ptr), "string")
}
func (dec *Decoder) ExpectNString(ptr *string) bool {
var s string
if dec.Atom(&s) {
if !dec.Expect(s == "NIL", "nstring") {
return false
}
*ptr = ""
return true
}
return dec.ExpectString(ptr)
}
func (dec *Decoder) ExpectNStringReader() (lit *LiteralReader, nonSync, ok bool) {
var s string
if dec.Atom(&s) {
if !dec.Expect(s == "NIL", "nstring") {
return nil, false, false
}
return nil, true, true
}
// TODO: read quoted string as a string instead of buffering
if dec.Quoted(&s) {
return newLiteralReaderFromString(s), true, true
}
if lit, nonSync, ok = dec.LiteralReader(); ok {
return lit, nonSync, true
} else {
return nil, false, dec.Expect(false, "nstring")
}
}
func (dec *Decoder) List(f func() error) (isList bool, err error) {
if !dec.Special('(') {
return false, nil
}
if dec.Special(')') {
return true, nil
}
dec.listDepth++
defer func() {
dec.listDepth--
}()
if dec.listDepth >= maxListDepth {
return false, fmt.Errorf("imapwire: exceeded max depth")
}
for {
if err := f(); err != nil {
return true, err
}
if dec.Special(')') {
return true, nil
} else if !dec.ExpectSP() {
return true, dec.Err()
}
}
}
func (dec *Decoder) ExpectList(f func() error) error {
isList, err := dec.List(f)
if err != nil {
return err
} else if !dec.Expect(isList, "(") {
return dec.Err()
}
return nil
}
func (dec *Decoder) ExpectNList(f func() error) error {
var s string
if dec.Atom(&s) {
if !dec.Expect(s == "NIL", "NIL") {
return dec.Err()
}
return nil
}
return dec.ExpectList(f)
}
func (dec *Decoder) ExpectMailbox(ptr *string) bool {
var name string
if !dec.ExpectAString(&name) {
return false
}
if strings.EqualFold(name, "INBOX") {
*ptr = "INBOX"
return true
}
name, err := utf7.Decode(name)
if err == nil {
*ptr = name
}
return dec.returnErr(err)
}
func (dec *Decoder) ExpectUID(ptr *imap.UID) bool {
var num uint32
if !dec.ExpectNumber(&num) {
return false
}
*ptr = imap.UID(num)
return true
}
func (dec *Decoder) ExpectNumSet(kind NumKind, ptr *imap.NumSet) bool {
if dec.Special('$') {
*ptr = imap.SearchRes()
return true
}
var s string
if !dec.Expect(dec.Func(&s, isNumSetChar), "sequence-set") {
return false
}
numSet, err := imapnum.ParseSet(s)
if err != nil {
return dec.returnErr(err)
}
switch kind {
case NumKindSeq:
*ptr = seqSetFromNumSet(numSet)
case NumKindUID:
*ptr = uidSetFromNumSet(numSet)
}
return true
}
func (dec *Decoder) ExpectUIDSet(ptr *imap.UIDSet) bool {
var numSet imap.NumSet
ok := dec.ExpectNumSet(NumKindUID, &numSet)
if ok {
*ptr = numSet.(imap.UIDSet)
}
return ok
}
func isNumSetChar(ch byte) bool {
return ch == '*' || IsAtomChar(ch)
}
func (dec *Decoder) Literal(ptr *string) bool {
lit, nonSync, ok := dec.LiteralReader()
if !ok {
return false
}
if dec.CheckBufferedLiteralFunc != nil {
if err := dec.CheckBufferedLiteralFunc(lit.Size(), nonSync); err != nil {
lit.cancel()
return false
}
}
var sb strings.Builder
_, err := io.Copy(&sb, lit)
if err == nil {
*ptr = sb.String()
}
return dec.returnErr(err)
}
func (dec *Decoder) LiteralReader() (lit *LiteralReader, nonSync, ok bool) {
if !dec.Special('{') {
return nil, false, false
}
var size int64
if !dec.ExpectNumber64(&size) {
return nil, false, false
}
if dec.side == ConnSideServer {
nonSync = dec.acceptByte('+')
}
if !dec.ExpectSpecial('}') || !dec.ExpectCRLF() {
return nil, false, false
}
dec.literal = true
lit = &LiteralReader{
dec: dec,
size: size,
r: io.LimitReader(dec.r, size),
}
return lit, nonSync, true
}
func (dec *Decoder) ExpectLiteralReader() (lit *LiteralReader, nonSync bool, err error) {
lit, nonSync, ok := dec.LiteralReader()
if !dec.Expect(ok, "literal") {
return nil, false, dec.Err()
}
return lit, nonSync, nil
}
type LiteralReader struct {
dec *Decoder
size int64
r io.Reader
}
func newLiteralReaderFromString(s string) *LiteralReader {
return &LiteralReader{
size: int64(len(s)),
r: strings.NewReader(s),
}
}
func (lit *LiteralReader) Size() int64 {
return lit.size
}
func (lit *LiteralReader) Read(b []byte) (int, error) {
n, err := lit.r.Read(b)
if err == io.EOF {
lit.cancel()
}
return n, err
}
func (lit *LiteralReader) cancel() {
if lit.dec == nil {
return
}
lit.dec.literal = false
lit.dec = nil
}

View File

@@ -0,0 +1,341 @@
package imapwire
import (
"bufio"
"fmt"
"io"
"strconv"
"strings"
"unicode"
"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/internal/utf7"
)
// An Encoder writes IMAP data.
//
// Most methods don't return an error, instead they defer error handling until
// CRLF is called. These methods return the Encoder so that calls can be
// chained.
type Encoder struct {
// QuotedUTF8 allows raw UTF-8 in quoted strings. This requires IMAP4rev2
// to be available, or UTF8=ACCEPT to be enabled.
QuotedUTF8 bool
// LiteralMinus enables non-synchronizing literals for short payloads.
// This requires IMAP4rev2 or LITERAL-. This is only meaningful for
// clients.
LiteralMinus bool
// LiteralPlus enables non-synchronizing literals for all payloads. This
// requires LITERAL+. This is only meaningful for clients.
LiteralPlus bool
// NewContinuationRequest creates a new continuation request. This is only
// meaningful for clients.
NewContinuationRequest func() *ContinuationRequest
w *bufio.Writer
side ConnSide
err error
literal bool
}
// NewEncoder creates a new encoder.
func NewEncoder(w *bufio.Writer, side ConnSide) *Encoder {
return &Encoder{w: w, side: side}
}
func (enc *Encoder) setErr(err error) {
if enc.err == nil {
enc.err = err
}
}
func (enc *Encoder) writeString(s string) *Encoder {
if enc.err != nil {
return enc
}
if enc.literal {
enc.err = fmt.Errorf("imapwire: cannot encode while a literal is open")
return enc
}
if _, err := enc.w.WriteString(s); err != nil {
enc.err = err
}
return enc
}
// CRLF writes a "\r\n" sequence and flushes the buffered writer.
func (enc *Encoder) CRLF() error {
enc.writeString("\r\n")
if enc.err != nil {
return enc.err
}
return enc.w.Flush()
}
func (enc *Encoder) Atom(s string) *Encoder {
return enc.writeString(s)
}
func (enc *Encoder) SP() *Encoder {
return enc.writeString(" ")
}
func (enc *Encoder) Special(ch byte) *Encoder {
return enc.writeString(string(ch))
}
func (enc *Encoder) Quoted(s string) *Encoder {
var sb strings.Builder
sb.Grow(2 + len(s))
sb.WriteByte('"')
for i := 0; i < len(s); i++ {
ch := s[i]
if ch == '"' || ch == '\\' {
sb.WriteByte('\\')
}
sb.WriteByte(ch)
}
sb.WriteByte('"')
return enc.writeString(sb.String())
}
func (enc *Encoder) String(s string) *Encoder {
if !enc.validQuoted(s) {
enc.stringLiteral(s)
return enc
}
return enc.Quoted(s)
}
func (enc *Encoder) validQuoted(s string) bool {
if len(s) > 4096 {
return false
}
for i := 0; i < len(s); i++ {
ch := s[i]
// NUL, CR and LF are never valid
switch ch {
case 0, '\r', '\n':
return false
}
if !enc.QuotedUTF8 && ch > unicode.MaxASCII {
return false
}
}
return true
}
func (enc *Encoder) stringLiteral(s string) {
var sync *ContinuationRequest
if enc.side == ConnSideClient && (!enc.LiteralMinus || len(s) > 4096) && !enc.LiteralPlus {
if enc.NewContinuationRequest != nil {
sync = enc.NewContinuationRequest()
}
if sync == nil {
enc.setErr(fmt.Errorf("imapwire: cannot send synchronizing literal"))
return
}
}
wc := enc.Literal(int64(len(s)), sync)
_, writeErr := io.WriteString(wc, s)
closeErr := wc.Close()
if writeErr != nil {
enc.setErr(writeErr)
} else if closeErr != nil {
enc.setErr(closeErr)
}
}
func (enc *Encoder) Mailbox(name string) *Encoder {
if strings.EqualFold(name, "INBOX") {
return enc.Atom("INBOX")
} else {
if enc.QuotedUTF8 {
name = utf7.Escape(name)
} else {
name = utf7.Encode(name)
}
return enc.String(name)
}
}
func (enc *Encoder) NumSet(numSet imap.NumSet) *Encoder {
s := numSet.String()
if s == "" {
enc.setErr(fmt.Errorf("imapwire: cannot encode empty sequence set"))
return enc
}
return enc.writeString(s)
}
func (enc *Encoder) Flag(flag imap.Flag) *Encoder {
if flag != "\\*" && !isValidFlag(string(flag)) {
enc.setErr(fmt.Errorf("imapwire: invalid flag %q", flag))
return enc
}
return enc.writeString(string(flag))
}
func (enc *Encoder) MailboxAttr(attr imap.MailboxAttr) *Encoder {
if !strings.HasPrefix(string(attr), "\\") || !isValidFlag(string(attr)) {
enc.setErr(fmt.Errorf("imapwire: invalid mailbox attribute %q", attr))
return enc
}
return enc.writeString(string(attr))
}
// isValidFlag checks whether the provided string satisfies
// flag-keyword / flag-extension.
func isValidFlag(s string) bool {
for i := 0; i < len(s); i++ {
ch := s[i]
if ch == '\\' {
if i != 0 {
return false
}
} else {
if !IsAtomChar(ch) {
return false
}
}
}
return len(s) > 0
}
func (enc *Encoder) Number(v uint32) *Encoder {
return enc.writeString(strconv.FormatUint(uint64(v), 10))
}
func (enc *Encoder) Number64(v int64) *Encoder {
// TODO: disallow negative values
return enc.writeString(strconv.FormatInt(v, 10))
}
func (enc *Encoder) ModSeq(v uint64) *Encoder {
// TODO: disallow zero values
return enc.writeString(strconv.FormatUint(v, 10))
}
// List writes a parenthesized list.
func (enc *Encoder) List(n int, f func(i int)) *Encoder {
enc.Special('(')
for i := 0; i < n; i++ {
if i > 0 {
enc.SP()
}
f(i)
}
enc.Special(')')
return enc
}
func (enc *Encoder) BeginList() *ListEncoder {
enc.Special('(')
return &ListEncoder{enc: enc}
}
func (enc *Encoder) NIL() *Encoder {
return enc.Atom("NIL")
}
func (enc *Encoder) Text(s string) *Encoder {
return enc.writeString(s)
}
func (enc *Encoder) UID(uid imap.UID) *Encoder {
return enc.Number(uint32(uid))
}
// Literal writes a literal.
//
// The caller must write exactly size bytes to the returned writer.
//
// If sync is non-nil, the literal is synchronizing: the encoder will wait for
// nil to be sent to the channel before writing the literal data. If an error
// is sent to the channel, the literal will be cancelled.
func (enc *Encoder) Literal(size int64, sync *ContinuationRequest) io.WriteCloser {
if sync != nil && enc.side == ConnSideServer {
panic("imapwire: sync must be nil on a server-side Encoder.Literal")
}
// TODO: literal8
enc.writeString("{")
enc.Number64(size)
if sync == nil && enc.side == ConnSideClient {
enc.writeString("+")
}
enc.writeString("}")
if sync == nil {
enc.writeString("\r\n")
} else {
if err := enc.CRLF(); err != nil {
return errorWriter{err}
}
if _, err := sync.Wait(); err != nil {
enc.setErr(err)
return errorWriter{err}
}
}
enc.literal = true
return &literalWriter{
enc: enc,
n: size,
}
}
type errorWriter struct {
err error
}
func (ew errorWriter) Write(b []byte) (int, error) {
return 0, ew.err
}
func (ew errorWriter) Close() error {
return ew.err
}
type literalWriter struct {
enc *Encoder
n int64
}
func (lw *literalWriter) Write(b []byte) (int, error) {
if lw.n-int64(len(b)) < 0 {
return 0, fmt.Errorf("wrote too many bytes in literal")
}
n, err := lw.enc.w.Write(b)
lw.n -= int64(n)
return n, err
}
func (lw *literalWriter) Close() error {
lw.enc.literal = false
if lw.n != 0 {
return fmt.Errorf("wrote too few bytes in literal (%v remaining)", lw.n)
}
return nil
}
type ListEncoder struct {
enc *Encoder
n int
}
func (le *ListEncoder) Item() *Encoder {
if le.n > 0 {
le.enc.SP()
}
le.n++
return le.enc
}
func (le *ListEncoder) End() {
le.enc.Special(')')
le.enc = nil
}

View File

@@ -0,0 +1,47 @@
// Package imapwire implements the IMAP wire protocol.
//
// The IMAP wire protocol is defined in RFC 9051 section 4.
package imapwire
import (
"fmt"
)
// ConnSide describes the side of a connection: client or server.
type ConnSide int
const (
ConnSideClient ConnSide = 1 + iota
ConnSideServer
)
// ContinuationRequest is a continuation request.
//
// The sender must call either Done or Cancel. The receiver must call Wait.
type ContinuationRequest struct {
done chan struct{}
err error
text string
}
func NewContinuationRequest() *ContinuationRequest {
return &ContinuationRequest{done: make(chan struct{})}
}
func (cont *ContinuationRequest) Cancel(err error) {
if err == nil {
err = fmt.Errorf("imapwire: continuation request cancelled")
}
cont.err = err
close(cont.done)
}
func (cont *ContinuationRequest) Done(text string) {
cont.text = text
close(cont.done)
}
func (cont *ContinuationRequest) Wait() (string, error) {
<-cont.done
return cont.text, cont.err
}

39
internal/imapwire/num.go Normal file
View File

@@ -0,0 +1,39 @@
package imapwire
import (
"unsafe"
"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/internal/imapnum"
)
type NumKind int
const (
NumKindSeq NumKind = iota + 1
NumKindUID
)
func seqSetFromNumSet(s imapnum.Set) imap.SeqSet {
return *(*imap.SeqSet)(unsafe.Pointer(&s))
}
func uidSetFromNumSet(s imapnum.Set) imap.UIDSet {
return *(*imap.UIDSet)(unsafe.Pointer(&s))
}
func NumSetKind(numSet imap.NumSet) NumKind {
switch numSet.(type) {
case imap.SeqSet:
return NumKindSeq
case imap.UIDSet:
return NumKindUID
default:
panic("imap: invalid NumSet type")
}
}
func ParseSeqSet(s string) (imap.SeqSet, error) {
numSet, err := imapnum.ParseSet(s)
return seqSetFromNumSet(numSet), err
}

188
internal/internal.go Normal file
View File

@@ -0,0 +1,188 @@
package internal
import (
"fmt"
"strings"
"sync"
"time"
"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/internal/imapwire"
)
const (
DateTimeLayout = "_2-Jan-2006 15:04:05 -0700"
DateLayout = "2-Jan-2006"
)
const FlagRecent imap.Flag = "\\Recent" // removed in IMAP4rev2
func DecodeDateTime(dec *imapwire.Decoder) (time.Time, error) {
var s string
if !dec.Quoted(&s) {
return time.Time{}, nil
}
t, err := time.Parse(DateTimeLayout, s)
if err != nil {
return time.Time{}, fmt.Errorf("in date-time: %v", err) // TODO: use imapwire.DecodeExpectError?
}
return t, err
}
func ExpectDateTime(dec *imapwire.Decoder) (time.Time, error) {
t, err := DecodeDateTime(dec)
if err != nil {
return t, err
}
if !dec.Expect(!t.IsZero(), "date-time") {
return t, dec.Err()
}
return t, nil
}
func ExpectDate(dec *imapwire.Decoder) (time.Time, error) {
var s string
if !dec.ExpectAString(&s) {
return time.Time{}, dec.Err()
}
t, err := time.Parse(DateLayout, s)
if err != nil {
return time.Time{}, fmt.Errorf("in date: %v", err) // use imapwire.DecodeExpectError?
}
return t, nil
}
func ExpectFlagList(dec *imapwire.Decoder) ([]imap.Flag, error) {
var flags []imap.Flag
err := dec.ExpectList(func() error {
// Some servers start the list with a space, so we need to skip it
// https://github.com/emersion/go-imap/pull/633
dec.SP()
flag, err := ExpectFlag(dec)
if err != nil {
return err
}
flags = append(flags, flag)
return nil
})
return flags, err
}
func ExpectCap(dec *imapwire.Decoder) (imap.Cap, error) {
var name string
if !dec.ExpectAtom(&name) {
return "", dec.Err()
}
return canonicalCap(name), nil
}
func ExpectFlag(dec *imapwire.Decoder) (imap.Flag, error) {
isSystem := dec.Special('\\')
if isSystem && dec.Special('*') {
return imap.FlagWildcard, nil // flag-perm
}
var name string
if !dec.ExpectAtom(&name) {
return "", fmt.Errorf("in flag: %w", dec.Err())
}
if isSystem {
name = "\\" + name
}
return canonicalFlag(name), nil
}
func ExpectMailboxAttrList(dec *imapwire.Decoder) ([]imap.MailboxAttr, error) {
var attrs []imap.MailboxAttr
err := dec.ExpectList(func() error {
attr, err := ExpectMailboxAttr(dec)
if err != nil {
return err
}
attrs = append(attrs, attr)
return nil
})
return attrs, err
}
func ExpectMailboxAttr(dec *imapwire.Decoder) (imap.MailboxAttr, error) {
flag, err := ExpectFlag(dec)
return canonicalMailboxAttr(string(flag)), err
}
var (
canonOnce sync.Once
canonFlag map[string]imap.Flag
canonMailboxAttr map[string]imap.MailboxAttr
)
func canonInit() {
flags := []imap.Flag{
imap.FlagSeen,
imap.FlagAnswered,
imap.FlagFlagged,
imap.FlagDeleted,
imap.FlagDraft,
imap.FlagForwarded,
imap.FlagMDNSent,
imap.FlagJunk,
imap.FlagNotJunk,
imap.FlagPhishing,
imap.FlagImportant,
}
mailboxAttrs := []imap.MailboxAttr{
imap.MailboxAttrNonExistent,
imap.MailboxAttrNoInferiors,
imap.MailboxAttrNoSelect,
imap.MailboxAttrHasChildren,
imap.MailboxAttrHasNoChildren,
imap.MailboxAttrMarked,
imap.MailboxAttrUnmarked,
imap.MailboxAttrSubscribed,
imap.MailboxAttrRemote,
imap.MailboxAttrAll,
imap.MailboxAttrArchive,
imap.MailboxAttrDrafts,
imap.MailboxAttrFlagged,
imap.MailboxAttrJunk,
imap.MailboxAttrSent,
imap.MailboxAttrTrash,
imap.MailboxAttrImportant,
}
canonFlag = make(map[string]imap.Flag)
for _, flag := range flags {
canonFlag[strings.ToLower(string(flag))] = flag
}
canonMailboxAttr = make(map[string]imap.MailboxAttr)
for _, attr := range mailboxAttrs {
canonMailboxAttr[strings.ToLower(string(attr))] = attr
}
}
func canonicalFlag(s string) imap.Flag {
canonOnce.Do(canonInit)
if flag, ok := canonFlag[strings.ToLower(s)]; ok {
return flag
}
return imap.Flag(s)
}
func canonicalMailboxAttr(s string) imap.MailboxAttr {
canonOnce.Do(canonInit)
if attr, ok := canonMailboxAttr[strings.ToLower(s)]; ok {
return attr
}
return imap.MailboxAttr(s)
}
func canonicalCap(s string) imap.Cap {
// Only two caps are not fully uppercase
for _, cap := range []imap.Cap{imap.CapIMAP4rev1, imap.CapIMAP4rev2} {
if strings.EqualFold(s, string(cap)) {
return cap
}
}
return imap.Cap(strings.ToUpper(s))
}

23
internal/sasl.go Normal file
View File

@@ -0,0 +1,23 @@
package internal
import (
"encoding/base64"
)
func EncodeSASL(b []byte) string {
if len(b) == 0 {
return "="
} else {
return base64.StdEncoding.EncodeToString(b)
}
}
func DecodeSASL(s string) ([]byte, error) {
if s == "=" {
// go-sasl treats nil as no challenge/response, so return a non-nil
// empty byte slice
return []byte{}, nil
} else {
return base64.StdEncoding.DecodeString(s)
}
}

118
internal/utf7/decoder.go Normal file
View File

@@ -0,0 +1,118 @@
package utf7
import (
"errors"
"strings"
"unicode/utf16"
"unicode/utf8"
)
// ErrInvalidUTF7 means that a decoder encountered invalid UTF-7.
var ErrInvalidUTF7 = errors.New("utf7: invalid UTF-7")
// Decode decodes a string encoded with modified UTF-7.
//
// Note, raw UTF-8 is accepted.
func Decode(src string) (string, error) {
if !utf8.ValidString(src) {
return "", errors.New("invalid UTF-8")
}
var sb strings.Builder
sb.Grow(len(src))
ascii := true
for i := 0; i < len(src); i++ {
ch := src[i]
if ch < min || (ch > max && ch < utf8.RuneSelf) {
// Illegal code point in ASCII mode. Note, UTF-8 codepoints are
// always allowed.
return "", ErrInvalidUTF7
}
if ch != '&' {
sb.WriteByte(ch)
ascii = true
continue
}
// Find the end of the Base64 or "&-" segment
start := i + 1
for i++; i < len(src) && src[i] != '-'; i++ {
if src[i] == '\r' || src[i] == '\n' { // base64 package ignores CR and LF
return "", ErrInvalidUTF7
}
}
if i == len(src) { // Implicit shift ("&...")
return "", ErrInvalidUTF7
}
if i == start { // Escape sequence "&-"
sb.WriteByte('&')
ascii = true
} else { // Control or non-ASCII code points in base64
if !ascii { // Null shift ("&...-&...-")
return "", ErrInvalidUTF7
}
b := decode([]byte(src[start:i]))
if len(b) == 0 { // Bad encoding
return "", ErrInvalidUTF7
}
sb.Write(b)
ascii = false
}
}
return sb.String(), nil
}
// Extracts UTF-16-BE bytes from base64 data and converts them to UTF-8.
// A nil slice is returned if the encoding is invalid.
func decode(b64 []byte) []byte {
var b []byte
// Allocate a single block of memory large enough to store the Base64 data
// (if padding is required), UTF-16-BE bytes, and decoded UTF-8 bytes.
// Since a 2-byte UTF-16 sequence may expand into a 3-byte UTF-8 sequence,
// double the space allocation for UTF-8.
if n := len(b64); b64[n-1] == '=' {
return nil
} else if n&3 == 0 {
b = make([]byte, b64Enc.DecodedLen(n)*3)
} else {
n += 4 - n&3
b = make([]byte, n+b64Enc.DecodedLen(n)*3)
copy(b[copy(b, b64):n], []byte("=="))
b64, b = b[:n], b[n:]
}
// Decode Base64 into the first 1/3rd of b
n, err := b64Enc.Decode(b, b64)
if err != nil || n&1 == 1 {
return nil
}
// Decode UTF-16-BE into the remaining 2/3rds of b
b, s := b[:n], b[n:]
j := 0
for i := 0; i < n; i += 2 {
r := rune(b[i])<<8 | rune(b[i+1])
if utf16.IsSurrogate(r) {
if i += 2; i == n {
return nil
}
r2 := rune(b[i])<<8 | rune(b[i+1])
if r = utf16.DecodeRune(r, r2); r == utf8.RuneError {
return nil
}
} else if min <= r && r <= max {
return nil
}
j += utf8.EncodeRune(s[j:], r)
}
return s[:j]
}

View File

@@ -0,0 +1,115 @@
package utf7_test
import (
"strings"
"testing"
"github.com/emersion/go-imap/v2/internal/utf7"
)
var decode = []struct {
in string
out string
ok bool
}{
// Basics (the inverse test on encode checks other valid inputs)
{"", "", true},
{"abc", "abc", true},
{"&-abc", "&abc", true},
{"abc&-", "abc&", true},
{"a&-b&-c", "a&b&c", true},
{"&ABk-", "\x19", true},
{"&AB8-", "\x1F", true},
{"ABk-", "ABk-", true},
{"&-,&-&AP8-&-", "&,&\u00FF&", true},
{"&-&-,&AP8-&-", "&&,\u00FF&", true},
{"abc &- &AP8A,wD,- &- xyz", "abc & \u00FF\u00FF\u00FF & xyz", true},
// Illegal code point in ASCII
{"\x00", "", false},
{"\x1F", "", false},
{"abc\n", "", false},
{"abc\x7Fxyz", "", false},
// Invalid UTF-8
{"\xc3\x28", "", false},
{"\xe2\x82\x28", "", false},
// Invalid Base64 alphabet
{"&/+8-", "", false},
{"&*-", "", false},
{"&ZeVnLIqe -", "", false},
// CR and LF in Base64
{"&ZeVnLIqe\r\n-", "", false},
{"&ZeVnLIqe\r\n\r\n-", "", false},
{"&ZeVn\r\n\r\nLIqe-", "", false},
// Padding not stripped
{"&AAAAHw=-", "", false},
{"&AAAAHw==-", "", false},
{"&AAAAHwB,AIA=-", "", false},
{"&AAAAHwB,AIA==-", "", false},
// One byte short
{"&2A-", "", false},
{"&2ADc-", "", false},
{"&AAAAHwB,A-", "", false},
{"&AAAAHwB,A=-", "", false},
{"&AAAAHwB,A==-", "", false},
{"&AAAAHwB,A===-", "", false},
{"&AAAAHwB,AI-", "", false},
{"&AAAAHwB,AI=-", "", false},
{"&AAAAHwB,AI==-", "", false},
// Implicit shift
{"&", "", false},
{"&Jjo", "", false},
{"Jjo&", "", false},
{"&Jjo&", "", false},
{"&Jjo!", "", false},
{"&Jjo+", "", false},
{"abc&Jjo", "", false},
// Null shift
{"&AGE-&Jjo-", "", false},
{"&U,BTFw-&ZeVnLIqe-", "", false},
// Long input with Base64 at the end
{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa &2D3eCg- &2D3eCw- &2D3eDg-",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \U0001f60a \U0001f60b \U0001f60e", true},
// Long input in Base64 between short ASCII
{"00000000000000000000 &MEIwQjBCMEIwQjBCMEIwQjBCMEIwQjBCMEIwQjBCMEIwQjBCMEIwQjBCMEIwQjBCMEIwQjBCMEIwQjBCMEIwQjBCMEIwQjBCMEI- 00000000000000000000",
"00000000000000000000 " + strings.Repeat("\U00003042", 37) + " 00000000000000000000", true},
// ASCII in Base64
{"&AGE-", "", false}, // "a"
{"&ACY-", "", false}, // "&"
{"&AGgAZQBsAGwAbw-", "", false}, // "hello"
{"&JjoAIQ-", "", false}, // "\u263a!"
// Bad surrogate
{"&2AA-", "", false}, // U+D800
{"&2AD-", "", false}, // U+D800
{"&3AA-", "", false}, // U+DC00
{"&2AAAQQ-", "", false}, // U+D800 'A'
{"&2AD,,w-", "", false}, // U+D800 U+FFFF
{"&3ADYAA-", "", false}, // U+DC00 U+D800
}
func TestDecoder(t *testing.T) {
for _, test := range decode {
out, err := utf7.Decode(test.in)
if out != test.out {
t.Errorf("UTF7Decode(%+q) expected %+q; got %+q", test.in, test.out, out)
}
if test.ok {
if err != nil {
t.Errorf("UTF7Decode(%+q) unexpected error; %v", test.in, err)
}
} else if err == nil {
t.Errorf("UTF7Decode(%+q) expected error", test.in)
}
}
}

88
internal/utf7/encoder.go Normal file
View File

@@ -0,0 +1,88 @@
package utf7
import (
"strings"
"unicode/utf16"
"unicode/utf8"
)
// Encode encodes a string with modified UTF-7.
func Encode(src string) string {
var sb strings.Builder
sb.Grow(len(src))
for i := 0; i < len(src); {
ch := src[i]
if min <= ch && ch <= max {
sb.WriteByte(ch)
if ch == '&' {
sb.WriteByte('-')
}
i++
} else {
start := i
// Find the next printable ASCII code point
i++
for i < len(src) && (src[i] < min || src[i] > max) {
i++
}
sb.Write(encode([]byte(src[start:i])))
}
}
return sb.String()
}
// Converts string s from UTF-8 to UTF-16-BE, encodes the result as base64,
// removes the padding, and adds UTF-7 shifts.
func encode(s []byte) []byte {
// len(s) is sufficient for UTF-8 to UTF-16 conversion if there are no
// control code points (see table below).
b := make([]byte, 0, len(s)+4)
for len(s) > 0 {
r, size := utf8.DecodeRune(s)
if r > utf8.MaxRune {
r, size = utf8.RuneError, 1 // Bug fix (issue 3785)
}
s = s[size:]
if r1, r2 := utf16.EncodeRune(r); r1 != utf8.RuneError {
b = append(b, byte(r1>>8), byte(r1))
r = r2
}
b = append(b, byte(r>>8), byte(r))
}
// Encode as base64
n := b64Enc.EncodedLen(len(b)) + 2
b64 := make([]byte, n)
b64Enc.Encode(b64[1:], b)
// Strip padding
n -= 2 - (len(b)+2)%3
b64 = b64[:n]
// Add UTF-7 shifts
b64[0] = '&'
b64[n-1] = '-'
return b64
}
// Escape passes through raw UTF-8 as-is and escapes the special UTF-7 marker
// (the ampersand character).
func Escape(src string) string {
var sb strings.Builder
sb.Grow(len(src))
for _, ch := range src {
sb.WriteRune(ch)
if ch == '&' {
sb.WriteByte('-')
}
}
return sb.String()
}

View File

@@ -0,0 +1,124 @@
package utf7_test
import (
"testing"
"github.com/emersion/go-imap/v2/internal/utf7"
)
var encode = []struct {
in string
out string
ok bool
}{
// Printable ASCII
{"", "", true},
{"a", "a", true},
{"ab", "ab", true},
{"-", "-", true},
{"&", "&-", true},
{"&&", "&-&-", true},
{"&&&-&", "&-&-&--&-", true},
{"-&*&-", "-&-*&--", true},
{"a&b", "a&-b", true},
{"a&", "a&-", true},
{"&b", "&-b", true},
{"-a&", "-a&-", true},
{"&b-", "&-b-", true},
// Unicode range
{"\u0000", "&AAA-", true},
{"\n", "&AAo-", true},
{"\r", "&AA0-", true},
{"\u001F", "&AB8-", true},
{"\u0020", " ", true},
{"\u0025", "%", true},
{"\u0026", "&-", true},
{"\u0027", "'", true},
{"\u007E", "~", true},
{"\u007F", "&AH8-", true},
{"\u0080", "&AIA-", true},
{"\u00FF", "&AP8-", true},
{"\u07FF", "&B,8-", true},
{"\u0800", "&CAA-", true},
{"\uFFEF", "&,+8-", true},
{"\uFFFF", "&,,8-", true},
{"\U00010000", "&2ADcAA-", true},
{"\U0010FFFF", "&2,,f,w-", true},
// Padding
{"\x00\x1F", "&AAAAHw-", true}, // 2
{"\x00\x1F\x7F", "&AAAAHwB,-", true}, // 0
{"\x00\x1F\x7F\u0080", "&AAAAHwB,AIA-", true}, // 1
{"\x00\x1F\x7F\u0080\u00FF", "&AAAAHwB,AIAA,w-", true}, // 2
// Mix
{"a\x00", "a&AAA-", true},
{"\x00a", "&AAA-a", true},
{"&\x00", "&-&AAA-", true},
{"\x00&", "&AAA-&-", true},
{"a\x00&", "a&AAA-&-", true},
{"a&\x00", "a&-&AAA-", true},
{"&a\x00", "&-a&AAA-", true},
{"&\x00a", "&-&AAA-a", true},
{"\x00&a", "&AAA-&-a", true},
{"\x00a&", "&AAA-a&-", true},
{"ab&\uFFFF", "ab&-&,,8-", true},
{"a&b\uFFFF", "a&-b&,,8-", true},
{"&ab\uFFFF", "&-ab&,,8-", true},
{"ab\uFFFF&", "ab&,,8-&-", true},
{"a\uFFFFb&", "a&,,8-b&-", true},
{"\uFFFFab&", "&,,8-ab&-", true},
{"\x20\x25&\x27\x7E", " %&-'~", true},
{"\x1F\x20&\x7E\x7F", "&AB8- &-~&AH8-", true},
{"&\x00\x19\x7F\u0080", "&-&AAAAGQB,AIA-", true},
{"\x00&\x19\x7F\u0080", "&AAA-&-&ABkAfwCA-", true},
{"\x00\x19&\x7F\u0080", "&AAAAGQ-&-&AH8AgA-", true},
{"\x00\x19\x7F&\u0080", "&AAAAGQB,-&-&AIA-", true},
{"\x00\x19\x7F\u0080&", "&AAAAGQB,AIA-&-", true},
{"&\x00\x1F\x7F\u0080", "&-&AAAAHwB,AIA-", true},
{"\x00&\x1F\x7F\u0080", "&AAA-&-&AB8AfwCA-", true},
{"\x00\x1F&\x7F\u0080", "&AAAAHw-&-&AH8AgA-", true},
{"\x00\x1F\x7F&\u0080", "&AAAAHwB,-&-&AIA-", true},
{"\x00\x1F\x7F\u0080&", "&AAAAHwB,AIA-&-", true},
// Russian
{"\u041C\u0430\u043A\u0441\u0438\u043C \u0425\u0438\u0442\u0440\u043E\u0432",
"&BBwEMAQ6BEEEOAQ8- &BCUEOARCBEAEPgQy-", true},
// RFC 3501
{"~peter/mail/\u53F0\u5317/\u65E5\u672C\u8A9E", "~peter/mail/&U,BTFw-/&ZeVnLIqe-", true},
{"~peter/mail/\u53F0\u5317/\u65E5\u672C\u8A9E", "~peter/mail/&U,BTFw-/&ZeVnLIqe-", true},
{"\u263A!", "&Jjo-!", true},
{"\u53F0\u5317\u65E5\u672C\u8A9E", "&U,BTF2XlZyyKng-", true},
// RFC 2152 (modified)
{"\u0041\u2262\u0391\u002E", "A&ImIDkQ-.", true},
{"Hi Mom -\u263A-!", "Hi Mom -&Jjo--!", true},
{"\u65E5\u672C\u8A9E", "&ZeVnLIqe-", true},
// 8->16 and 24->16 byte UTF-8 to UTF-16 conversion
{"\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007", "&AAAAAQACAAMABAAFAAYABw-", true},
{"\u0800\u0801\u0802\u0803\u0804\u0805\u0806\u0807", "&CAAIAQgCCAMIBAgFCAYIBw-", true},
// Invalid UTF-8 (bad bytes are converted to U+FFFD)
{"\xC0\x80", "&,,3,,Q-", false}, // U+0000
{"\xF4\x90\x80\x80", "&,,3,,f,9,,0-", false}, // U+110000
{"\xF7\xBF\xBF\xBF", "&,,3,,f,9,,0-", false}, // U+1FFFFF
{"\xF8\x88\x80\x80\x80", "&,,3,,f,9,,3,,Q-", false}, // U+200000
{"\xF4\x8F\xBF\x3F", "&,,3,,f,9-?", false}, // U+10FFFF (bad byte)
{"\xF4\x8F\xBF", "&,,3,,f,9-", false}, // U+10FFFF (short)
{"\xF4\x8F", "&,,3,,Q-", false},
{"\xF4", "&,,0-", false},
{"\x00\xF4\x00", "&AAD,,QAA-", false},
}
func TestEncoder(t *testing.T) {
for _, test := range encode {
out := utf7.Encode(test.in)
if out != test.out {
t.Errorf("UTF7Encode(%+q) expected %+q; got %+q", test.in, test.out, out)
}
}
}

13
internal/utf7/utf7.go Normal file
View File

@@ -0,0 +1,13 @@
// Package utf7 implements modified UTF-7 encoding defined in RFC 3501 section 5.1.3
package utf7
import (
"encoding/base64"
)
const (
min = 0x20 // Minimum self-representing UTF-7 value
max = 0x7E // Maximum self-representing UTF-7 value
)
var b64Enc = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,")