mirror of
https://github.com/emersion/go-imap
synced 2026-07-02 12:42:31 +00:00
An email from outlook.com contained an embedded part with Content-ID and filename starting with a long sequence of digits: 279351849188E3D84D6DCC5B@namprd05.prod.outlook.com When sent unquoted, the Apple Mail IMAP client attempts to parse this initially as an integer. When it overflows a uint32 parsing is stopped, so the message contents are never displayed. Quoting all parameter values is safe and avoids this. Indeed the IMAP RFC includes examples of far more aggressive quoting of almost all strings from a MIME message. That may be worth considering in the future
603 lines
15 KiB
Go
603 lines
15 KiB
Go
package imap
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestCanonicalFlag(t *testing.T) {
|
|
if got := CanonicalFlag("\\SEEN"); got != SeenFlag {
|
|
t.Errorf("Invalid canonical flag: expected %q but got %q", SeenFlag, got)
|
|
}
|
|
|
|
if got := CanonicalFlag("Junk"); got != "junk" {
|
|
t.Errorf("Invalid canonical flag: expected %q but got %q", "junk", got)
|
|
}
|
|
}
|
|
|
|
func TestNewMessage(t *testing.T) {
|
|
msg := NewMessage(42, []FetchItem{FetchBodyStructure, FetchFlags})
|
|
|
|
expected := &Message{
|
|
SeqNum: 42,
|
|
Items: map[FetchItem]interface{}{FetchBodyStructure: nil, FetchFlags: nil},
|
|
Body: make(map[*BodySectionName]Literal),
|
|
itemsOrder: []FetchItem{FetchBodyStructure, FetchFlags},
|
|
}
|
|
|
|
if !reflect.DeepEqual(expected, msg) {
|
|
t.Errorf("Invalid message: expected \n%+v\n but got \n%+v", expected, msg)
|
|
}
|
|
}
|
|
|
|
func formatFields(fields []interface{}) (string, error) {
|
|
b := &bytes.Buffer{}
|
|
w := NewWriter(b)
|
|
|
|
if err := w.writeList(fields); err != nil {
|
|
return "", fmt.Errorf("Cannot format \n%+v\n got error: \n%v", fields, err)
|
|
}
|
|
|
|
return b.String(), nil
|
|
}
|
|
|
|
var messageTests = []struct {
|
|
message *Message
|
|
fields []interface{}
|
|
}{
|
|
{
|
|
message: &Message{
|
|
Items: map[FetchItem]interface{}{
|
|
FetchEnvelope: nil,
|
|
FetchBody: nil,
|
|
FetchFlags: nil,
|
|
FetchRFC822Size: nil,
|
|
FetchUid: nil,
|
|
},
|
|
Body: map[*BodySectionName]Literal{},
|
|
Envelope: envelopeTests[0].envelope,
|
|
BodyStructure: bodyStructureTests[0].bodyStructure,
|
|
Flags: []string{SeenFlag, AnsweredFlag},
|
|
Size: 4242,
|
|
Uid: 2424,
|
|
itemsOrder: []FetchItem{FetchEnvelope, FetchBody, FetchFlags, FetchRFC822Size, FetchUid},
|
|
},
|
|
fields: []interface{}{
|
|
"ENVELOPE", envelopeTests[0].fields,
|
|
"BODY", bodyStructureTests[0].fields,
|
|
"FLAGS", []interface{}{Atom(SeenFlag), Atom(AnsweredFlag)},
|
|
"RFC822.SIZE", "4242",
|
|
"UID", "2424",
|
|
},
|
|
},
|
|
}
|
|
|
|
func TestMessage_Parse(t *testing.T) {
|
|
for i, test := range messageTests {
|
|
m := &Message{}
|
|
if err := m.Parse(test.fields); err != nil {
|
|
t.Errorf("Cannot parse message for #%v: %v", i, err)
|
|
} else if !reflect.DeepEqual(m, test.message) {
|
|
t.Errorf("Invalid parsed message for #%v: got \n%+v\n but expected \n%+v", i, m, test.message)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMessage_Format(t *testing.T) {
|
|
for i, test := range messageTests {
|
|
fields := test.message.Format()
|
|
|
|
got, err := formatFields(fields)
|
|
if err != nil {
|
|
t.Error(err)
|
|
continue
|
|
}
|
|
|
|
expected, _ := formatFields(test.fields)
|
|
|
|
if got != expected {
|
|
t.Errorf("Invalid message fields for #%v: got \n%v\n but expected \n%v", i, got, expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
var bodySectionNameTests = []struct {
|
|
raw string
|
|
parsed *BodySectionName
|
|
formatted string
|
|
}{
|
|
{
|
|
raw: "BODY[]",
|
|
parsed: &BodySectionName{BodyPartName: BodyPartName{}},
|
|
},
|
|
{
|
|
raw: "RFC822",
|
|
parsed: &BodySectionName{BodyPartName: BodyPartName{}},
|
|
formatted: "BODY[]",
|
|
},
|
|
{
|
|
raw: "BODY[HEADER]",
|
|
parsed: &BodySectionName{BodyPartName: BodyPartName{Specifier: HeaderSpecifier}},
|
|
},
|
|
{
|
|
raw: "BODY.PEEK[]",
|
|
parsed: &BodySectionName{BodyPartName: BodyPartName{}, Peek: true},
|
|
},
|
|
{
|
|
raw: "BODY[TEXT]",
|
|
parsed: &BodySectionName{BodyPartName: BodyPartName{Specifier: TextSpecifier}},
|
|
},
|
|
{
|
|
raw: "RFC822.TEXT",
|
|
parsed: &BodySectionName{BodyPartName: BodyPartName{Specifier: TextSpecifier}},
|
|
formatted: "BODY[TEXT]",
|
|
},
|
|
{
|
|
raw: "RFC822.HEADER",
|
|
parsed: &BodySectionName{BodyPartName: BodyPartName{Specifier: HeaderSpecifier}, Peek: true},
|
|
formatted: "BODY.PEEK[HEADER]",
|
|
},
|
|
{
|
|
raw: "BODY[]<0.512>",
|
|
parsed: &BodySectionName{BodyPartName: BodyPartName{}, Partial: []int{0, 512}},
|
|
},
|
|
{
|
|
raw: "BODY[]<512>",
|
|
parsed: &BodySectionName{BodyPartName: BodyPartName{}, Partial: []int{512}},
|
|
},
|
|
{
|
|
raw: "BODY[1.2.3]",
|
|
parsed: &BodySectionName{BodyPartName: BodyPartName{Path: []int{1, 2, 3}}},
|
|
},
|
|
{
|
|
raw: "BODY[1.2.3.HEADER]",
|
|
parsed: &BodySectionName{BodyPartName: BodyPartName{Specifier: HeaderSpecifier, Path: []int{1, 2, 3}}},
|
|
},
|
|
{
|
|
raw: "BODY[5.MIME]",
|
|
parsed: &BodySectionName{BodyPartName: BodyPartName{Specifier: MIMESpecifier, Path: []int{5}}},
|
|
},
|
|
{
|
|
raw: "BODY[HEADER.FIELDS (From To)]",
|
|
parsed: &BodySectionName{BodyPartName: BodyPartName{Specifier: HeaderSpecifier, Fields: []string{"From", "To"}}},
|
|
},
|
|
{
|
|
raw: "BODY[HEADER.FIELDS.NOT (Content-Id)]",
|
|
parsed: &BodySectionName{BodyPartName: BodyPartName{Specifier: HeaderSpecifier, Fields: []string{"Content-Id"}, NotFields: true}},
|
|
},
|
|
}
|
|
|
|
func TestNewBodySectionName(t *testing.T) {
|
|
for i, test := range bodySectionNameTests {
|
|
bsn, err := ParseBodySectionName(FetchItem(test.raw))
|
|
if err != nil {
|
|
t.Errorf("Cannot parse #%v: %v", i, err)
|
|
continue
|
|
}
|
|
|
|
if !reflect.DeepEqual(bsn.BodyPartName, test.parsed.BodyPartName) {
|
|
t.Errorf("Invalid body part name for #%v: %#+v", i, bsn.BodyPartName)
|
|
} else if bsn.Peek != test.parsed.Peek {
|
|
t.Errorf("Invalid peek value for #%v: %#+v", i, bsn.Peek)
|
|
} else if !reflect.DeepEqual(bsn.Partial, test.parsed.Partial) {
|
|
t.Errorf("Invalid partial for #%v: %#+v", i, bsn.Partial)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBodySectionName_String(t *testing.T) {
|
|
for i, test := range bodySectionNameTests {
|
|
s := string(test.parsed.FetchItem())
|
|
|
|
expected := test.formatted
|
|
if expected == "" {
|
|
expected = test.raw
|
|
}
|
|
|
|
if expected != s {
|
|
t.Errorf("Invalid body section name for #%v: got %v but expected %v", i, s, expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBodySectionName_ExtractPartial(t *testing.T) {
|
|
tests := []struct {
|
|
bsn string
|
|
whole string
|
|
partial string
|
|
}{
|
|
{
|
|
bsn: "BODY[]",
|
|
whole: "Hello World!",
|
|
partial: "Hello World!",
|
|
},
|
|
{
|
|
bsn: "BODY[]<6.5>",
|
|
whole: "Hello World!",
|
|
partial: "World",
|
|
},
|
|
{
|
|
bsn: "BODY[]<6.1000>",
|
|
whole: "Hello World!",
|
|
partial: "World!",
|
|
},
|
|
{
|
|
bsn: "BODY[]<0.1>",
|
|
whole: "Hello World!",
|
|
partial: "H",
|
|
},
|
|
{
|
|
bsn: "BODY[]<1000.2000>",
|
|
whole: "Hello World!",
|
|
partial: "",
|
|
},
|
|
}
|
|
|
|
for i, test := range tests {
|
|
bsn, err := ParseBodySectionName(FetchItem(test.bsn))
|
|
if err != nil {
|
|
t.Errorf("Cannot parse body section name #%v: %v", i, err)
|
|
continue
|
|
}
|
|
|
|
partial := string(bsn.ExtractPartial([]byte(test.whole)))
|
|
if partial != test.partial {
|
|
t.Errorf("Invalid partial for #%v: got %v but expected %v", i, partial, test.partial)
|
|
}
|
|
}
|
|
}
|
|
|
|
var t = time.Date(2009, time.November, 10, 23, 0, 0, 0, time.FixedZone("", -6*60*60))
|
|
|
|
var envelopeTests = []struct {
|
|
envelope *Envelope
|
|
fields []interface{}
|
|
}{
|
|
{
|
|
envelope: &Envelope{
|
|
Date: t,
|
|
Subject: "Hello World!",
|
|
From: []*Address{addrTests[0].addr},
|
|
Sender: []*Address{},
|
|
ReplyTo: []*Address{},
|
|
To: []*Address{},
|
|
Cc: []*Address{},
|
|
Bcc: []*Address{},
|
|
InReplyTo: "42@example.org",
|
|
MessageId: "43@example.org",
|
|
},
|
|
fields: []interface{}{
|
|
"Tue, 10 Nov 2009 23:00:00 -0600",
|
|
"Hello World!",
|
|
[]interface{}{addrTests[0].fields},
|
|
[]interface{}{},
|
|
[]interface{}{},
|
|
[]interface{}{},
|
|
[]interface{}{},
|
|
[]interface{}{},
|
|
"42@example.org",
|
|
"43@example.org",
|
|
},
|
|
},
|
|
}
|
|
|
|
func TestEnvelope_Parse(t *testing.T) {
|
|
for i, test := range envelopeTests {
|
|
e := &Envelope{}
|
|
if err := e.Parse(test.fields); err != nil {
|
|
t.Error("Error parsing envelope:", err)
|
|
} else if !reflect.DeepEqual(e, test.envelope) {
|
|
t.Errorf("Invalid envelope for #%v: got %v but expected %v", i, e, test.envelope)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEnvelope_Parse_literal(t *testing.T) {
|
|
subject := "Hello World!"
|
|
l := bytes.NewBufferString(subject)
|
|
fields := []interface{}{
|
|
"Tue, 10 Nov 2009 23:00:00 -0600",
|
|
l,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
"42@example.org",
|
|
"43@example.org",
|
|
}
|
|
|
|
e := &Envelope{}
|
|
if err := e.Parse(fields); err != nil {
|
|
t.Error("Error parsing envelope:", err)
|
|
} else if e.Subject != subject {
|
|
t.Errorf("Invalid envelope subject: got %v but expected %v", e.Subject, subject)
|
|
}
|
|
}
|
|
|
|
func TestEnvelope_Format(t *testing.T) {
|
|
for i, test := range envelopeTests {
|
|
fields := test.envelope.Format()
|
|
|
|
got, err := formatFields(fields)
|
|
if err != nil {
|
|
t.Error(err)
|
|
continue
|
|
}
|
|
|
|
expected, _ := formatFields(test.fields)
|
|
|
|
if got != expected {
|
|
t.Errorf("Invalid envelope fields for #%v: got %v but expected %v", i, got, expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
var addrTests = []struct {
|
|
fields []interface{}
|
|
addr *Address
|
|
}{
|
|
{
|
|
fields: []interface{}{"The NSA", nil, "root", "nsa.gov"},
|
|
addr: &Address{
|
|
PersonalName: "The NSA",
|
|
MailboxName: "root",
|
|
HostName: "nsa.gov",
|
|
},
|
|
},
|
|
}
|
|
|
|
func TestAddress_Parse(t *testing.T) {
|
|
for i, test := range addrTests {
|
|
addr := &Address{}
|
|
|
|
if err := addr.Parse(test.fields); err != nil {
|
|
t.Error("Error parsing address:", err)
|
|
} else if !reflect.DeepEqual(addr, test.addr) {
|
|
t.Errorf("Invalid address for #%v: got %v but expected %v", i, addr, test.addr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAddress_Format(t *testing.T) {
|
|
for i, test := range addrTests {
|
|
fields := test.addr.Format()
|
|
if !reflect.DeepEqual(fields, test.fields) {
|
|
t.Errorf("Invalid address fields for #%v: got %v but expected %v", i, fields, test.fields)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAddressList(t *testing.T) {
|
|
fields := make([]interface{}, len(addrTests))
|
|
addrs := make([]*Address, len(addrTests))
|
|
for i, test := range addrTests {
|
|
fields[i] = test.fields
|
|
addrs[i] = test.addr
|
|
}
|
|
|
|
gotAddrs := ParseAddressList(fields)
|
|
if !reflect.DeepEqual(gotAddrs, addrs) {
|
|
t.Error("Invalid address list: got", gotAddrs, "but expected", addrs)
|
|
}
|
|
|
|
gotFields := FormatAddressList(addrs)
|
|
if !reflect.DeepEqual(gotFields, fields) {
|
|
t.Error("Invalid address list fields: got", gotFields, "but expected", fields)
|
|
}
|
|
}
|
|
|
|
var paramsListTest = []struct {
|
|
fields []interface{}
|
|
params map[string]string
|
|
}{
|
|
{
|
|
fields: nil,
|
|
params: map[string]string{},
|
|
},
|
|
{
|
|
fields: []interface{}{"a", Quoted("b")},
|
|
params: map[string]string{"a": "b"},
|
|
},
|
|
}
|
|
|
|
func TestParseParamList(t *testing.T) {
|
|
for i, test := range paramsListTest {
|
|
if params, err := ParseParamList(test.fields); err != nil {
|
|
t.Errorf("Cannot parse params fields for #%v: %v", i, err)
|
|
} else if !reflect.DeepEqual(params, test.params) {
|
|
t.Errorf("Invalid params for #%v: got %v but expected %v", i, params, test.params)
|
|
}
|
|
}
|
|
|
|
// Malformed params lists
|
|
|
|
fields := []interface{}{"cc", []interface{}{"dille"}}
|
|
if params, err := ParseParamList(fields); err == nil {
|
|
t.Error("Parsed invalid params list:", params)
|
|
}
|
|
|
|
fields = []interface{}{"cc"}
|
|
if params, err := ParseParamList(fields); err == nil {
|
|
t.Error("Parsed invalid params list:", params)
|
|
}
|
|
}
|
|
|
|
func TestFormatParamList(t *testing.T) {
|
|
for i, test := range paramsListTest {
|
|
fields := FormatParamList(test.params)
|
|
|
|
if !reflect.DeepEqual(fields, test.fields) {
|
|
t.Errorf("Invalid params fields for #%v: got %v but expected %v", i, fields, test.fields)
|
|
}
|
|
}
|
|
}
|
|
|
|
var bodyStructureTests = []struct {
|
|
fields []interface{}
|
|
bodyStructure *BodyStructure
|
|
}{
|
|
{
|
|
fields: []interface{}{"image", "jpeg", []interface{}{}, "<foo4%25foo1@bar.net>", "A picture of cat", "base64", "4242"},
|
|
bodyStructure: &BodyStructure{
|
|
MIMEType: "image",
|
|
MIMESubType: "jpeg",
|
|
Params: map[string]string{},
|
|
Id: "<foo4%25foo1@bar.net>",
|
|
Description: "A picture of cat",
|
|
Encoding: "base64",
|
|
Size: 4242,
|
|
},
|
|
},
|
|
{
|
|
fields: []interface{}{"text", "plain", []interface{}{"charset", Quoted("utf-8")}, nil, nil, "us-ascii", "42", "2"},
|
|
bodyStructure: &BodyStructure{
|
|
MIMEType: "text",
|
|
MIMESubType: "plain",
|
|
Params: map[string]string{"charset": "utf-8"},
|
|
Encoding: "us-ascii",
|
|
Size: 42,
|
|
Lines: 2,
|
|
},
|
|
},
|
|
{
|
|
fields: []interface{}{
|
|
"message", "rfc822", []interface{}{}, nil, nil, "us-ascii", "42",
|
|
(&Envelope{}).Format(),
|
|
(&BodyStructure{}).Format(),
|
|
"67",
|
|
},
|
|
bodyStructure: &BodyStructure{
|
|
MIMEType: "message",
|
|
MIMESubType: "rfc822",
|
|
Params: map[string]string{},
|
|
Encoding: "us-ascii",
|
|
Size: 42,
|
|
Lines: 67,
|
|
Envelope: &Envelope{
|
|
From: []*Address{},
|
|
Sender: []*Address{},
|
|
ReplyTo: []*Address{},
|
|
To: []*Address{},
|
|
Cc: []*Address{},
|
|
Bcc: []*Address{},
|
|
},
|
|
BodyStructure: &BodyStructure{
|
|
Params: map[string]string{},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
fields: []interface{}{
|
|
"application", "pdf", []interface{}{}, nil, nil, "base64", "4242",
|
|
"e0323a9039add2978bf5b49550572c7c",
|
|
[]interface{}{"attachment", []interface{}{"filename", Quoted("document.pdf")}},
|
|
[]interface{}{"en-US"}, []interface{}{},
|
|
},
|
|
bodyStructure: &BodyStructure{
|
|
MIMEType: "application",
|
|
MIMESubType: "pdf",
|
|
Params: map[string]string{},
|
|
Encoding: "base64",
|
|
Size: 4242,
|
|
Extended: true,
|
|
MD5: "e0323a9039add2978bf5b49550572c7c",
|
|
Disposition: "attachment",
|
|
DispositionParams: map[string]string{"filename": "document.pdf"},
|
|
Language: []string{"en-US"},
|
|
Location: []string{},
|
|
},
|
|
},
|
|
{
|
|
fields: []interface{}{
|
|
[]interface{}{"text", "plain", []interface{}{}, nil, nil, "us-ascii", "87", "22"},
|
|
[]interface{}{"text", "html", []interface{}{}, nil, nil, "us-ascii", "106", "36"},
|
|
"alternative",
|
|
},
|
|
bodyStructure: &BodyStructure{
|
|
MIMEType: "multipart",
|
|
MIMESubType: "alternative",
|
|
Params: map[string]string{},
|
|
Parts: []*BodyStructure{
|
|
{
|
|
MIMEType: "text",
|
|
MIMESubType: "plain",
|
|
Params: map[string]string{},
|
|
Encoding: "us-ascii",
|
|
Size: 87,
|
|
Lines: 22,
|
|
},
|
|
{
|
|
MIMEType: "text",
|
|
MIMESubType: "html",
|
|
Params: map[string]string{},
|
|
Encoding: "us-ascii",
|
|
Size: 106,
|
|
Lines: 36,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
fields: []interface{}{
|
|
[]interface{}{"text", "plain", []interface{}{}, nil, nil, "us-ascii", "87", "22"},
|
|
"alternative", []interface{}{"hello", Quoted("world")},
|
|
[]interface{}{"inline", []interface{}{}},
|
|
[]interface{}{"en-US"}, []interface{}{},
|
|
},
|
|
bodyStructure: &BodyStructure{
|
|
MIMEType: "multipart",
|
|
MIMESubType: "alternative",
|
|
Params: map[string]string{"hello": "world"},
|
|
Parts: []*BodyStructure{
|
|
{
|
|
MIMEType: "text",
|
|
MIMESubType: "plain",
|
|
Params: map[string]string{},
|
|
Encoding: "us-ascii",
|
|
Size: 87,
|
|
Lines: 22,
|
|
},
|
|
},
|
|
Extended: true,
|
|
Disposition: "inline",
|
|
DispositionParams: map[string]string{},
|
|
Language: []string{"en-US"},
|
|
Location: []string{},
|
|
},
|
|
},
|
|
}
|
|
|
|
func TestBodyStructure_Parse(t *testing.T) {
|
|
for i, test := range bodyStructureTests {
|
|
bs := &BodyStructure{}
|
|
|
|
if err := bs.Parse(test.fields); err != nil {
|
|
t.Errorf("Cannot parse #%v: %v", i, err)
|
|
} else if !reflect.DeepEqual(bs, test.bodyStructure) {
|
|
t.Errorf("Invalid body structure for #%v: got \n%+v\n but expected \n%+v", i, bs, test.bodyStructure)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBodyStructure_Format(t *testing.T) {
|
|
for i, test := range bodyStructureTests {
|
|
fields := test.bodyStructure.Format()
|
|
got, err := formatFields(fields)
|
|
if err != nil {
|
|
t.Error(err)
|
|
continue
|
|
}
|
|
|
|
expected, _ := formatFields(test.fields)
|
|
|
|
if got != expected {
|
|
t.Errorf("Invalid body structure fields for #%v: has \n%v\n but expected \n%v", i, got, expected)
|
|
}
|
|
}
|
|
}
|