This is the v1 version, had the v2 before.

This commit is contained in:
2025-05-01 12:02:19 +03:00
parent bcc3f95e8e
commit 0e253ba422
130 changed files with 18061 additions and 2179 deletions

View File

@@ -1,8 +1,8 @@
The MIT License (MIT)
Copyright (c) 2013 The Go-IMAP Authors
Copyright (c) 2016 emersion
Copyright (c) 2016 Proton Technologies AG
Copyright (c) 2023 Simon Ser
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

181
README.md
View File

@@ -1,29 +1,182 @@
# go-imap
[![Go Reference](https://pkg.go.dev/badge/github.com/emersion/go-imap/v2.svg)](https://pkg.go.dev/github.com/emersion/go-imap/v2)
[![godocs.io](https://godocs.io/github.com/emersion/go-imap?status.svg)](https://godocs.io/github.com/emersion/go-imap)
[![builds.sr.ht status](https://builds.sr.ht/~emersion/go-imap/commits/master.svg)](https://builds.sr.ht/~emersion/go-imap/commits/master?)
An [IMAP4rev2] library for Go.
An [IMAP4rev1](https://tools.ietf.org/html/rfc3501) library written in Go. It
can be used to build a client and/or a server.
> **Note**
> This is the README for go-imap v2. This new major version is still in
> development. For go-imap v1, see the [v1 branch].
> This is the README for go-imap v1. go-imap v2 is in development, see the
> [v2 branch](https://github.com/emersion/go-imap/tree/v2) for more details.
## Usage
To add go-imap to your project, run:
### Client [![godocs.io](https://godocs.io/github.com/emersion/go-imap/client?status.svg)](https://godocs.io/github.com/emersion/go-imap/client)
go get github.com/emersion/go-imap/v2
```go
package main
Documentation and examples for the module are available here:
import (
"log"
- [Client docs]
- [Server docs]
"github.com/emersion/go-imap/client"
"github.com/emersion/go-imap"
)
func main() {
log.Println("Connecting to server...")
// Connect to server
c, err := client.DialTLS("mail.example.org:993", nil)
if err != nil {
log.Fatal(err)
}
log.Println("Connected")
// Don't forget to logout
defer c.Logout()
// Login
if err := c.Login("username", "password"); err != nil {
log.Fatal(err)
}
log.Println("Logged in")
// List mailboxes
mailboxes := make(chan *imap.MailboxInfo, 10)
done := make(chan error, 1)
go func () {
done <- c.List("", "*", mailboxes)
}()
log.Println("Mailboxes:")
for m := range mailboxes {
log.Println("* " + m.Name)
}
if err := <-done; err != nil {
log.Fatal(err)
}
// Select INBOX
mbox, err := c.Select("INBOX", false)
if err != nil {
log.Fatal(err)
}
log.Println("Flags for INBOX:", mbox.Flags)
// Get the last 4 messages
from := uint32(1)
to := mbox.Messages
if mbox.Messages > 3 {
// We're using unsigned integers here, only subtract if the result is > 0
from = mbox.Messages - 3
}
seqset := new(imap.SeqSet)
seqset.AddRange(from, to)
messages := make(chan *imap.Message, 10)
done = make(chan error, 1)
go func() {
done <- c.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope}, messages)
}()
log.Println("Last 4 messages:")
for msg := range messages {
log.Println("* " + msg.Envelope.Subject)
}
if err := <-done; err != nil {
log.Fatal(err)
}
log.Println("Done!")
}
```
### Server [![godocs.io](https://godocs.io/github.com/emersion/go-imap/server?status.svg)](https://godocs.io/github.com/emersion/go-imap/server)
```go
package main
import (
"log"
"github.com/emersion/go-imap/server"
"github.com/emersion/go-imap/backend/memory"
)
func main() {
// Create a memory backend
be := memory.New()
// Create a new server
s := server.New(be)
s.Addr = ":1143"
// Since we will use this server for testing only, we can allow plain text
// authentication over unencrypted connections
s.AllowInsecureAuth = true
log.Println("Starting IMAP server at localhost:1143")
if err := s.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
```
You can now use `telnet localhost 1143` to manually connect to the server.
## Extensions
Support for several IMAP extensions is included in go-imap itself. This
includes:
* [APPENDLIMIT](https://tools.ietf.org/html/rfc7889)
* [CHILDREN](https://tools.ietf.org/html/rfc3348)
* [ENABLE](https://tools.ietf.org/html/rfc5161)
* [IDLE](https://tools.ietf.org/html/rfc2177)
* [IMPORTANT](https://tools.ietf.org/html/rfc8457)
* [LITERAL+](https://tools.ietf.org/html/rfc7888)
* [MOVE](https://tools.ietf.org/html/rfc6851)
* [SASL-IR](https://tools.ietf.org/html/rfc4959)
* [SPECIAL-USE](https://tools.ietf.org/html/rfc6154)
* [UNSELECT](https://tools.ietf.org/html/rfc3691)
Support for other extensions is provided via separate packages. See below.
## Extending go-imap
### Extensions
Commands defined in IMAP extensions are available in other packages. See [the
wiki](https://github.com/emersion/go-imap/wiki/Using-extensions#using-client-extensions)
to learn how to use them.
* [COMPRESS](https://github.com/emersion/go-imap-compress)
* [ID](https://github.com/ProtonMail/go-imap-id)
* [METADATA](https://github.com/emersion/go-imap-metadata)
* [NAMESPACE](https://github.com/foxcpp/go-imap-namespace)
* [QUOTA](https://github.com/emersion/go-imap-quota)
* [SORT and THREAD](https://github.com/emersion/go-imap-sortthread)
* [UIDPLUS](https://github.com/emersion/go-imap-uidplus)
### Server backends
* [Memory](https://github.com/emersion/go-imap/tree/master/backend/memory) (for testing)
* [Multi](https://github.com/emersion/go-imap-multi)
* [PGP](https://github.com/emersion/go-imap-pgp)
* [Proxy](https://github.com/emersion/go-imap-proxy)
* [Notmuch](https://github.com/stbenjam/go-imap-notmuch) - Experimental gateway for [Notmuch](https://notmuchmail.org/)
### Related projects
* [go-message](https://github.com/emersion/go-message) - parsing and formatting MIME and mail messages
* [go-msgauth](https://github.com/emersion/go-msgauth) - handle DKIM, DMARC and Authentication-Results
* [go-pgpmail](https://github.com/emersion/go-pgpmail) - decrypting and encrypting mails with OpenPGP
* [go-sasl](https://github.com/emersion/go-sasl) - sending and receiving SASL authentications
* [go-smtp](https://github.com/emersion/go-smtp) - building SMTP clients and servers
## License
MIT
[IMAP4rev2]: https://www.rfc-editor.org/rfc/rfc9051.html
[v1 branch]: https://github.com/emersion/go-imap/tree/v1
[Client docs]: https://pkg.go.dev/github.com/emersion/go-imap/v2/imapclient
[Server docs]: https://pkg.go.dev/github.com/emersion/go-imap/v2/imapserver

29
backend/appendlimit.go Normal file
View File

@@ -0,0 +1,29 @@
package backend
import (
"errors"
)
// An error that should be returned by User.CreateMessage when the message size
// is too big.
var ErrTooBig = errors.New("Message size exceeding limit")
// A backend that supports retrieving per-user message size limits.
type AppendLimitBackend interface {
Backend
// Get the fixed maximum message size in octets that the backend will accept
// when creating a new message. If there is no limit, return nil.
CreateMessageLimit() *uint32
}
// A user that supports retrieving per-user message size limits.
type AppendLimitUser interface {
User
// Get the fixed maximum message size in octets that the backend will accept
// when creating a new message. If there is no limit, return nil.
//
// This overrides the global backend limit.
CreateMessageLimit() *uint32
}

20
backend/backend.go Normal file
View File

@@ -0,0 +1,20 @@
// Package backend defines an IMAP server backend interface.
package backend
import (
"errors"
"github.com/emersion/go-imap"
)
// ErrInvalidCredentials is returned by Backend.Login when a username or a
// password is incorrect.
var ErrInvalidCredentials = errors.New("Invalid credentials")
// Backend is an IMAP server backend. A backend operation always deals with
// users.
type Backend interface {
// Login authenticates a user. If the username or the password is incorrect,
// it returns ErrInvalidCredentials.
Login(connInfo *imap.ConnInfo, username, password string) (User, error)
}

View File

@@ -0,0 +1,2 @@
// Package backendutil provides utility functions to implement IMAP backends.
package backendutil

View File

@@ -0,0 +1,79 @@
package backendutil
import (
"time"
)
var testDate, _ = time.Parse(time.RFC1123Z, "Sat, 18 Jun 2016 12:00:00 +0900")
const testHeaderString = "Content-Type: multipart/mixed; boundary=message-boundary\r\n" +
"Date: Sat, 18 Jun 2016 12:00:00 +0900\r\n" +
"Date: Sat, 19 Jun 2016 12:00:00 +0900\r\n" +
"From: Mitsuha Miyamizu <mitsuha.miyamizu@example.org>\r\n" +
"Reply-To: Mitsuha Miyamizu <mitsuha.miyamizu+replyto@example.org>\r\n" +
"Message-Id: 42@example.org\r\n" +
"Subject: Your Name.\r\n" +
"To: Taki Tachibana <taki.tachibana@example.org>\r\n" +
"\r\n"
const testHeaderFromToString = "From: Mitsuha Miyamizu <mitsuha.miyamizu@example.org>\r\n" +
"To: Taki Tachibana <taki.tachibana@example.org>\r\n" +
"\r\n"
const testHeaderDateString = "Date: Sat, 18 Jun 2016 12:00:00 +0900\r\n" +
"Date: Sat, 19 Jun 2016 12:00:00 +0900\r\n" +
"\r\n"
const testHeaderNoFromToString = "Content-Type: multipart/mixed; boundary=message-boundary\r\n" +
"Date: Sat, 18 Jun 2016 12:00:00 +0900\r\n" +
"Date: Sat, 19 Jun 2016 12:00:00 +0900\r\n" +
"Reply-To: Mitsuha Miyamizu <mitsuha.miyamizu+replyto@example.org>\r\n" +
"Message-Id: 42@example.org\r\n" +
"Subject: Your Name.\r\n" +
"\r\n"
const testAltHeaderString = "Content-Type: multipart/alternative; boundary=b2\r\n" +
"\r\n"
const testTextHeaderString = "Content-Disposition: inline\r\n" +
"Content-Type: text/plain\r\n" +
"\r\n"
const testTextContentTypeString = "Content-Type: text/plain\r\n" +
"\r\n"
const testTextNoContentTypeString = "Content-Disposition: inline\r\n" +
"\r\n"
const testTextBodyString = "What's your name?"
const testTextString = testTextHeaderString + testTextBodyString
const testHTMLHeaderString = "Content-Disposition: inline\r\n" +
"Content-Type: text/html\r\n" +
"\r\n"
const testHTMLBodyString = "<div>What's <i>your\r\n</i> name?</div>"
const testHTMLString = testHTMLHeaderString + testHTMLBodyString
const testAttachmentHeaderString = "Content-Disposition: attachment; filename=note.txt\r\n" +
"Content-Type: text/plain\r\n" +
"\r\n"
const testAttachmentBodyString = "My name is Mitsuha."
const testAttachmentString = testAttachmentHeaderString + testAttachmentBodyString
const testBodyString = "--message-boundary\r\n" +
testAltHeaderString +
"\r\n--b2\r\n" +
testTextString +
"\r\n--b2\r\n" +
testHTMLString +
"\r\n--b2--\r\n" +
"\r\n--message-boundary\r\n" +
testAttachmentString +
"\r\n--message-boundary--\r\n"
const testMailString = testHeaderString + testBodyString

121
backend/backendutil/body.go Normal file
View File

@@ -0,0 +1,121 @@
package backendutil
import (
"bytes"
"errors"
"io"
"mime"
nettextproto "net/textproto"
"strings"
"github.com/emersion/go-imap"
"github.com/emersion/go-message/textproto"
)
var errNoSuchPart = errors.New("backendutil: no such message body part")
func multipartReader(header textproto.Header, body io.Reader) *textproto.MultipartReader {
contentType := header.Get("Content-Type")
if !strings.HasPrefix(strings.ToLower(contentType), "multipart/") {
return nil
}
_, params, err := mime.ParseMediaType(contentType)
if err != nil {
return nil
}
return textproto.NewMultipartReader(body, params["boundary"])
}
// FetchBodySection extracts a body section from a message.
func FetchBodySection(header textproto.Header, body io.Reader, section *imap.BodySectionName) (imap.Literal, error) {
// First, find the requested part using the provided path
for i := 0; i < len(section.Path); i++ {
n := section.Path[i]
mr := multipartReader(header, body)
if mr == nil {
// First part of non-multipart message refers to the message itself.
// See RFC 3501, Page 55.
if len(section.Path) == 1 && section.Path[0] == 1 {
break
}
return nil, errNoSuchPart
}
for j := 1; j <= n; j++ {
p, err := mr.NextPart()
if err == io.EOF {
return nil, errNoSuchPart
} else if err != nil {
return nil, err
}
if j == n {
body = p
header = p.Header
break
}
}
}
// Then, write the requested data to a buffer
b := new(bytes.Buffer)
resHeader := header
if section.Fields != nil {
// Copy header so we will not change value passed to us.
resHeader = header.Copy()
if section.NotFields {
for _, fieldName := range section.Fields {
resHeader.Del(fieldName)
}
} else {
fieldsMap := make(map[string]struct{}, len(section.Fields))
for _, field := range section.Fields {
fieldsMap[nettextproto.CanonicalMIMEHeaderKey(field)] = struct{}{}
}
for field := resHeader.Fields(); field.Next(); {
if _, ok := fieldsMap[field.Key()]; !ok {
field.Del()
}
}
}
}
// Write the header
err := textproto.WriteHeader(b, resHeader)
if err != nil {
return nil, err
}
switch section.Specifier {
case imap.TextSpecifier:
// The header hasn't been requested. Discard it.
b.Reset()
case imap.EntireSpecifier:
if len(section.Path) > 0 {
// When selecting a specific part by index, IMAP servers
// return only the text, not the associated MIME header.
b.Reset()
}
}
// Write the body, if requested
switch section.Specifier {
case imap.EntireSpecifier, imap.TextSpecifier:
if _, err := io.Copy(b, body); err != nil {
return nil, err
}
}
var l imap.Literal = b
if section.Partial != nil {
l = bytes.NewReader(section.ExtractPartial(b.Bytes()))
}
return l, nil
}

View File

@@ -0,0 +1,196 @@
package backendutil
import (
"bufio"
"io/ioutil"
"strings"
"testing"
"github.com/emersion/go-imap"
"github.com/emersion/go-message/textproto"
)
var bodyTests = []struct {
section string
body string
}{
{
section: "BODY[]",
body: testMailString,
},
{
section: "BODY[1.1]",
body: testTextBodyString,
},
{
section: "BODY[1.2]",
body: testHTMLBodyString,
},
{
section: "BODY[2]",
body: testAttachmentBodyString,
},
{
section: "BODY[HEADER]",
body: testHeaderString,
},
{
section: "BODY[HEADER.FIELDS (From To)]",
body: testHeaderFromToString,
},
{
section: "BODY[HEADER.FIELDS (FROM to)]",
body: testHeaderFromToString,
},
{
section: "BODY[HEADER.FIELDS.NOT (From To)]",
body: testHeaderNoFromToString,
},
{
section: "BODY[HEADER.FIELDS (Date)]",
body: testHeaderDateString,
},
{
section: "BODY[1.1.HEADER]",
body: testTextHeaderString,
},
{
section: "BODY[1.1.HEADER.FIELDS (Content-Type)]",
body: testTextContentTypeString,
},
{
section: "BODY[1.1.HEADER.FIELDS.NOT (Content-Type)]",
body: testTextNoContentTypeString,
},
{
section: "BODY[2.HEADER]",
body: testAttachmentHeaderString,
},
{
section: "BODY[2.MIME]",
body: testAttachmentHeaderString,
},
{
section: "BODY[TEXT]",
body: testBodyString,
},
{
section: "BODY[1.1.TEXT]",
body: testTextBodyString,
},
{
section: "BODY[2.TEXT]",
body: testAttachmentBodyString,
},
{
section: "BODY[2.1]",
body: "",
},
{
section: "BODY[3]",
body: "",
},
{
section: "BODY[2.TEXT]<0.9>",
body: testAttachmentBodyString[:9],
},
}
func TestFetchBodySection(t *testing.T) {
for _, test := range bodyTests {
test := test
t.Run(test.section, func(t *testing.T) {
bufferedBody := bufio.NewReader(strings.NewReader(testMailString))
header, err := textproto.ReadHeader(bufferedBody)
if err != nil {
t.Fatal("Expected no error while reading mail, got:", err)
}
section, err := imap.ParseBodySectionName(imap.FetchItem(test.section))
if err != nil {
t.Fatal("Expected no error while parsing body section name, got:", err)
}
r, err := FetchBodySection(header, bufferedBody, section)
if test.body == "" {
if err == nil {
t.Error("Expected an error while extracting non-existing body section")
}
} else {
if err != nil {
t.Fatal("Expected no error while extracting body section, got:", err)
}
b, err := ioutil.ReadAll(r)
if err != nil {
t.Fatal("Expected no error while reading body section, got:", err)
}
if s := string(b); s != test.body {
t.Errorf("Expected body section %q to be \n%s\n but got \n%s", test.section, test.body, s)
}
}
})
}
}
func TestFetchBodySection_NonMultipart(t *testing.T) {
// https://tools.ietf.org/html/rfc3501#page-55:
// Every message has at least one part number. Non-[MIME-IMB]
// messages, and non-multipart [MIME-IMB] messages with no
// encapsulated message, only have a part 1.
testMsgHdr := "From: Mitsuha Miyamizu <mitsuha.miyamizu@example.org>\r\n" +
"To: Taki Tachibana <taki.tachibana@example.org>\r\n" +
"Subject: Your Name.\r\n" +
"Message-Id: 42@example.org\r\n" +
"\r\n"
testMsgBody := "That's not multipart message. Thought it should be possible to get this text using BODY[1]."
testMsg := testMsgHdr + testMsgBody
tests := []struct {
section string
body string
}{
{
section: "BODY[1.MIME]",
body: testMsgHdr,
},
{
section: "BODY[1]",
body: testMsgBody,
},
}
for _, test := range tests {
test := test
t.Run(test.section, func(t *testing.T) {
bufferedBody := bufio.NewReader(strings.NewReader(testMsg))
header, err := textproto.ReadHeader(bufferedBody)
if err != nil {
t.Fatal("Expected no error while reading mail, got:", err)
}
section, err := imap.ParseBodySectionName(imap.FetchItem(test.section))
if err != nil {
t.Fatal("Expected no error while parsing body section name, got:", err)
}
r, err := FetchBodySection(header, bufferedBody, section)
if err != nil {
t.Fatal("Expected no error while extracting body section, got:", err)
}
b, err := ioutil.ReadAll(r)
if err != nil {
t.Fatal("Expected no error while reading body section, got:", err)
}
if s := string(b); s != test.body {
t.Errorf("Expected body section %q to be \n%s\n but got \n%s", test.section, test.body, s)
}
})
}
}

View File

@@ -0,0 +1,117 @@
package backendutil
import (
"bufio"
"bytes"
"io"
"io/ioutil"
"mime"
"strings"
"github.com/emersion/go-imap"
"github.com/emersion/go-message/textproto"
)
type countReader struct {
r io.Reader
bytes uint32
newlines uint32
endsWithLF bool
}
func (r *countReader) Read(b []byte) (int, error) {
n, err := r.r.Read(b)
r.bytes += uint32(n)
if n != 0 {
r.newlines += uint32(bytes.Count(b[:n], []byte{'\n'}))
r.endsWithLF = b[n-1] == '\n'
}
// If the stream does not end with a newline - count missing newline.
if err == io.EOF {
if !r.endsWithLF {
r.newlines++
}
}
return n, err
}
// FetchBodyStructure computes a message's body structure from its content.
func FetchBodyStructure(header textproto.Header, body io.Reader, extended bool) (*imap.BodyStructure, error) {
bs := new(imap.BodyStructure)
mediaType, mediaParams, err := mime.ParseMediaType(header.Get("Content-Type"))
if err == nil {
typeParts := strings.SplitN(mediaType, "/", 2)
bs.MIMEType = typeParts[0]
if len(typeParts) == 2 {
bs.MIMESubType = typeParts[1]
}
bs.Params = mediaParams
} else {
bs.MIMEType = "text"
bs.MIMESubType = "plain"
}
bs.Id = header.Get("Content-Id")
bs.Description = header.Get("Content-Description")
bs.Encoding = header.Get("Content-Transfer-Encoding")
if mr := multipartReader(header, body); mr != nil {
var parts []*imap.BodyStructure
for {
p, err := mr.NextPart()
if err == io.EOF {
break
} else if err != nil {
return nil, err
}
pbs, err := FetchBodyStructure(p.Header, p, extended)
if err != nil {
return nil, err
}
parts = append(parts, pbs)
}
bs.Parts = parts
} else {
countedBody := countReader{r: body}
needLines := false
if bs.MIMEType == "message" && bs.MIMESubType == "rfc822" {
// This will result in double-buffering if body is already a
// bufio.Reader (most likely it is). :\
bufBody := bufio.NewReader(&countedBody)
subMsgHdr, err := textproto.ReadHeader(bufBody)
if err != nil {
return nil, err
}
bs.Envelope, err = FetchEnvelope(subMsgHdr)
if err != nil {
return nil, err
}
bs.BodyStructure, err = FetchBodyStructure(subMsgHdr, bufBody, extended)
if err != nil {
return nil, err
}
needLines = true
} else if bs.MIMEType == "text" {
needLines = true
}
if _, err := io.Copy(ioutil.Discard, &countedBody); err != nil {
return nil, err
}
bs.Size = countedBody.bytes
if needLines {
bs.Lines = countedBody.newlines
}
}
if extended {
bs.Extended = true
bs.Disposition, bs.DispositionParams, _ = mime.ParseMediaType(header.Get("Content-Disposition"))
// TODO: bs.Language, bs.Location
// TODO: bs.MD5
}
return bs, nil
}

View File

@@ -0,0 +1,76 @@
package backendutil
import (
"bufio"
"reflect"
"strings"
"testing"
"github.com/emersion/go-imap"
"github.com/emersion/go-message/textproto"
)
var testBodyStructure = &imap.BodyStructure{
MIMEType: "multipart",
MIMESubType: "mixed",
Params: map[string]string{"boundary": "message-boundary"},
Parts: []*imap.BodyStructure{
{
MIMEType: "multipart",
MIMESubType: "alternative",
Params: map[string]string{"boundary": "b2"},
Extended: true,
Parts: []*imap.BodyStructure{
{
MIMEType: "text",
MIMESubType: "plain",
Params: map[string]string{},
Extended: true,
Disposition: "inline",
DispositionParams: map[string]string{},
Lines: 1,
Size: 17,
},
{
MIMEType: "text",
MIMESubType: "html",
Params: map[string]string{},
Extended: true,
Disposition: "inline",
DispositionParams: map[string]string{},
Lines: 2,
Size: 37,
},
},
},
{
MIMEType: "text",
MIMESubType: "plain",
Params: map[string]string{},
Extended: true,
Disposition: "attachment",
DispositionParams: map[string]string{"filename": "note.txt"},
Lines: 1,
Size: 19,
},
},
Extended: true,
}
func TestFetchBodyStructure(t *testing.T) {
bufferedBody := bufio.NewReader(strings.NewReader(testMailString))
header, err := textproto.ReadHeader(bufferedBody)
if err != nil {
t.Fatal("Expected no error while reading mail, got:", err)
}
bs, err := FetchBodyStructure(header, bufferedBody, true)
if err != nil {
t.Fatal("Expected no error while fetching body structure, got:", err)
}
if !reflect.DeepEqual(testBodyStructure, bs) {
t.Errorf("Expected body structure \n%+v\n but got \n%+v", testBodyStructure, bs)
}
}

View File

@@ -0,0 +1,58 @@
package backendutil
import (
"net/mail"
"strings"
"github.com/emersion/go-imap"
"github.com/emersion/go-message/textproto"
)
func headerAddressList(value string) ([]*imap.Address, error) {
addrs, err := mail.ParseAddressList(value)
if err != nil {
return []*imap.Address{}, err
}
list := make([]*imap.Address, len(addrs))
for i, a := range addrs {
parts := strings.SplitN(a.Address, "@", 2)
mailbox := parts[0]
var hostname string
if len(parts) == 2 {
hostname = parts[1]
}
list[i] = &imap.Address{
PersonalName: a.Name,
MailboxName: mailbox,
HostName: hostname,
}
}
return list, err
}
// FetchEnvelope returns a message's envelope from its header.
func FetchEnvelope(h textproto.Header) (*imap.Envelope, error) {
env := new(imap.Envelope)
env.Date, _ = mail.ParseDate(h.Get("Date"))
env.Subject = h.Get("Subject")
env.From, _ = headerAddressList(h.Get("From"))
env.Sender, _ = headerAddressList(h.Get("Sender"))
if len(env.Sender) == 0 {
env.Sender = env.From
}
env.ReplyTo, _ = headerAddressList(h.Get("Reply-To"))
if len(env.ReplyTo) == 0 {
env.ReplyTo = env.From
}
env.To, _ = headerAddressList(h.Get("To"))
env.Cc, _ = headerAddressList(h.Get("Cc"))
env.Bcc, _ = headerAddressList(h.Get("Bcc"))
env.InReplyTo = h.Get("In-Reply-To")
env.MessageId = h.Get("Message-Id")
return env, nil
}

View File

@@ -0,0 +1,40 @@
package backendutil
import (
"bufio"
"reflect"
"strings"
"testing"
"github.com/emersion/go-imap"
"github.com/emersion/go-message/textproto"
)
var testEnvelope = &imap.Envelope{
Date: testDate,
Subject: "Your Name.",
From: []*imap.Address{{PersonalName: "Mitsuha Miyamizu", MailboxName: "mitsuha.miyamizu", HostName: "example.org"}},
Sender: []*imap.Address{{PersonalName: "Mitsuha Miyamizu", MailboxName: "mitsuha.miyamizu", HostName: "example.org"}},
ReplyTo: []*imap.Address{{PersonalName: "Mitsuha Miyamizu", MailboxName: "mitsuha.miyamizu+replyto", HostName: "example.org"}},
To: []*imap.Address{{PersonalName: "Taki Tachibana", MailboxName: "taki.tachibana", HostName: "example.org"}},
Cc: []*imap.Address{},
Bcc: []*imap.Address{},
InReplyTo: "",
MessageId: "42@example.org",
}
func TestFetchEnvelope(t *testing.T) {
hdr, err := textproto.ReadHeader(bufio.NewReader(strings.NewReader(testMailString)))
if err != nil {
t.Fatal("Expected no error while reading mail, got:", err)
}
env, err := FetchEnvelope(hdr)
if err != nil {
t.Fatal("Expected no error while fetching envelope, got:", err)
}
if !reflect.DeepEqual(env, testEnvelope) {
t.Errorf("Expected envelope \n%+v\n but got \n%+v", testEnvelope, env)
}
}

View File

@@ -0,0 +1,73 @@
package backendutil
import (
"github.com/emersion/go-imap"
)
// UpdateFlags executes a flag operation on the flag set current.
func UpdateFlags(current []string, op imap.FlagsOp, flags []string) []string {
// Don't modify contents of 'flags' slice. Only modify 'current'.
// See https://github.com/golang/go/wiki/SliceTricks
// Re-use current's backing store
newFlags := current[:0]
switch op {
case imap.SetFlags:
hasRecent := false
// keep recent flag
for _, flag := range current {
if flag == imap.RecentFlag {
newFlags = append(newFlags, imap.RecentFlag)
hasRecent = true
break
}
}
// append new flags
for _, flag := range flags {
if flag == imap.RecentFlag {
// Make sure we don't add the recent flag multiple times.
if hasRecent {
// Already have the recent flag, skip.
continue
}
hasRecent = true
}
// append new flag
newFlags = append(newFlags, flag)
}
case imap.AddFlags:
// keep current flags
newFlags = current
// Only add new flag if it isn't already in current list.
for _, addFlag := range flags {
found := false
for _, flag := range current {
if addFlag == flag {
found = true
break
}
}
// new flag not found, add it.
if !found {
newFlags = append(newFlags, addFlag)
}
}
case imap.RemoveFlags:
// Filter current flags
for _, flag := range current {
remove := false
for _, removeFlag := range flags {
if removeFlag == flag {
remove = true
}
}
if !remove {
newFlags = append(newFlags, flag)
}
}
default:
// Unknown operation, return current flags unchanged
newFlags = current
}
return newFlags
}

View File

@@ -0,0 +1,86 @@
package backendutil
import (
"reflect"
"testing"
"github.com/emersion/go-imap"
)
var updateFlagsTests = []struct {
op imap.FlagsOp
flags []string
res []string
}{
{
op: imap.AddFlags,
flags: []string{"d", "e"},
res: []string{"a", "b", "c", "d", "e"},
},
{
op: imap.AddFlags,
flags: []string{"a", "d", "b"},
res: []string{"a", "b", "c", "d"},
},
{
op: imap.RemoveFlags,
flags: []string{"b", "v", "e", "a"},
res: []string{"c"},
},
{
op: imap.SetFlags,
flags: []string{"a", "d", "e"},
res: []string{"a", "d", "e"},
},
// Test unknown op for code coverage.
{
op: imap.FlagsOp("TestUnknownOp"),
flags: []string{"a", "d", "e"},
res: []string{"a", "b", "c"},
},
}
func TestUpdateFlags(t *testing.T) {
flagsList := []string{"a", "b", "c"}
for _, test := range updateFlagsTests {
// Make a backup copy of 'test.flags'
origFlags := append(test.flags[:0:0], test.flags...)
// Copy flags
current := append(flagsList[:0:0], flagsList...)
got := UpdateFlags(current, test.op, test.flags)
if !reflect.DeepEqual(got, test.res) {
t.Errorf("Expected result to be \n%v\n but got \n%v", test.res, got)
}
// Verify that 'test.flags' wasn't modified
if !reflect.DeepEqual(origFlags, test.flags) {
t.Errorf("Unexpected change to operation flags list changed \nbefore %v\n after \n%v",
origFlags, test.flags)
}
}
}
func TestUpdateFlags_Recent(t *testing.T) {
current := []string{}
current = UpdateFlags(current, imap.SetFlags, []string{imap.RecentFlag})
res := []string{imap.RecentFlag}
if !reflect.DeepEqual(current, res) {
t.Errorf("Expected result to be \n%v\n but got \n%v", res, current)
}
current = UpdateFlags(current, imap.SetFlags, []string{"something"})
res = []string{imap.RecentFlag, "something"}
if !reflect.DeepEqual(current, res) {
t.Errorf("Expected result to be \n%v\n but got \n%v", res, current)
}
current = UpdateFlags(current, imap.SetFlags, []string{"another", imap.RecentFlag})
res = []string{imap.RecentFlag, "another"}
if !reflect.DeepEqual(current, res) {
t.Errorf("Expected result to be \n%v\n but got \n%v", res, current)
}
}

View File

@@ -0,0 +1,230 @@
package backendutil
import (
"bytes"
"fmt"
"io"
"strings"
"time"
"github.com/emersion/go-imap"
"github.com/emersion/go-message"
"github.com/emersion/go-message/mail"
"github.com/emersion/go-message/textproto"
)
func matchString(s, substr string) bool {
return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
}
func bufferBody(e *message.Entity) (*bytes.Buffer, error) {
b := new(bytes.Buffer)
if _, err := io.Copy(b, e.Body); err != nil {
return nil, err
}
e.Body = b
return b, nil
}
func matchBody(e *message.Entity, substr string) (bool, error) {
if s, ok := e.Body.(fmt.Stringer); ok {
return matchString(s.String(), substr), nil
}
b, err := bufferBody(e)
if err != nil {
return false, err
}
return matchString(b.String(), substr), nil
}
type lengther interface {
Len() int
}
type countWriter struct {
N int
}
func (w *countWriter) Write(b []byte) (int, error) {
w.N += len(b)
return len(b), nil
}
func bodyLen(e *message.Entity) (int, error) {
headerSize := countWriter{}
textproto.WriteHeader(&headerSize, e.Header.Header)
if l, ok := e.Body.(lengther); ok {
return l.Len() + headerSize.N, nil
}
b, err := bufferBody(e)
if err != nil {
return 0, err
}
return b.Len() + headerSize.N, nil
}
// Match returns true if a message and its metadata matches the provided
// criteria.
func Match(e *message.Entity, seqNum, uid uint32, date time.Time, flags []string, c *imap.SearchCriteria) (bool, error) {
// TODO: support encoded header fields for Bcc, Cc, From, To
// TODO: add header size for Larger and Smaller
h := mail.Header{Header: e.Header}
if !c.SentBefore.IsZero() || !c.SentSince.IsZero() {
t, err := h.Date()
if err != nil {
return false, err
}
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC)
if !c.SentBefore.IsZero() && !t.Before(c.SentBefore) {
return false, nil
}
if !c.SentSince.IsZero() && t.Before(c.SentSince) {
return false, nil
}
}
for key, wantValues := range c.Header {
ok := e.Header.Has(key)
for _, wantValue := range wantValues {
if wantValue == "" && !ok {
return false, nil
}
if wantValue != "" {
ok := false
values := e.Header.FieldsByKey(key)
for values.Next() {
decoded, _ := values.Text()
if matchString(decoded, wantValue) {
ok = true
break
}
}
if !ok {
return false, nil
}
}
}
}
for _, body := range c.Body {
if ok, err := matchBody(e, body); err != nil || !ok {
return false, err
}
}
for _, text := range c.Text {
headerMatch := false
for f := e.Header.Fields(); f.Next(); {
decoded, err := f.Text()
if err != nil {
continue
}
if strings.Contains(f.Key()+": "+decoded, text) {
headerMatch = true
}
}
if ok, err := matchBody(e, text); err != nil || !ok && !headerMatch {
return false, err
}
}
if c.Larger > 0 || c.Smaller > 0 {
n, err := bodyLen(e)
if err != nil {
return false, err
}
if c.Larger > 0 && uint32(n) <= c.Larger {
return false, nil
}
if c.Smaller > 0 && uint32(n) >= c.Smaller {
return false, nil
}
}
if !c.Since.IsZero() || !c.Before.IsZero() {
if !matchDate(date, c) {
return false, nil
}
}
if c.WithFlags != nil || c.WithoutFlags != nil {
if !matchFlags(flags, c) {
return false, nil
}
}
if c.SeqNum != nil || c.Uid != nil {
if !matchSeqNumAndUid(seqNum, uid, c) {
return false, nil
}
}
for _, not := range c.Not {
ok, err := Match(e, seqNum, uid, date, flags, not)
if err != nil || ok {
return false, err
}
}
for _, or := range c.Or {
ok1, err := Match(e, seqNum, uid, date, flags, or[0])
if err != nil {
return ok1, err
}
ok2, err := Match(e, seqNum, uid, date, flags, or[1])
if err != nil || (!ok1 && !ok2) {
return false, err
}
}
return true, nil
}
func matchFlags(flags []string, c *imap.SearchCriteria) bool {
flagsMap := make(map[string]bool)
for _, f := range flags {
flagsMap[f] = true
}
for _, f := range c.WithFlags {
if !flagsMap[f] {
return false
}
}
for _, f := range c.WithoutFlags {
if flagsMap[f] {
return false
}
}
return true
}
func matchSeqNumAndUid(seqNum uint32, uid uint32, c *imap.SearchCriteria) bool {
if c.SeqNum != nil && !c.SeqNum.Contains(seqNum) {
return false
}
if c.Uid != nil && !c.Uid.Contains(uid) {
return false
}
return true
}
func matchDate(date time.Time, c *imap.SearchCriteria) bool {
// We discard time zone information by setting it to UTC.
// RFC 3501 explicitly requires zone unaware date comparison.
date = time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.UTC)
if !c.Since.IsZero() && !date.After(c.Since) {
return false
}
if !c.Before.IsZero() && !date.Before(c.Before) {
return false
}
return true
}

View File

@@ -0,0 +1,432 @@
package backendutil
import (
"net/textproto"
"strings"
"testing"
"time"
"github.com/emersion/go-imap"
"github.com/emersion/go-message"
)
var testInternalDate = time.Unix(1483997966, 0)
var matchTests = []struct {
criteria *imap.SearchCriteria
seqNum uint32
uid uint32
date time.Time
flags []string
res bool
}{
{
criteria: &imap.SearchCriteria{
Header: textproto.MIMEHeader{"From": {"Mitsuha"}},
},
res: true,
},
{
criteria: &imap.SearchCriteria{
Header: textproto.MIMEHeader{"To": {"Mitsuha"}},
},
res: false,
},
{
criteria: &imap.SearchCriteria{SentBefore: testDate.Add(48 * time.Hour)},
res: true,
},
{
criteria: &imap.SearchCriteria{
Not: []*imap.SearchCriteria{{SentSince: testDate.Add(48 * time.Hour)}},
},
res: true,
},
{
criteria: &imap.SearchCriteria{
Not: []*imap.SearchCriteria{{Body: []string{"name"}}},
},
res: false,
},
{
criteria: &imap.SearchCriteria{
Text: []string{"name"},
},
res: true,
},
{
criteria: &imap.SearchCriteria{
Or: [][2]*imap.SearchCriteria{{
{Text: []string{"i'm not in the text"}},
{Body: []string{"i'm not in the body"}},
}},
},
res: false,
},
{
criteria: &imap.SearchCriteria{
Header: textproto.MIMEHeader{"Message-Id": {"42@example.org"}},
},
res: true,
},
{
criteria: &imap.SearchCriteria{
Header: textproto.MIMEHeader{"Message-Id": {"43@example.org"}},
},
res: false,
},
{
criteria: &imap.SearchCriteria{
Header: textproto.MIMEHeader{"Message-Id": {""}},
},
res: true,
},
{
criteria: &imap.SearchCriteria{
Header: textproto.MIMEHeader{"Totally-Not-Reply-To": {""}},
},
res: false,
},
{
criteria: &imap.SearchCriteria{
Larger: 10,
},
res: true,
},
{
criteria: &imap.SearchCriteria{
Smaller: 10,
},
res: false,
},
{
criteria: &imap.SearchCriteria{
Header: textproto.MIMEHeader{"Subject": {"your"}},
},
res: true,
},
{
criteria: &imap.SearchCriteria{
Header: textproto.MIMEHeader{"Subject": {"Taki"}},
},
res: false,
},
{
flags: []string{imap.SeenFlag},
criteria: &imap.SearchCriteria{
WithFlags: []string{imap.SeenFlag},
WithoutFlags: []string{imap.FlaggedFlag},
},
res: true,
},
{
flags: []string{imap.SeenFlag},
criteria: &imap.SearchCriteria{
WithFlags: []string{imap.DraftFlag},
WithoutFlags: []string{imap.FlaggedFlag},
},
res: false,
},
{
flags: []string{imap.SeenFlag, imap.FlaggedFlag},
criteria: &imap.SearchCriteria{
WithFlags: []string{imap.SeenFlag},
WithoutFlags: []string{imap.FlaggedFlag},
},
res: false,
},
{
flags: []string{imap.SeenFlag, imap.FlaggedFlag},
criteria: &imap.SearchCriteria{
Or: [][2]*imap.SearchCriteria{{
{WithFlags: []string{imap.DraftFlag}},
{WithoutFlags: []string{imap.SeenFlag}},
}},
},
res: false,
},
{
flags: []string{imap.SeenFlag, imap.FlaggedFlag},
criteria: &imap.SearchCriteria{
Not: []*imap.SearchCriteria{
{WithFlags: []string{imap.SeenFlag}},
},
},
res: false,
},
{
seqNum: 42,
uid: 69,
criteria: &imap.SearchCriteria{
Or: [][2]*imap.SearchCriteria{{
{
Uid: new(imap.SeqSet),
Not: []*imap.SearchCriteria{{SeqNum: new(imap.SeqSet)}},
},
{
SeqNum: new(imap.SeqSet),
},
}},
},
res: false,
},
{
seqNum: 42,
uid: 69,
criteria: &imap.SearchCriteria{
Or: [][2]*imap.SearchCriteria{{
{
Uid: &imap.SeqSet{Set: []imap.Seq{{69, 69}}},
Not: []*imap.SearchCriteria{{SeqNum: new(imap.SeqSet)}},
},
{
SeqNum: new(imap.SeqSet),
},
}},
},
res: true,
},
{
seqNum: 42,
uid: 69,
criteria: &imap.SearchCriteria{
Or: [][2]*imap.SearchCriteria{{
{
Uid: &imap.SeqSet{Set: []imap.Seq{{69, 69}}},
Not: []*imap.SearchCriteria{{
SeqNum: &imap.SeqSet{Set: []imap.Seq{imap.Seq{42, 42}}},
}},
},
{
SeqNum: new(imap.SeqSet),
},
}},
},
res: false,
},
{
seqNum: 42,
uid: 69,
criteria: &imap.SearchCriteria{
Or: [][2]*imap.SearchCriteria{{
{
Uid: &imap.SeqSet{Set: []imap.Seq{{69, 69}}},
Not: []*imap.SearchCriteria{{
SeqNum: &imap.SeqSet{Set: []imap.Seq{{42, 42}}},
}},
},
{
SeqNum: &imap.SeqSet{Set: []imap.Seq{{42, 42}}},
},
}},
},
res: true,
},
{
date: testInternalDate,
criteria: &imap.SearchCriteria{
Or: [][2]*imap.SearchCriteria{{
{
Since: testInternalDate.Add(48 * time.Hour),
Not: []*imap.SearchCriteria{{
Since: testInternalDate.Add(48 * time.Hour),
}},
},
{
Before: testInternalDate.Add(-48 * time.Hour),
},
}},
},
res: false,
},
{
date: testInternalDate,
criteria: &imap.SearchCriteria{
Or: [][2]*imap.SearchCriteria{{
{
Since: testInternalDate.Add(-48 * time.Hour),
Not: []*imap.SearchCriteria{{
Since: testInternalDate.Add(48 * time.Hour),
}},
},
{
Before: testInternalDate.Add(-48 * time.Hour),
},
}},
},
res: true,
},
{
date: testInternalDate,
criteria: &imap.SearchCriteria{
Or: [][2]*imap.SearchCriteria{{
{
Since: testInternalDate.Add(-48 * time.Hour),
Not: []*imap.SearchCriteria{{
Since: testInternalDate.Add(-48 * time.Hour),
}},
},
{
Before: testInternalDate.Add(-48 * time.Hour),
},
}},
},
res: false,
},
{
date: testInternalDate,
criteria: &imap.SearchCriteria{
Or: [][2]*imap.SearchCriteria{{
{
Since: testInternalDate.Add(-48 * time.Hour),
Not: []*imap.SearchCriteria{{
Since: testInternalDate.Add(-48 * time.Hour),
}},
},
{
Before: testInternalDate.Add(48 * time.Hour),
},
}},
},
res: true,
},
}
func TestMatch(t *testing.T) {
for i, test := range matchTests {
e, err := message.Read(strings.NewReader(testMailString))
if err != nil {
t.Fatal("Expected no error while reading entity, got:", err)
}
ok, err := Match(e, test.seqNum, test.uid, test.date, test.flags, test.criteria)
if err != nil {
t.Fatal("Expected no error while matching entity, got:", err)
}
if test.res && !ok {
t.Errorf("Expected #%v to match search criteria", i+1)
}
if !test.res && ok {
t.Errorf("Expected #%v not to match search criteria", i+1)
}
}
}
func TestMatchEncoded(t *testing.T) {
encodedTestMsg := `From: "fox.cpp" <foxcpp@foxcpp.dev>
To: "fox.cpp" <foxcpp@foxcpp.dev>
Subject: =?utf-8?B?0J/RgNC+0LLQtdGA0LrQsCE=?=
Date: Sun, 09 Jun 2019 00:06:43 +0300
MIME-Version: 1.0
Message-ID: <a2aeb99e-52dd-40d3-b99f-1fdaad77ed98@foxcpp.dev>
Content-Type: text/plain; charset=utf-8; format=flowed
Content-Transfer-Encoding: quoted-printable
=D0=AD=D1=82=D0=BE=D1=82 =D1=82=D0=B5=D0=BA=D1=81=D1=82 =D0=B4=D0=BE=D0=BB=
=D0=B6=D0=B5=D0=BD =D0=B1=D1=8B=D1=82=D1=8C =D0=B7=D0=B0=D0=BA=D0=BE=D0=B4=
=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD =D0=B2 base64 =D0=B8=D0=BB=D0=B8 quote=
d-encoding.`
e, err := message.Read(strings.NewReader(encodedTestMsg))
if err != nil {
t.Fatal("Expected no error while reading entity, got:", err)
}
// Check encoded header.
crit := imap.SearchCriteria{
Header: textproto.MIMEHeader{"Subject": []string{"Проверка!"}},
}
ok, err := Match(e, 0, 0, time.Now(), []string{}, &crit)
if err != nil {
t.Fatal("Expected no error while matching entity, got:", err)
}
if !ok {
t.Error("Expected match for encoded header")
}
// Encoded body.
crit = imap.SearchCriteria{
Body: []string{"или"},
}
ok, err = Match(e, 0, 0, time.Now(), []string{}, &crit)
if err != nil {
t.Fatal("Expected no error while matching entity, got:", err)
}
if !ok {
t.Error("Expected match for encoded body")
}
}
func TestMatchIssue298Regression(t *testing.T) {
raw1 := "Subject: 1\r\n\r\n1"
raw2 := "Subject: 2\r\n\r\n22"
raw3 := "Subject: 3\r\n\r\n333"
e1, err := message.Read(strings.NewReader(raw1))
if err != nil {
t.Fatal("Expected no error while reading entity, got:", err)
}
e2, err := message.Read(strings.NewReader(raw2))
if err != nil {
t.Fatal("Expected no error while reading entity, got:", err)
}
e3, err := message.Read(strings.NewReader(raw3))
if err != nil {
t.Fatal("Expected no error while reading entity, got:", err)
}
// Search for body size > 15 ("LARGER 15"), which should match messages #2 and #3
criteria := &imap.SearchCriteria{
Larger: 15,
}
ok1, err := Match(e1, 1, 101, time.Now(), nil, criteria)
if err != nil {
t.Fatal("Expected no error while matching entity, got:", err)
}
if ok1 {
t.Errorf("Expected message #1 to not match search criteria")
}
ok2, err := Match(e2, 2, 102, time.Now(), nil, criteria)
if err != nil {
t.Fatal("Expected no error while matching entity, got:", err)
}
if !ok2 {
t.Errorf("Expected message #2 to match search criteria")
}
ok3, err := Match(e3, 3, 103, time.Now(), nil, criteria)
if err != nil {
t.Fatal("Expected no error while matching entity, got:", err)
}
if !ok3 {
t.Errorf("Expected message #3 to match search criteria")
}
// Search for body size < 17 ("SMALLER 17"), which should match messages #1 and #2
criteria = &imap.SearchCriteria{
Smaller: 17,
}
ok1, err = Match(e1, 1, 101, time.Now(), nil, criteria)
if err != nil {
t.Fatal("Expected no error while matching entity, got:", err)
}
if !ok1 {
t.Errorf("Expected message #1 to match search criteria")
}
ok2, err = Match(e2, 2, 102, time.Now(), nil, criteria)
if err != nil {
t.Fatal("Expected no error while matching entity, got:", err)
}
if !ok2 {
t.Errorf("Expected message #2 to match search criteria")
}
ok3, err = Match(e3, 3, 103, time.Now(), nil, criteria)
if err != nil {
t.Fatal("Expected no error while matching entity, got:", err)
}
if ok3 {
t.Errorf("Expected message #3 to not match search criteria")
}
}

78
backend/mailbox.go Normal file
View File

@@ -0,0 +1,78 @@
package backend
import (
"time"
"github.com/emersion/go-imap"
)
// Mailbox represents a mailbox belonging to a user in the mail storage system.
// A mailbox operation always deals with messages.
type Mailbox interface {
// Name returns this mailbox name.
Name() string
// Info returns this mailbox info.
Info() (*imap.MailboxInfo, error)
// Status returns this mailbox status. The fields Name, Flags, PermanentFlags
// and UnseenSeqNum in the returned MailboxStatus must be always populated.
// This function does not affect the state of any messages in the mailbox. See
// RFC 3501 section 6.3.10 for a list of items that can be requested.
Status(items []imap.StatusItem) (*imap.MailboxStatus, error)
// SetSubscribed adds or removes the mailbox to the server's set of "active"
// or "subscribed" mailboxes.
SetSubscribed(subscribed bool) error
// Check requests a checkpoint of the currently selected mailbox. A checkpoint
// refers to any implementation-dependent housekeeping associated with the
// mailbox (e.g., resolving the server's in-memory state of the mailbox with
// the state on its disk). A checkpoint MAY take a non-instantaneous amount of
// real time to complete. If a server implementation has no such housekeeping
// considerations, CHECK is equivalent to NOOP.
Check() error
// ListMessages returns a list of messages. seqset must be interpreted as UIDs
// if uid is set to true and as message sequence numbers otherwise. See RFC
// 3501 section 6.4.5 for a list of items that can be requested.
//
// Messages must be sent to ch. When the function returns, ch must be closed.
ListMessages(uid bool, seqset *imap.SeqSet, items []imap.FetchItem, ch chan<- *imap.Message) error
// SearchMessages searches messages. The returned list must contain UIDs if
// uid is set to true, or sequence numbers otherwise.
SearchMessages(uid bool, criteria *imap.SearchCriteria) ([]uint32, error)
// CreateMessage appends a new message to this mailbox. The \Recent flag will
// be added no matter flags is empty or not. If date is nil, the current time
// will be used.
//
// If the Backend implements Updater, it must notify the client immediately
// via a mailbox update.
CreateMessage(flags []string, date time.Time, body imap.Literal) error
// UpdateMessagesFlags alters flags for the specified message(s).
//
// If the Backend implements Updater, it must notify the client immediately
// via a message update.
UpdateMessagesFlags(uid bool, seqset *imap.SeqSet, operation imap.FlagsOp, flags []string) error
// CopyMessages copies the specified message(s) to the end of the specified
// destination mailbox. The flags and internal date of the message(s) SHOULD
// be preserved, and the Recent flag SHOULD be set, in the copy.
//
// If the destination mailbox does not exist, a server SHOULD return an error.
// It SHOULD NOT automatically create the mailbox.
//
// If the Backend implements Updater, it must notify the client immediately
// via a mailbox update.
CopyMessages(uid bool, seqset *imap.SeqSet, dest string) error
// Expunge permanently removes all messages that have the \Deleted flag set
// from the currently selected mailbox.
//
// If the Backend implements Updater, it must notify the client immediately
// via an expunge update.
Expunge() error
}

78
backend/memory/backend.go Normal file
View File

@@ -0,0 +1,78 @@
// A memory backend.
package memory
import (
"errors"
"fmt"
"time"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/backend"
)
type Backend struct {
users map[string]*User
}
func (be *Backend) Login(_ *imap.ConnInfo, username, password string) (backend.User, error) {
user, ok := be.users[username]
if ok && user.password == password {
return user, nil
}
return nil, errors.New("Bad username or password")
}
func New() *Backend {
user := &User{username: "username", password: "password"}
body := "From: contact@example.org\r\n" +
"To: contact@example.org\r\n" +
"Subject: A little message, just for you\r\n" +
"Date: Wed, 11 May 2016 14:31:59 +0000\r\n" +
"Message-ID: <0000000@localhost/>\r\n" +
"Content-Type: text/plain\r\n" +
"\r\n" +
"Hi there :)"
user.mailboxes = map[string]*Mailbox{
"INBOX": {
name: "INBOX",
user: user,
Messages: []*Message{
{
Uid: 6,
Date: time.Now(),
Flags: []string{"\\Seen"},
Size: uint32(len(body)),
Body: []byte(body),
},
},
},
}
return &Backend{
users: map[string]*User{user.username: user},
}
}
// NewUser adds a user to the backend.
func (be *Backend) NewUser(username, password string) (*User, error) {
_, ok := be.users[username]
if ok {
return nil, fmt.Errorf("user %s is already defined.", username)
}
u := &User{username: username, password: password, mailboxes: make(map[string]*Mailbox)}
be.users[username] = u
return u, nil
}
// DeleteUser removes a user from the backend.
func (be *Backend) DeleteUser(username string) error {
_, ok := be.users[username]
if !ok {
return fmt.Errorf("user %s is not defined.", username)
}
delete(be.users, username)
return nil
}

243
backend/memory/mailbox.go Normal file
View File

@@ -0,0 +1,243 @@
package memory
import (
"io/ioutil"
"time"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/backend"
"github.com/emersion/go-imap/backend/backendutil"
)
var Delimiter = "/"
type Mailbox struct {
Subscribed bool
Messages []*Message
name string
user *User
}
func (mbox *Mailbox) Name() string {
return mbox.name
}
func (mbox *Mailbox) Info() (*imap.MailboxInfo, error) {
info := &imap.MailboxInfo{
Delimiter: Delimiter,
Name: mbox.name,
}
return info, nil
}
func (mbox *Mailbox) uidNext() uint32 {
var uid uint32
for _, msg := range mbox.Messages {
if msg.Uid > uid {
uid = msg.Uid
}
}
uid++
return uid
}
func (mbox *Mailbox) flags() []string {
flagsMap := make(map[string]bool)
for _, msg := range mbox.Messages {
for _, f := range msg.Flags {
if !flagsMap[f] {
flagsMap[f] = true
}
}
}
var flags []string
for f := range flagsMap {
flags = append(flags, f)
}
return flags
}
func (mbox *Mailbox) unseenSeqNum() uint32 {
for i, msg := range mbox.Messages {
seqNum := uint32(i + 1)
seen := false
for _, flag := range msg.Flags {
if flag == imap.SeenFlag {
seen = true
break
}
}
if !seen {
return seqNum
}
}
return 0
}
func (mbox *Mailbox) Status(items []imap.StatusItem) (*imap.MailboxStatus, error) {
status := imap.NewMailboxStatus(mbox.name, items)
status.Flags = mbox.flags()
status.PermanentFlags = []string{"\\*"}
status.UnseenSeqNum = mbox.unseenSeqNum()
for _, name := range items {
switch name {
case imap.StatusMessages:
status.Messages = uint32(len(mbox.Messages))
case imap.StatusUidNext:
status.UidNext = mbox.uidNext()
case imap.StatusUidValidity:
status.UidValidity = 1
case imap.StatusRecent:
status.Recent = 0 // TODO
case imap.StatusUnseen:
status.Unseen = 0 // TODO
}
}
return status, nil
}
func (mbox *Mailbox) SetSubscribed(subscribed bool) error {
mbox.Subscribed = subscribed
return nil
}
func (mbox *Mailbox) Check() error {
return nil
}
func (mbox *Mailbox) ListMessages(uid bool, seqSet *imap.SeqSet, items []imap.FetchItem, ch chan<- *imap.Message) error {
defer close(ch)
for i, msg := range mbox.Messages {
seqNum := uint32(i + 1)
var id uint32
if uid {
id = msg.Uid
} else {
id = seqNum
}
if !seqSet.Contains(id) {
continue
}
m, err := msg.Fetch(seqNum, items)
if err != nil {
continue
}
ch <- m
}
return nil
}
func (mbox *Mailbox) SearchMessages(uid bool, criteria *imap.SearchCriteria) ([]uint32, error) {
var ids []uint32
for i, msg := range mbox.Messages {
seqNum := uint32(i + 1)
ok, err := msg.Match(seqNum, criteria)
if err != nil || !ok {
continue
}
var id uint32
if uid {
id = msg.Uid
} else {
id = seqNum
}
ids = append(ids, id)
}
return ids, nil
}
func (mbox *Mailbox) CreateMessage(flags []string, date time.Time, body imap.Literal) error {
if date.IsZero() {
date = time.Now()
}
b, err := ioutil.ReadAll(body)
if err != nil {
return err
}
mbox.Messages = append(mbox.Messages, &Message{
Uid: mbox.uidNext(),
Date: date,
Size: uint32(len(b)),
Flags: flags,
Body: b,
})
return nil
}
func (mbox *Mailbox) UpdateMessagesFlags(uid bool, seqset *imap.SeqSet, op imap.FlagsOp, flags []string) error {
for i, msg := range mbox.Messages {
var id uint32
if uid {
id = msg.Uid
} else {
id = uint32(i + 1)
}
if !seqset.Contains(id) {
continue
}
msg.Flags = backendutil.UpdateFlags(msg.Flags, op, flags)
}
return nil
}
func (mbox *Mailbox) CopyMessages(uid bool, seqset *imap.SeqSet, destName string) error {
dest, ok := mbox.user.mailboxes[destName]
if !ok {
return backend.ErrNoSuchMailbox
}
for i, msg := range mbox.Messages {
var id uint32
if uid {
id = msg.Uid
} else {
id = uint32(i + 1)
}
if !seqset.Contains(id) {
continue
}
msgCopy := *msg
msgCopy.Uid = dest.uidNext()
dest.Messages = append(dest.Messages, &msgCopy)
}
return nil
}
func (mbox *Mailbox) Expunge() error {
for i := len(mbox.Messages) - 1; i >= 0; i-- {
msg := mbox.Messages[i]
deleted := false
for _, flag := range msg.Flags {
if flag == imap.DeletedFlag {
deleted = true
break
}
}
if deleted {
mbox.Messages = append(mbox.Messages[:i], mbox.Messages[i+1:]...)
}
}
return nil
}

74
backend/memory/message.go Normal file
View File

@@ -0,0 +1,74 @@
package memory
import (
"bufio"
"bytes"
"io"
"time"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/backend/backendutil"
"github.com/emersion/go-message"
"github.com/emersion/go-message/textproto"
)
type Message struct {
Uid uint32
Date time.Time
Size uint32
Flags []string
Body []byte
}
func (m *Message) entity() (*message.Entity, error) {
return message.Read(bytes.NewReader(m.Body))
}
func (m *Message) headerAndBody() (textproto.Header, io.Reader, error) {
body := bufio.NewReader(bytes.NewReader(m.Body))
hdr, err := textproto.ReadHeader(body)
return hdr, body, err
}
func (m *Message) Fetch(seqNum uint32, items []imap.FetchItem) (*imap.Message, error) {
fetched := imap.NewMessage(seqNum, items)
for _, item := range items {
switch item {
case imap.FetchEnvelope:
hdr, _, _ := m.headerAndBody()
fetched.Envelope, _ = backendutil.FetchEnvelope(hdr)
case imap.FetchBody, imap.FetchBodyStructure:
hdr, body, _ := m.headerAndBody()
fetched.BodyStructure, _ = backendutil.FetchBodyStructure(hdr, body, item == imap.FetchBodyStructure)
case imap.FetchFlags:
fetched.Flags = m.Flags
case imap.FetchInternalDate:
fetched.InternalDate = m.Date
case imap.FetchRFC822Size:
fetched.Size = m.Size
case imap.FetchUid:
fetched.Uid = m.Uid
default:
section, err := imap.ParseBodySectionName(item)
if err != nil {
break
}
body := bufio.NewReader(bytes.NewReader(m.Body))
hdr, err := textproto.ReadHeader(body)
if err != nil {
return nil, err
}
l, _ := backendutil.FetchBodySection(hdr, body, section)
fetched.Body[section] = l
}
}
return fetched, nil
}
func (m *Message) Match(seqNum uint32, c *imap.SearchCriteria) (bool, error) {
e, _ := m.entity()
return backendutil.Match(e, seqNum, m.Uid, m.Date, m.Flags, c)
}

82
backend/memory/user.go Normal file
View File

@@ -0,0 +1,82 @@
package memory
import (
"errors"
"github.com/emersion/go-imap/backend"
)
type User struct {
username string
password string
mailboxes map[string]*Mailbox
}
func (u *User) Username() string {
return u.username
}
func (u *User) ListMailboxes(subscribed bool) (mailboxes []backend.Mailbox, err error) {
for _, mailbox := range u.mailboxes {
if subscribed && !mailbox.Subscribed {
continue
}
mailboxes = append(mailboxes, mailbox)
}
return
}
func (u *User) GetMailbox(name string) (mailbox backend.Mailbox, err error) {
mailbox, ok := u.mailboxes[name]
if !ok {
err = errors.New("No such mailbox")
}
return
}
func (u *User) CreateMailbox(name string) error {
if _, ok := u.mailboxes[name]; ok {
return errors.New("Mailbox already exists")
}
u.mailboxes[name] = &Mailbox{name: name, user: u}
return nil
}
func (u *User) DeleteMailbox(name string) error {
if name == "INBOX" {
return errors.New("Cannot delete INBOX")
}
if _, ok := u.mailboxes[name]; !ok {
return errors.New("No such mailbox")
}
delete(u.mailboxes, name)
return nil
}
func (u *User) RenameMailbox(existingName, newName string) error {
mbox, ok := u.mailboxes[existingName]
if !ok {
return errors.New("No such mailbox")
}
u.mailboxes[newName] = &Mailbox{
name: newName,
Messages: mbox.Messages,
user: u,
}
mbox.Messages = nil
if existingName != "INBOX" {
delete(u.mailboxes, existingName)
}
return nil
}
func (u *User) Logout() error {
return nil
}

19
backend/move.go Normal file
View File

@@ -0,0 +1,19 @@
package backend
import (
"github.com/emersion/go-imap"
)
// MoveMailbox is a mailbox that supports moving messages.
type MoveMailbox interface {
Mailbox
// Move the specified message(s) to the end of the specified destination
// mailbox. This means that a new message is created in the target mailbox
// with a new UID, the original message is removed from the source mailbox,
// and it appears to the client as a single action.
//
// If the destination mailbox does not exist, a server SHOULD return an error.
// It SHOULD NOT automatically create the mailbox.
MoveMessages(uid bool, seqset *imap.SeqSet, dest string) error
}

98
backend/updates.go Normal file
View File

@@ -0,0 +1,98 @@
package backend
import (
"github.com/emersion/go-imap"
)
// Update contains user and mailbox information about an unilateral backend
// update.
type Update interface {
// The user targeted by this update. If empty, all connected users will
// be notified.
Username() string
// The mailbox targeted by this update. If empty, the update targets all
// mailboxes.
Mailbox() string
// Done returns a channel that is closed when the update has been broadcast to
// all clients.
Done() chan struct{}
}
// NewUpdate creates a new update.
func NewUpdate(username, mailbox string) Update {
return &update{
username: username,
mailbox: mailbox,
}
}
type update struct {
username string
mailbox string
done chan struct{}
}
func (u *update) Username() string {
return u.username
}
func (u *update) Mailbox() string {
return u.mailbox
}
func (u *update) Done() chan struct{} {
if u.done == nil {
u.done = make(chan struct{})
}
return u.done
}
// StatusUpdate is a status update. See RFC 3501 section 7.1 for a list of
// status responses.
type StatusUpdate struct {
Update
*imap.StatusResp
}
// MailboxUpdate is a mailbox update.
type MailboxUpdate struct {
Update
*imap.MailboxStatus
}
// MailboxInfoUpdate is a maiblox info update.
type MailboxInfoUpdate struct {
Update
*imap.MailboxInfo
}
// MessageUpdate is a message update.
type MessageUpdate struct {
Update
*imap.Message
}
// ExpungeUpdate is an expunge update.
type ExpungeUpdate struct {
Update
SeqNum uint32
}
// BackendUpdater is a Backend that implements Updater is able to send
// unilateral backend updates. Backends not implementing this interface don't
// correctly send unilateral updates, for instance if a user logs in from two
// connections and deletes a message from one of them, the over is not aware
// that such a mesage has been deleted. More importantly, backends implementing
// Updater can notify the user for external updates such as new message
// notifications.
type BackendUpdater interface {
// Updates returns a set of channels where updates are sent to.
Updates() <-chan Update
}
// MailboxPoller is a Mailbox that is able to poll updates for new messages or
// message status updates during a period of inactivity.
type MailboxPoller interface {
// Poll requests mailbox updates.
Poll() error
}

92
backend/user.go Normal file
View File

@@ -0,0 +1,92 @@
package backend
import "errors"
var (
// ErrNoSuchMailbox is returned by User.GetMailbox, User.DeleteMailbox and
// User.RenameMailbox when retrieving, deleting or renaming a mailbox that
// doesn't exist.
ErrNoSuchMailbox = errors.New("No such mailbox")
// ErrMailboxAlreadyExists is returned by User.CreateMailbox and
// User.RenameMailbox when creating or renaming mailbox that already exists.
ErrMailboxAlreadyExists = errors.New("Mailbox already exists")
)
// User represents a user in the mail storage system. A user operation always
// deals with mailboxes.
type User interface {
// Username returns this user's username.
Username() string
// ListMailboxes returns a list of mailboxes belonging to this user. If
// subscribed is set to true, only returns subscribed mailboxes.
ListMailboxes(subscribed bool) ([]Mailbox, error)
// GetMailbox returns a mailbox. If it doesn't exist, it returns
// ErrNoSuchMailbox.
GetMailbox(name string) (Mailbox, error)
// CreateMailbox creates a new mailbox.
//
// If the mailbox already exists, an error must be returned. If the mailbox
// name is suffixed with the server's hierarchy separator character, this is a
// declaration that the client intends to create mailbox names under this name
// in the hierarchy.
//
// If the server's hierarchy separator character appears elsewhere in the
// name, the server SHOULD create any superior hierarchical names that are
// needed for the CREATE command to be successfully completed. In other
// words, an attempt to create "foo/bar/zap" on a server in which "/" is the
// hierarchy separator character SHOULD create foo/ and foo/bar/ if they do
// not already exist.
//
// If a new mailbox is created with the same name as a mailbox which was
// deleted, its unique identifiers MUST be greater than any unique identifiers
// used in the previous incarnation of the mailbox UNLESS the new incarnation
// has a different unique identifier validity value.
CreateMailbox(name string) error
// DeleteMailbox permanently remove the mailbox with the given name. It is an
// error to // attempt to delete INBOX or a mailbox name that does not exist.
//
// The DELETE command MUST NOT remove inferior hierarchical names. For
// example, if a mailbox "foo" has an inferior "foo.bar" (assuming "." is the
// hierarchy delimiter character), removing "foo" MUST NOT remove "foo.bar".
//
// The value of the highest-used unique identifier of the deleted mailbox MUST
// be preserved so that a new mailbox created with the same name will not
// reuse the identifiers of the former incarnation, UNLESS the new incarnation
// has a different unique identifier validity value.
DeleteMailbox(name string) error
// RenameMailbox changes the name of a mailbox. It is an error to attempt to
// rename from a mailbox name that does not exist or to a mailbox name that
// already exists.
//
// If the name has inferior hierarchical names, then the inferior hierarchical
// names MUST also be renamed. For example, a rename of "foo" to "zap" will
// rename "foo/bar" (assuming "/" is the hierarchy delimiter character) to
// "zap/bar".
//
// If the server's hierarchy separator character appears in the name, the
// server SHOULD create any superior hierarchical names that are needed for
// the RENAME command to complete successfully. In other words, an attempt to
// rename "foo/bar/zap" to baz/rag/zowie on a server in which "/" is the
// hierarchy separator character SHOULD create baz/ and baz/rag/ if they do
// not already exist.
//
// The value of the highest-used unique identifier of the old mailbox name
// MUST be preserved so that a new mailbox created with the same name will not
// reuse the identifiers of the former incarnation, UNLESS the new incarnation
// has a different unique identifier validity value.
//
// Renaming INBOX is permitted, and has special behavior. It moves all
// messages in INBOX to a new mailbox with the given name, leaving INBOX
// empty. If the server implementation supports inferior hierarchical names
// of INBOX, these are unaffected by a rename of INBOX.
RenameMailbox(existingName, newName string) error
// Logout is called when this User will no longer be used, likely because the
// client closed the connection.
Logout() error
}

695
client/client.go Normal file
View File

@@ -0,0 +1,695 @@
// Package client provides an IMAP client.
//
// It is not safe to use the same Client from multiple goroutines. In general,
// the IMAP protocol doesn't make it possible to send multiple independent
// IMAP commands on the same connection.
package client
import (
"crypto/tls"
"fmt"
"io"
"log"
"net"
"os"
"sync"
"time"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/commands"
"github.com/emersion/go-imap/responses"
)
// errClosed is used when a connection is closed while waiting for a command
// response.
var errClosed = fmt.Errorf("imap: connection closed")
// errUnregisterHandler is returned by a response handler to unregister itself.
var errUnregisterHandler = fmt.Errorf("imap: unregister handler")
// Update is an unilateral server update.
type Update interface {
update()
}
// StatusUpdate is delivered when a status update is received.
type StatusUpdate struct {
Status *imap.StatusResp
}
func (u *StatusUpdate) update() {}
// MailboxUpdate is delivered when a mailbox status changes.
type MailboxUpdate struct {
Mailbox *imap.MailboxStatus
}
func (u *MailboxUpdate) update() {}
// ExpungeUpdate is delivered when a message is deleted.
type ExpungeUpdate struct {
SeqNum uint32
}
func (u *ExpungeUpdate) update() {}
// MessageUpdate is delivered when a message attribute changes.
type MessageUpdate struct {
Message *imap.Message
}
func (u *MessageUpdate) update() {}
// Client is an IMAP client.
type Client struct {
conn *imap.Conn
isTLS bool
serverName string
loggedOut chan struct{}
continues chan<- bool
upgrading bool
handlers []responses.Handler
handlersLocker sync.Mutex
// The current connection state.
state imap.ConnState
// The selected mailbox, if there is one.
mailbox *imap.MailboxStatus
// The cached server capabilities.
caps map[string]bool
// state, mailbox and caps may be accessed in different goroutines. Protect
// access.
locker sync.Mutex
// This flag is set when the first search query fails with a BADCHARSET
// error. Subsequent queries will be performed with the US-ASCII
// charset. According to RFC 3501, SEARCH must only support US-ASCII;
// other charsets are optional.
utf8SearchUnsupported bool
// A channel to which unilateral updates from the server will be sent. An
// update can be one of: *StatusUpdate, *MailboxUpdate, *MessageUpdate,
// *ExpungeUpdate. Note that blocking this channel blocks the whole client,
// so it's recommended to use a separate goroutine and a buffered channel to
// prevent deadlocks.
Updates chan<- Update
// ErrorLog specifies an optional logger for errors accepting connections and
// unexpected behavior from handlers. By default, logging goes to os.Stderr
// via the log package's standard logger. The logger must be safe to use
// simultaneously from multiple goroutines.
ErrorLog imap.Logger
// Timeout specifies a maximum amount of time to wait on a command.
//
// A Timeout of zero means no timeout. This is the default.
Timeout time.Duration
}
func (c *Client) registerHandler(h responses.Handler) {
if h == nil {
return
}
c.handlersLocker.Lock()
c.handlers = append(c.handlers, h)
c.handlersLocker.Unlock()
}
func (c *Client) handle(resp imap.Resp) error {
c.handlersLocker.Lock()
for i := len(c.handlers) - 1; i >= 0; i-- {
if err := c.handlers[i].Handle(resp); err != responses.ErrUnhandled {
if err == errUnregisterHandler {
c.handlers = append(c.handlers[:i], c.handlers[i+1:]...)
err = nil
}
c.handlersLocker.Unlock()
return err
}
}
c.handlersLocker.Unlock()
return responses.ErrUnhandled
}
func (c *Client) reader() {
defer close(c.loggedOut)
// Loop while connected.
for {
connected, err := c.readOnce()
if err != nil {
c.ErrorLog.Println("error reading response:", err)
}
if !connected {
return
}
}
}
func (c *Client) readOnce() (bool, error) {
if c.State() == imap.LogoutState {
return false, nil
}
resp, err := imap.ReadResp(c.conn.Reader)
if err == io.EOF || c.State() == imap.LogoutState {
return false, nil
} else if err != nil {
if imap.IsParseError(err) {
return true, err
} else {
return false, err
}
}
if err := c.handle(resp); err != nil && err != responses.ErrUnhandled {
c.ErrorLog.Println("cannot handle response ", resp, err)
}
return true, nil
}
func (c *Client) writeReply(reply []byte) error {
if _, err := c.conn.Writer.Write(reply); err != nil {
return err
}
// Flush reply
return c.conn.Writer.Flush()
}
type handleResult struct {
status *imap.StatusResp
err error
}
func (c *Client) execute(cmdr imap.Commander, h responses.Handler) (*imap.StatusResp, error) {
cmd := cmdr.Command()
cmd.Tag = generateTag()
var replies <-chan []byte
if replier, ok := h.(responses.Replier); ok {
replies = replier.Replies()
}
if c.Timeout > 0 {
err := c.conn.SetDeadline(time.Now().Add(c.Timeout))
if err != nil {
return nil, err
}
} else {
// It's possible the client had a timeout set from a previous command, but no
// longer does. Ensure we respect that. The zero time means no deadline.
if err := c.conn.SetDeadline(time.Time{}); err != nil {
return nil, err
}
}
// Check if we are upgrading.
upgrading := c.upgrading
// Add handler before sending command, to be sure to get the response in time
// (in tests, the response is sent right after our command is received, so
// sometimes the response was received before the setup of this handler)
doneHandle := make(chan handleResult, 1)
unregister := make(chan struct{})
c.registerHandler(responses.HandlerFunc(func(resp imap.Resp) error {
select {
case <-unregister:
// If an error occured while sending the command, abort
return errUnregisterHandler
default:
}
if s, ok := resp.(*imap.StatusResp); ok && s.Tag == cmd.Tag {
// This is the command's status response, we're done
doneHandle <- handleResult{s, nil}
// Special handling of connection upgrading.
if upgrading {
c.upgrading = false
// Wait for upgrade to finish.
c.conn.Wait()
}
// Cancel any pending literal write
select {
case c.continues <- false:
default:
}
return errUnregisterHandler
}
if h != nil {
// Pass the response to the response handler
if err := h.Handle(resp); err != nil && err != responses.ErrUnhandled {
// If the response handler returns an error, abort
doneHandle <- handleResult{nil, err}
return errUnregisterHandler
} else {
return err
}
}
return responses.ErrUnhandled
}))
// Send the command to the server
if err := cmd.WriteTo(c.conn.Writer); err != nil {
// Error while sending the command
close(unregister)
if err, ok := err.(imap.LiteralLengthErr); ok {
// Expected > Actual
// The server is waiting for us to write
// more bytes, we don't have them. Run.
// Expected < Actual
// We are about to send a potentially truncated message, we don't
// want this (ths terminating CRLF is not sent at this point).
c.conn.Close()
return nil, err
}
return nil, err
}
// Flush writer if we are upgrading
if upgrading {
if err := c.conn.Writer.Flush(); err != nil {
// Error while sending the command
close(unregister)
return nil, err
}
}
for {
select {
case reply := <-replies:
// Response handler needs to send a reply (Used for AUTHENTICATE)
if err := c.writeReply(reply); err != nil {
close(unregister)
return nil, err
}
case <-c.loggedOut:
// If the connection is closed (such as from an I/O error), ensure we
// realize this and don't block waiting on a response that will never
// come. loggedOut is a channel that closes when the reader goroutine
// ends.
close(unregister)
return nil, errClosed
case result := <-doneHandle:
return result.status, result.err
}
}
}
// State returns the current connection state.
func (c *Client) State() imap.ConnState {
c.locker.Lock()
state := c.state
c.locker.Unlock()
return state
}
// Mailbox returns the selected mailbox. It returns nil if there isn't one.
func (c *Client) Mailbox() *imap.MailboxStatus {
// c.Mailbox fields are not supposed to change, so we can return the pointer.
c.locker.Lock()
mbox := c.mailbox
c.locker.Unlock()
return mbox
}
// SetState sets this connection's internal state.
//
// This function should not be called directly, it must only be used by
// libraries implementing extensions of the IMAP protocol.
func (c *Client) SetState(state imap.ConnState, mailbox *imap.MailboxStatus) {
c.locker.Lock()
c.state = state
c.mailbox = mailbox
c.locker.Unlock()
}
// Execute executes a generic command. cmdr is a value that can be converted to
// a raw command and h is a response handler. The function returns when the
// command has completed or failed, in this case err is nil. A non-nil err value
// indicates a network error.
//
// This function should not be called directly, it must only be used by
// libraries implementing extensions of the IMAP protocol.
func (c *Client) Execute(cmdr imap.Commander, h responses.Handler) (*imap.StatusResp, error) {
return c.execute(cmdr, h)
}
func (c *Client) handleContinuationReqs() {
c.registerHandler(responses.HandlerFunc(func(resp imap.Resp) error {
if _, ok := resp.(*imap.ContinuationReq); ok {
go func() {
c.continues <- true
}()
return nil
}
return responses.ErrUnhandled
}))
}
func (c *Client) gotStatusCaps(args []interface{}) {
c.locker.Lock()
c.caps = make(map[string]bool)
for _, cap := range args {
if cap, ok := cap.(string); ok {
c.caps[cap] = true
}
}
c.locker.Unlock()
}
// The server can send unilateral data. This function handles it.
func (c *Client) handleUnilateral() {
c.registerHandler(responses.HandlerFunc(func(resp imap.Resp) error {
switch resp := resp.(type) {
case *imap.StatusResp:
if resp.Tag != "*" {
return responses.ErrUnhandled
}
switch resp.Type {
case imap.StatusRespOk, imap.StatusRespNo, imap.StatusRespBad:
if c.Updates != nil {
c.Updates <- &StatusUpdate{resp}
}
case imap.StatusRespBye:
c.locker.Lock()
c.state = imap.LogoutState
c.mailbox = nil
c.locker.Unlock()
c.conn.Close()
if c.Updates != nil {
c.Updates <- &StatusUpdate{resp}
}
default:
return responses.ErrUnhandled
}
case *imap.DataResp:
name, fields, ok := imap.ParseNamedResp(resp)
if !ok {
return responses.ErrUnhandled
}
switch name {
case "CAPABILITY":
c.gotStatusCaps(fields)
case "EXISTS":
if c.Mailbox() == nil {
break
}
if messages, err := imap.ParseNumber(fields[0]); err == nil {
c.locker.Lock()
c.mailbox.Messages = messages
c.locker.Unlock()
c.mailbox.ItemsLocker.Lock()
c.mailbox.Items[imap.StatusMessages] = nil
c.mailbox.ItemsLocker.Unlock()
}
if c.Updates != nil {
c.Updates <- &MailboxUpdate{c.Mailbox()}
}
case "RECENT":
if c.Mailbox() == nil {
break
}
if recent, err := imap.ParseNumber(fields[0]); err == nil {
c.locker.Lock()
c.mailbox.Recent = recent
c.locker.Unlock()
c.mailbox.ItemsLocker.Lock()
c.mailbox.Items[imap.StatusRecent] = nil
c.mailbox.ItemsLocker.Unlock()
}
if c.Updates != nil {
c.Updates <- &MailboxUpdate{c.Mailbox()}
}
case "EXPUNGE":
seqNum, _ := imap.ParseNumber(fields[0])
if c.Updates != nil {
c.Updates <- &ExpungeUpdate{seqNum}
}
case "FETCH":
seqNum, _ := imap.ParseNumber(fields[0])
fields, _ := fields[1].([]interface{})
msg := &imap.Message{SeqNum: seqNum}
if err := msg.Parse(fields); err != nil {
break
}
if c.Updates != nil {
c.Updates <- &MessageUpdate{msg}
}
default:
return responses.ErrUnhandled
}
default:
return responses.ErrUnhandled
}
return nil
}))
}
func (c *Client) handleGreetAndStartReading() error {
var greetErr error
gotGreet := false
c.registerHandler(responses.HandlerFunc(func(resp imap.Resp) error {
status, ok := resp.(*imap.StatusResp)
if !ok {
greetErr = fmt.Errorf("invalid greeting received from server: not a status response")
return errUnregisterHandler
}
c.locker.Lock()
switch status.Type {
case imap.StatusRespPreauth:
c.state = imap.AuthenticatedState
case imap.StatusRespBye:
c.state = imap.LogoutState
case imap.StatusRespOk:
c.state = imap.NotAuthenticatedState
default:
c.state = imap.LogoutState
c.locker.Unlock()
greetErr = fmt.Errorf("invalid greeting received from server: %v", status.Type)
return errUnregisterHandler
}
c.locker.Unlock()
if status.Code == imap.CodeCapability {
c.gotStatusCaps(status.Arguments)
}
gotGreet = true
return errUnregisterHandler
}))
// call `readOnce` until we get the greeting or an error
for !gotGreet {
connected, err := c.readOnce()
// Check for read errors
if err != nil {
// return read errors
return err
}
// Check for invalid greet
if greetErr != nil {
// return read errors
return greetErr
}
// Check if connection was closed.
if !connected {
// connection closed.
return io.EOF
}
}
// We got the greeting, now start the reader goroutine.
go c.reader()
return nil
}
// Upgrade a connection, e.g. wrap an unencrypted connection with an encrypted
// tunnel.
//
// This function should not be called directly, it must only be used by
// libraries implementing extensions of the IMAP protocol.
func (c *Client) Upgrade(upgrader imap.ConnUpgrader) error {
return c.conn.Upgrade(upgrader)
}
// Writer returns the imap.Writer for this client's connection.
//
// This function should not be called directly, it must only be used by
// libraries implementing extensions of the IMAP protocol.
func (c *Client) Writer() *imap.Writer {
return c.conn.Writer
}
// IsTLS checks if this client's connection has TLS enabled.
func (c *Client) IsTLS() bool {
return c.isTLS
}
// LoggedOut returns a channel which is closed when the connection to the server
// is closed.
func (c *Client) LoggedOut() <-chan struct{} {
return c.loggedOut
}
// SetDebug defines an io.Writer to which all network activity will be logged.
// If nil is provided, network activity will not be logged.
func (c *Client) SetDebug(w io.Writer) {
// Need to send a command to unblock the reader goroutine.
cmd := new(commands.Noop)
err := c.Upgrade(func(conn net.Conn) (net.Conn, error) {
// Flag connection as in upgrading
c.upgrading = true
if status, err := c.execute(cmd, nil); err != nil {
return nil, err
} else if err := status.Err(); err != nil {
return nil, err
}
// Wait for reader to block.
c.conn.WaitReady()
c.conn.SetDebug(w)
return conn, nil
})
if err != nil {
log.Println("SetDebug:", err)
}
}
// New creates a new client from an existing connection.
func New(conn net.Conn) (*Client, error) {
continues := make(chan bool)
w := imap.NewClientWriter(nil, continues)
r := imap.NewReader(nil)
c := &Client{
conn: imap.NewConn(conn, r, w),
loggedOut: make(chan struct{}),
continues: continues,
state: imap.ConnectingState,
ErrorLog: log.New(os.Stderr, "imap/client: ", log.LstdFlags),
}
c.handleContinuationReqs()
c.handleUnilateral()
if err := c.handleGreetAndStartReading(); err != nil {
return c, err
}
plusOk, _ := c.Support("LITERAL+")
minusOk, _ := c.Support("LITERAL-")
// We don't use non-sync literal if it is bigger than 4096 bytes, so
// LITERAL- is fine too.
c.conn.AllowAsyncLiterals = plusOk || minusOk
return c, nil
}
// Dial connects to an IMAP server using an unencrypted connection.
func Dial(addr string) (*Client, error) {
return DialWithDialer(new(net.Dialer), addr)
}
type Dialer interface {
// Dial connects to the given address.
Dial(network, addr string) (net.Conn, error)
}
// DialWithDialer connects to an IMAP server using an unencrypted connection
// using dialer.Dial.
//
// Among other uses, this allows to apply a dial timeout.
func DialWithDialer(dialer Dialer, addr string) (*Client, error) {
conn, err := dialer.Dial("tcp", addr)
if err != nil {
return nil, err
}
// We don't return to the caller until we try to receive a greeting. As such,
// there is no way to set the client's Timeout for that action. As a
// workaround, if the dialer has a timeout set, use that for the connection's
// deadline.
if netDialer, ok := dialer.(*net.Dialer); ok && netDialer.Timeout > 0 {
err := conn.SetDeadline(time.Now().Add(netDialer.Timeout))
if err != nil {
return nil, err
}
}
c, err := New(conn)
if err != nil {
return nil, err
}
c.serverName, _, _ = net.SplitHostPort(addr)
return c, nil
}
// DialTLS connects to an IMAP server using an encrypted connection.
func DialTLS(addr string, tlsConfig *tls.Config) (*Client, error) {
return DialWithDialerTLS(new(net.Dialer), addr, tlsConfig)
}
// DialWithDialerTLS connects to an IMAP server using an encrypted connection
// using dialer.Dial.
//
// Among other uses, this allows to apply a dial timeout.
func DialWithDialerTLS(dialer Dialer, addr string, tlsConfig *tls.Config) (*Client, error) {
conn, err := dialer.Dial("tcp", addr)
if err != nil {
return nil, err
}
serverName, _, _ := net.SplitHostPort(addr)
if tlsConfig == nil {
tlsConfig = &tls.Config{}
}
if tlsConfig.ServerName == "" {
tlsConfig = tlsConfig.Clone()
tlsConfig.ServerName = serverName
}
tlsConn := tls.Client(conn, tlsConfig)
// We don't return to the caller until we try to receive a greeting. As such,
// there is no way to set the client's Timeout for that action. As a
// workaround, if the dialer has a timeout set, use that for the connection's
// deadline.
if netDialer, ok := dialer.(*net.Dialer); ok && netDialer.Timeout > 0 {
err := tlsConn.SetDeadline(time.Now().Add(netDialer.Timeout))
if err != nil {
return nil, err
}
}
c, err := New(tlsConn)
if err != nil {
return nil, err
}
c.isTLS = true
c.serverName = serverName
return c, nil
}

187
client/client_test.go Normal file
View File

@@ -0,0 +1,187 @@
package client
import (
"bufio"
"bytes"
"io"
"net"
"strings"
"testing"
"github.com/emersion/go-imap"
)
type cmdScanner struct {
scanner *bufio.Scanner
}
func (s *cmdScanner) ScanLine() string {
s.scanner.Scan()
return s.scanner.Text()
}
func (s *cmdScanner) ScanCmd() (tag string, cmd string) {
parts := strings.SplitN(s.ScanLine(), " ", 2)
return parts[0], parts[1]
}
func newCmdScanner(r io.Reader) *cmdScanner {
return &cmdScanner{
scanner: bufio.NewScanner(r),
}
}
type serverConn struct {
*cmdScanner
net.Conn
net.Listener
}
func (c *serverConn) Close() error {
if err := c.Conn.Close(); err != nil {
return err
}
return c.Listener.Close()
}
func (c *serverConn) WriteString(s string) (n int, err error) {
return io.WriteString(c.Conn, s)
}
func newTestClient(t *testing.T) (c *Client, s *serverConn) {
return newTestClientWithGreeting(t, "* OK [CAPABILITY IMAP4rev1 STARTTLS AUTH=PLAIN UNSELECT] Server ready.\r\n")
}
func newTestClientWithGreeting(t *testing.T, greeting string) (c *Client, s *serverConn) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
done := make(chan struct{})
go func() {
conn, err := l.Accept()
if err != nil {
panic(err)
}
if _, err := io.WriteString(conn, greeting); err != nil {
panic(err)
}
s = &serverConn{newCmdScanner(conn), conn, l}
close(done)
}()
c, err = Dial(l.Addr().String())
if err != nil {
t.Fatal(err)
}
<-done
return
}
func setClientState(c *Client, state imap.ConnState, mailbox *imap.MailboxStatus) {
c.locker.Lock()
c.state = state
c.mailbox = mailbox
c.locker.Unlock()
}
func TestClient(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
if ok, err := c.Support("IMAP4rev1"); err != nil {
t.Fatal("c.Support(IMAP4rev1) =", err)
} else if !ok {
t.Fatal("c.Support(IMAP4rev1) = false, want true")
}
}
func TestClient_SetDebug(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
var b bytes.Buffer
done := make(chan error)
go func() {
c.SetDebug(&b)
done <- nil
}()
if tag, cmd := s.ScanCmd(); cmd != "NOOP" {
t.Fatal("Bad command:", cmd)
} else {
s.WriteString(tag + " OK NOOP completed.\r\n")
}
// wait for SetDebug to finish.
<-done
go func() {
_, err := c.Capability()
done <- err
}()
tag, cmd := s.ScanCmd()
if cmd != "CAPABILITY" {
t.Fatal("Bad command:", cmd)
}
s.WriteString("* CAPABILITY IMAP4rev1\r\n")
s.WriteString(tag + " OK CAPABILITY completed.\r\n")
if err := <-done; err != nil {
t.Fatal("c.Capability() =", err)
}
if b.Len() == 0 {
t.Error("empty debug buffer")
}
}
func TestClient_unilateral(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, imap.NewMailboxStatus("INBOX", nil))
updates := make(chan Update, 1)
c.Updates = updates
s.WriteString("* 42 EXISTS\r\n")
if update, ok := (<-updates).(*MailboxUpdate); !ok || update.Mailbox.Messages != 42 {
t.Errorf("Invalid messages count: expected %v but got %v", 42, update.Mailbox.Messages)
}
s.WriteString("* 587 RECENT\r\n")
if update, ok := (<-updates).(*MailboxUpdate); !ok || update.Mailbox.Recent != 587 {
t.Errorf("Invalid recent count: expected %v but got %v", 587, update.Mailbox.Recent)
}
s.WriteString("* 65535 EXPUNGE\r\n")
if update, ok := (<-updates).(*ExpungeUpdate); !ok || update.SeqNum != 65535 {
t.Errorf("Invalid expunged sequence number: expected %v but got %v", 65535, update.SeqNum)
}
s.WriteString("* 431 FETCH (FLAGS (\\Seen))\r\n")
if update, ok := (<-updates).(*MessageUpdate); !ok || update.Message.SeqNum != 431 {
t.Errorf("Invalid expunged sequence number: expected %v but got %v", 431, update.Message.SeqNum)
}
s.WriteString("* OK Reticulating splines...\r\n")
if update, ok := (<-updates).(*StatusUpdate); !ok || update.Status.Info != "Reticulating splines..." {
t.Errorf("Invalid info: got %v", update.Status.Info)
}
s.WriteString("* NO Kansai band competition is in 30 seconds !\r\n")
if update, ok := (<-updates).(*StatusUpdate); !ok || update.Status.Info != "Kansai band competition is in 30 seconds !" {
t.Errorf("Invalid warning: got %v", update.Status.Info)
}
s.WriteString("* BAD Battery level too low, shutting down.\r\n")
if update, ok := (<-updates).(*StatusUpdate); !ok || update.Status.Info != "Battery level too low, shutting down." {
t.Errorf("Invalid error: got %v", update.Status.Info)
}
}

88
client/cmd_any.go Normal file
View File

@@ -0,0 +1,88 @@
package client
import (
"errors"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/commands"
)
// ErrAlreadyLoggedOut is returned if Logout is called when the client is
// already logged out.
var ErrAlreadyLoggedOut = errors.New("Already logged out")
// Capability requests a listing of capabilities that the server supports.
// Capabilities are often returned by the server with the greeting or with the
// STARTTLS and LOGIN responses, so usually explicitly requesting capabilities
// isn't needed.
//
// Most of the time, Support should be used instead.
func (c *Client) Capability() (map[string]bool, error) {
cmd := &commands.Capability{}
if status, err := c.execute(cmd, nil); err != nil {
return nil, err
} else if err := status.Err(); err != nil {
return nil, err
}
c.locker.Lock()
caps := c.caps
c.locker.Unlock()
return caps, nil
}
// Support checks if cap is a capability supported by the server. If the server
// hasn't sent its capabilities yet, Support requests them.
func (c *Client) Support(cap string) (bool, error) {
c.locker.Lock()
ok := c.caps != nil
c.locker.Unlock()
// If capabilities are not cached, request them
if !ok {
if _, err := c.Capability(); err != nil {
return false, err
}
}
c.locker.Lock()
supported := c.caps[cap]
c.locker.Unlock()
return supported, nil
}
// Noop always succeeds and does nothing.
//
// It can be used as a periodic poll for new messages or message status updates
// during a period of inactivity. It can also be used to reset any inactivity
// autologout timer on the server.
func (c *Client) Noop() error {
cmd := new(commands.Noop)
status, err := c.execute(cmd, nil)
if err != nil {
return err
}
return status.Err()
}
// Logout gracefully closes the connection.
func (c *Client) Logout() error {
if c.State() == imap.LogoutState {
return ErrAlreadyLoggedOut
}
cmd := new(commands.Logout)
if status, err := c.execute(cmd, nil); err == errClosed {
// Server closed connection, that's what we want anyway
return nil
} else if err != nil {
return err
} else if status != nil {
return status.Err()
}
return nil
}

80
client/cmd_any_test.go Normal file
View File

@@ -0,0 +1,80 @@
package client
import (
"testing"
"github.com/emersion/go-imap"
)
func TestClient_Capability(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
var caps map[string]bool
done := make(chan error, 1)
go func() {
var err error
caps, err = c.Capability()
done <- err
}()
tag, cmd := s.ScanCmd()
if cmd != "CAPABILITY" {
t.Fatalf("client sent command %v, want CAPABILITY", cmd)
}
s.WriteString("* CAPABILITY IMAP4rev1 XTEST\r\n")
s.WriteString(tag + " OK CAPABILITY completed.\r\n")
if err := <-done; err != nil {
t.Error("c.Capability() = ", err)
}
if !caps["XTEST"] {
t.Error("XTEST capability missing")
}
}
func TestClient_Noop(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
done := make(chan error, 1)
go func() {
done <- c.Noop()
}()
tag, cmd := s.ScanCmd()
if cmd != "NOOP" {
t.Fatalf("client sent command %v, want NOOP", cmd)
}
s.WriteString(tag + " OK NOOP completed\r\n")
if err := <-done; err != nil {
t.Error("c.Noop() = ", err)
}
}
func TestClient_Logout(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
done := make(chan error, 1)
go func() {
done <- c.Logout()
}()
tag, cmd := s.ScanCmd()
if cmd != "LOGOUT" {
t.Fatalf("client sent command %v, want LOGOUT", cmd)
}
s.WriteString("* BYE Client asked to close the connection.\r\n")
s.WriteString(tag + " OK LOGOUT completed\r\n")
if err := <-done; err != nil {
t.Error("c.Logout() =", err)
}
if state := c.State(); state != imap.LogoutState {
t.Errorf("c.State() = %v, want %v", state, imap.LogoutState)
}
}

380
client/cmd_auth.go Normal file
View File

@@ -0,0 +1,380 @@
package client
import (
"errors"
"time"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/commands"
"github.com/emersion/go-imap/responses"
)
// ErrNotLoggedIn is returned if a function that requires the client to be
// logged in is called then the client isn't.
var ErrNotLoggedIn = errors.New("Not logged in")
func (c *Client) ensureAuthenticated() error {
state := c.State()
if state != imap.AuthenticatedState && state != imap.SelectedState {
return ErrNotLoggedIn
}
return nil
}
// Select selects a mailbox so that messages in the mailbox can be accessed. Any
// currently selected mailbox is deselected before attempting the new selection.
// Even if the readOnly parameter is set to false, the server can decide to open
// the mailbox in read-only mode.
func (c *Client) Select(name string, readOnly bool) (*imap.MailboxStatus, error) {
if err := c.ensureAuthenticated(); err != nil {
return nil, err
}
cmd := &commands.Select{
Mailbox: name,
ReadOnly: readOnly,
}
mbox := &imap.MailboxStatus{Name: name, Items: make(map[imap.StatusItem]interface{})}
res := &responses.Select{
Mailbox: mbox,
}
c.locker.Lock()
c.mailbox = mbox
c.locker.Unlock()
status, err := c.execute(cmd, res)
if err != nil {
c.locker.Lock()
c.mailbox = nil
c.locker.Unlock()
return nil, err
}
if err := status.Err(); err != nil {
c.locker.Lock()
c.mailbox = nil
c.locker.Unlock()
return nil, err
}
c.locker.Lock()
mbox.ReadOnly = (status.Code == imap.CodeReadOnly)
c.state = imap.SelectedState
c.locker.Unlock()
return mbox, nil
}
// Create creates a mailbox with the given name.
func (c *Client) Create(name string) error {
if err := c.ensureAuthenticated(); err != nil {
return err
}
cmd := &commands.Create{
Mailbox: name,
}
status, err := c.execute(cmd, nil)
if err != nil {
return err
}
return status.Err()
}
// Delete permanently removes the mailbox with the given name.
func (c *Client) Delete(name string) error {
if err := c.ensureAuthenticated(); err != nil {
return err
}
cmd := &commands.Delete{
Mailbox: name,
}
status, err := c.execute(cmd, nil)
if err != nil {
return err
}
return status.Err()
}
// Rename changes the name of a mailbox.
func (c *Client) Rename(existingName, newName string) error {
if err := c.ensureAuthenticated(); err != nil {
return err
}
cmd := &commands.Rename{
Existing: existingName,
New: newName,
}
status, err := c.execute(cmd, nil)
if err != nil {
return err
}
return status.Err()
}
// Subscribe adds the specified mailbox name to the server's set of "active" or
// "subscribed" mailboxes.
func (c *Client) Subscribe(name string) error {
if err := c.ensureAuthenticated(); err != nil {
return err
}
cmd := &commands.Subscribe{
Mailbox: name,
}
status, err := c.execute(cmd, nil)
if err != nil {
return err
}
return status.Err()
}
// Unsubscribe removes the specified mailbox name from the server's set of
// "active" or "subscribed" mailboxes.
func (c *Client) Unsubscribe(name string) error {
if err := c.ensureAuthenticated(); err != nil {
return err
}
cmd := &commands.Unsubscribe{
Mailbox: name,
}
status, err := c.execute(cmd, nil)
if err != nil {
return err
}
return status.Err()
}
// List returns a subset of names from the complete set of all names available
// to the client.
//
// An empty name argument is a special request to return the hierarchy delimiter
// and the root name of the name given in the reference. The character "*" is a
// wildcard, and matches zero or more characters at this position. The
// character "%" is similar to "*", but it does not match a hierarchy delimiter.
func (c *Client) List(ref, name string, ch chan *imap.MailboxInfo) error {
defer close(ch)
if err := c.ensureAuthenticated(); err != nil {
return err
}
cmd := &commands.List{
Reference: ref,
Mailbox: name,
}
res := &responses.List{Mailboxes: ch}
status, err := c.execute(cmd, res)
if err != nil {
return err
}
return status.Err()
}
// Lsub returns a subset of names from the set of names that the user has
// declared as being "active" or "subscribed".
func (c *Client) Lsub(ref, name string, ch chan *imap.MailboxInfo) error {
defer close(ch)
if err := c.ensureAuthenticated(); err != nil {
return err
}
cmd := &commands.List{
Reference: ref,
Mailbox: name,
Subscribed: true,
}
res := &responses.List{
Mailboxes: ch,
Subscribed: true,
}
status, err := c.execute(cmd, res)
if err != nil {
return err
}
return status.Err()
}
// Status requests the status of the indicated mailbox. It does not change the
// currently selected mailbox, nor does it affect the state of any messages in
// the queried mailbox.
//
// See RFC 3501 section 6.3.10 for a list of items that can be requested.
func (c *Client) Status(name string, items []imap.StatusItem) (*imap.MailboxStatus, error) {
if err := c.ensureAuthenticated(); err != nil {
return nil, err
}
cmd := &commands.Status{
Mailbox: name,
Items: items,
}
res := &responses.Status{
Mailbox: new(imap.MailboxStatus),
}
status, err := c.execute(cmd, res)
if err != nil {
return nil, err
}
return res.Mailbox, status.Err()
}
// Append appends the literal argument as a new message to the end of the
// specified destination mailbox. This argument SHOULD be in the format of an
// RFC 2822 message. flags and date are optional arguments and can be set to
// nil and the empty struct.
func (c *Client) Append(mbox string, flags []string, date time.Time, msg imap.Literal) error {
if err := c.ensureAuthenticated(); err != nil {
return err
}
cmd := &commands.Append{
Mailbox: mbox,
Flags: flags,
Date: date,
Message: msg,
}
status, err := c.execute(cmd, nil)
if err != nil {
return err
}
return status.Err()
}
// Enable requests the server to enable the named extensions. The extensions
// which were successfully enabled are returned.
//
// See RFC 5161 section 3.1.
func (c *Client) Enable(caps []string) ([]string, error) {
if ok, err := c.Support("ENABLE"); !ok || err != nil {
return nil, ErrExtensionUnsupported
}
// ENABLE is invalid if a mailbox has been selected.
if c.State() != imap.AuthenticatedState {
return nil, ErrNotLoggedIn
}
cmd := &commands.Enable{Caps: caps}
res := &responses.Enabled{}
if status, err := c.Execute(cmd, res); err != nil {
return nil, err
} else {
return res.Caps, status.Err()
}
}
func (c *Client) idle(stop <-chan struct{}) error {
cmd := &commands.Idle{}
res := &responses.Idle{
Stop: stop,
RepliesCh: make(chan []byte, 10),
}
if status, err := c.Execute(cmd, res); err != nil {
return err
} else {
return status.Err()
}
}
// IdleOptions holds options for Client.Idle.
type IdleOptions struct {
// LogoutTimeout is used to avoid being logged out by the server when
// idling. Each LogoutTimeout, the IDLE command is restarted. If set to
// zero, a default is used. If negative, this behavior is disabled.
LogoutTimeout time.Duration
// Poll interval when the server doesn't support IDLE. If zero, a default
// is used. If negative, polling is always disabled.
PollInterval time.Duration
}
// Idle indicates to the server that the client is ready to receive unsolicited
// mailbox update messages. When the client wants to send commands again, it
// must first close stop.
//
// If the server doesn't support IDLE, go-imap falls back to polling.
func (c *Client) Idle(stop <-chan struct{}, opts *IdleOptions) error {
if ok, err := c.Support("IDLE"); err != nil {
return err
} else if !ok {
return c.idleFallback(stop, opts)
}
logoutTimeout := 25 * time.Minute
if opts != nil {
if opts.LogoutTimeout > 0 {
logoutTimeout = opts.LogoutTimeout
} else if opts.LogoutTimeout < 0 {
return c.idle(stop)
}
}
t := time.NewTicker(logoutTimeout)
defer t.Stop()
for {
stopOrRestart := make(chan struct{})
done := make(chan error, 1)
go func() {
done <- c.idle(stopOrRestart)
}()
select {
case <-t.C:
close(stopOrRestart)
if err := <-done; err != nil {
return err
}
case <-stop:
close(stopOrRestart)
return <-done
case err := <-done:
close(stopOrRestart)
if err != nil {
return err
}
}
}
}
func (c *Client) idleFallback(stop <-chan struct{}, opts *IdleOptions) error {
pollInterval := time.Minute
if opts != nil {
if opts.PollInterval > 0 {
pollInterval = opts.PollInterval
} else if opts.PollInterval < 0 {
return ErrExtensionUnsupported
}
}
t := time.NewTicker(pollInterval)
defer t.Stop()
for {
select {
case <-t.C:
if err := c.Noop(); err != nil {
return err
}
case <-stop:
return nil
case <-c.LoggedOut():
return errors.New("disconnected while idling")
}
}
}

499
client/cmd_auth_test.go Normal file
View File

@@ -0,0 +1,499 @@
package client
import (
"bytes"
"io"
"reflect"
"testing"
"time"
"github.com/emersion/go-imap"
)
func TestClient_Select(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.AuthenticatedState, nil)
var mbox *imap.MailboxStatus
done := make(chan error, 1)
go func() {
var err error
mbox, err = c.Select("INBOX", false)
done <- err
}()
tag, cmd := s.ScanCmd()
if cmd != "SELECT INBOX" {
t.Fatalf("client sent command %v, want SELECT \"INBOX\"", cmd)
}
s.WriteString("* 172 EXISTS\r\n")
s.WriteString("* 1 RECENT\r\n")
s.WriteString("* OK [UNSEEN 12] Message 12 is first unseen\r\n")
s.WriteString("* OK [UIDVALIDITY 3857529045] UIDs valid\r\n")
s.WriteString("* OK [UIDNEXT 4392] Predicted next UID\r\n")
s.WriteString("* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n")
s.WriteString("* OK [PERMANENTFLAGS (\\Deleted \\Seen \\*)] Limited\r\n")
s.WriteString(tag + " OK SELECT completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Select() = %v", err)
}
want := &imap.MailboxStatus{
Name: "INBOX",
ReadOnly: false,
Flags: []string{imap.AnsweredFlag, imap.FlaggedFlag, imap.DeletedFlag, imap.SeenFlag, imap.DraftFlag},
PermanentFlags: []string{imap.DeletedFlag, imap.SeenFlag, "\\*"},
UnseenSeqNum: 12,
Messages: 172,
Recent: 1,
UidNext: 4392,
UidValidity: 3857529045,
}
mbox.Items = nil
if !reflect.DeepEqual(mbox, want) {
t.Errorf("c.Select() = \n%+v\n want \n%+v", mbox, want)
}
}
func TestClient_Select_ReadOnly(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.AuthenticatedState, nil)
var mbox *imap.MailboxStatus
done := make(chan error, 1)
go func() {
var err error
mbox, err = c.Select("INBOX", true)
done <- err
}()
tag, cmd := s.ScanCmd()
if cmd != "EXAMINE INBOX" {
t.Fatalf("client sent command %v, want EXAMINE \"INBOX\"", cmd)
}
s.WriteString(tag + " OK [READ-ONLY] EXAMINE completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Select() = %v", err)
}
if !mbox.ReadOnly {
t.Errorf("c.Select().ReadOnly = false, want true")
}
}
func TestClient_Create(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.AuthenticatedState, nil)
done := make(chan error, 1)
go func() {
done <- c.Create("New Mailbox")
}()
tag, cmd := s.ScanCmd()
if cmd != "CREATE \"New Mailbox\"" {
t.Fatalf("client sent command %v, want %v", cmd, "CREATE \"New Mailbox\"")
}
s.WriteString(tag + " OK CREATE completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Create() = %v", err)
}
}
func TestClient_Delete(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.AuthenticatedState, nil)
done := make(chan error, 1)
go func() {
done <- c.Delete("Old Mailbox")
}()
tag, cmd := s.ScanCmd()
if cmd != "DELETE \"Old Mailbox\"" {
t.Fatalf("client sent command %v, want %v", cmd, "DELETE \"Old Mailbox\"")
}
s.WriteString(tag + " OK DELETE completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Delete() = %v", err)
}
}
func TestClient_Rename(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.AuthenticatedState, nil)
done := make(chan error, 1)
go func() {
done <- c.Rename("Old Mailbox", "New Mailbox")
}()
tag, cmd := s.ScanCmd()
if cmd != "RENAME \"Old Mailbox\" \"New Mailbox\"" {
t.Fatalf("client sent command %v, want %v", cmd, "RENAME \"Old Mailbox\" \"New Mailbox\"")
}
s.WriteString(tag + " OK RENAME completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Rename() = %v", err)
}
}
func TestClient_Subscribe(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.AuthenticatedState, nil)
done := make(chan error, 1)
go func() {
done <- c.Subscribe("Mailbox")
}()
tag, cmd := s.ScanCmd()
if cmd != "SUBSCRIBE \"Mailbox\"" {
t.Fatalf("client sent command %v, want %v", cmd, "SUBSCRIBE \"Mailbox\"")
}
s.WriteString(tag + " OK SUBSCRIBE completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Subscribe() = %v", err)
}
}
func TestClient_Unsubscribe(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.AuthenticatedState, nil)
done := make(chan error, 1)
go func() {
done <- c.Unsubscribe("Mailbox")
}()
tag, cmd := s.ScanCmd()
if cmd != "UNSUBSCRIBE \"Mailbox\"" {
t.Fatalf("client sent command %v, want %v", cmd, "UNSUBSCRIBE \"Mailbox\"")
}
s.WriteString(tag + " OK UNSUBSCRIBE completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Unsubscribe() = %v", err)
}
}
func TestClient_List(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.AuthenticatedState, nil)
done := make(chan error, 1)
mailboxes := make(chan *imap.MailboxInfo, 3)
go func() {
done <- c.List("", "%", mailboxes)
}()
tag, cmd := s.ScanCmd()
if cmd != "LIST \"\" \"%\"" {
t.Fatalf("client sent command %v, want %v", cmd, "LIST \"\" \"%\"")
}
s.WriteString("* LIST (flag1) \"/\" INBOX\r\n")
s.WriteString("* LIST (flag2 flag3) \"/\" Drafts\r\n")
s.WriteString("* LIST () \"/\" Sent\r\n")
s.WriteString(tag + " OK LIST completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.List() = %v", err)
}
want := []struct {
name string
attributes []string
}{
{"INBOX", []string{"flag1"}},
{"Drafts", []string{"flag2", "flag3"}},
{"Sent", []string{}},
}
i := 0
for mbox := range mailboxes {
if mbox.Name != want[i].name {
t.Errorf("Bad mailbox name for %v: %v, want %v", i, mbox.Name, want[i].name)
}
if !reflect.DeepEqual(mbox.Attributes, want[i].attributes) {
t.Errorf("Bad mailbox attributes for %v: %v, want %v", i, mbox.Attributes, want[i].attributes)
}
i++
}
}
func TestClient_Lsub(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.AuthenticatedState, nil)
done := make(chan error, 1)
mailboxes := make(chan *imap.MailboxInfo, 1)
go func() {
done <- c.Lsub("", "%", mailboxes)
}()
tag, cmd := s.ScanCmd()
if cmd != "LSUB \"\" \"%\"" {
t.Fatalf("client sent command %v, want %v", cmd, "LSUB \"\" \"%\"")
}
s.WriteString("* LSUB () \"/\" INBOX\r\n")
s.WriteString(tag + " OK LSUB completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Lsub() = %v", err)
}
mbox := <-mailboxes
if mbox.Name != "INBOX" {
t.Errorf("Bad mailbox name: %v", mbox.Name)
}
if len(mbox.Attributes) != 0 {
t.Errorf("Bad mailbox flags: %v", mbox.Attributes)
}
}
func TestClient_Status(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.AuthenticatedState, nil)
done := make(chan error, 1)
var mbox *imap.MailboxStatus
go func() {
var err error
mbox, err = c.Status("INBOX", []imap.StatusItem{imap.StatusMessages, imap.StatusRecent})
done <- err
}()
tag, cmd := s.ScanCmd()
if cmd != "STATUS INBOX (MESSAGES RECENT)" {
t.Fatalf("client sent command %v, want %v", cmd, "STATUS \"INBOX\" (MESSAGES RECENT)")
}
s.WriteString("* STATUS INBOX (MESSAGES 42 RECENT 1)\r\n")
s.WriteString(tag + " OK STATUS completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Status() = %v", err)
}
if mbox.Messages != 42 {
t.Errorf("Bad mailbox messages: %v", mbox.Messages)
}
if mbox.Recent != 1 {
t.Errorf("Bad mailbox recent: %v", mbox.Recent)
}
}
type literalWrap struct {
io.Reader
L int
}
func (lw literalWrap) Len() int {
return lw.L
}
func TestClient_Append_SmallerLiteral(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.AuthenticatedState, nil)
msg := "Hello World!\r\nHello Gophers!\r\n"
date := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
flags := []string{imap.SeenFlag, imap.DraftFlag}
r := bytes.NewBufferString(msg)
done := make(chan error, 1)
go func() {
done <- c.Append("INBOX", flags, date, literalWrap{r, 35})
// The buffer is not flushed on error, force it so io.ReadFull can
// continue.
c.conn.Flush()
}()
tag, _ := s.ScanCmd()
s.WriteString("+ send literal\r\n")
b := make([]byte, 30)
// The client will close connection.
if _, err := io.ReadFull(s, b); err != io.EOF {
t.Error("Expected EOF, got", err)
}
s.WriteString(tag + " OK APPEND completed\r\n")
err, ok := (<-done).(imap.LiteralLengthErr)
if !ok {
t.Fatalf("c.Append() = %v", err)
}
if err.Expected != 35 {
t.Fatalf("err.Expected = %v", err.Expected)
}
if err.Actual != 30 {
t.Fatalf("err.Actual = %v", err.Actual)
}
}
func TestClient_Append_BiggerLiteral(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.AuthenticatedState, nil)
msg := "Hello World!\r\nHello Gophers!\r\n"
date := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
flags := []string{imap.SeenFlag, imap.DraftFlag}
r := bytes.NewBufferString(msg)
done := make(chan error, 1)
go func() {
done <- c.Append("INBOX", flags, date, literalWrap{r, 25})
// The buffer is not flushed on error, force it so io.ReadFull can
// continue.
c.conn.Flush()
}()
tag, _ := s.ScanCmd()
s.WriteString("+ send literal\r\n")
// The client will close connection.
b := make([]byte, 25)
if _, err := io.ReadFull(s, b); err != io.EOF {
t.Error("Expected EOF, got", err)
}
s.WriteString(tag + " OK APPEND completed\r\n")
err, ok := (<-done).(imap.LiteralLengthErr)
if !ok {
t.Fatalf("c.Append() = %v", err)
}
if err.Expected != 25 {
t.Fatalf("err.Expected = %v", err.Expected)
}
if err.Actual != 30 {
t.Fatalf("err.Actual = %v", err.Actual)
}
}
func TestClient_Append(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.AuthenticatedState, nil)
msg := "Hello World!\r\nHello Gophers!\r\n"
date := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
flags := []string{imap.SeenFlag, imap.DraftFlag}
done := make(chan error, 1)
go func() {
done <- c.Append("INBOX", flags, date, bytes.NewBufferString(msg))
}()
tag, cmd := s.ScanCmd()
if cmd != "APPEND INBOX (\\Seen \\Draft) \"10-Nov-2009 23:00:00 +0000\" {30}" {
t.Fatalf("client sent command %v, want %v", cmd, "APPEND \"INBOX\" (\\Seen \\Draft) \"10-Nov-2009 23:00:00 +0000\" {30}")
}
s.WriteString("+ send literal\r\n")
b := make([]byte, 30)
if _, err := io.ReadFull(s, b); err != nil {
t.Fatal(err)
} else if string(b) != msg {
t.Fatal("Bad literal:", string(b))
}
s.WriteString(tag + " OK APPEND completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Append() = %v", err)
}
}
func TestClient_Append_failed(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.AuthenticatedState, nil)
// First the server refuses
msg := "First try"
done := make(chan error, 1)
go func() {
done <- c.Append("INBOX", nil, time.Time{}, bytes.NewBufferString(msg))
}()
tag, _ := s.ScanCmd()
s.WriteString(tag + " BAD APPEND failed\r\n")
if err := <-done; err == nil {
t.Fatal("c.Append() = nil, want an error from the server")
}
// Try a second time, the server accepts
msg = "Second try"
go func() {
done <- c.Append("INBOX", nil, time.Time{}, bytes.NewBufferString(msg))
}()
tag, _ = s.ScanCmd()
s.WriteString("+ send literal\r\n")
b := make([]byte, len(msg))
if _, err := io.ReadFull(s, b); err != nil {
t.Fatal(err)
} else if string(b) != msg {
t.Fatal("Bad literal:", string(b))
}
s.WriteString(tag + " OK APPEND completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Append() = %v", err)
}
}

174
client/cmd_noauth.go Normal file
View File

@@ -0,0 +1,174 @@
package client
import (
"crypto/tls"
"errors"
"net"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/commands"
"github.com/emersion/go-imap/responses"
"github.com/emersion/go-sasl"
)
var (
// ErrAlreadyLoggedIn is returned if Login or Authenticate is called when the
// client is already logged in.
ErrAlreadyLoggedIn = errors.New("Already logged in")
// ErrTLSAlreadyEnabled is returned if StartTLS is called when TLS is already
// enabled.
ErrTLSAlreadyEnabled = errors.New("TLS is already enabled")
// ErrLoginDisabled is returned if Login or Authenticate is called when the
// server has disabled authentication. Most of the time, calling enabling TLS
// solves the problem.
ErrLoginDisabled = errors.New("Login is disabled in current state")
)
// SupportStartTLS checks if the server supports STARTTLS.
func (c *Client) SupportStartTLS() (bool, error) {
return c.Support("STARTTLS")
}
// StartTLS starts TLS negotiation.
func (c *Client) StartTLS(tlsConfig *tls.Config) error {
if c.isTLS {
return ErrTLSAlreadyEnabled
}
if tlsConfig == nil {
tlsConfig = new(tls.Config)
}
if tlsConfig.ServerName == "" {
tlsConfig = tlsConfig.Clone()
tlsConfig.ServerName = c.serverName
}
cmd := new(commands.StartTLS)
err := c.Upgrade(func(conn net.Conn) (net.Conn, error) {
// Flag connection as in upgrading
c.upgrading = true
if status, err := c.execute(cmd, nil); err != nil {
return nil, err
} else if err := status.Err(); err != nil {
return nil, err
}
// Wait for reader to block.
c.conn.WaitReady()
tlsConn := tls.Client(conn, tlsConfig)
if err := tlsConn.Handshake(); err != nil {
return nil, err
}
// Capabilities change when TLS is enabled
c.locker.Lock()
c.caps = nil
c.locker.Unlock()
return tlsConn, nil
})
if err != nil {
return err
}
c.isTLS = true
return nil
}
// SupportAuth checks if the server supports a given authentication mechanism.
func (c *Client) SupportAuth(mech string) (bool, error) {
return c.Support("AUTH=" + mech)
}
// Authenticate indicates a SASL authentication mechanism to the server. If the
// server supports the requested authentication mechanism, it performs an
// authentication protocol exchange to authenticate and identify the client.
func (c *Client) Authenticate(auth sasl.Client) error {
if c.State() != imap.NotAuthenticatedState {
return ErrAlreadyLoggedIn
}
mech, ir, err := auth.Start()
if err != nil {
return err
}
cmd := &commands.Authenticate{
Mechanism: mech,
}
irOk, err := c.Support("SASL-IR")
if err != nil {
return err
}
if irOk {
cmd.InitialResponse = ir
}
res := &responses.Authenticate{
Mechanism: auth,
InitialResponse: ir,
RepliesCh: make(chan []byte, 10),
}
if irOk {
res.InitialResponse = nil
}
status, err := c.execute(cmd, res)
if err != nil {
return err
}
if err = status.Err(); err != nil {
return err
}
c.locker.Lock()
c.state = imap.AuthenticatedState
c.caps = nil // Capabilities change when user is logged in
c.locker.Unlock()
if status.Code == "CAPABILITY" {
c.gotStatusCaps(status.Arguments)
}
return nil
}
// Login identifies the client to the server and carries the plaintext password
// authenticating this user.
func (c *Client) Login(username, password string) error {
if state := c.State(); state == imap.AuthenticatedState || state == imap.SelectedState {
return ErrAlreadyLoggedIn
}
c.locker.Lock()
loginDisabled := c.caps != nil && c.caps["LOGINDISABLED"]
c.locker.Unlock()
if loginDisabled {
return ErrLoginDisabled
}
cmd := &commands.Login{
Username: username,
Password: password,
}
status, err := c.execute(cmd, nil)
if err != nil {
return err
}
if err = status.Err(); err != nil {
return err
}
c.locker.Lock()
c.state = imap.AuthenticatedState
c.caps = nil // Capabilities change when user is logged in
c.locker.Unlock()
if status.Code == "CAPABILITY" {
c.gotStatusCaps(status.Arguments)
}
return nil
}

299
client/cmd_noauth_test.go Normal file
View File

@@ -0,0 +1,299 @@
package client
import (
"crypto/tls"
"io"
"testing"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/internal"
"github.com/emersion/go-sasl"
)
func TestClient_StartTLS(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
cert, err := tls.X509KeyPair(internal.LocalhostCert, internal.LocalhostKey)
if err != nil {
t.Fatal("cannot load test certificate:", err)
}
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
Certificates: []tls.Certificate{cert},
}
if c.IsTLS() {
t.Fatal("Client has TLS enabled before STARTTLS")
}
if ok, err := c.SupportStartTLS(); err != nil {
t.Fatalf("c.SupportStartTLS() = %v", err)
} else if !ok {
t.Fatalf("c.SupportStartTLS() = %v, want true", ok)
}
done := make(chan error, 1)
go func() {
done <- c.StartTLS(tlsConfig)
}()
tag, cmd := s.ScanCmd()
if cmd != "STARTTLS" {
t.Fatalf("client sent command %v, want STARTTLS", cmd)
}
s.WriteString(tag + " OK Begin TLS negotiation now\r\n")
ss := tls.Server(s.Conn, tlsConfig)
if err := ss.Handshake(); err != nil {
t.Fatal("cannot perform TLS handshake:", err)
}
if err := <-done; err != nil {
t.Error("c.StartTLS() =", err)
}
if !c.IsTLS() {
t.Errorf("Client has not TLS enabled after STARTTLS")
}
go func() {
_, err := c.Capability()
done <- err
}()
tag, cmd = newCmdScanner(ss).ScanCmd()
if cmd != "CAPABILITY" {
t.Fatalf("client sent command %v, want CAPABILITY", cmd)
}
io.WriteString(ss, "* CAPABILITY IMAP4rev1 AUTH=PLAIN\r\n")
io.WriteString(ss, tag+" OK CAPABILITY completed.\r\n")
}
func TestClient_Authenticate(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
if ok, err := c.SupportAuth(sasl.Plain); err != nil {
t.Fatalf("c.SupportAuth(sasl.Plain) = %v", err)
} else if !ok {
t.Fatalf("c.SupportAuth(sasl.Plain) = %v, want true", ok)
}
sasl := sasl.NewPlainClient("", "username", "password")
done := make(chan error, 1)
go func() {
done <- c.Authenticate(sasl)
}()
tag, cmd := s.ScanCmd()
if cmd != "AUTHENTICATE PLAIN" {
t.Fatalf("client sent command %v, want AUTHENTICATE PLAIN", cmd)
}
s.WriteString("+ \r\n")
wantLine := "AHVzZXJuYW1lAHBhc3N3b3Jk"
if line := s.ScanLine(); line != wantLine {
t.Fatalf("client sent auth %v, want %v", line, wantLine)
}
s.WriteString(tag + " OK AUTHENTICATE completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Authenticate() = %v", err)
}
if state := c.State(); state != imap.AuthenticatedState {
t.Errorf("c.State() = %v, want %v", state, imap.AuthenticatedState)
}
}
func TestClient_Authenticate_InitialResponse(t *testing.T) {
c, s := newTestClientWithGreeting(t, "* OK [CAPABILITY IMAP4rev1 SASL-IR STARTTLS AUTH=PLAIN] Server ready.\r\n")
defer s.Close()
if ok, err := c.SupportAuth(sasl.Plain); err != nil {
t.Fatalf("c.SupportAuth(sasl.Plain) = %v", err)
} else if !ok {
t.Fatalf("c.SupportAuth(sasl.Plain) = %v, want true", ok)
}
sasl := sasl.NewPlainClient("", "username", "password")
done := make(chan error, 1)
go func() {
done <- c.Authenticate(sasl)
}()
tag, cmd := s.ScanCmd()
if cmd != "AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk" {
t.Fatalf("client sent command %v, want AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk", cmd)
}
s.WriteString(tag + " OK AUTHENTICATE completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Authenticate() = %v", err)
}
if state := c.State(); state != imap.AuthenticatedState {
t.Errorf("c.State() = %v, want %v", state, imap.AuthenticatedState)
}
}
func TestClient_Login_Success(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
done := make(chan error, 1)
go func() {
done <- c.Login("username", "password")
}()
tag, cmd := s.ScanCmd()
if cmd != "LOGIN \"username\" \"password\"" {
t.Fatalf("client sent command %v, want LOGIN username password", cmd)
}
s.WriteString(tag + " OK LOGIN completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Login() = %v", err)
}
if state := c.State(); state != imap.AuthenticatedState {
t.Errorf("c.State() = %v, want %v", state, imap.AuthenticatedState)
}
}
func TestClient_Login_8bitSync(t *testing.T) {
c, s := newTestClientWithGreeting(t, "* OK [CAPABILITY IMAP4rev1 SASL-IR STARTTLS AUTH=PLAIN] Server ready.\r\n")
defer s.Close()
// Use of UTF-8 will force go-imap to send password in literal.
done := make(chan error, 1)
go func() {
done <- c.Login("username", "пароль")
}()
tag, cmd := s.ScanCmd()
if cmd != "LOGIN \"username\" {12}" {
t.Fatalf("client sent command %v, want LOGIN \"username\" {12}", cmd)
}
s.WriteString("+ send literal\r\n")
pass := s.ScanLine()
if pass != "пароль" {
t.Fatalf("client sent %v, want {12}'пароль' literal", pass)
}
s.WriteString(tag + " OK LOGIN completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Login() = %v", err)
}
if state := c.State(); state != imap.AuthenticatedState {
t.Errorf("c.State() = %v, want %v", state, imap.AuthenticatedState)
}
}
func TestClient_Login_8bitNonSync(t *testing.T) {
c, s := newTestClientWithGreeting(t, "* OK [CAPABILITY IMAP4rev1 LITERAL- SASL-IR STARTTLS AUTH=PLAIN] Server ready.\r\n")
defer s.Close()
// Use of UTF-8 will force go-imap to send password in literal.
done := make(chan error, 1)
go func() {
done <- c.Login("username", "пароль")
}()
tag, cmd := s.ScanCmd()
if cmd != "LOGIN \"username\" {12+}" {
t.Fatalf("client sent command %v, want LOGIN \"username\" {12+}", cmd)
}
pass := s.ScanLine()
if pass != "пароль" {
t.Fatalf("client sent %v, want {12+}'пароль' literal", pass)
}
s.WriteString(tag + " OK LOGIN completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Login() = %v", err)
}
if state := c.State(); state != imap.AuthenticatedState {
t.Errorf("c.State() = %v, want %v", state, imap.AuthenticatedState)
}
}
func TestClient_Login_Error(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
done := make(chan error, 1)
go func() {
done <- c.Login("username", "password")
}()
tag, cmd := s.ScanCmd()
if cmd != "LOGIN \"username\" \"password\"" {
t.Fatalf("client sent command %v, want LOGIN username password", cmd)
}
s.WriteString(tag + " NO LOGIN incorrect\r\n")
if err := <-done; err == nil {
t.Fatal("c.Login() = nil, want LOGIN incorrect")
}
if state := c.State(); state != imap.NotAuthenticatedState {
t.Errorf("c.State() = %v, want %v", state, imap.NotAuthenticatedState)
}
}
func TestClient_Login_State_Allowed(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
done := make(chan error, 1)
go func() {
done <- c.Login("username", "password")
}()
tag, cmd := s.ScanCmd()
if cmd != "LOGIN \"username\" \"password\"" {
t.Fatalf("client sent command %v, want LOGIN \"username\" \"password\"", cmd)
}
s.WriteString(tag + " OK LOGIN completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Login() = %v", err)
}
if state := c.State(); state != imap.AuthenticatedState {
t.Errorf("c.State() = %v, want %v", state, imap.AuthenticatedState)
}
go func() {
done <- c.Login("username", "password")
}()
if err := <-done; err != ErrAlreadyLoggedIn {
t.Fatalf("c.Login() = %v, want %v", err, ErrAlreadyLoggedIn)
}
go func() {
done <- c.Logout()
}()
s.ScanCmd()
s.WriteString("* BYE Client asked to close the connection.\r\n")
s.WriteString(tag + " OK LOGOUT completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Logout() = %v", err)
}
if err := c.Login("username", "password"); err == ErrAlreadyLoggedIn {
t.Errorf("Client is logout, login must not give %v", ErrAlreadyLoggedIn)
}
}

372
client/cmd_selected.go Normal file
View File

@@ -0,0 +1,372 @@
package client
import (
"errors"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/commands"
"github.com/emersion/go-imap/responses"
)
var (
// ErrNoMailboxSelected is returned if a command that requires a mailbox to be
// selected is called when there isn't.
ErrNoMailboxSelected = errors.New("No mailbox selected")
// ErrExtensionUnsupported is returned if a command uses a extension that
// is not supported by the server.
ErrExtensionUnsupported = errors.New("The required extension is not supported by the server")
)
// Check requests a checkpoint of the currently selected mailbox. A checkpoint
// refers to any implementation-dependent housekeeping associated with the
// mailbox that is not normally executed as part of each command.
func (c *Client) Check() error {
if c.State() != imap.SelectedState {
return ErrNoMailboxSelected
}
cmd := new(commands.Check)
status, err := c.execute(cmd, nil)
if err != nil {
return err
}
return status.Err()
}
// Close permanently removes all messages that have the \Deleted flag set from
// the currently selected mailbox, and returns to the authenticated state from
// the selected state.
func (c *Client) Close() error {
if c.State() != imap.SelectedState {
return ErrNoMailboxSelected
}
cmd := new(commands.Close)
status, err := c.execute(cmd, nil)
if err != nil {
return err
} else if err := status.Err(); err != nil {
return err
}
c.locker.Lock()
c.state = imap.AuthenticatedState
c.mailbox = nil
c.locker.Unlock()
return nil
}
// Terminate closes the tcp connection
func (c *Client) Terminate() error {
return c.conn.Close()
}
// Expunge permanently removes all messages that have the \Deleted flag set from
// the currently selected mailbox. If ch is not nil, sends sequence IDs of each
// deleted message to this channel.
func (c *Client) Expunge(ch chan uint32) error {
if ch != nil {
defer close(ch)
}
if c.State() != imap.SelectedState {
return ErrNoMailboxSelected
}
cmd := new(commands.Expunge)
var h responses.Handler
if ch != nil {
h = &responses.Expunge{SeqNums: ch}
}
status, err := c.execute(cmd, h)
if err != nil {
return err
}
return status.Err()
}
func (c *Client) executeSearch(uid bool, criteria *imap.SearchCriteria, charset string) (ids []uint32, status *imap.StatusResp, err error) {
if c.State() != imap.SelectedState {
err = ErrNoMailboxSelected
return
}
var cmd imap.Commander = &commands.Search{
Charset: charset,
Criteria: criteria,
}
if uid {
cmd = &commands.Uid{Cmd: cmd}
}
res := new(responses.Search)
status, err = c.execute(cmd, res)
if err != nil {
return
}
err, ids = status.Err(), res.Ids
return
}
func (c *Client) search(uid bool, criteria *imap.SearchCriteria) (ids []uint32, err error) {
charset := "UTF-8"
if c.utf8SearchUnsupported {
charset = "US-ASCII"
}
ids, status, err := c.executeSearch(uid, criteria, charset)
if status != nil && status.Code == imap.CodeBadCharset {
// Some servers don't support UTF-8
ids, _, err = c.executeSearch(uid, criteria, "US-ASCII")
c.utf8SearchUnsupported = true
}
return
}
// Search searches the mailbox for messages that match the given searching
// criteria. Searching criteria consist of one or more search keys. The response
// contains a list of message sequence IDs corresponding to those messages that
// match the searching criteria. When multiple keys are specified, the result is
// the intersection (AND function) of all the messages that match those keys.
// Criteria must be UTF-8 encoded. See RFC 3501 section 6.4.4 for a list of
// searching criteria. When no criteria has been set, all messages in the mailbox
// will be searched using ALL criteria.
func (c *Client) Search(criteria *imap.SearchCriteria) (seqNums []uint32, err error) {
return c.search(false, criteria)
}
// UidSearch is identical to Search, but UIDs are returned instead of message
// sequence numbers.
func (c *Client) UidSearch(criteria *imap.SearchCriteria) (uids []uint32, err error) {
return c.search(true, criteria)
}
func (c *Client) fetch(uid bool, seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error {
defer close(ch)
if c.State() != imap.SelectedState {
return ErrNoMailboxSelected
}
var cmd imap.Commander = &commands.Fetch{
SeqSet: seqset,
Items: items,
}
if uid {
cmd = &commands.Uid{Cmd: cmd}
}
res := &responses.Fetch{Messages: ch, SeqSet: seqset, Uid: uid}
status, err := c.execute(cmd, res)
if err != nil {
return err
}
return status.Err()
}
// Fetch retrieves data associated with a message in the mailbox. See RFC 3501
// section 6.4.5 for a list of items that can be requested.
func (c *Client) Fetch(seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error {
return c.fetch(false, seqset, items, ch)
}
// UidFetch is identical to Fetch, but seqset is interpreted as containing
// unique identifiers instead of message sequence numbers.
func (c *Client) UidFetch(seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error {
return c.fetch(true, seqset, items, ch)
}
func (c *Client) store(uid bool, seqset *imap.SeqSet, item imap.StoreItem, value interface{}, ch chan *imap.Message) error {
if ch != nil {
defer close(ch)
}
if c.State() != imap.SelectedState {
return ErrNoMailboxSelected
}
// TODO: this could break extensions (this only works when item is FLAGS)
if fields, ok := value.([]interface{}); ok {
for i, field := range fields {
if s, ok := field.(string); ok {
fields[i] = imap.RawString(s)
}
}
}
// If ch is nil, the updated values are data which will be lost, so don't
// retrieve it.
if ch == nil {
op, _, err := imap.ParseFlagsOp(item)
if err == nil {
item = imap.FormatFlagsOp(op, true)
}
}
var cmd imap.Commander = &commands.Store{
SeqSet: seqset,
Item: item,
Value: value,
}
if uid {
cmd = &commands.Uid{Cmd: cmd}
}
var h responses.Handler
if ch != nil {
h = &responses.Fetch{Messages: ch, SeqSet: seqset, Uid: uid}
}
status, err := c.execute(cmd, h)
if err != nil {
return err
}
return status.Err()
}
// Store alters data associated with a message in the mailbox. If ch is not nil,
// the updated value of the data will be sent to this channel. See RFC 3501
// section 6.4.6 for a list of items that can be updated.
func (c *Client) Store(seqset *imap.SeqSet, item imap.StoreItem, value interface{}, ch chan *imap.Message) error {
return c.store(false, seqset, item, value, ch)
}
// UidStore is identical to Store, but seqset is interpreted as containing
// unique identifiers instead of message sequence numbers.
func (c *Client) UidStore(seqset *imap.SeqSet, item imap.StoreItem, value interface{}, ch chan *imap.Message) error {
return c.store(true, seqset, item, value, ch)
}
func (c *Client) copy(uid bool, seqset *imap.SeqSet, dest string) error {
if c.State() != imap.SelectedState {
return ErrNoMailboxSelected
}
var cmd imap.Commander = &commands.Copy{
SeqSet: seqset,
Mailbox: dest,
}
if uid {
cmd = &commands.Uid{Cmd: cmd}
}
status, err := c.execute(cmd, nil)
if err != nil {
return err
}
return status.Err()
}
// Copy copies the specified message(s) to the end of the specified destination
// mailbox.
func (c *Client) Copy(seqset *imap.SeqSet, dest string) error {
return c.copy(false, seqset, dest)
}
// UidCopy is identical to Copy, but seqset is interpreted as containing unique
// identifiers instead of message sequence numbers.
func (c *Client) UidCopy(seqset *imap.SeqSet, dest string) error {
return c.copy(true, seqset, dest)
}
func (c *Client) move(uid bool, seqset *imap.SeqSet, dest string) error {
if c.State() != imap.SelectedState {
return ErrNoMailboxSelected
}
if ok, err := c.Support("MOVE"); err != nil {
return err
} else if !ok {
return c.moveFallback(uid, seqset, dest)
}
var cmd imap.Commander = &commands.Move{
SeqSet: seqset,
Mailbox: dest,
}
if uid {
cmd = &commands.Uid{Cmd: cmd}
}
if status, err := c.Execute(cmd, nil); err != nil {
return err
} else {
return status.Err()
}
}
// moveFallback uses COPY, STORE and EXPUNGE for servers which don't support
// MOVE.
func (c *Client) moveFallback(uid bool, seqset *imap.SeqSet, dest string) error {
item := imap.FormatFlagsOp(imap.AddFlags, true)
flags := []interface{}{imap.DeletedFlag}
if uid {
if err := c.UidCopy(seqset, dest); err != nil {
return err
}
if err := c.UidStore(seqset, item, flags, nil); err != nil {
return err
}
} else {
if err := c.Copy(seqset, dest); err != nil {
return err
}
if err := c.Store(seqset, item, flags, nil); err != nil {
return err
}
}
return c.Expunge(nil)
}
// Move moves the specified message(s) to the end of the specified destination
// mailbox.
//
// If the server doesn't support the MOVE extension defined in RFC 6851,
// go-imap will fallback to copy, store and expunge.
func (c *Client) Move(seqset *imap.SeqSet, dest string) error {
return c.move(false, seqset, dest)
}
// UidMove is identical to Move, but seqset is interpreted as containing unique
// identifiers instead of message sequence numbers.
func (c *Client) UidMove(seqset *imap.SeqSet, dest string) error {
return c.move(true, seqset, dest)
}
// Unselect frees server's resources associated with the selected mailbox and
// returns the server to the authenticated state. This command performs the same
// actions as Close, except that no messages are permanently removed from the
// currently selected mailbox.
//
// If client does not support the UNSELECT extension, ErrExtensionUnsupported
// is returned.
func (c *Client) Unselect() error {
if ok, err := c.Support("UNSELECT"); !ok || err != nil {
return ErrExtensionUnsupported
}
if c.State() != imap.SelectedState {
return ErrNoMailboxSelected
}
cmd := &commands.Unselect{}
if status, err := c.Execute(cmd, nil); err != nil {
return err
} else if err := status.Err(); err != nil {
return err
}
c.SetState(imap.AuthenticatedState, nil)
return nil
}

816
client/cmd_selected_test.go Normal file
View File

@@ -0,0 +1,816 @@
package client
import (
"io/ioutil"
"net/textproto"
"reflect"
"testing"
"time"
"github.com/emersion/go-imap"
)
func TestClient_Check(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
done := make(chan error, 1)
go func() {
done <- c.Check()
}()
tag, cmd := s.ScanCmd()
if cmd != "CHECK" {
t.Fatalf("client sent command %v, want %v", cmd, "CHECK")
}
s.WriteString(tag + " OK CHECK completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Check() = %v", err)
}
}
func TestClient_Close(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, &imap.MailboxStatus{Name: "INBOX"})
done := make(chan error, 1)
go func() {
done <- c.Close()
}()
tag, cmd := s.ScanCmd()
if cmd != "CLOSE" {
t.Fatalf("client sent command %v, want %v", cmd, "CLOSE")
}
s.WriteString(tag + " OK CLOSE completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Check() = %v", err)
}
if state := c.State(); state != imap.AuthenticatedState {
t.Errorf("Bad state: %v", state)
}
if mailbox := c.Mailbox(); mailbox != nil {
t.Errorf("Client selected mailbox is not nil: %v", mailbox)
}
}
func TestClient_Expunge(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
done := make(chan error, 1)
expunged := make(chan uint32, 4)
go func() {
done <- c.Expunge(expunged)
}()
tag, cmd := s.ScanCmd()
if cmd != "EXPUNGE" {
t.Fatalf("client sent command %v, want %v", cmd, "EXPUNGE")
}
s.WriteString("* 3 EXPUNGE\r\n")
s.WriteString("* 3 EXPUNGE\r\n")
s.WriteString("* 5 EXPUNGE\r\n")
s.WriteString("* 8 EXPUNGE\r\n")
s.WriteString(tag + " OK EXPUNGE completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Expunge() = %v", err)
}
expected := []uint32{3, 3, 5, 8}
i := 0
for id := range expunged {
if id != expected[i] {
t.Errorf("Bad expunged sequence number: got %v instead of %v", id, expected[i])
}
i++
}
}
func TestClient_Search(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
date, _ := time.Parse(imap.DateLayout, "1-Feb-1994")
criteria := &imap.SearchCriteria{
WithFlags: []string{imap.DeletedFlag},
Header: textproto.MIMEHeader{"From": {"Smith"}},
Since: date,
Not: []*imap.SearchCriteria{{
Header: textproto.MIMEHeader{"To": {"Pauline"}},
}},
}
done := make(chan error, 1)
var results []uint32
go func() {
var err error
results, err = c.Search(criteria)
done <- err
}()
wantCmd := `SEARCH CHARSET UTF-8 SINCE "1-Feb-1994" FROM "Smith" DELETED NOT (TO "Pauline")`
tag, cmd := s.ScanCmd()
if cmd != wantCmd {
t.Fatalf("client sent command %v, want %v", cmd, wantCmd)
}
s.WriteString("* SEARCH 2 84 882\r\n")
s.WriteString(tag + " OK SEARCH completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Search() = %v", err)
}
want := []uint32{2, 84, 882}
if !reflect.DeepEqual(results, want) {
t.Errorf("c.Search() = %v, want %v", results, want)
}
}
func TestClient_Search_Badcharset(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
date, _ := time.Parse(imap.DateLayout, "1-Feb-1994")
criteria := &imap.SearchCriteria{
WithFlags: []string{imap.DeletedFlag},
Header: textproto.MIMEHeader{"From": {"Smith"}},
Since: date,
Not: []*imap.SearchCriteria{{
Header: textproto.MIMEHeader{"To": {"Pauline"}},
}},
}
done := make(chan error, 1)
var results []uint32
go func() {
var err error
results, err = c.Search(criteria)
done <- err
}()
// first search call with default UTF-8 charset (assume server does not support UTF-8)
wantCmd := `SEARCH CHARSET UTF-8 SINCE "1-Feb-1994" FROM "Smith" DELETED NOT (TO "Pauline")`
tag, cmd := s.ScanCmd()
if cmd != wantCmd {
t.Fatalf("client sent command %v, want %v", cmd, wantCmd)
}
s.WriteString(tag + " NO [BADCHARSET (US-ASCII)]\r\n")
// internal fall-back to US-ASCII which sets utf8SearchUnsupported = true
wantCmd = `SEARCH CHARSET US-ASCII SINCE "1-Feb-1994" FROM "Smith" DELETED NOT (TO "Pauline")`
tag, cmd = s.ScanCmd()
if cmd != wantCmd {
t.Fatalf("client sent command %v, want %v", cmd, wantCmd)
}
s.WriteString("* SEARCH 2 84 882\r\n")
s.WriteString(tag + " OK SEARCH completed\r\n")
if err := <-done; err != nil {
t.Fatalf("err: %v", err)
}
want := []uint32{2, 84, 882}
if !reflect.DeepEqual(results, want) {
t.Errorf("c.Search() = %v, want %v", results, want)
}
if !c.utf8SearchUnsupported {
t.Fatal("client should have utf8SearchUnsupported set to true")
}
// second call to search (with utf8SearchUnsupported=true)
go func() {
var err error
results, err = c.Search(criteria)
done <- err
}()
tag, cmd = s.ScanCmd()
if cmd != wantCmd {
t.Fatalf("client sent command %v, want %v", cmd, wantCmd)
}
s.WriteString("* SEARCH 2 84 882\r\n")
s.WriteString(tag + " OK SEARCH completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Search() = %v", err)
}
if !reflect.DeepEqual(results, want) {
t.Errorf("c.Search() = %v, want %v", results, want)
}
}
func TestClient_Search_Uid(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
criteria := &imap.SearchCriteria{
WithoutFlags: []string{imap.DeletedFlag},
}
done := make(chan error, 1)
var results []uint32
go func() {
var err error
results, err = c.UidSearch(criteria)
done <- err
}()
wantCmd := "UID SEARCH CHARSET UTF-8 UNDELETED"
tag, cmd := s.ScanCmd()
if cmd != wantCmd {
t.Fatalf("client sent command %v, want %v", cmd, wantCmd)
}
s.WriteString("* SEARCH 1 78 2010\r\n")
s.WriteString(tag + " OK UID SEARCH completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Search() = %v", err)
}
want := []uint32{1, 78, 2010}
if !reflect.DeepEqual(results, want) {
t.Errorf("c.Search() = %v, want %v", results, want)
}
}
func TestClient_Search_Uid_Badcharset(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
criteria := &imap.SearchCriteria{
WithoutFlags: []string{imap.DeletedFlag},
}
done := make(chan error, 1)
var results []uint32
go func() {
var err error
results, err = c.UidSearch(criteria)
done <- err
}()
// first search call with default UTF-8 charset (assume server does not support UTF-8)
wantCmd := "UID SEARCH CHARSET UTF-8 UNDELETED"
tag, cmd := s.ScanCmd()
if cmd != wantCmd {
t.Fatalf("client sent command %v, want %v", cmd, wantCmd)
}
s.WriteString(tag + " NO [BADCHARSET (US-ASCII)]\r\n")
// internal fall-back to US-ASCII which sets utf8SearchUnsupported = true
wantCmd = "UID SEARCH CHARSET US-ASCII UNDELETED"
tag, cmd = s.ScanCmd()
if cmd != wantCmd {
t.Fatalf("client sent command %v, want %v", cmd, wantCmd)
}
s.WriteString("* SEARCH 1 78 2010\r\n")
s.WriteString(tag + " OK UID SEARCH completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.UidSearch() = %v", err)
}
want := []uint32{1, 78, 2010}
if !reflect.DeepEqual(results, want) {
t.Errorf("c.UidSearch() = %v, want %v", results, want)
}
if !c.utf8SearchUnsupported {
t.Fatal("client should have utf8SearchUnsupported set to true")
}
// second call to search (with utf8SearchUnsupported=true)
go func() {
var err error
results, err = c.UidSearch(criteria)
done <- err
}()
tag, cmd = s.ScanCmd()
if cmd != wantCmd {
t.Fatalf("client sent command %v, want %v", cmd, wantCmd)
}
s.WriteString("* SEARCH 1 78 2010\r\n")
s.WriteString(tag + " OK UID SEARCH completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.UidSearch() = %v", err)
}
if !reflect.DeepEqual(results, want) {
t.Errorf("c.UidSearch() = %v, want %v", results, want)
}
}
func TestClient_Fetch(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
seqset, _ := imap.ParseSeqSet("2:3")
fields := []imap.FetchItem{imap.FetchUid, imap.FetchItem("BODY[]")}
done := make(chan error, 1)
messages := make(chan *imap.Message, 2)
go func() {
done <- c.Fetch(seqset, fields, messages)
}()
tag, cmd := s.ScanCmd()
if cmd != "FETCH 2:3 (UID BODY[])" {
t.Fatalf("client sent command %v, want %v", cmd, "FETCH 2:3 (UID BODY[])")
}
s.WriteString("* 2 FETCH (UID 42 BODY[] {16}\r\n")
s.WriteString("I love potatoes.")
s.WriteString(")\r\n")
s.WriteString("* 3 FETCH (UID 28 BODY[] {12}\r\n")
s.WriteString("Hello World!")
s.WriteString(")\r\n")
s.WriteString(tag + " OK FETCH completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Fetch() = %v", err)
}
section, _ := imap.ParseBodySectionName("BODY[]")
msg := <-messages
if msg.SeqNum != 2 {
t.Errorf("First message has bad sequence number: %v", msg.SeqNum)
}
if msg.Uid != 42 {
t.Errorf("First message has bad UID: %v", msg.Uid)
}
if body, _ := ioutil.ReadAll(msg.GetBody(section)); string(body) != "I love potatoes." {
t.Errorf("First message has bad body: %q", body)
}
msg = <-messages
if msg.SeqNum != 3 {
t.Errorf("First message has bad sequence number: %v", msg.SeqNum)
}
if msg.Uid != 28 {
t.Errorf("Second message has bad UID: %v", msg.Uid)
}
if body, _ := ioutil.ReadAll(msg.GetBody(section)); string(body) != "Hello World!" {
t.Errorf("Second message has bad body: %q", body)
}
}
func TestClient_Fetch_ClosedState(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.AuthenticatedState, nil)
seqset, _ := imap.ParseSeqSet("2:3")
fields := []imap.FetchItem{imap.FetchUid, imap.FetchItem("BODY[]")}
done := make(chan error, 1)
messages := make(chan *imap.Message, 2)
go func() {
done <- c.Fetch(seqset, fields, messages)
}()
_, more := <-messages
if more {
t.Fatalf("Messages channel has more messages, but it must be closed with no messages sent")
}
err := <-done
if err != ErrNoMailboxSelected {
t.Fatalf("Expected error to be IMAP Client ErrNoMailboxSelected")
}
}
func TestClient_Fetch_Partial(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
seqset, _ := imap.ParseSeqSet("1")
fields := []imap.FetchItem{imap.FetchItem("BODY.PEEK[]<0.10>")}
done := make(chan error, 1)
messages := make(chan *imap.Message, 1)
go func() {
done <- c.Fetch(seqset, fields, messages)
}()
tag, cmd := s.ScanCmd()
if cmd != "FETCH 1 (BODY.PEEK[]<0.10>)" {
t.Fatalf("client sent command %v, want %v", cmd, "FETCH 1 (BODY.PEEK[]<0.10>)")
}
s.WriteString("* 1 FETCH (BODY[]<0> {10}\r\n")
s.WriteString("I love pot")
s.WriteString(")\r\n")
s.WriteString(tag + " OK FETCH completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Fetch() = %v", err)
}
section, _ := imap.ParseBodySectionName("BODY.PEEK[]<0.10>")
msg := <-messages
if body, _ := ioutil.ReadAll(msg.GetBody(section)); string(body) != "I love pot" {
t.Errorf("Message has bad body: %q", body)
}
}
func TestClient_Fetch_part(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
seqset, _ := imap.ParseSeqSet("1")
fields := []imap.FetchItem{imap.FetchItem("BODY.PEEK[1]")}
done := make(chan error, 1)
messages := make(chan *imap.Message, 1)
go func() {
done <- c.Fetch(seqset, fields, messages)
}()
tag, cmd := s.ScanCmd()
if cmd != "FETCH 1 (BODY.PEEK[1])" {
t.Fatalf("client sent command %v, want %v", cmd, "FETCH 1 (BODY.PEEK[1])")
}
s.WriteString("* 1 FETCH (BODY[1] {3}\r\n")
s.WriteString("Hey")
s.WriteString(")\r\n")
s.WriteString(tag + " OK FETCH completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Fetch() = %v", err)
}
<-messages
}
func TestClient_Fetch_Uid(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
seqset, _ := imap.ParseSeqSet("1:867")
fields := []imap.FetchItem{imap.FetchFlags}
done := make(chan error, 1)
messages := make(chan *imap.Message, 1)
go func() {
done <- c.UidFetch(seqset, fields, messages)
}()
tag, cmd := s.ScanCmd()
if cmd != "UID FETCH 1:867 (FLAGS)" {
t.Fatalf("client sent command %v, want %v", cmd, "UID FETCH 1:867 (FLAGS)")
}
s.WriteString("* 23 FETCH (UID 42 FLAGS (\\Seen))\r\n")
s.WriteString(tag + " OK UID FETCH completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.UidFetch() = %v", err)
}
msg := <-messages
if msg.SeqNum != 23 {
t.Errorf("First message has bad sequence number: %v", msg.SeqNum)
}
if msg.Uid != 42 {
t.Errorf("Message has bad UID: %v", msg.Uid)
}
if len(msg.Flags) != 1 || msg.Flags[0] != "\\Seen" {
t.Errorf("Message has bad flags: %v", msg.Flags)
}
}
func TestClient_Fetch_Unilateral(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
seqset, _ := imap.ParseSeqSet("1:4")
fields := []imap.FetchItem{imap.FetchFlags}
done := make(chan error, 1)
messages := make(chan *imap.Message, 3)
go func() {
done <- c.Fetch(seqset, fields, messages)
}()
tag, cmd := s.ScanCmd()
if cmd != "FETCH 1:4 (FLAGS)" {
t.Fatalf("client sent command %v, want %v", cmd, "FETCH 1:4 (FLAGS)")
}
s.WriteString("* 2 FETCH (FLAGS (\\Seen))\r\n")
s.WriteString("* 123 FETCH (FLAGS (\\Deleted))\r\n")
s.WriteString("* 4 FETCH (FLAGS (\\Seen))\r\n")
s.WriteString(tag + " OK FETCH completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Fetch() = %v", err)
}
msg := <-messages
if msg.SeqNum != 2 {
t.Errorf("First message has bad sequence number: %v", msg.SeqNum)
}
msg = <-messages
if msg.SeqNum != 4 {
t.Errorf("Second message has bad sequence number: %v", msg.SeqNum)
}
_, ok := <-messages
if ok {
t.Errorf("More than two messages")
}
}
func TestClient_Fetch_Unilateral_Uid(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
seqset, _ := imap.ParseSeqSet("1:4")
fields := []imap.FetchItem{imap.FetchFlags}
done := make(chan error, 1)
messages := make(chan *imap.Message, 3)
go func() {
done <- c.UidFetch(seqset, fields, messages)
}()
tag, cmd := s.ScanCmd()
if cmd != "UID FETCH 1:4 (FLAGS)" {
t.Fatalf("client sent command %v, want %v", cmd, "UID FETCH 1:4 (FLAGS)")
}
s.WriteString("* 23 FETCH (UID 2 FLAGS (\\Seen))\r\n")
s.WriteString("* 123 FETCH (FLAGS (\\Deleted))\r\n")
s.WriteString("* 49 FETCH (UID 4 FLAGS (\\Seen))\r\n")
s.WriteString(tag + " OK FETCH completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Fetch() = %v", err)
}
msg := <-messages
if msg.Uid != 2 {
t.Errorf("First message has bad UID: %v", msg.Uid)
}
msg = <-messages
if msg.Uid != 4 {
t.Errorf("Second message has bad UID: %v", msg.Uid)
}
_, ok := <-messages
if ok {
t.Errorf("More than two messages")
}
}
func TestClient_Fetch_Uid_Dynamic(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
seqset, _ := imap.ParseSeqSet("4:*")
fields := []imap.FetchItem{imap.FetchFlags}
done := make(chan error, 1)
messages := make(chan *imap.Message, 1)
go func() {
done <- c.UidFetch(seqset, fields, messages)
}()
tag, cmd := s.ScanCmd()
if cmd != "UID FETCH 4:* (FLAGS)" {
t.Fatalf("client sent command %v, want %v", cmd, "UID FETCH 4:* (FLAGS)")
}
s.WriteString("* 23 FETCH (UID 2 FLAGS (\\Seen))\r\n")
s.WriteString(tag + " OK FETCH completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Fetch() = %v", err)
}
msg, ok := <-messages
if !ok {
t.Errorf("No message supplied")
} else if msg.Uid != 2 {
t.Errorf("First message has bad UID: %v", msg.Uid)
}
}
func TestClient_Store(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
seqset, _ := imap.ParseSeqSet("2")
done := make(chan error, 1)
updates := make(chan *imap.Message, 1)
go func() {
done <- c.Store(seqset, imap.AddFlags, []interface{}{imap.SeenFlag, "foobar"}, updates)
}()
tag, cmd := s.ScanCmd()
if cmd != "STORE 2 +FLAGS (\\Seen foobar)" {
t.Fatalf("client sent command %v, want %v", cmd, "STORE 2 +FLAGS (\\Seen foobar)")
}
s.WriteString("* 2 FETCH (FLAGS (\\Seen foobar))\r\n")
s.WriteString(tag + " OK STORE completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Store() = %v", err)
}
msg := <-updates
if len(msg.Flags) != 2 || msg.Flags[0] != "\\Seen" || msg.Flags[1] != "foobar" {
t.Errorf("Bad message flags: %v", msg.Flags)
}
}
func TestClient_Store_Silent(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
seqset, _ := imap.ParseSeqSet("2:3")
done := make(chan error, 1)
go func() {
done <- c.Store(seqset, imap.AddFlags, []interface{}{imap.SeenFlag, "foobar"}, nil)
}()
tag, cmd := s.ScanCmd()
if cmd != "STORE 2:3 +FLAGS.SILENT (\\Seen foobar)" {
t.Fatalf("client sent command %v, want %v", cmd, "STORE 2:3 +FLAGS.SILENT (\\Seen foobar)")
}
s.WriteString(tag + " OK STORE completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Store() = %v", err)
}
}
func TestClient_Store_Uid(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
seqset, _ := imap.ParseSeqSet("27:901")
done := make(chan error, 1)
go func() {
done <- c.UidStore(seqset, imap.AddFlags, []interface{}{imap.DeletedFlag, "foobar"}, nil)
}()
tag, cmd := s.ScanCmd()
if cmd != "UID STORE 27:901 +FLAGS.SILENT (\\Deleted foobar)" {
t.Fatalf("client sent command %v, want %v", cmd, "UID STORE 27:901 +FLAGS.SILENT (\\Deleted foobar)")
}
s.WriteString(tag + " OK STORE completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.UidStore() = %v", err)
}
}
func TestClient_Copy(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
seqset, _ := imap.ParseSeqSet("2:4")
done := make(chan error, 1)
go func() {
done <- c.Copy(seqset, "Sent")
}()
tag, cmd := s.ScanCmd()
if cmd != "COPY 2:4 \"Sent\"" {
t.Fatalf("client sent command %v, want %v", cmd, "COPY 2:4 \"Sent\"")
}
s.WriteString(tag + " OK COPY completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Copy() = %v", err)
}
}
func TestClient_Copy_Uid(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
seqset, _ := imap.ParseSeqSet("78:102")
done := make(chan error, 1)
go func() {
done <- c.UidCopy(seqset, "Drafts")
}()
tag, cmd := s.ScanCmd()
if cmd != "UID COPY 78:102 \"Drafts\"" {
t.Fatalf("client sent command %v, want %v", cmd, "UID COPY 78:102 \"Drafts\"")
}
s.WriteString(tag + " OK UID COPY completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.UidCopy() = %v", err)
}
}
func TestClient_Unselect(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
done := make(chan error, 1)
go func() {
done <- c.Unselect()
}()
tag, cmd := s.ScanCmd()
if cmd != "UNSELECT" {
t.Fatalf("client sent command %v, want %v", cmd, "UNSELECT")
}
s.WriteString(tag + " OK UNSELECT completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Unselect() = %v", err)
}
if c.State() != imap.AuthenticatedState {
t.Fatal("Client is not Authenticated after UNSELECT")
}
}

323
client/example_test.go Normal file
View File

@@ -0,0 +1,323 @@
package client_test
import (
"bytes"
"crypto/tls"
"io/ioutil"
"log"
"net/mail"
"time"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/client"
)
func ExampleClient() {
log.Println("Connecting to server...")
// Connect to server
c, err := client.DialTLS("mail.example.org:993", nil)
if err != nil {
log.Fatal(err)
}
log.Println("Connected")
// Don't forget to logout
defer c.Logout()
// Login
if err := c.Login("username", "password"); err != nil {
log.Fatal(err)
}
log.Println("Logged in")
// List mailboxes
mailboxes := make(chan *imap.MailboxInfo, 10)
done := make(chan error, 1)
go func() {
done <- c.List("", "*", mailboxes)
}()
log.Println("Mailboxes:")
for m := range mailboxes {
log.Println("* " + m.Name)
}
if err := <-done; err != nil {
log.Fatal(err)
}
// Select INBOX
mbox, err := c.Select("INBOX", false)
if err != nil {
log.Fatal(err)
}
log.Println("Flags for INBOX:", mbox.Flags)
// Get the last 4 messages
from := uint32(1)
to := mbox.Messages
if mbox.Messages > 3 {
// We're using unsigned integers here, only substract if the result is > 0
from = mbox.Messages - 3
}
seqset := new(imap.SeqSet)
seqset.AddRange(from, to)
items := []imap.FetchItem{imap.FetchEnvelope}
messages := make(chan *imap.Message, 10)
done = make(chan error, 1)
go func() {
done <- c.Fetch(seqset, items, messages)
}()
log.Println("Last 4 messages:")
for msg := range messages {
log.Println("* " + msg.Envelope.Subject)
}
if err := <-done; err != nil {
log.Fatal(err)
}
log.Println("Done!")
}
func ExampleClient_Fetch() {
// Let's assume c is a client
var c *client.Client
// Select INBOX
mbox, err := c.Select("INBOX", false)
if err != nil {
log.Fatal(err)
}
// Get the last message
if mbox.Messages == 0 {
log.Fatal("No message in mailbox")
}
seqset := new(imap.SeqSet)
seqset.AddRange(mbox.Messages, mbox.Messages)
// Get the whole message body
section := &imap.BodySectionName{}
items := []imap.FetchItem{section.FetchItem()}
messages := make(chan *imap.Message, 1)
done := make(chan error, 1)
go func() {
done <- c.Fetch(seqset, items, messages)
}()
log.Println("Last message:")
msg := <-messages
r := msg.GetBody(section)
if r == nil {
log.Fatal("Server didn't returned message body")
}
if err := <-done; err != nil {
log.Fatal(err)
}
m, err := mail.ReadMessage(r)
if err != nil {
log.Fatal(err)
}
header := m.Header
log.Println("Date:", header.Get("Date"))
log.Println("From:", header.Get("From"))
log.Println("To:", header.Get("To"))
log.Println("Subject:", header.Get("Subject"))
body, err := ioutil.ReadAll(m.Body)
if err != nil {
log.Fatal(err)
}
log.Println(body)
}
func ExampleClient_Append() {
// Let's assume c is a client
var c *client.Client
// Write the message to a buffer
var b bytes.Buffer
b.WriteString("From: <root@nsa.gov>\r\n")
b.WriteString("To: <root@gchq.gov.uk>\r\n")
b.WriteString("Subject: Hey there\r\n")
b.WriteString("\r\n")
b.WriteString("Hey <3")
// Append it to INBOX, with two flags
flags := []string{imap.FlaggedFlag, "foobar"}
if err := c.Append("INBOX", flags, time.Now(), &b); err != nil {
log.Fatal(err)
}
}
func ExampleClient_Expunge() {
// Let's assume c is a client
var c *client.Client
// Select INBOX
mbox, err := c.Select("INBOX", false)
if err != nil {
log.Fatal(err)
}
// We will delete the last message
if mbox.Messages == 0 {
log.Fatal("No message in mailbox")
}
seqset := new(imap.SeqSet)
seqset.AddNum(mbox.Messages)
// First mark the message as deleted
item := imap.FormatFlagsOp(imap.AddFlags, true)
flags := []interface{}{imap.DeletedFlag}
if err := c.Store(seqset, item, flags, nil); err != nil {
log.Fatal(err)
}
// Then delete it
if err := c.Expunge(nil); err != nil {
log.Fatal(err)
}
log.Println("Last message has been deleted")
}
func ExampleClient_StartTLS() {
log.Println("Connecting to server...")
// Connect to server
c, err := client.Dial("mail.example.org:143")
if err != nil {
log.Fatal(err)
}
log.Println("Connected")
// Don't forget to logout
defer c.Logout()
// Start a TLS session
tlsConfig := &tls.Config{ServerName: "mail.example.org"}
if err := c.StartTLS(tlsConfig); err != nil {
log.Fatal(err)
}
log.Println("TLS started")
// Now we can login
if err := c.Login("username", "password"); err != nil {
log.Fatal(err)
}
log.Println("Logged in")
}
func ExampleClient_Store() {
// Let's assume c is a client
var c *client.Client
// Select INBOX
_, err := c.Select("INBOX", false)
if err != nil {
log.Fatal(err)
}
// Mark message 42 as seen
seqSet := new(imap.SeqSet)
seqSet.AddNum(42)
item := imap.FormatFlagsOp(imap.AddFlags, true)
flags := []interface{}{imap.SeenFlag}
err = c.Store(seqSet, item, flags, nil)
if err != nil {
log.Fatal(err)
}
log.Println("Message has been marked as seen")
}
func ExampleClient_Search() {
// Let's assume c is a client
var c *client.Client
// Select INBOX
_, err := c.Select("INBOX", false)
if err != nil {
log.Fatal(err)
}
// Set search criteria
criteria := imap.NewSearchCriteria()
criteria.WithoutFlags = []string{imap.SeenFlag}
ids, err := c.Search(criteria)
if err != nil {
log.Fatal(err)
}
log.Println("IDs found:", ids)
if len(ids) > 0 {
seqset := new(imap.SeqSet)
seqset.AddNum(ids...)
messages := make(chan *imap.Message, 10)
done := make(chan error, 1)
go func() {
done <- c.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope}, messages)
}()
log.Println("Unseen messages:")
for msg := range messages {
log.Println("* " + msg.Envelope.Subject)
}
if err := <-done; err != nil {
log.Fatal(err)
}
}
log.Println("Done!")
}
func ExampleClient_Idle() {
// Let's assume c is a client
var c *client.Client
// Select a mailbox
if _, err := c.Select("INBOX", false); err != nil {
log.Fatal(err)
}
// Create a channel to receive mailbox updates
updates := make(chan client.Update)
c.Updates = updates
// Start idling
stopped := false
stop := make(chan struct{})
done := make(chan error, 1)
go func() {
done <- c.Idle(stop, nil)
}()
// Listen for updates
for {
select {
case update := <-updates:
log.Println("New update:", update)
if !stopped {
close(stop)
stopped = true
}
case err := <-done:
if err != nil {
log.Fatal(err)
}
log.Println("Not idling anymore")
return
}
}
}

24
client/tag.go Normal file
View File

@@ -0,0 +1,24 @@
package client
import (
"crypto/rand"
"encoding/base64"
)
func randomString(n int) (string, error) {
b := make([]byte, n)
_, err := rand.Read(b)
if err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}
func generateTag() string {
tag, err := randomString(4)
if err != nil {
panic(err)
}
return tag
}

57
command.go Normal file
View File

@@ -0,0 +1,57 @@
package imap
import (
"errors"
"strings"
)
// A value that can be converted to a command.
type Commander interface {
Command() *Command
}
// A command.
type Command struct {
// The command tag. It acts as a unique identifier for this command. If empty,
// the command is untagged.
Tag string
// The command name.
Name string
// The command arguments.
Arguments []interface{}
}
// Implements the Commander interface.
func (cmd *Command) Command() *Command {
return cmd
}
func (cmd *Command) WriteTo(w *Writer) error {
tag := cmd.Tag
if tag == "" {
tag = "*"
}
fields := []interface{}{RawString(tag), RawString(cmd.Name)}
fields = append(fields, cmd.Arguments...)
return w.writeLine(fields...)
}
// Parse a command from fields.
func (cmd *Command) Parse(fields []interface{}) error {
if len(fields) < 2 {
return errors.New("imap: cannot parse command: no enough fields")
}
var ok bool
if cmd.Tag, ok = fields[0].(string); !ok {
return errors.New("imap: cannot parse command: invalid tag")
}
if cmd.Name, ok = fields[1].(string); !ok {
return errors.New("imap: cannot parse command: invalid name")
}
cmd.Name = strings.ToUpper(cmd.Name) // Command names are case-insensitive
cmd.Arguments = fields[2:]
return nil
}

98
command_test.go Normal file
View File

@@ -0,0 +1,98 @@
package imap_test
import (
"bytes"
"testing"
"github.com/emersion/go-imap"
)
func TestCommand_Command(t *testing.T) {
cmd := &imap.Command{
Tag: "A001",
Name: "NOOP",
}
if cmd.Command() != cmd {
t.Error("Command should return itself")
}
}
func TestCommand_WriteTo_NoArgs(t *testing.T) {
var b bytes.Buffer
w := imap.NewWriter(&b)
cmd := &imap.Command{
Tag: "A001",
Name: "NOOP",
}
if err := cmd.WriteTo(w); err != nil {
t.Fatal(err)
}
if b.String() != "A001 NOOP\r\n" {
t.Fatal("Not the expected command: ", b.String())
}
}
func TestCommand_WriteTo_WithArgs(t *testing.T) {
var b bytes.Buffer
w := imap.NewWriter(&b)
cmd := &imap.Command{
Tag: "A002",
Name: "LOGIN",
Arguments: []interface{}{"username", "password"},
}
if err := cmd.WriteTo(w); err != nil {
t.Fatal(err)
}
if b.String() != "A002 LOGIN \"username\" \"password\"\r\n" {
t.Fatal("Not the expected command: ", b.String())
}
}
func TestCommand_Parse_NoArgs(t *testing.T) {
fields := []interface{}{"a", "NOOP"}
cmd := &imap.Command{}
if err := cmd.Parse(fields); err != nil {
t.Fatal(err)
}
if cmd.Tag != "a" {
t.Error("Invalid tag:", cmd.Tag)
}
if cmd.Name != "NOOP" {
t.Error("Invalid name:", cmd.Name)
}
if len(cmd.Arguments) != 0 {
t.Error("Invalid arguments:", cmd.Arguments)
}
}
func TestCommand_Parse_WithArgs(t *testing.T) {
fields := []interface{}{"a", "LOGIN", "username", "password"}
cmd := &imap.Command{}
if err := cmd.Parse(fields); err != nil {
t.Fatal(err)
}
if cmd.Tag != "a" {
t.Error("Invalid tag:", cmd.Tag)
}
if cmd.Name != "LOGIN" {
t.Error("Invalid name:", cmd.Name)
}
if len(cmd.Arguments) != 2 {
t.Error("Invalid arguments:", cmd.Arguments)
}
if username, ok := cmd.Arguments[0].(string); !ok || username != "username" {
t.Error("Invalid first argument:", cmd.Arguments[0])
}
if password, ok := cmd.Arguments[1].(string); !ok || password != "password" {
t.Error("Invalid second argument:", cmd.Arguments[1])
}
}

93
commands/append.go Normal file
View File

@@ -0,0 +1,93 @@
package commands
import (
"errors"
"time"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/utf7"
)
// Append is an APPEND command, as defined in RFC 3501 section 6.3.11.
type Append struct {
Mailbox string
Flags []string
Date time.Time
Message imap.Literal
}
func (cmd *Append) Command() *imap.Command {
var args []interface{}
mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox)
args = append(args, imap.FormatMailboxName(mailbox))
if cmd.Flags != nil {
flags := make([]interface{}, len(cmd.Flags))
for i, flag := range cmd.Flags {
flags[i] = imap.RawString(flag)
}
args = append(args, flags)
}
if !cmd.Date.IsZero() {
args = append(args, cmd.Date)
}
args = append(args, cmd.Message)
return &imap.Command{
Name: "APPEND",
Arguments: args,
}
}
func (cmd *Append) Parse(fields []interface{}) (err error) {
if len(fields) < 2 {
return errors.New("No enough arguments")
}
// Parse mailbox name
if mailbox, err := imap.ParseString(fields[0]); err != nil {
return err
} else if mailbox, err = utf7.Encoding.NewDecoder().String(mailbox); err != nil {
return err
} else {
cmd.Mailbox = imap.CanonicalMailboxName(mailbox)
}
// Parse message literal
litIndex := len(fields) - 1
var ok bool
if cmd.Message, ok = fields[litIndex].(imap.Literal); !ok {
return errors.New("Message must be a literal")
}
// Remaining fields a optional
fields = fields[1:litIndex]
if len(fields) > 0 {
// Parse flags list
if flags, ok := fields[0].([]interface{}); ok {
if cmd.Flags, err = imap.ParseStringList(flags); err != nil {
return err
}
for i, flag := range cmd.Flags {
cmd.Flags[i] = imap.CanonicalFlag(flag)
}
fields = fields[1:]
}
// Parse date
if len(fields) > 0 {
if date, ok := fields[0].(string); !ok {
return errors.New("Date must be a string")
} else if cmd.Date, err = time.Parse(imap.DateTimeLayout, date); err != nil {
return err
}
}
}
return
}

124
commands/authenticate.go Normal file
View File

@@ -0,0 +1,124 @@
package commands
import (
"bufio"
"encoding/base64"
"errors"
"io"
"strings"
"github.com/emersion/go-imap"
"github.com/emersion/go-sasl"
)
// AuthenticateConn is a connection that supports IMAP authentication.
type AuthenticateConn interface {
io.Reader
// WriteResp writes an IMAP response to this connection.
WriteResp(res imap.WriterTo) error
}
// Authenticate is an AUTHENTICATE command, as defined in RFC 3501 section
// 6.2.2.
type Authenticate struct {
Mechanism string
InitialResponse []byte
}
func (cmd *Authenticate) Command() *imap.Command {
args := []interface{}{imap.RawString(cmd.Mechanism)}
if cmd.InitialResponse != nil {
var encodedResponse string
if len(cmd.InitialResponse) == 0 {
// Empty initial response should be encoded as "=", not empty
// string.
encodedResponse = "="
} else {
encodedResponse = base64.StdEncoding.EncodeToString(cmd.InitialResponse)
}
args = append(args, imap.RawString(encodedResponse))
}
return &imap.Command{
Name: "AUTHENTICATE",
Arguments: args,
}
}
func (cmd *Authenticate) Parse(fields []interface{}) error {
if len(fields) < 1 {
return errors.New("Not enough arguments")
}
var ok bool
if cmd.Mechanism, ok = fields[0].(string); !ok {
return errors.New("Mechanism must be a string")
}
cmd.Mechanism = strings.ToUpper(cmd.Mechanism)
if len(fields) != 2 {
return nil
}
encodedResponse, ok := fields[1].(string)
if !ok {
return errors.New("Initial response must be a string")
}
if encodedResponse == "=" {
cmd.InitialResponse = []byte{}
return nil
}
var err error
cmd.InitialResponse, err = base64.StdEncoding.DecodeString(encodedResponse)
if err != nil {
return err
}
return nil
}
func (cmd *Authenticate) Handle(mechanisms map[string]sasl.Server, conn AuthenticateConn) error {
sasl, ok := mechanisms[cmd.Mechanism]
if !ok {
return errors.New("Unsupported mechanism")
}
scanner := bufio.NewScanner(conn)
response := cmd.InitialResponse
for {
challenge, done, err := sasl.Next(response)
if err != nil || done {
return err
}
encoded := base64.StdEncoding.EncodeToString(challenge)
cont := &imap.ContinuationReq{Info: encoded}
if err := conn.WriteResp(cont); err != nil {
return err
}
if !scanner.Scan() {
if err := scanner.Err(); err != nil {
return err
}
return errors.New("unexpected EOF")
}
encoded = scanner.Text()
if encoded != "" {
if encoded == "*" {
return &imap.ErrStatusResp{Resp: &imap.StatusResp{
Type: imap.StatusRespBad,
Info: "negotiation cancelled",
}}
}
response, err = base64.StdEncoding.DecodeString(encoded)
if err != nil {
return err
}
}
}
}

18
commands/capability.go Normal file
View File

@@ -0,0 +1,18 @@
package commands
import (
"github.com/emersion/go-imap"
)
// Capability is a CAPABILITY command, as defined in RFC 3501 section 6.1.1.
type Capability struct{}
func (c *Capability) Command() *imap.Command {
return &imap.Command{
Name: "CAPABILITY",
}
}
func (c *Capability) Parse(fields []interface{}) error {
return nil
}

18
commands/check.go Normal file
View File

@@ -0,0 +1,18 @@
package commands
import (
"github.com/emersion/go-imap"
)
// Check is a CHECK command, as defined in RFC 3501 section 6.4.1.
type Check struct{}
func (cmd *Check) Command() *imap.Command {
return &imap.Command{
Name: "CHECK",
}
}
func (cmd *Check) Parse(fields []interface{}) error {
return nil
}

18
commands/close.go Normal file
View File

@@ -0,0 +1,18 @@
package commands
import (
"github.com/emersion/go-imap"
)
// Close is a CLOSE command, as defined in RFC 3501 section 6.4.2.
type Close struct{}
func (cmd *Close) Command() *imap.Command {
return &imap.Command{
Name: "CLOSE",
}
}
func (cmd *Close) Parse(fields []interface{}) error {
return nil
}

2
commands/commands.go Normal file
View File

@@ -0,0 +1,2 @@
// Package commands implements IMAP commands defined in RFC 3501.
package commands

47
commands/copy.go Normal file
View File

@@ -0,0 +1,47 @@
package commands
import (
"errors"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/utf7"
)
// Copy is a COPY command, as defined in RFC 3501 section 6.4.7.
type Copy struct {
SeqSet *imap.SeqSet
Mailbox string
}
func (cmd *Copy) Command() *imap.Command {
mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox)
return &imap.Command{
Name: "COPY",
Arguments: []interface{}{cmd.SeqSet, imap.FormatMailboxName(mailbox)},
}
}
func (cmd *Copy) Parse(fields []interface{}) error {
if len(fields) < 2 {
return errors.New("No enough arguments")
}
if seqSet, ok := fields[0].(string); !ok {
return errors.New("Invalid sequence set")
} else if seqSet, err := imap.ParseSeqSet(seqSet); err != nil {
return err
} else {
cmd.SeqSet = seqSet
}
if mailbox, err := imap.ParseString(fields[1]); err != nil {
return err
} else if mailbox, err := utf7.Encoding.NewDecoder().String(mailbox); err != nil {
return err
} else {
cmd.Mailbox = imap.CanonicalMailboxName(mailbox)
}
return nil
}

38
commands/create.go Normal file
View File

@@ -0,0 +1,38 @@
package commands
import (
"errors"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/utf7"
)
// Create is a CREATE command, as defined in RFC 3501 section 6.3.3.
type Create struct {
Mailbox string
}
func (cmd *Create) Command() *imap.Command {
mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox)
return &imap.Command{
Name: "CREATE",
Arguments: []interface{}{mailbox},
}
}
func (cmd *Create) Parse(fields []interface{}) error {
if len(fields) < 1 {
return errors.New("No enough arguments")
}
if mailbox, err := imap.ParseString(fields[0]); err != nil {
return err
} else if mailbox, err := utf7.Encoding.NewDecoder().String(mailbox); err != nil {
return err
} else {
cmd.Mailbox = imap.CanonicalMailboxName(mailbox)
}
return nil
}

38
commands/delete.go Normal file
View File

@@ -0,0 +1,38 @@
package commands
import (
"errors"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/utf7"
)
// Delete is a DELETE command, as defined in RFC 3501 section 6.3.3.
type Delete struct {
Mailbox string
}
func (cmd *Delete) Command() *imap.Command {
mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox)
return &imap.Command{
Name: "DELETE",
Arguments: []interface{}{imap.FormatMailboxName(mailbox)},
}
}
func (cmd *Delete) Parse(fields []interface{}) error {
if len(fields) < 1 {
return errors.New("No enough arguments")
}
if mailbox, err := imap.ParseString(fields[0]); err != nil {
return err
} else if mailbox, err := utf7.Encoding.NewDecoder().String(mailbox); err != nil {
return err
} else {
cmd.Mailbox = imap.CanonicalMailboxName(mailbox)
}
return nil
}

28
commands/enable.go Normal file
View File

@@ -0,0 +1,28 @@
package commands
import (
"github.com/emersion/go-imap"
)
// An ENABLE command, defined in RFC 5161 section 3.1.
type Enable struct {
Caps []string
}
func (cmd *Enable) Command() *imap.Command {
args := make([]interface{}, len(cmd.Caps))
for i, c := range cmd.Caps {
args[i] = imap.RawString(c)
}
return &imap.Command{
Name: "ENABLE",
Arguments: args,
}
}
func (cmd *Enable) Parse(fields []interface{}) error {
var err error
cmd.Caps, err = imap.ParseStringList(fields)
return err
}

16
commands/expunge.go Normal file
View File

@@ -0,0 +1,16 @@
package commands
import (
"github.com/emersion/go-imap"
)
// Expunge is an EXPUNGE command, as defined in RFC 3501 section 6.4.3.
type Expunge struct{}
func (cmd *Expunge) Command() *imap.Command {
return &imap.Command{Name: "EXPUNGE"}
}
func (cmd *Expunge) Parse(fields []interface{}) error {
return nil
}

63
commands/fetch.go Normal file
View File

@@ -0,0 +1,63 @@
package commands
import (
"errors"
"strings"
"github.com/emersion/go-imap"
)
// Fetch is a FETCH command, as defined in RFC 3501 section 6.4.5.
type Fetch struct {
SeqSet *imap.SeqSet
Items []imap.FetchItem
}
func (cmd *Fetch) Command() *imap.Command {
// Handle FETCH macros separately as they should not be serialized within parentheses
if len(cmd.Items) == 1 && (cmd.Items[0] == imap.FetchAll || cmd.Items[0] == imap.FetchFast || cmd.Items[0] == imap.FetchFull) {
return &imap.Command{
Name: "FETCH",
Arguments: []interface{}{cmd.SeqSet, imap.RawString(cmd.Items[0])},
}
} else {
items := make([]interface{}, len(cmd.Items))
for i, item := range cmd.Items {
items[i] = imap.RawString(item)
}
return &imap.Command{
Name: "FETCH",
Arguments: []interface{}{cmd.SeqSet, items},
}
}
}
func (cmd *Fetch) Parse(fields []interface{}) error {
if len(fields) < 2 {
return errors.New("No enough arguments")
}
var err error
if seqset, ok := fields[0].(string); !ok {
return errors.New("Sequence set must be an atom")
} else if cmd.SeqSet, err = imap.ParseSeqSet(seqset); err != nil {
return err
}
switch items := fields[1].(type) {
case string: // A macro or a single item
cmd.Items = imap.FetchItem(strings.ToUpper(items)).Expand()
case []interface{}: // A list of items
cmd.Items = make([]imap.FetchItem, 0, len(items))
for _, v := range items {
itemStr, _ := v.(string)
item := imap.FetchItem(strings.ToUpper(itemStr))
cmd.Items = append(cmd.Items, item.Expand()...)
}
default:
return errors.New("Items must be either a string or a list")
}
return nil
}

17
commands/idle.go Normal file
View File

@@ -0,0 +1,17 @@
package commands
import (
"github.com/emersion/go-imap"
)
// An IDLE command.
// Se RFC 2177 section 3.
type Idle struct{}
func (cmd *Idle) Command() *imap.Command {
return &imap.Command{Name: "IDLE"}
}
func (cmd *Idle) Parse(fields []interface{}) error {
return nil
}

60
commands/list.go Normal file
View File

@@ -0,0 +1,60 @@
package commands
import (
"errors"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/utf7"
)
// List is a LIST command, as defined in RFC 3501 section 6.3.8. If Subscribed
// is set to true, LSUB will be used instead.
type List struct {
Reference string
Mailbox string
Subscribed bool
}
func (cmd *List) Command() *imap.Command {
name := "LIST"
if cmd.Subscribed {
name = "LSUB"
}
enc := utf7.Encoding.NewEncoder()
ref, _ := enc.String(cmd.Reference)
mailbox, _ := enc.String(cmd.Mailbox)
return &imap.Command{
Name: name,
Arguments: []interface{}{ref, mailbox},
}
}
func (cmd *List) Parse(fields []interface{}) error {
if len(fields) < 2 {
return errors.New("No enough arguments")
}
dec := utf7.Encoding.NewDecoder()
if mailbox, err := imap.ParseString(fields[0]); err != nil {
return err
} else if mailbox, err := dec.String(mailbox); err != nil {
return err
} else {
// TODO: canonical mailbox path
cmd.Reference = imap.CanonicalMailboxName(mailbox)
}
if mailbox, err := imap.ParseString(fields[1]); err != nil {
return err
} else if mailbox, err := dec.String(mailbox); err != nil {
return err
} else {
cmd.Mailbox = imap.CanonicalMailboxName(mailbox)
}
return nil
}

36
commands/login.go Normal file
View File

@@ -0,0 +1,36 @@
package commands
import (
"errors"
"github.com/emersion/go-imap"
)
// Login is a LOGIN command, as defined in RFC 3501 section 6.2.2.
type Login struct {
Username string
Password string
}
func (cmd *Login) Command() *imap.Command {
return &imap.Command{
Name: "LOGIN",
Arguments: []interface{}{cmd.Username, cmd.Password},
}
}
func (cmd *Login) Parse(fields []interface{}) error {
if len(fields) < 2 {
return errors.New("Not enough arguments")
}
var err error
if cmd.Username, err = imap.ParseString(fields[0]); err != nil {
return err
}
if cmd.Password, err = imap.ParseString(fields[1]); err != nil {
return err
}
return nil
}

18
commands/logout.go Normal file
View File

@@ -0,0 +1,18 @@
package commands
import (
"github.com/emersion/go-imap"
)
// Logout is a LOGOUT command, as defined in RFC 3501 section 6.1.3.
type Logout struct{}
func (c *Logout) Command() *imap.Command {
return &imap.Command{
Name: "LOGOUT",
}
}
func (c *Logout) Parse(fields []interface{}) error {
return nil
}

48
commands/move.go Normal file
View File

@@ -0,0 +1,48 @@
package commands
import (
"errors"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/utf7"
)
// A MOVE command.
// See RFC 6851 section 3.1.
type Move struct {
SeqSet *imap.SeqSet
Mailbox string
}
func (cmd *Move) Command() *imap.Command {
mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox)
return &imap.Command{
Name: "MOVE",
Arguments: []interface{}{cmd.SeqSet, mailbox},
}
}
func (cmd *Move) Parse(fields []interface{}) (err error) {
if len(fields) < 2 {
return errors.New("No enough arguments")
}
seqset, ok := fields[0].(string)
if !ok {
return errors.New("Invalid sequence set")
}
if cmd.SeqSet, err = imap.ParseSeqSet(seqset); err != nil {
return err
}
mailbox, ok := fields[1].(string)
if !ok {
return errors.New("Mailbox name must be a string")
}
if cmd.Mailbox, err = utf7.Encoding.NewDecoder().String(mailbox); err != nil {
return err
}
return
}

18
commands/noop.go Normal file
View File

@@ -0,0 +1,18 @@
package commands
import (
"github.com/emersion/go-imap"
)
// Noop is a NOOP command, as defined in RFC 3501 section 6.1.2.
type Noop struct{}
func (c *Noop) Command() *imap.Command {
return &imap.Command{
Name: "NOOP",
}
}
func (c *Noop) Parse(fields []interface{}) error {
return nil
}

51
commands/rename.go Normal file
View File

@@ -0,0 +1,51 @@
package commands
import (
"errors"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/utf7"
)
// Rename is a RENAME command, as defined in RFC 3501 section 6.3.5.
type Rename struct {
Existing string
New string
}
func (cmd *Rename) Command() *imap.Command {
enc := utf7.Encoding.NewEncoder()
existingName, _ := enc.String(cmd.Existing)
newName, _ := enc.String(cmd.New)
return &imap.Command{
Name: "RENAME",
Arguments: []interface{}{imap.FormatMailboxName(existingName), imap.FormatMailboxName(newName)},
}
}
func (cmd *Rename) Parse(fields []interface{}) error {
if len(fields) < 2 {
return errors.New("No enough arguments")
}
dec := utf7.Encoding.NewDecoder()
if existingName, err := imap.ParseString(fields[0]); err != nil {
return err
} else if existingName, err := dec.String(existingName); err != nil {
return err
} else {
cmd.Existing = imap.CanonicalMailboxName(existingName)
}
if newName, err := imap.ParseString(fields[1]); err != nil {
return err
} else if newName, err := dec.String(newName); err != nil {
return err
} else {
cmd.New = imap.CanonicalMailboxName(newName)
}
return nil
}

57
commands/search.go Normal file
View File

@@ -0,0 +1,57 @@
package commands
import (
"errors"
"io"
"strings"
"github.com/emersion/go-imap"
)
// Search is a SEARCH command, as defined in RFC 3501 section 6.4.4.
type Search struct {
Charset string
Criteria *imap.SearchCriteria
}
func (cmd *Search) Command() *imap.Command {
var args []interface{}
if cmd.Charset != "" {
args = append(args, imap.RawString("CHARSET"), imap.RawString(cmd.Charset))
}
args = append(args, cmd.Criteria.Format()...)
return &imap.Command{
Name: "SEARCH",
Arguments: args,
}
}
func (cmd *Search) Parse(fields []interface{}) error {
if len(fields) == 0 {
return errors.New("Missing search criteria")
}
// Parse charset
if f, ok := fields[0].(string); ok && strings.EqualFold(f, "CHARSET") {
if len(fields) < 2 {
return errors.New("Missing CHARSET value")
}
if cmd.Charset, ok = fields[1].(string); !ok {
return errors.New("Charset must be a string")
}
fields = fields[2:]
}
var charsetReader func(io.Reader) io.Reader
charset := strings.ToLower(cmd.Charset)
if charset != "utf-8" && charset != "us-ascii" && charset != "" {
charsetReader = func(r io.Reader) io.Reader {
r, _ = imap.CharsetReader(charset, r)
return r
}
}
cmd.Criteria = new(imap.SearchCriteria)
return cmd.Criteria.ParseWithCharset(fields, charsetReader)
}

45
commands/select.go Normal file
View File

@@ -0,0 +1,45 @@
package commands
import (
"errors"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/utf7"
)
// Select is a SELECT command, as defined in RFC 3501 section 6.3.1. If ReadOnly
// is set to true, the EXAMINE command will be used instead.
type Select struct {
Mailbox string
ReadOnly bool
}
func (cmd *Select) Command() *imap.Command {
name := "SELECT"
if cmd.ReadOnly {
name = "EXAMINE"
}
mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox)
return &imap.Command{
Name: name,
Arguments: []interface{}{imap.FormatMailboxName(mailbox)},
}
}
func (cmd *Select) Parse(fields []interface{}) error {
if len(fields) < 1 {
return errors.New("No enough arguments")
}
if mailbox, err := imap.ParseString(fields[0]); err != nil {
return err
} else if mailbox, err := utf7.Encoding.NewDecoder().String(mailbox); err != nil {
return err
} else {
cmd.Mailbox = imap.CanonicalMailboxName(mailbox)
}
return nil
}

18
commands/starttls.go Normal file
View File

@@ -0,0 +1,18 @@
package commands
import (
"github.com/emersion/go-imap"
)
// StartTLS is a STARTTLS command, as defined in RFC 3501 section 6.2.1.
type StartTLS struct{}
func (cmd *StartTLS) Command() *imap.Command {
return &imap.Command{
Name: "STARTTLS",
}
}
func (cmd *StartTLS) Parse(fields []interface{}) error {
return nil
}

58
commands/status.go Normal file
View File

@@ -0,0 +1,58 @@
package commands
import (
"errors"
"strings"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/utf7"
)
// Status is a STATUS command, as defined in RFC 3501 section 6.3.10.
type Status struct {
Mailbox string
Items []imap.StatusItem
}
func (cmd *Status) Command() *imap.Command {
mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox)
items := make([]interface{}, len(cmd.Items))
for i, item := range cmd.Items {
items[i] = imap.RawString(item)
}
return &imap.Command{
Name: "STATUS",
Arguments: []interface{}{imap.FormatMailboxName(mailbox), items},
}
}
func (cmd *Status) Parse(fields []interface{}) error {
if len(fields) < 2 {
return errors.New("No enough arguments")
}
if mailbox, err := imap.ParseString(fields[0]); err != nil {
return err
} else if mailbox, err := utf7.Encoding.NewDecoder().String(mailbox); err != nil {
return err
} else {
cmd.Mailbox = imap.CanonicalMailboxName(mailbox)
}
items, ok := fields[1].([]interface{})
if !ok {
return errors.New("STATUS command parameter is not a list")
}
cmd.Items = make([]imap.StatusItem, len(items))
for i, f := range items {
if s, ok := f.(string); !ok {
return errors.New("Got a non-string field in a STATUS command parameter")
} else {
cmd.Items[i] = imap.StatusItem(strings.ToUpper(s))
}
}
return nil
}

50
commands/store.go Normal file
View File

@@ -0,0 +1,50 @@
package commands
import (
"errors"
"strings"
"github.com/emersion/go-imap"
)
// Store is a STORE command, as defined in RFC 3501 section 6.4.6.
type Store struct {
SeqSet *imap.SeqSet
Item imap.StoreItem
Value interface{}
}
func (cmd *Store) Command() *imap.Command {
return &imap.Command{
Name: "STORE",
Arguments: []interface{}{cmd.SeqSet, imap.RawString(cmd.Item), cmd.Value},
}
}
func (cmd *Store) Parse(fields []interface{}) error {
if len(fields) < 3 {
return errors.New("No enough arguments")
}
seqset, ok := fields[0].(string)
if !ok {
return errors.New("Invalid sequence set")
}
var err error
if cmd.SeqSet, err = imap.ParseSeqSet(seqset); err != nil {
return err
}
if item, ok := fields[1].(string); !ok {
return errors.New("Item name must be a string")
} else {
cmd.Item = imap.StoreItem(strings.ToUpper(item))
}
if len(fields[2:]) == 1 {
cmd.Value = fields[2]
} else {
cmd.Value = fields[2:]
}
return nil
}

63
commands/subscribe.go Normal file
View File

@@ -0,0 +1,63 @@
package commands
import (
"errors"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/utf7"
)
// Subscribe is a SUBSCRIBE command, as defined in RFC 3501 section 6.3.6.
type Subscribe struct {
Mailbox string
}
func (cmd *Subscribe) Command() *imap.Command {
mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox)
return &imap.Command{
Name: "SUBSCRIBE",
Arguments: []interface{}{imap.FormatMailboxName(mailbox)},
}
}
func (cmd *Subscribe) Parse(fields []interface{}) error {
if len(fields) < 1 {
return errors.New("No enough arguments")
}
if mailbox, err := imap.ParseString(fields[0]); err != nil {
return err
} else if cmd.Mailbox, err = utf7.Encoding.NewDecoder().String(mailbox); err != nil {
return err
}
return nil
}
// An UNSUBSCRIBE command.
// See RFC 3501 section 6.3.7
type Unsubscribe struct {
Mailbox string
}
func (cmd *Unsubscribe) Command() *imap.Command {
mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox)
return &imap.Command{
Name: "UNSUBSCRIBE",
Arguments: []interface{}{imap.FormatMailboxName(mailbox)},
}
}
func (cmd *Unsubscribe) Parse(fields []interface{}) error {
if len(fields) < 1 {
return errors.New("No enogh arguments")
}
if mailbox, err := imap.ParseString(fields[0]); err != nil {
return err
} else if cmd.Mailbox, err = utf7.Encoding.NewDecoder().String(mailbox); err != nil {
return err
}
return nil
}

44
commands/uid.go Normal file
View File

@@ -0,0 +1,44 @@
package commands
import (
"errors"
"strings"
"github.com/emersion/go-imap"
)
// Uid is a UID command, as defined in RFC 3501 section 6.4.8. It wraps another
// command (e.g. wrapping a Fetch command will result in a UID FETCH).
type Uid struct {
Cmd imap.Commander
}
func (cmd *Uid) Command() *imap.Command {
inner := cmd.Cmd.Command()
args := []interface{}{imap.RawString(inner.Name)}
args = append(args, inner.Arguments...)
return &imap.Command{
Name: "UID",
Arguments: args,
}
}
func (cmd *Uid) Parse(fields []interface{}) error {
if len(fields) < 1 {
return errors.New("No command name specified")
}
name, ok := fields[0].(string)
if !ok {
return errors.New("Command name must be a string")
}
cmd.Cmd = &imap.Command{
Name: strings.ToUpper(name), // Command names are case-insensitive
Arguments: fields[1:],
}
return nil
}

17
commands/unselect.go Normal file
View File

@@ -0,0 +1,17 @@
package commands
import (
"github.com/emersion/go-imap"
)
// An UNSELECT command.
// See RFC 3691 section 2.
type Unselect struct{}
func (cmd *Unselect) Command() *imap.Command {
return &imap.Command{Name: "UNSELECT"}
}
func (cmd *Unselect) Parse(fields []interface{}) error {
return nil
}

284
conn.go Normal file
View File

@@ -0,0 +1,284 @@
package imap
import (
"bufio"
"crypto/tls"
"io"
"net"
"sync"
)
// A connection state.
// See RFC 3501 section 3.
type ConnState int
const (
// In the connecting state, the server has not yet sent a greeting and no
// command can be issued.
ConnectingState = 0
// In the not authenticated state, the client MUST supply
// authentication credentials before most commands will be
// permitted. This state is entered when a connection starts
// unless the connection has been pre-authenticated.
NotAuthenticatedState ConnState = 1 << 0
// In the authenticated state, the client is authenticated and MUST
// select a mailbox to access before commands that affect messages
// will be permitted. This state is entered when a
// pre-authenticated connection starts, when acceptable
// authentication credentials have been provided, after an error in
// selecting a mailbox, or after a successful CLOSE command.
AuthenticatedState = 1 << 1
// In a selected state, a mailbox has been selected to access.
// This state is entered when a mailbox has been successfully
// selected.
SelectedState = AuthenticatedState + 1<<2
// In the logout state, the connection is being terminated. This
// state can be entered as a result of a client request (via the
// LOGOUT command) or by unilateral action on the part of either
// the client or server.
LogoutState = 1 << 3
// ConnectedState is either NotAuthenticatedState, AuthenticatedState or
// SelectedState.
ConnectedState = NotAuthenticatedState | AuthenticatedState | SelectedState
)
// A function that upgrades a connection.
//
// This should only be used by libraries implementing an IMAP extension (e.g.
// COMPRESS).
type ConnUpgrader func(conn net.Conn) (net.Conn, error)
type Waiter struct {
start sync.WaitGroup
end sync.WaitGroup
finished bool
}
func NewWaiter() *Waiter {
w := &Waiter{finished: false}
w.start.Add(1)
w.end.Add(1)
return w
}
func (w *Waiter) Wait() {
if !w.finished {
// Signal that we are ready for upgrade to continue.
w.start.Done()
// Wait for upgrade to finish.
w.end.Wait()
w.finished = true
}
}
func (w *Waiter) WaitReady() {
if !w.finished {
// Wait for reader/writer goroutine to be ready for upgrade.
w.start.Wait()
}
}
func (w *Waiter) Close() {
if !w.finished {
// Upgrade is finished, close chanel to release reader/writer
w.end.Done()
}
}
type LockedWriter struct {
lock sync.Mutex
writer io.Writer
}
// NewLockedWriter - goroutine safe writer.
func NewLockedWriter(w io.Writer) io.Writer {
return &LockedWriter{writer: w}
}
func (w *LockedWriter) Write(b []byte) (int, error) {
w.lock.Lock()
defer w.lock.Unlock()
return w.writer.Write(b)
}
type debugWriter struct {
io.Writer
local io.Writer
remote io.Writer
}
// NewDebugWriter creates a new io.Writer that will write local network activity
// to local and remote network activity to remote.
func NewDebugWriter(local, remote io.Writer) io.Writer {
return &debugWriter{Writer: local, local: local, remote: remote}
}
type multiFlusher struct {
flushers []flusher
}
func (mf *multiFlusher) Flush() error {
for _, f := range mf.flushers {
if err := f.Flush(); err != nil {
return err
}
}
return nil
}
func newMultiFlusher(flushers ...flusher) flusher {
return &multiFlusher{flushers}
}
// Underlying connection state information.
type ConnInfo struct {
RemoteAddr net.Addr
LocalAddr net.Addr
// nil if connection is not using TLS.
TLS *tls.ConnectionState
}
// An IMAP connection.
type Conn struct {
net.Conn
*Reader
*Writer
br *bufio.Reader
bw *bufio.Writer
waiter *Waiter
// Print all commands and responses to this io.Writer.
debug io.Writer
}
// NewConn creates a new IMAP connection.
func NewConn(conn net.Conn, r *Reader, w *Writer) *Conn {
c := &Conn{Conn: conn, Reader: r, Writer: w}
c.init()
return c
}
func (c *Conn) createWaiter() *Waiter {
// create new waiter each time.
w := NewWaiter()
c.waiter = w
return w
}
func (c *Conn) init() {
r := io.Reader(c.Conn)
w := io.Writer(c.Conn)
if c.debug != nil {
localDebug, remoteDebug := c.debug, c.debug
if debug, ok := c.debug.(*debugWriter); ok {
localDebug, remoteDebug = debug.local, debug.remote
}
// If local and remote are the same, then we need a LockedWriter.
if localDebug == remoteDebug {
localDebug = NewLockedWriter(localDebug)
remoteDebug = localDebug
}
if localDebug != nil {
w = io.MultiWriter(c.Conn, localDebug)
}
if remoteDebug != nil {
r = io.TeeReader(c.Conn, remoteDebug)
}
}
if c.br == nil {
c.br = bufio.NewReader(r)
c.Reader.reader = c.br
} else {
c.br.Reset(r)
}
if c.bw == nil {
c.bw = bufio.NewWriter(w)
c.Writer.Writer = c.bw
} else {
c.bw.Reset(w)
}
if f, ok := c.Conn.(flusher); ok {
c.Writer.Writer = struct {
io.Writer
flusher
}{
c.bw,
newMultiFlusher(c.bw, f),
}
}
}
func (c *Conn) Info() *ConnInfo {
info := &ConnInfo{
RemoteAddr: c.RemoteAddr(),
LocalAddr: c.LocalAddr(),
}
tlsConn, ok := c.Conn.(*tls.Conn)
if ok {
state := tlsConn.ConnectionState()
info.TLS = &state
}
return info
}
// Write implements io.Writer.
func (c *Conn) Write(b []byte) (n int, err error) {
return c.Writer.Write(b)
}
// Flush writes any buffered data to the underlying connection.
func (c *Conn) Flush() error {
return c.Writer.Flush()
}
// Upgrade a connection, e.g. wrap an unencrypted connection with an encrypted
// tunnel.
func (c *Conn) Upgrade(upgrader ConnUpgrader) error {
// Block reads and writes during the upgrading process
w := c.createWaiter()
defer w.Close()
upgraded, err := upgrader(c.Conn)
if err != nil {
return err
}
c.Conn = upgraded
c.init()
return nil
}
// Called by reader/writer goroutines to wait for Upgrade to finish
func (c *Conn) Wait() {
c.waiter.Wait()
}
// Called by Upgrader to wait for reader/writer goroutines to be ready for
// upgrade.
func (c *Conn) WaitReady() {
c.waiter.WaitReady()
}
// SetDebug defines an io.Writer to which all network activity will be logged.
// If nil is provided, network activity will not be logged.
func (c *Conn) SetDebug(w io.Writer) {
c.debug = w
c.init()
}

107
conn_test.go Normal file
View File

@@ -0,0 +1,107 @@
package imap_test
import (
"bytes"
"io"
"net"
"testing"
"github.com/emersion/go-imap"
)
func TestNewConn(t *testing.T) {
b := &bytes.Buffer{}
c, s := net.Pipe()
done := make(chan error)
go (func() {
_, err := io.Copy(b, s)
done <- err
})()
r := imap.NewReader(nil)
w := imap.NewWriter(nil)
ic := imap.NewConn(c, r, w)
sent := []byte("hi")
ic.Write(sent)
ic.Flush()
ic.Close()
if err := <-done; err != nil {
t.Fatal(err)
}
s.Close()
received := b.Bytes()
if string(sent) != string(received) {
t.Errorf("Sent %v but received %v", sent, received)
}
}
func transform(b []byte) []byte {
bb := make([]byte, len(b))
for i, c := range b {
if rune(c) == 'c' {
bb[i] = byte('d')
} else {
bb[i] = c
}
}
return bb
}
type upgraded struct {
net.Conn
}
func (c *upgraded) Write(b []byte) (int, error) {
return c.Conn.Write(transform(b))
}
func TestConn_Upgrade(t *testing.T) {
b := &bytes.Buffer{}
c, s := net.Pipe()
done := make(chan error)
go (func() {
_, err := io.Copy(b, s)
done <- err
})()
r := imap.NewReader(nil)
w := imap.NewWriter(nil)
ic := imap.NewConn(c, r, w)
began := make(chan struct{})
go ic.Upgrade(func(conn net.Conn) (net.Conn, error) {
began <- struct{}{}
ic.WaitReady()
return &upgraded{conn}, nil
})
<-began
ic.Wait()
sent := []byte("abcd")
expected := transform(sent)
ic.Write(sent)
ic.Flush()
ic.Close()
if err := <-done; err != nil {
t.Fatal(err)
}
s.Close()
received := b.Bytes()
if string(expected) != string(received) {
t.Errorf("Expected %v but received %v", expected, received)
}
}

71
date.go Normal file
View File

@@ -0,0 +1,71 @@
package imap
import (
"fmt"
"regexp"
"time"
)
// Date and time layouts.
// Dovecot adds a leading zero to dates:
// https://github.com/dovecot/core/blob/4fbd5c5e113078e72f29465ccc96d44955ceadc2/src/lib-imap/imap-date.c#L166
// Cyrus adds a leading space to dates:
// https://github.com/cyrusimap/cyrus-imapd/blob/1cb805a3bffbdf829df0964f3b802cdc917e76db/lib/times.c#L543
// GMail doesn't support leading spaces in dates used in SEARCH commands.
const (
// Defined in RFC 3501 as date-text on page 83.
DateLayout = "_2-Jan-2006"
// Defined in RFC 3501 as date-time on page 83.
DateTimeLayout = "_2-Jan-2006 15:04:05 -0700"
// Defined in RFC 5322 section 3.3, mentioned as env-date in RFC 3501 page 84.
envelopeDateTimeLayout = "Mon, 02 Jan 2006 15:04:05 -0700"
// Use as an example in RFC 3501 page 54.
searchDateLayout = "2-Jan-2006"
)
// time.Time with a specific layout.
type (
Date time.Time
DateTime time.Time
envelopeDateTime time.Time
searchDate time.Time
)
// Permutations of the layouts defined in RFC 5322, section 3.3.
var envelopeDateTimeLayouts = [...]string{
envelopeDateTimeLayout, // popular, try it first
"_2 Jan 2006 15:04:05 -0700",
"_2 Jan 2006 15:04:05 MST",
"_2 Jan 2006 15:04 -0700",
"_2 Jan 2006 15:04 MST",
"_2 Jan 06 15:04:05 -0700",
"_2 Jan 06 15:04:05 MST",
"_2 Jan 06 15:04 -0700",
"_2 Jan 06 15:04 MST",
"Mon, _2 Jan 2006 15:04:05 -0700",
"Mon, _2 Jan 2006 15:04:05 MST",
"Mon, _2 Jan 2006 15:04 -0700",
"Mon, _2 Jan 2006 15:04 MST",
"Mon, _2 Jan 06 15:04:05 -0700",
"Mon, _2 Jan 06 15:04:05 MST",
"Mon, _2 Jan 06 15:04 -0700",
"Mon, _2 Jan 06 15:04 MST",
}
// TODO: this is a blunt way to strip any trailing CFWS (comment). A sharper
// one would strip multiple CFWS, and only if really valid according to
// RFC5322.
var commentRE = regexp.MustCompile(`[ \t]+\(.*\)$`)
// Try parsing the date based on the layouts defined in RFC 5322, section 3.3.
// Inspired by https://github.com/golang/go/blob/master/src/net/mail/message.go
func parseMessageDateTime(maybeDate string) (time.Time, error) {
maybeDate = commentRE.ReplaceAllString(maybeDate, "")
for _, layout := range envelopeDateTimeLayouts {
parsed, err := time.Parse(layout, maybeDate)
if err == nil {
return parsed, nil
}
}
return time.Time{}, fmt.Errorf("date %s could not be parsed", maybeDate)
}

95
date_test.go Normal file
View File

@@ -0,0 +1,95 @@
package imap
import (
"testing"
"time"
)
var expectedDateTime = time.Date(2009, time.November, 2, 23, 0, 0, 0, time.FixedZone("", -6*60*60))
var expectedDate = time.Date(2009, time.November, 2, 0, 0, 0, 0, time.FixedZone("", 0))
func TestParseMessageDateTime(t *testing.T) {
tests := []struct {
in string
out time.Time
ok bool
}{
// some permutations
{"2 Nov 2009 23:00 -0600", expectedDateTime, true},
{"Tue, 2 Nov 2009 23:00:00 -0600", expectedDateTime, true},
{"Tue, 2 Nov 2009 23:00:00 -0600 (MST)", expectedDateTime, true},
// whitespace
{" 2 Nov 2009 23:00 -0600", expectedDateTime, true},
{"Tue, 2 Nov 2009 23:00:00 -0600", expectedDateTime, true},
{"Tue, 2 Nov 2009 23:00:00 -0600 (MST)", expectedDateTime, true},
// invalid
{"abc10 Nov 2009 23:00 -0600123", expectedDateTime, false},
{"10.Nov.2009 11:00:00 -9900", expectedDateTime, false},
}
for _, test := range tests {
out, err := parseMessageDateTime(test.in)
if !test.ok {
if err == nil {
t.Errorf("ParseMessageDateTime(%q) expected error; got %q", test.in, out)
}
} else if err != nil {
t.Errorf("ParseMessageDateTime(%q) expected %q; got %v", test.in, test.out, err)
} else if !out.Equal(test.out) {
t.Errorf("ParseMessageDateTime(%q) expected %q; got %q", test.in, test.out, out)
}
}
}
func TestParseDateTime(t *testing.T) {
tests := []struct {
in string
out time.Time
ok bool
}{
{"2-Nov-2009 23:00:00 -0600", expectedDateTime, true},
// whitespace
{" 2-Nov-2009 23:00:00 -0600", expectedDateTime, true},
// invalid or incorrect
{"10-Nov-2009", time.Time{}, false},
{"abc10-Nov-2009 23:00:00 -0600123", time.Time{}, false},
}
for _, test := range tests {
out, err := time.Parse(DateTimeLayout, test.in)
if !test.ok {
if err == nil {
t.Errorf("ParseDateTime(%q) expected error; got %q", test.in, out)
}
} else if err != nil {
t.Errorf("ParseDateTime(%q) expected %q; got %v", test.in, test.out, err)
} else if !out.Equal(test.out) {
t.Errorf("ParseDateTime(%q) expected %q; got %q", test.in, test.out, out)
}
}
}
func TestParseDate(t *testing.T) {
tests := []struct {
in string
out time.Time
ok bool
}{
{"2-Nov-2009", expectedDate, true},
{" 2-Nov-2009", expectedDate, true},
}
for _, test := range tests {
out, err := time.Parse(DateLayout, test.in)
if !test.ok {
if err == nil {
t.Errorf("ParseDate(%q) expected error; got %q", test.in, out)
}
} else if err != nil {
t.Errorf("ParseDate(%q) expected %q; got %v", test.in, test.out, err)
} else if !out.Equal(test.out) {
t.Errorf("ParseDate(%q) expected %q; got %q", test.in, test.out, out)
}
}
}

BIN
go-imap-1.zip Normal file

Binary file not shown.

9
go.mod
View File

@@ -1,8 +1,9 @@
module github.com/emersion/go-imap/v2
module github.com/emersion/go-imap
go 1.18
go 1.13
require (
github.com/emersion/go-message v0.18.1
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43
github.com/emersion/go-message v0.15.0
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
golang.org/x/text v0.3.7
)

41
go.sum
View File

@@ -1,35 +1,10 @@
github.com/emersion/go-message v0.18.1 h1:tfTxIoXFSFRwWaZsgnqS1DSZuGpYGzSmCZD8SK3QA2E=
github.com/emersion/go-message v0.18.1/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA=
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 h1:hH4PQfOndHDlpzYfLAAfl63E8Le6F2+EL/cdhlkyRJY=
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
github.com/emersion/go-message v0.15.0 h1:urgKGqt2JAc9NFJcgncQcohHdiYb803YTH9OQwHBHIY=
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

172
imap.go
View File

@@ -1,105 +1,111 @@
// Package imap implements IMAP4rev2.
//
// IMAP4rev2 is defined in RFC 9051.
//
// This package contains types and functions common to both the client and
// server. See the imapclient and imapserver sub-packages.
// Package imap implements IMAP4rev1 (RFC 3501).
package imap
import (
"fmt"
"errors"
"io"
"strings"
)
// ConnState describes the connection state.
//
// See RFC 9051 section 3.
type ConnState int
// A StatusItem is a mailbox status data item that can be retrieved with a
// STATUS command. See RFC 3501 section 6.3.10.
type StatusItem string
const (
ConnStateNone ConnState = iota
ConnStateNotAuthenticated
ConnStateAuthenticated
ConnStateSelected
ConnStateLogout
StatusMessages StatusItem = "MESSAGES"
StatusRecent StatusItem = "RECENT"
StatusUidNext StatusItem = "UIDNEXT"
StatusUidValidity StatusItem = "UIDVALIDITY"
StatusUnseen StatusItem = "UNSEEN"
StatusAppendLimit StatusItem = "APPENDLIMIT"
)
// String implements fmt.Stringer.
func (state ConnState) String() string {
switch state {
case ConnStateNone:
return "none"
case ConnStateNotAuthenticated:
return "not authenticated"
case ConnStateAuthenticated:
return "authenticated"
case ConnStateSelected:
return "selected"
case ConnStateLogout:
return "logout"
// A FetchItem is a message data item that can be fetched.
type FetchItem string
// List of items that can be fetched. See RFC 3501 section 6.4.5.
//
// Warning: FetchBody will not return the raw message body, instead it will
// return a subset of FetchBodyStructure.
const (
// Macros
FetchAll FetchItem = "ALL"
FetchFast FetchItem = "FAST"
FetchFull FetchItem = "FULL"
// Items
FetchBody FetchItem = "BODY"
FetchBodyStructure FetchItem = "BODYSTRUCTURE"
FetchEnvelope FetchItem = "ENVELOPE"
FetchFlags FetchItem = "FLAGS"
FetchInternalDate FetchItem = "INTERNALDATE"
FetchRFC822 FetchItem = "RFC822"
FetchRFC822Header FetchItem = "RFC822.HEADER"
FetchRFC822Size FetchItem = "RFC822.SIZE"
FetchRFC822Text FetchItem = "RFC822.TEXT"
FetchUid FetchItem = "UID"
)
// Expand expands the item if it's a macro.
func (item FetchItem) Expand() []FetchItem {
switch item {
case FetchAll:
return []FetchItem{FetchFlags, FetchInternalDate, FetchRFC822Size, FetchEnvelope}
case FetchFast:
return []FetchItem{FetchFlags, FetchInternalDate, FetchRFC822Size}
case FetchFull:
return []FetchItem{FetchFlags, FetchInternalDate, FetchRFC822Size, FetchEnvelope, FetchBody}
default:
panic(fmt.Errorf("imap: unknown connection state %v", int(state)))
return []FetchItem{item}
}
}
// MailboxAttr is a mailbox attribute.
//
// Mailbox attributes are defined in RFC 9051 section 7.3.1.
type MailboxAttr string
// FlagsOp is an operation that will be applied on message flags.
type FlagsOp string
const (
// Base attributes
MailboxAttrNonExistent MailboxAttr = "\\NonExistent"
MailboxAttrNoInferiors MailboxAttr = "\\Noinferiors"
MailboxAttrNoSelect MailboxAttr = "\\Noselect"
MailboxAttrHasChildren MailboxAttr = "\\HasChildren"
MailboxAttrHasNoChildren MailboxAttr = "\\HasNoChildren"
MailboxAttrMarked MailboxAttr = "\\Marked"
MailboxAttrUnmarked MailboxAttr = "\\Unmarked"
MailboxAttrSubscribed MailboxAttr = "\\Subscribed"
MailboxAttrRemote MailboxAttr = "\\Remote"
// Role (aka. "special-use") attributes
MailboxAttrAll MailboxAttr = "\\All"
MailboxAttrArchive MailboxAttr = "\\Archive"
MailboxAttrDrafts MailboxAttr = "\\Drafts"
MailboxAttrFlagged MailboxAttr = "\\Flagged"
MailboxAttrJunk MailboxAttr = "\\Junk"
MailboxAttrSent MailboxAttr = "\\Sent"
MailboxAttrTrash MailboxAttr = "\\Trash"
MailboxAttrImportant MailboxAttr = "\\Important" // RFC 8457
// SetFlags replaces existing flags by new ones.
SetFlags FlagsOp = "FLAGS"
// AddFlags adds new flags.
AddFlags = "+FLAGS"
// RemoveFlags removes existing flags.
RemoveFlags = "-FLAGS"
)
// Flag is a message flag.
//
// Message flags are defined in RFC 9051 section 2.3.2.
type Flag string
// silentOp can be appended to a FlagsOp to prevent the operation from
// triggering unilateral message updates.
const silentOp = ".SILENT"
const (
// System flags
FlagSeen Flag = "\\Seen"
FlagAnswered Flag = "\\Answered"
FlagFlagged Flag = "\\Flagged"
FlagDeleted Flag = "\\Deleted"
FlagDraft Flag = "\\Draft"
// A StoreItem is a message data item that can be updated.
type StoreItem string
// Widely used flags
FlagForwarded Flag = "$Forwarded"
FlagMDNSent Flag = "$MDNSent" // Message Disposition Notification sent
FlagJunk Flag = "$Junk"
FlagNotJunk Flag = "$NotJunk"
FlagPhishing Flag = "$Phishing"
FlagImportant Flag = "$Important" // RFC 8457
// Permanent flags
FlagWildcard Flag = "\\*"
)
// LiteralReader is a reader for IMAP literals.
type LiteralReader interface {
io.Reader
Size() int64
// FormatFlagsOp returns the StoreItem that executes the flags operation op.
func FormatFlagsOp(op FlagsOp, silent bool) StoreItem {
s := string(op)
if silent {
s += silentOp
}
return StoreItem(s)
}
// UID is a message unique identifier.
type UID uint32
// ParseFlagsOp parses a flags operation from StoreItem.
func ParseFlagsOp(item StoreItem) (op FlagsOp, silent bool, err error) {
itemStr := string(item)
silent = strings.HasSuffix(itemStr, silentOp)
if silent {
itemStr = strings.TrimSuffix(itemStr, silentOp)
}
op = FlagsOp(itemStr)
if op != SetFlags && op != AddFlags && op != RemoveFlags {
err = errors.New("Unsupported STORE operation")
}
return
}
// CharsetReader, if non-nil, defines a function to generate charset-conversion
// readers, converting from the provided charset into UTF-8. Charsets are always
// lower-case. utf-8 and us-ascii charsets are handled by default. One of the
// the CharsetReader's result values must be non-nil.
var CharsetReader func(charset string, r io.Reader) (io.Reader, error)

View File

@@ -1,13 +0,0 @@
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)
}

View File

@@ -1,306 +0,0 @@
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

@@ -1,654 +0,0 @@
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

@@ -1,341 +0,0 @@
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

@@ -1,47 +0,0 @@
// 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
}

View File

@@ -1,39 +0,0 @@
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
}

View File

@@ -1,170 +0,0 @@
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 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)
}

View File

@@ -1,23 +0,0 @@
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)
}
}

37
internal/testcert.go Normal file
View File

@@ -0,0 +1,37 @@
package internal
// LocalhostCert is a PEM-encoded TLS cert with SAN IPs
// "127.0.0.1" and "[::1]", expiring at Jan 29 16:00:00 2084 GMT.
// generated from src/crypto/tls:
// go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
var LocalhostCert = []byte(`-----BEGIN CERTIFICATE-----
MIICEzCCAXygAwIBAgIQMIMChMLGrR+QvmQvpwAU6zANBgkqhkiG9w0BAQsFADAS
MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw
MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB
iQKBgQDuLnQAI3mDgey3VBzWnB2L39JUU4txjeVE6myuDqkM/uGlfjb9SjY1bIw4
iA5sBBZzHi3z0h1YV8QPuxEbi4nW91IJm2gsvvZhIrCHS3l6afab4pZBl2+XsDul
rKBxKKtD1rGxlG4LjncdabFn9gvLZad2bSysqz/qTAUStTvqJQIDAQABo2gwZjAO
BgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUw
AwEB/zAuBgNVHREEJzAlggtleGFtcGxlLmNvbYcEfwAAAYcQAAAAAAAAAAAAAAAA
AAAAATANBgkqhkiG9w0BAQsFAAOBgQCEcetwO59EWk7WiJsG4x8SY+UIAA+flUI9
tyC4lNhbcF2Idq9greZwbYCqTTTr2XiRNSMLCOjKyI7ukPoPjo16ocHj+P3vZGfs
h1fIw3cSS2OolhloGw/XM6RWPWtPAlGykKLciQrBru5NAPvCMsb/I1DAceTiotQM
fblo6RBxUQ==
-----END CERTIFICATE-----`)
// LocalhostKey is the private key for localhostCert.
var LocalhostKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIICXgIBAAKBgQDuLnQAI3mDgey3VBzWnB2L39JUU4txjeVE6myuDqkM/uGlfjb9
SjY1bIw4iA5sBBZzHi3z0h1YV8QPuxEbi4nW91IJm2gsvvZhIrCHS3l6afab4pZB
l2+XsDulrKBxKKtD1rGxlG4LjncdabFn9gvLZad2bSysqz/qTAUStTvqJQIDAQAB
AoGAGRzwwir7XvBOAy5tM/uV6e+Zf6anZzus1s1Y1ClbjbE6HXbnWWF/wbZGOpet
3Zm4vD6MXc7jpTLryzTQIvVdfQbRc6+MUVeLKwZatTXtdZrhu+Jk7hx0nTPy8Jcb
uJqFk541aEw+mMogY/xEcfbWd6IOkp+4xqjlFLBEDytgbIECQQDvH/E6nk+hgN4H
qzzVtxxr397vWrjrIgPbJpQvBsafG7b0dA4AFjwVbFLmQcj2PprIMmPcQrooz8vp
jy4SHEg1AkEA/v13/5M47K9vCxmb8QeD/asydfsgS5TeuNi8DoUBEmiSJwma7FXY
fFUtxuvL7XvjwjN5B30pNEbc6Iuyt7y4MQJBAIt21su4b3sjXNueLKH85Q+phy2U
fQtuUE9txblTu14q3N7gHRZB4ZMhFYyDy8CKrN2cPg/Fvyt0Xlp/DoCzjA0CQQDU
y2ptGsuSmgUtWj3NM9xuwYPm+Z/F84K6+ARYiZ6PYj013sovGKUFfYAqVXVlxtIX
qyUBnu3X9ps8ZfjLZO7BAkEAlT4R5Yl6cGhaJQYZHOde3JEMhNRcVFMO8dJDaFeo
f9Oeos0UUothgiDktdQHxdNEwLjQf7lJJBzV+5OtwswCWA==
-----END RSA PRIVATE KEY-----`)

16
internal/testutil.go Normal file
View File

@@ -0,0 +1,16 @@
package internal
type MapListSorter []interface{}
func (s MapListSorter) Len() int {
return len(s) / 2
}
func (s MapListSorter) Less(i, j int) bool {
return s[i*2].(string) < s[j*2].(string)
}
func (s MapListSorter) Swap(i, j int) {
s[i*2], s[j*2] = s[j*2], s[i*2]
s[i*2+1], s[j*2+1] = s[j*2+1], s[i*2+1]
}

View File

@@ -1,13 +0,0 @@
// 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+,")

13
literal.go Normal file
View File

@@ -0,0 +1,13 @@
package imap
import (
"io"
)
// A literal, as defined in RFC 3501 section 4.3.
type Literal interface {
io.Reader
// Len returns the number of bytes of the literal.
Len() int
}

8
logger.go Normal file
View File

@@ -0,0 +1,8 @@
package imap
// Logger is the behaviour used by server/client to
// report errors for accepting connections and unexpected behavior from handlers.
type Logger interface {
Printf(format string, v ...interface{})
Println(v ...interface{})
}

314
mailbox.go Normal file
View File

@@ -0,0 +1,314 @@
package imap
import (
"errors"
"fmt"
"strings"
"sync"
"github.com/emersion/go-imap/utf7"
)
// The primary mailbox, as defined in RFC 3501 section 5.1.
const InboxName = "INBOX"
// CanonicalMailboxName returns the canonical form of a mailbox name. Mailbox names can be
// case-sensitive or case-insensitive depending on the backend implementation.
// The special INBOX mailbox is case-insensitive.
func CanonicalMailboxName(name string) string {
if strings.EqualFold(name, InboxName) {
return InboxName
}
return name
}
// Mailbox attributes definied in RFC 3501 section 7.2.2.
const (
// It is not possible for any child levels of hierarchy to exist under this\
// name; no child levels exist now and none can be created in the future.
NoInferiorsAttr = "\\Noinferiors"
// It is not possible to use this name as a selectable mailbox.
NoSelectAttr = "\\Noselect"
// The mailbox has been marked "interesting" by the server; the mailbox
// probably contains messages that have been added since the last time the
// mailbox was selected.
MarkedAttr = "\\Marked"
// The mailbox does not contain any additional messages since the last time
// the mailbox was selected.
UnmarkedAttr = "\\Unmarked"
)
// Mailbox attributes defined in RFC 6154 section 2 (SPECIAL-USE extension).
const (
// This mailbox presents all messages in the user's message store.
AllAttr = "\\All"
// This mailbox is used to archive messages.
ArchiveAttr = "\\Archive"
// This mailbox is used to hold draft messages -- typically, messages that are
// being composed but have not yet been sent.
DraftsAttr = "\\Drafts"
// This mailbox presents all messages marked in some way as "important".
FlaggedAttr = "\\Flagged"
// This mailbox is where messages deemed to be junk mail are held.
JunkAttr = "\\Junk"
// This mailbox is used to hold copies of messages that have been sent.
SentAttr = "\\Sent"
// This mailbox is used to hold messages that have been deleted or marked for
// deletion.
TrashAttr = "\\Trash"
)
// Mailbox attributes defined in RFC 3348 (CHILDREN extension)
const (
// The presence of this attribute indicates that the mailbox has child
// mailboxes.
HasChildrenAttr = "\\HasChildren"
// The presence of this attribute indicates that the mailbox has no child
// mailboxes.
HasNoChildrenAttr = "\\HasNoChildren"
)
// This mailbox attribute is a signal that the mailbox contains messages that
// are likely important to the user. This attribute is defined in RFC 8457
// section 3.
const ImportantAttr = "\\Important"
// Basic mailbox info.
type MailboxInfo struct {
// The mailbox attributes.
Attributes []string
// The server's path separator.
Delimiter string
// The mailbox name.
Name string
}
// Parse mailbox info from fields.
func (info *MailboxInfo) Parse(fields []interface{}) error {
if len(fields) < 3 {
return errors.New("Mailbox info needs at least 3 fields")
}
var err error
if info.Attributes, err = ParseStringList(fields[0]); err != nil {
return err
}
var ok bool
if info.Delimiter, ok = fields[1].(string); !ok {
// The delimiter may be specified as NIL, which gets converted to a nil interface.
if fields[1] != nil {
return errors.New("Mailbox delimiter must be a string")
}
info.Delimiter = ""
}
if name, err := ParseString(fields[2]); err != nil {
return err
} else if name, err := utf7.Encoding.NewDecoder().String(name); err != nil {
return err
} else {
info.Name = CanonicalMailboxName(name)
}
return nil
}
// Format mailbox info to fields.
func (info *MailboxInfo) Format() []interface{} {
name, _ := utf7.Encoding.NewEncoder().String(info.Name)
attrs := make([]interface{}, len(info.Attributes))
for i, attr := range info.Attributes {
attrs[i] = RawString(attr)
}
// If the delimiter is NIL, we need to treat it specially by inserting
// a nil field (so that it's later converted to an unquoted NIL atom).
var del interface{}
if info.Delimiter != "" {
del = info.Delimiter
}
// Thunderbird doesn't understand delimiters if not quoted
return []interface{}{attrs, del, FormatMailboxName(name)}
}
// TODO: optimize this
func (info *MailboxInfo) match(name, pattern string) bool {
i := strings.IndexAny(pattern, "*%")
if i == -1 {
// No more wildcards
return name == pattern
}
// Get parts before and after wildcard
chunk, wildcard, rest := pattern[0:i], pattern[i], pattern[i+1:]
// Check that name begins with chunk
if len(chunk) > 0 && !strings.HasPrefix(name, chunk) {
return false
}
name = strings.TrimPrefix(name, chunk)
// Expand wildcard
var j int
for j = 0; j < len(name); j++ {
if wildcard == '%' && string(name[j]) == info.Delimiter {
break // Stop on delimiter if wildcard is %
}
// Try to match the rest from here
if info.match(name[j:], rest) {
return true
}
}
return info.match(name[j:], rest)
}
// Match checks if a reference and a pattern matches this mailbox name, as
// defined in RFC 3501 section 6.3.8.
func (info *MailboxInfo) Match(reference, pattern string) bool {
name := info.Name
if info.Delimiter != "" && strings.HasPrefix(pattern, info.Delimiter) {
reference = ""
pattern = strings.TrimPrefix(pattern, info.Delimiter)
}
if reference != "" {
if info.Delimiter != "" && !strings.HasSuffix(reference, info.Delimiter) {
reference += info.Delimiter
}
if !strings.HasPrefix(name, reference) {
return false
}
name = strings.TrimPrefix(name, reference)
}
return info.match(name, pattern)
}
// A mailbox status.
type MailboxStatus struct {
// The mailbox name.
Name string
// True if the mailbox is open in read-only mode.
ReadOnly bool
// The mailbox items that are currently filled in. This map's values
// should not be used directly, they must only be used by libraries
// implementing extensions of the IMAP protocol.
Items map[StatusItem]interface{}
// The Items map may be accessed in different goroutines. Protect
// concurrent writes.
ItemsLocker sync.Mutex
// The mailbox flags.
Flags []string
// The mailbox permanent flags.
PermanentFlags []string
// The sequence number of the first unseen message in the mailbox.
UnseenSeqNum uint32
// The number of messages in this mailbox.
Messages uint32
// The number of messages not seen since the last time the mailbox was opened.
Recent uint32
// The number of unread messages.
Unseen uint32
// The next UID.
UidNext uint32
// Together with a UID, it is a unique identifier for a message.
// Must be greater than or equal to 1.
UidValidity uint32
// Per-mailbox limit of message size. Set only if server supports the
// APPENDLIMIT extension.
AppendLimit uint32
}
// Create a new mailbox status that will contain the specified items.
func NewMailboxStatus(name string, items []StatusItem) *MailboxStatus {
status := &MailboxStatus{
Name: name,
Items: make(map[StatusItem]interface{}),
}
for _, k := range items {
status.Items[k] = nil
}
return status
}
func (status *MailboxStatus) Parse(fields []interface{}) error {
status.Items = make(map[StatusItem]interface{})
var k StatusItem
for i, f := range fields {
if i%2 == 0 {
if kstr, ok := f.(string); !ok {
return fmt.Errorf("cannot parse mailbox status: key is not a string, but a %T", f)
} else {
k = StatusItem(strings.ToUpper(kstr))
}
} else {
status.Items[k] = nil
var err error
switch k {
case StatusMessages:
status.Messages, err = ParseNumber(f)
case StatusRecent:
status.Recent, err = ParseNumber(f)
case StatusUnseen:
status.Unseen, err = ParseNumber(f)
case StatusUidNext:
status.UidNext, err = ParseNumber(f)
case StatusUidValidity:
status.UidValidity, err = ParseNumber(f)
case StatusAppendLimit:
status.AppendLimit, err = ParseNumber(f)
default:
status.Items[k] = f
}
if err != nil {
return err
}
}
}
return nil
}
func (status *MailboxStatus) Format() []interface{} {
var fields []interface{}
for k, v := range status.Items {
switch k {
case StatusMessages:
v = status.Messages
case StatusRecent:
v = status.Recent
case StatusUnseen:
v = status.Unseen
case StatusUidNext:
v = status.UidNext
case StatusUidValidity:
v = status.UidValidity
case StatusAppendLimit:
v = status.AppendLimit
}
fields = append(fields, RawString(k), v)
}
return fields
}
func FormatMailboxName(name string) interface{} {
// Some e-mails servers don't handle quoted INBOX names correctly so we special-case it.
if strings.EqualFold(name, "INBOX") {
return RawString(name)
}
return name
}

191
mailbox_test.go Normal file
View File

@@ -0,0 +1,191 @@
package imap_test
import (
"fmt"
"reflect"
"sort"
"testing"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/internal"
)
func TestCanonicalMailboxName(t *testing.T) {
if got := imap.CanonicalMailboxName("Inbox"); got != imap.InboxName {
t.Errorf("Invalid canonical mailbox name: expected %q but got %q", imap.InboxName, got)
}
if got := imap.CanonicalMailboxName("Drafts"); got != "Drafts" {
t.Errorf("Invalid canonical mailbox name: expected %q but got %q", "Drafts", got)
}
}
var mailboxInfoTests = []struct {
fields []interface{}
info *imap.MailboxInfo
}{
{
fields: []interface{}{
[]interface{}{"\\Noselect", "\\Recent", "\\Unseen"},
"/",
"INBOX",
},
info: &imap.MailboxInfo{
Attributes: []string{"\\Noselect", "\\Recent", "\\Unseen"},
Delimiter: "/",
Name: "INBOX",
},
},
}
func TestMailboxInfo_Parse(t *testing.T) {
for _, test := range mailboxInfoTests {
info := &imap.MailboxInfo{}
if err := info.Parse(test.fields); err != nil {
t.Fatal(err)
}
if fmt.Sprint(info.Attributes) != fmt.Sprint(test.info.Attributes) {
t.Fatal("Invalid flags:", info.Attributes)
}
if info.Delimiter != test.info.Delimiter {
t.Fatal("Invalid delimiter:", info.Delimiter)
}
if info.Name != test.info.Name {
t.Fatal("Invalid name:", info.Name)
}
}
}
func TestMailboxInfo_Format(t *testing.T) {
for _, test := range mailboxInfoTests {
fields := test.info.Format()
if fmt.Sprint(fields) != fmt.Sprint(test.fields) {
t.Fatal("Invalid fields:", fields)
}
}
}
var mailboxInfoMatchTests = []struct {
name, ref, pattern string
result bool
}{
{name: "INBOX", pattern: "INBOX", result: true},
{name: "INBOX", pattern: "Asuka", result: false},
{name: "INBOX", pattern: "*", result: true},
{name: "INBOX", pattern: "%", result: true},
{name: "Neon Genesis Evangelion/Misato", pattern: "*", result: true},
{name: "Neon Genesis Evangelion/Misato", pattern: "%", result: false},
{name: "Neon Genesis Evangelion/Misato", pattern: "Neon Genesis Evangelion/*", result: true},
{name: "Neon Genesis Evangelion/Misato", pattern: "Neon Genesis Evangelion/%", result: true},
{name: "Neon Genesis Evangelion/Misato", pattern: "Neo* Evangelion/Misato", result: true},
{name: "Neon Genesis Evangelion/Misato", pattern: "Neo% Evangelion/Misato", result: true},
{name: "Neon Genesis Evangelion/Misato", pattern: "*Eva*/Misato", result: true},
{name: "Neon Genesis Evangelion/Misato", pattern: "%Eva%/Misato", result: true},
{name: "Neon Genesis Evangelion/Misato", pattern: "*X*/Misato", result: false},
{name: "Neon Genesis Evangelion/Misato", pattern: "%X%/Misato", result: false},
{name: "Neon Genesis Evangelion/Misato", pattern: "Neon Genesis Evangelion/Mi%o", result: true},
{name: "Neon Genesis Evangelion/Misato", pattern: "Neon Genesis Evangelion/Mi%too", result: false},
{name: "Misato/Misato", pattern: "Mis*to/Misato", result: true},
{name: "Misato/Misato", pattern: "Mis*to", result: true},
{name: "Misato/Misato/Misato", pattern: "Mis*to/Mis%to", result: true},
{name: "Misato/Misato", pattern: "Mis**to/Misato", result: true},
{name: "Misato/Misato", pattern: "Misat%/Misato", result: true},
{name: "Misato/Misato", pattern: "Misat%Misato", result: false},
{name: "Misato/Misato", ref: "Misato", pattern: "Misato", result: true},
{name: "Misato/Misato", ref: "Misato/", pattern: "Misato", result: true},
{name: "Misato/Misato", ref: "Shinji", pattern: "/Misato/*", result: true},
{name: "Misato/Misato", ref: "Misato", pattern: "/Misato", result: false},
{name: "Misato/Misato", ref: "Misato", pattern: "Shinji", result: false},
{name: "Misato/Misato", ref: "Shinji", pattern: "Misato", result: false},
}
func TestMailboxInfo_Match(t *testing.T) {
for _, test := range mailboxInfoMatchTests {
info := &imap.MailboxInfo{Name: test.name, Delimiter: "/"}
result := info.Match(test.ref, test.pattern)
if result != test.result {
t.Errorf("Matching name %q with pattern %q and reference %q returns %v, but expected %v", test.name, test.pattern, test.ref, result, test.result)
}
}
}
func TestNewMailboxStatus(t *testing.T) {
status := imap.NewMailboxStatus("INBOX", []imap.StatusItem{imap.StatusMessages, imap.StatusUnseen})
expected := &imap.MailboxStatus{
Name: "INBOX",
Items: map[imap.StatusItem]interface{}{imap.StatusMessages: nil, imap.StatusUnseen: nil},
}
if !reflect.DeepEqual(expected, status) {
t.Errorf("Invalid mailbox status: expected \n%+v\n but got \n%+v", expected, status)
}
}
var mailboxStatusTests = [...]struct {
fields []interface{}
status *imap.MailboxStatus
}{
{
fields: []interface{}{
"MESSAGES", uint32(42),
"RECENT", uint32(1),
"UNSEEN", uint32(6),
"UIDNEXT", uint32(65536),
"UIDVALIDITY", uint32(4242),
},
status: &imap.MailboxStatus{
Items: map[imap.StatusItem]interface{}{
imap.StatusMessages: nil,
imap.StatusRecent: nil,
imap.StatusUnseen: nil,
imap.StatusUidNext: nil,
imap.StatusUidValidity: nil,
},
Messages: 42,
Recent: 1,
Unseen: 6,
UidNext: 65536,
UidValidity: 4242,
},
},
}
func TestMailboxStatus_Parse(t *testing.T) {
for i, test := range mailboxStatusTests {
status := &imap.MailboxStatus{}
if err := status.Parse(test.fields); err != nil {
t.Errorf("Expected no error while parsing mailbox status #%v, got: %v", i, err)
continue
}
if !reflect.DeepEqual(status, test.status) {
t.Errorf("Invalid parsed mailbox status for #%v: got \n%+v\n but expected \n%+v", i, status, test.status)
}
}
}
func TestMailboxStatus_Format(t *testing.T) {
for i, test := range mailboxStatusTests {
fields := test.status.Format()
// MapListSorter does not know about RawString and will panic.
stringFields := make([]interface{}, 0, len(fields))
for _, field := range fields {
if s, ok := field.(imap.RawString); ok {
stringFields = append(stringFields, string(s))
} else {
stringFields = append(stringFields, field)
}
}
sort.Sort(internal.MapListSorter(stringFields))
sort.Sort(internal.MapListSorter(test.fields))
if !reflect.DeepEqual(stringFields, test.fields) {
t.Errorf("Invalid mailbox status fields for #%v: got \n%+v\n but expected \n%+v", i, fields, test.fields)
}
}
}

1186
message.go Normal file

File diff suppressed because it is too large Load Diff

797
message_test.go Normal file
View File

@@ -0,0 +1,797 @@
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{}{
RawString("ENVELOPE"), envelopeTests[0].fields,
RawString("BODY"), bodyStructureTests[0].fields,
RawString("FLAGS"), []interface{}{RawString(SeenFlag), RawString(AnsweredFlag)},
RawString("RFC822.SIZE"), RawString("4242"),
RawString("UID"), RawString("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: nil,
ReplyTo: nil,
To: nil,
Cc: nil,
Bcc: nil,
InReplyTo: "42@example.org",
MessageId: "43@example.org",
},
fields: []interface{}{
"Tue, 10 Nov 2009 23:00:00 -0600",
"Hello World!",
[]interface{}{addrTests[0].fields},
nil,
nil,
nil,
nil,
nil,
"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 TestEmptyAddress(t *testing.T) {
fields := []interface{}{nil, nil, nil, nil}
addr := &Address{}
err := addr.Parse(fields)
if err == nil {
t.Error("A nil address did not return an error")
}
}
func TestEmptyGroupAddress(t *testing.T) {
fields := []interface{}{nil, nil, "undisclosed-recipients", nil}
addr := &Address{}
err := addr.Parse(fields)
if err == nil {
t.Error("An empty group did not return an error when parsed as address")
}
}
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)
}
}
func TestEmptyAddressList(t *testing.T) {
addrs := make([]*Address, 0)
gotFields := FormatAddressList(addrs)
if !reflect.DeepEqual(gotFields, nil) {
t.Error("Invalid address list fields: got", gotFields, "but expected nil")
}
}
var paramsListTest = []struct {
fields []interface{}
params map[string]string
}{
{
fields: nil,
params: map[string]string{},
},
{
fields: []interface{}{"a", "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", RawString("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", "utf-8"}, nil, nil, "us-ascii", RawString("42"), RawString("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", RawString("42"),
(&Envelope{}).Format(),
(&BodyStructure{}).Format(),
RawString("67"),
},
bodyStructure: &BodyStructure{
MIMEType: "message",
MIMESubType: "rfc822",
Params: map[string]string{},
Encoding: "us-ascii",
Size: 42,
Lines: 67,
Envelope: &Envelope{
From: nil,
Sender: nil,
ReplyTo: nil,
To: nil,
Cc: nil,
Bcc: nil,
},
BodyStructure: &BodyStructure{
Params: map[string]string{},
},
},
},
{
fields: []interface{}{
"application", "pdf", []interface{}{}, nil, nil, "base64", RawString("4242"),
"e0323a9039add2978bf5b49550572c7c",
[]interface{}{"attachment", []interface{}{"filename", "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", RawString("87"), RawString("22")},
[]interface{}{"text", "html", []interface{}{}, nil, nil, "us-ascii", RawString("106"), RawString("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", RawString("87"), RawString("22")},
"alternative", []interface{}{"hello", "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_Parse_uppercase(t *testing.T) {
fields := []interface{}{
"APPLICATION", "PDF", []interface{}{"NAME", "Document.pdf"}, nil, nil,
"BASE64", RawString("4242"), nil,
[]interface{}{"ATTACHMENT", []interface{}{"FILENAME", "Document.pdf"}},
nil, nil,
}
expected := &BodyStructure{
MIMEType: "application",
MIMESubType: "pdf",
Params: map[string]string{"name": "Document.pdf"},
Encoding: "base64",
Size: 4242,
Extended: true,
MD5: "",
Disposition: "attachment",
DispositionParams: map[string]string{"filename": "Document.pdf"},
Language: nil,
Location: []string{},
}
bs := &BodyStructure{}
if err := bs.Parse(fields); err != nil {
t.Errorf("Cannot parse: %v", err)
} else if !reflect.DeepEqual(bs, expected) {
t.Errorf("Invalid body structure: got \n%+v\n but expected \n%+v", bs, expected)
}
}
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)
}
}
}
func TestBodyStructureFilename(t *testing.T) {
tests := []struct {
bs BodyStructure
filename string
}{
{
bs: BodyStructure{
DispositionParams: map[string]string{"filename": "cat.png"},
},
filename: "cat.png",
},
{
bs: BodyStructure{
Params: map[string]string{"name": "cat.png"},
},
filename: "cat.png",
},
{
bs: BodyStructure{},
filename: "",
},
{
bs: BodyStructure{
DispositionParams: map[string]string{"filename": "=?UTF-8?Q?Opis_przedmiotu_zam=c3=b3wienia_-_za=c5=82=c4=85cznik_nr_1?= =?UTF-8?Q?=2epdf?="},
},
filename: "Opis przedmiotu zamówienia - załącznik nr 1.pdf",
},
}
for i, test := range tests {
got, err := test.bs.Filename()
if err != nil {
t.Errorf("Invalid body structure filename for #%v: error: %v", i, err)
continue
}
if got != test.filename {
t.Errorf("Invalid body structure filename for #%v: got '%v', want '%v'", i, got, test.filename)
}
}
}
func TestBodyStructureWalk(t *testing.T) {
textPlain := &BodyStructure{
MIMEType: "text",
MIMESubType: "plain",
}
textHTML := &BodyStructure{
MIMEType: "text",
MIMESubType: "plain",
}
multipartAlternative := &BodyStructure{
MIMEType: "multipart",
MIMESubType: "alternative",
Parts: []*BodyStructure{textPlain, textHTML},
}
imagePNG := &BodyStructure{
MIMEType: "image",
MIMESubType: "png",
}
multipartMixed := &BodyStructure{
MIMEType: "multipart",
MIMESubType: "mixed",
Parts: []*BodyStructure{multipartAlternative, imagePNG},
}
type testNode struct {
path []int
part *BodyStructure
}
tests := []struct {
bs *BodyStructure
nodes []testNode
walkChildren bool
}{
{
bs: textPlain,
nodes: []testNode{
{path: []int{1}, part: textPlain},
},
},
{
bs: multipartAlternative,
nodes: []testNode{
{path: nil, part: multipartAlternative},
{path: []int{1}, part: textPlain},
{path: []int{2}, part: textHTML},
},
walkChildren: true,
},
{
bs: multipartMixed,
nodes: []testNode{
{path: nil, part: multipartMixed},
{path: []int{1}, part: multipartAlternative},
{path: []int{1, 1}, part: textPlain},
{path: []int{1, 2}, part: textHTML},
{path: []int{2}, part: imagePNG},
},
walkChildren: true,
},
{
bs: multipartMixed,
nodes: []testNode{
{path: nil, part: multipartMixed},
},
walkChildren: false,
},
}
for i, test := range tests {
j := 0
test.bs.Walk(func(path []int, part *BodyStructure) bool {
if j >= len(test.nodes) {
t.Errorf("Test #%v: invalid node count: got > %v, want %v", i, j, len(test.nodes))
return false
}
n := &test.nodes[j]
if !reflect.DeepEqual(path, n.path) {
t.Errorf("Test #%v: node #%v: invalid path: got %v, want %v", i, j, path, n.path)
}
if part != n.part {
t.Errorf("Test #%v: node #%v: invalid part: got %v, want %v", i, j, part, n.part)
}
j++
return test.walkChildren
})
if j != len(test.nodes) {
t.Errorf("Test #%v: invalid node count: got %v, want %v", i, j, len(test.nodes))
}
}
}

467
read.go Normal file
View File

@@ -0,0 +1,467 @@
package imap
import (
"bytes"
"errors"
"io"
"strconv"
"strings"
)
const (
sp = ' '
cr = '\r'
lf = '\n'
dquote = '"'
literalStart = '{'
literalEnd = '}'
listStart = '('
listEnd = ')'
respCodeStart = '['
respCodeEnd = ']'
)
const (
crlf = "\r\n"
nilAtom = "NIL"
)
// TODO: add CTL to atomSpecials
var (
quotedSpecials = string([]rune{dquote, '\\'})
respSpecials = string([]rune{respCodeEnd})
atomSpecials = string([]rune{listStart, listEnd, literalStart, sp, '%', '*'}) + quotedSpecials + respSpecials
)
type parseError struct {
error
}
func newParseError(text string) error {
return &parseError{errors.New(text)}
}
// IsParseError returns true if the provided error is a parse error produced by
// Reader.
func IsParseError(err error) bool {
_, ok := err.(*parseError)
return ok
}
// A string reader.
type StringReader interface {
// ReadString reads until the first occurrence of delim in the input,
// returning a string containing the data up to and including the delimiter.
// See https://golang.org/pkg/bufio/#Reader.ReadString
ReadString(delim byte) (line string, err error)
}
type reader interface {
io.Reader
io.RuneScanner
StringReader
}
// ParseNumber parses a number.
func ParseNumber(f interface{}) (uint32, error) {
// Useful for tests
if n, ok := f.(uint32); ok {
return n, nil
}
var s string
switch f := f.(type) {
case RawString:
s = string(f)
case string:
s = f
default:
return 0, newParseError("expected a number, got a non-atom")
}
nbr, err := strconv.ParseUint(string(s), 10, 32)
if err != nil {
return 0, &parseError{err}
}
return uint32(nbr), nil
}
// ParseString parses a string, which is either a literal, a quoted string or an
// atom.
func ParseString(f interface{}) (string, error) {
if s, ok := f.(string); ok {
return s, nil
}
// Useful for tests
if a, ok := f.(RawString); ok {
return string(a), nil
}
if l, ok := f.(Literal); ok {
b := make([]byte, l.Len())
if _, err := io.ReadFull(l, b); err != nil {
return "", err
}
return string(b), nil
}
return "", newParseError("expected a string")
}
// Convert a field list to a string list.
func ParseStringList(f interface{}) ([]string, error) {
fields, ok := f.([]interface{})
if !ok {
return nil, newParseError("expected a string list, got a non-list")
}
list := make([]string, len(fields))
for i, f := range fields {
var err error
if list[i], err = ParseString(f); err != nil {
return nil, newParseError("cannot parse string in string list: " + err.Error())
}
}
return list, nil
}
func trimSuffix(str string, suffix rune) string {
return str[:len(str)-1]
}
// An IMAP reader.
type Reader struct {
MaxLiteralSize uint32 // The maximum literal size.
reader
continues chan<- bool
brackets int
inRespCode bool
}
func (r *Reader) ReadSp() error {
char, _, err := r.ReadRune()
if err != nil {
return err
}
if char != sp {
return newParseError("expected a space")
}
return nil
}
func (r *Reader) ReadCrlf() (err error) {
var char rune
if char, _, err = r.ReadRune(); err != nil {
return
}
if char == lf {
return
}
if char != cr {
err = newParseError("line doesn't end with a CR")
return
}
if char, _, err = r.ReadRune(); err != nil {
return
}
if char != lf {
err = newParseError("line doesn't end with a LF")
}
return
}
func (r *Reader) ReadAtom() (interface{}, error) {
r.brackets = 0
var atom string
for {
char, _, err := r.ReadRune()
if err != nil {
return nil, err
}
// TODO: list-wildcards and \
if r.brackets == 0 && (char == listStart || char == literalStart || char == dquote) {
return nil, newParseError("atom contains forbidden char: " + string(char))
}
if char == cr || char == lf {
break
}
if r.brackets == 0 && (char == sp || char == listEnd) {
break
}
if char == respCodeEnd {
if r.brackets == 0 {
if r.inRespCode {
break
} else {
return nil, newParseError("atom contains bad brackets nesting")
}
}
r.brackets--
}
if char == respCodeStart {
r.brackets++
}
atom += string(char)
}
r.UnreadRune()
if atom == nilAtom {
return nil, nil
}
return atom, nil
}
func (r *Reader) ReadLiteral() (Literal, error) {
char, _, err := r.ReadRune()
if err != nil {
return nil, err
} else if char != literalStart {
return nil, newParseError("literal string doesn't start with an open brace")
}
lstr, err := r.ReadString(byte(literalEnd))
if err != nil {
return nil, err
}
lstr = trimSuffix(lstr, literalEnd)
nonSync := strings.HasSuffix(lstr, "+")
if nonSync {
lstr = trimSuffix(lstr, '+')
}
n, err := strconv.ParseUint(lstr, 10, 32)
if err != nil {
return nil, newParseError("cannot parse literal length: " + err.Error())
}
if r.MaxLiteralSize > 0 && uint32(n) > r.MaxLiteralSize {
return nil, newParseError("literal exceeding maximum size")
}
if err := r.ReadCrlf(); err != nil {
return nil, err
}
// Send continuation request if necessary
if r.continues != nil && !nonSync {
r.continues <- true
}
// Read literal
b := make([]byte, n)
if _, err := io.ReadFull(r, b); err != nil {
return nil, err
}
return bytes.NewBuffer(b), nil
}
func (r *Reader) ReadQuotedString() (string, error) {
if char, _, err := r.ReadRune(); err != nil {
return "", err
} else if char != dquote {
return "", newParseError("quoted string doesn't start with a double quote")
}
var buf bytes.Buffer
var escaped bool
for {
char, _, err := r.ReadRune()
if err != nil {
return "", err
}
if char == '\\' && !escaped {
escaped = true
} else {
if char == cr || char == lf {
r.UnreadRune()
return "", newParseError("CR or LF not allowed in quoted string")
}
if char == dquote && !escaped {
break
}
if !strings.ContainsRune(quotedSpecials, char) && escaped {
return "", newParseError("quoted string cannot contain backslash followed by a non-quoted-specials char")
}
buf.WriteRune(char)
escaped = false
}
}
return buf.String(), nil
}
func (r *Reader) ReadFields() (fields []interface{}, err error) {
var char rune
for {
if char, _, err = r.ReadRune(); err != nil {
return
}
if err = r.UnreadRune(); err != nil {
return
}
var field interface{}
ok := true
switch char {
case literalStart:
field, err = r.ReadLiteral()
case dquote:
field, err = r.ReadQuotedString()
case listStart:
field, err = r.ReadList()
case listEnd:
ok = false
case cr:
return
default:
field, err = r.ReadAtom()
}
if err != nil {
return
}
if ok {
fields = append(fields, field)
}
if char, _, err = r.ReadRune(); err != nil {
return
}
if char == cr || char == lf || char == listEnd || char == respCodeEnd {
if char == cr || char == lf {
r.UnreadRune()
}
return
}
if char == listStart {
r.UnreadRune()
continue
}
if char != sp {
err = newParseError("fields are not separated by a space")
return
}
}
}
func (r *Reader) ReadList() (fields []interface{}, err error) {
char, _, err := r.ReadRune()
if err != nil {
return
}
if char != listStart {
err = newParseError("list doesn't start with an open parenthesis")
return
}
fields, err = r.ReadFields()
if err != nil {
return
}
r.UnreadRune()
if char, _, err = r.ReadRune(); err != nil {
return
}
if char != listEnd {
err = newParseError("list doesn't end with a close parenthesis")
}
return
}
func (r *Reader) ReadLine() (fields []interface{}, err error) {
fields, err = r.ReadFields()
if err != nil {
return
}
r.UnreadRune()
err = r.ReadCrlf()
return
}
func (r *Reader) ReadRespCode() (code StatusRespCode, fields []interface{}, err error) {
char, _, err := r.ReadRune()
if err != nil {
return
}
if char != respCodeStart {
err = newParseError("response code doesn't start with an open bracket")
return
}
r.inRespCode = true
fields, err = r.ReadFields()
r.inRespCode = false
if err != nil {
return
}
if len(fields) == 0 {
err = newParseError("response code doesn't contain any field")
return
}
codeStr, ok := fields[0].(string)
if !ok {
err = newParseError("response code doesn't start with a string atom")
return
}
if codeStr == "" {
err = newParseError("response code is empty")
return
}
code = StatusRespCode(strings.ToUpper(codeStr))
fields = fields[1:]
r.UnreadRune()
char, _, err = r.ReadRune()
if err != nil {
return
}
if char != respCodeEnd {
err = newParseError("response code doesn't end with a close bracket")
}
return
}
func (r *Reader) ReadInfo() (info string, err error) {
info, err = r.ReadString(byte(lf))
if err != nil {
return
}
info = strings.TrimSuffix(info, string(lf))
info = strings.TrimSuffix(info, string(cr))
info = strings.TrimLeft(info, " ")
return
}
func NewReader(r reader) *Reader {
return &Reader{reader: r}
}
func NewServerReader(r reader, continues chan<- bool) *Reader {
return &Reader{reader: r, continues: continues}
}
type Parser interface {
Parse(fields []interface{}) error
}

536
read_test.go Normal file
View File

@@ -0,0 +1,536 @@
package imap_test
import (
"bytes"
"io"
"io/ioutil"
"reflect"
"testing"
"github.com/emersion/go-imap"
)
func TestParseNumber(t *testing.T) {
tests := []struct {
f interface{}
n uint32
err bool
}{
{f: "42", n: 42},
{f: "0", n: 0},
{f: "-1", err: true},
{f: "1.2", err: true},
{f: nil, err: true},
{f: bytes.NewBufferString("cc"), err: true},
}
for _, test := range tests {
n, err := imap.ParseNumber(test.f)
if err != nil {
if !test.err {
t.Errorf("Cannot parse number %v", test.f)
}
} else {
if test.err {
t.Errorf("Parsed invalid number %v", test.f)
} else if n != test.n {
t.Errorf("Invalid parsed number: got %v but expected %v", n, test.n)
}
}
}
}
func TestParseStringList(t *testing.T) {
tests := []struct {
field interface{}
list []string
}{
{
field: []interface{}{"a", "b", "c", "d"},
list: []string{"a", "b", "c", "d"},
},
{
field: []interface{}{"a"},
list: []string{"a"},
},
{
field: []interface{}{},
list: []string{},
},
{
field: []interface{}{"a", 42, "c", "d"},
list: nil,
},
{
field: []interface{}{"a", nil, "c", "d"},
list: nil,
},
{
field: "Asuka FTW",
list: nil,
},
}
for _, test := range tests {
list, err := imap.ParseStringList(test.field)
if err != nil {
if test.list != nil {
t.Errorf("Cannot parse string list: %v", err)
}
} else if !reflect.DeepEqual(list, test.list) {
t.Errorf("Invalid parsed string list: got \n%+v\n but expected \n%+v", list, test.list)
}
}
}
func newReader(s string) (b *bytes.Buffer, r *imap.Reader) {
b = bytes.NewBuffer([]byte(s))
r = imap.NewReader(b)
return
}
func TestReader_ReadSp(t *testing.T) {
b, r := newReader(" ")
if err := r.ReadSp(); err != nil {
t.Error(err)
}
if b.Len() > 0 {
t.Error("Buffer is not empty after read")
}
_, r = newReader("")
if err := r.ReadSp(); err == nil {
t.Error("Invalid read didn't fail")
}
}
func TestReader_ReadCrlf(t *testing.T) {
b, r := newReader("\r\n")
if err := r.ReadCrlf(); err != nil {
t.Error(err)
}
if b.Len() > 0 {
t.Error("Buffer is not empty after read")
}
_, r = newReader("")
if err := r.ReadCrlf(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("\n")
if err := r.ReadCrlf(); err != nil {
t.Error(err)
}
_, r = newReader("\r")
if err := r.ReadCrlf(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("\r42")
if err := r.ReadCrlf(); err == nil {
t.Error("Invalid read didn't fail")
}
}
func TestReader_ReadAtom(t *testing.T) {
b, r := newReader("NIL\r\n")
if atom, err := r.ReadAtom(); err != nil {
t.Error(err)
} else if atom != nil {
t.Error("NIL atom is not nil:", atom)
} else {
if err := r.ReadCrlf(); err != nil && err != io.EOF {
t.Error("Cannot read CRLF after atom:", err)
}
if b.Len() > 0 {
t.Error("Buffer is not empty after read")
}
}
b, r = newReader("atom\r\n")
if atom, err := r.ReadAtom(); err != nil {
t.Error(err)
} else if s, ok := atom.(string); !ok || s != "atom" {
t.Error("String atom has not the expected value:", atom)
} else {
if err := r.ReadCrlf(); err != nil && err != io.EOF {
t.Error("Cannot read CRLF after atom:", err)
}
if b.Len() > 0 {
t.Error("Buffer is not empty after read")
}
}
_, r = newReader("")
if _, err := r.ReadAtom(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("(hi there)\r\n")
if _, err := r.ReadAtom(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("{42}\r\n")
if _, err := r.ReadAtom(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("\"\r\n")
if _, err := r.ReadAtom(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("abc]")
if _, err := r.ReadAtom(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("[abc]def]ghi")
if _, err := r.ReadAtom(); err == nil {
t.Error("Invalid read didn't fail")
}
}
func TestReader_ReadLiteral_NonSync(t *testing.T) {
// For synchronizing literal we should send continuation request.
b := bytes.NewBuffer([]byte("{7}\r\nabcdefg"))
cont := make(chan bool, 5)
r := imap.NewServerReader(b, cont)
if litr, err := r.ReadLiteral(); err != nil {
t.Error(err)
} else if litr.Len() != 7 {
t.Error("Invalid literal length")
} else {
if len(cont) != 1 {
t.Error("Missing continuation rejqest")
}
<-cont
}
b = bytes.NewBuffer([]byte("{7+}\r\nabcdefg"))
r = imap.NewServerReader(b, cont)
if litr, err := r.ReadLiteral(); err != nil {
t.Error(err)
} else if litr.Len() != 7 {
t.Error("Invalid literal length")
} else {
if len(cont) != 0 {
t.Error("Unexpected continuation rejqest")
}
if contents, err := ioutil.ReadAll(litr); err != nil {
t.Error(err)
} else if string(contents) != "abcdefg" {
t.Error("Literal has not the expected value:", string(contents))
} else if b.Len() > 0 {
t.Error("Buffer is not empty after read")
}
}
}
func TestReader_ReadLiteral(t *testing.T) {
b, r := newReader("{7}\r\nabcdefg")
if literal, err := r.ReadLiteral(); err != nil {
t.Error(err)
} else if literal.Len() != 7 {
t.Error("Invalid literal length:", literal.Len())
} else {
if contents, err := ioutil.ReadAll(literal); err != nil {
t.Error(err)
} else if string(contents) != "abcdefg" {
t.Error("Literal has not the expected value:", string(contents))
} else if b.Len() > 0 {
t.Error("Buffer is not empty after read")
}
}
_, r = newReader("")
if _, err := r.ReadLiteral(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("[7}\r\nabcdefg")
if _, err := r.ReadLiteral(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("{7]\r\nabcdefg")
if _, err := r.ReadLiteral(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("{7.4}\r\nabcdefg")
if _, err := r.ReadLiteral(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("{7}abcdefg")
if _, err := r.ReadLiteral(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("{7}\rabcdefg")
if _, err := r.ReadLiteral(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("{7}\nabcdefg")
if _, err := r.ReadLiteral(); err != nil {
t.Error(err)
}
_, r = newReader("{7}\r\nabcd")
if _, err := r.ReadLiteral(); err == nil {
t.Error("Invalid read didn't fail")
}
}
func TestReader_ReadQuotedString(t *testing.T) {
b, r := newReader("\"hello gopher\"\r\n")
if s, err := r.ReadQuotedString(); err != nil {
t.Error(err)
} else if s != "hello gopher" {
t.Error("Quoted string has not the expected value:", s)
} else {
if err := r.ReadCrlf(); err != nil && err != io.EOF {
t.Error("Cannot read CRLF after quoted string:", err)
}
if b.Len() > 0 {
t.Error("Buffer is not empty after read")
}
}
_, r = newReader("\"here's a backslash: \\\\, and here's a double quote: \\\" !\"\r\n")
if s, err := r.ReadQuotedString(); err != nil {
t.Error(err)
} else if s != "here's a backslash: \\, and here's a double quote: \" !" {
t.Error("Quoted string has not the expected value:", s)
}
_, r = newReader("")
if _, err := r.ReadQuotedString(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("hello gopher\"\r\n")
if _, err := r.ReadQuotedString(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("\"hello gopher\r\n")
if _, err := r.ReadQuotedString(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("\"hello \\gopher\"\r\n")
if _, err := r.ReadQuotedString(); err == nil {
t.Error("Invalid read didn't fail")
}
}
func TestReader_ReadFields(t *testing.T) {
b, r := newReader("field1 \"field2\"\r\n")
if fields, err := r.ReadFields(); err != nil {
t.Error(err)
} else if len(fields) != 2 {
t.Error("Expected 2 fields, but got", len(fields))
} else if s, ok := fields[0].(string); !ok || s != "field1" {
t.Error("Field 1 has not the expected value:", fields[0])
} else if s, ok := fields[1].(string); !ok || s != "field2" {
t.Error("Field 2 has not the expected value:", fields[1])
} else {
if err := r.ReadCrlf(); err != nil && err != io.EOF {
t.Error("Cannot read CRLF after fields:", err)
}
if b.Len() > 0 {
t.Error("Buffer is not empty after read")
}
}
_, r = newReader("")
if _, err := r.ReadFields(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("fi\"eld1 \"field2\"\r\n")
if _, err := r.ReadFields(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("field1 ")
if _, err := r.ReadFields(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("field1 (")
if _, err := r.ReadFields(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("field1\"field2\"\r\n")
if _, err := r.ReadFields(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("\"field1\"\"field2\"\r\n")
if _, err := r.ReadFields(); err == nil {
t.Error("Invalid read didn't fail")
}
}
func TestReader_ReadList(t *testing.T) {
b, r := newReader("(field1 \"field2\" {6}\r\nfield3 field4)")
if fields, err := r.ReadList(); err != nil {
t.Error(err)
} else if len(fields) != 4 {
t.Error("Expected 2 fields, but got", len(fields))
} else if s, ok := fields[0].(string); !ok || s != "field1" {
t.Error("Field 1 has not the expected value:", fields[0])
} else if s, ok := fields[1].(string); !ok || s != "field2" {
t.Error("Field 2 has not the expected value:", fields[1])
} else if literal, ok := fields[2].(imap.Literal); !ok {
t.Error("Field 3 has not the expected value:", fields[2])
} else if contents, err := ioutil.ReadAll(literal); err != nil {
t.Error(err)
} else if string(contents) != "field3" {
t.Error("Field 3 has not the expected value:", string(contents))
} else if s, ok := fields[3].(string); !ok || s != "field4" {
t.Error("Field 4 has not the expected value:", fields[3])
} else if b.Len() > 0 {
t.Error("Buffer is not empty after read")
}
b, r = newReader("()")
if fields, err := r.ReadList(); err != nil {
t.Error(err)
} else if len(fields) != 0 {
t.Error("Expected 0 fields, but got", len(fields))
} else if b.Len() > 0 {
t.Error("Buffer is not empty after read")
}
_, r = newReader("")
if _, err := r.ReadList(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("[field1 field2 field3)")
if _, err := r.ReadList(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("(field1 fie\"ld2 field3)")
if _, err := r.ReadList(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("(field1 field2 field3\r\n")
if _, err := r.ReadList(); err == nil {
t.Error("Invalid read didn't fail")
}
}
func TestReader_ReadLine(t *testing.T) {
b, r := newReader("field1 field2\r\n")
if fields, err := r.ReadLine(); err != nil {
t.Error(err)
} else if len(fields) != 2 {
t.Error("Expected 2 fields, but got", len(fields))
} else if s, ok := fields[0].(string); !ok || s != "field1" {
t.Error("Field 1 has not the expected value:", fields[0])
} else if s, ok := fields[1].(string); !ok || s != "field2" {
t.Error("Field 2 has not the expected value:", fields[1])
} else if b.Len() > 0 {
t.Error("Buffer is not empty after read")
}
_, r = newReader("")
if _, err := r.ReadLine(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("field1 field2\rabc")
if _, err := r.ReadLine(); err == nil {
t.Error("Invalid read didn't fail")
}
}
func TestReader_ReadRespCode(t *testing.T) {
b, r := newReader("[CAPABILITY NOOP STARTTLS]")
if code, fields, err := r.ReadRespCode(); err != nil {
t.Error(err)
} else if code != imap.CodeCapability {
t.Error("Response code has not the expected value:", code)
} else if len(fields) != 2 {
t.Error("Expected 2 fields, but got", len(fields))
} else if s, ok := fields[0].(string); !ok || s != "NOOP" {
t.Error("Field 1 has not the expected value:", fields[0])
} else if s, ok := fields[1].(string); !ok || s != "STARTTLS" {
t.Error("Field 2 has not the expected value:", fields[1])
} else if b.Len() > 0 {
t.Error("Buffer is not empty after read")
}
_, r = newReader("")
if _, _, err := r.ReadRespCode(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("{CAPABILITY NOOP STARTTLS]")
if _, _, err := r.ReadRespCode(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("[CAPABILITY NO\"OP STARTTLS]")
if _, _, err := r.ReadRespCode(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("[]")
if _, _, err := r.ReadRespCode(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("[{3}\r\nabc]")
if _, _, err := r.ReadRespCode(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("[CAPABILITY NOOP STARTTLS\r\n")
if _, _, err := r.ReadRespCode(); err == nil {
t.Error("Invalid read didn't fail")
}
}
func TestReader_ReadInfo(t *testing.T) {
b, r := newReader("I love potatoes.\r\n")
if info, err := r.ReadInfo(); err != nil {
t.Error(err)
} else if info != "I love potatoes." {
t.Error("Info has not the expected value:", info)
} else if b.Len() > 0 {
t.Error("Buffer is not empty after read")
}
_, r = newReader("I love potatoes.")
if _, err := r.ReadInfo(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("I love potatoes.\r")
if _, err := r.ReadInfo(); err == nil {
t.Error("Invalid read didn't fail")
}
_, r = newReader("I love potatoes.\n")
if _, err := r.ReadInfo(); err != nil {
t.Error(err)
}
_, r = newReader("I love potatoes.\rabc")
if _, err := r.ReadInfo(); err == nil {
t.Error("Invalid read didn't fail")
}
}

View File

@@ -1,81 +1,181 @@
package imap
import (
"fmt"
"strings"
)
// StatusResponseType is a generic status response type.
type StatusResponseType string
const (
StatusResponseTypeOK StatusResponseType = "OK"
StatusResponseTypeNo StatusResponseType = "NO"
StatusResponseTypeBad StatusResponseType = "BAD"
StatusResponseTypePreAuth StatusResponseType = "PREAUTH"
StatusResponseTypeBye StatusResponseType = "BYE"
)
// ResponseCode is a response code.
type ResponseCode string
const (
ResponseCodeAlert ResponseCode = "ALERT"
ResponseCodeAlreadyExists ResponseCode = "ALREADYEXISTS"
ResponseCodeAuthenticationFailed ResponseCode = "AUTHENTICATIONFAILED"
ResponseCodeAuthorizationFailed ResponseCode = "AUTHORIZATIONFAILED"
ResponseCodeBadCharset ResponseCode = "BADCHARSET"
ResponseCodeCannot ResponseCode = "CANNOT"
ResponseCodeClientBug ResponseCode = "CLIENTBUG"
ResponseCodeContactAdmin ResponseCode = "CONTACTADMIN"
ResponseCodeCorruption ResponseCode = "CORRUPTION"
ResponseCodeExpired ResponseCode = "EXPIRED"
ResponseCodeHasChildren ResponseCode = "HASCHILDREN"
ResponseCodeInUse ResponseCode = "INUSE"
ResponseCodeLimit ResponseCode = "LIMIT"
ResponseCodeNonExistent ResponseCode = "NONEXISTENT"
ResponseCodeNoPerm ResponseCode = "NOPERM"
ResponseCodeOverQuota ResponseCode = "OVERQUOTA"
ResponseCodeParse ResponseCode = "PARSE"
ResponseCodePrivacyRequired ResponseCode = "PRIVACYREQUIRED"
ResponseCodeServerBug ResponseCode = "SERVERBUG"
ResponseCodeTryCreate ResponseCode = "TRYCREATE"
ResponseCodeUnavailable ResponseCode = "UNAVAILABLE"
ResponseCodeUnknownCTE ResponseCode = "UNKNOWN-CTE"
// METADATA
ResponseCodeTooMany ResponseCode = "TOOMANY"
ResponseCodeNoPrivate ResponseCode = "NOPRIVATE"
// APPENDLIMIT
ResponseCodeTooBig ResponseCode = "TOOBIG"
)
// StatusResponse is a generic status response.
//
// See RFC 9051 section 7.1.
type StatusResponse struct {
Type StatusResponseType
Code ResponseCode
Text string
// Resp is an IMAP response. It is either a *DataResp, a
// *ContinuationReq or a *StatusResp.
type Resp interface {
resp()
}
// Error is an IMAP error caused by a status response.
type Error StatusResponse
var _ error = (*Error)(nil)
// Error implements the error interface.
func (err *Error) Error() string {
var sb strings.Builder
fmt.Fprintf(&sb, "imap: %v", err.Type)
if err.Code != "" {
fmt.Fprintf(&sb, " [%v]", err.Code)
// ReadResp reads a single response from a Reader.
func ReadResp(r *Reader) (Resp, error) {
atom, err := r.ReadAtom()
if err != nil {
return nil, err
}
text := err.Text
if text == "" {
text = "<unknown>"
tag, ok := atom.(string)
if !ok {
return nil, newParseError("response tag is not an atom")
}
fmt.Fprintf(&sb, " %v", text)
return sb.String()
if tag == "+" {
if err := r.ReadSp(); err != nil {
r.UnreadRune()
}
resp := &ContinuationReq{}
resp.Info, err = r.ReadInfo()
if err != nil {
return nil, err
}
return resp, nil
}
if err := r.ReadSp(); err != nil {
return nil, err
}
// Can be either data or status
// Try to parse a status
var fields []interface{}
if atom, err := r.ReadAtom(); err == nil {
fields = append(fields, atom)
if err := r.ReadSp(); err == nil {
if name, ok := atom.(string); ok {
status := StatusRespType(name)
switch status {
case StatusRespOk, StatusRespNo, StatusRespBad, StatusRespPreauth, StatusRespBye:
resp := &StatusResp{
Tag: tag,
Type: status,
}
char, _, err := r.ReadRune()
if err != nil {
return nil, err
}
r.UnreadRune()
if char == '[' {
// Contains code & arguments
resp.Code, resp.Arguments, err = r.ReadRespCode()
if err != nil {
return nil, err
}
}
resp.Info, err = r.ReadInfo()
if err != nil {
return nil, err
}
return resp, nil
}
}
} else {
r.UnreadRune()
}
} else {
r.UnreadRune()
}
// Not a status so it's data
resp := &DataResp{Tag: tag}
var remaining []interface{}
remaining, err = r.ReadLine()
if err != nil {
return nil, err
}
resp.Fields = append(fields, remaining...)
return resp, nil
}
// DataResp is an IMAP response containing data.
type DataResp struct {
// The response tag. Can be either "" for untagged responses, "+" for continuation
// requests or a previous command's tag.
Tag string
// The parsed response fields.
Fields []interface{}
}
// NewUntaggedResp creates a new untagged response.
func NewUntaggedResp(fields []interface{}) *DataResp {
return &DataResp{
Tag: "*",
Fields: fields,
}
}
func (r *DataResp) resp() {}
func (r *DataResp) WriteTo(w *Writer) error {
tag := RawString(r.Tag)
if tag == "" {
tag = RawString("*")
}
fields := []interface{}{RawString(tag)}
fields = append(fields, r.Fields...)
return w.writeLine(fields...)
}
// ContinuationReq is a continuation request response.
type ContinuationReq struct {
// The info message sent with the continuation request.
Info string
}
func (r *ContinuationReq) resp() {}
func (r *ContinuationReq) WriteTo(w *Writer) error {
if err := w.writeString("+"); err != nil {
return err
}
if r.Info != "" {
if err := w.writeString(string(sp) + r.Info); err != nil {
return err
}
}
return w.writeCrlf()
}
// ParseNamedResp attempts to parse a named data response.
func ParseNamedResp(resp Resp) (name string, fields []interface{}, ok bool) {
data, ok := resp.(*DataResp)
if !ok || len(data.Fields) == 0 {
return
}
// Some responses (namely EXISTS and RECENT) are formatted like so:
// [num] [name] [...]
// Which is fucking stupid. But we handle that here by checking if the
// response name is a number and then rearranging it.
if len(data.Fields) > 1 {
name, ok := data.Fields[1].(string)
if ok {
if _, err := ParseNumber(data.Fields[0]); err == nil {
fields := []interface{}{data.Fields[0]}
fields = append(fields, data.Fields[2:]...)
return strings.ToUpper(name), fields, true
}
}
}
// IMAP commands are formatted like this:
// [name] [...]
name, ok = data.Fields[0].(string)
if !ok {
return
}
return strings.ToUpper(name), data.Fields[1:], true
}

252
response_test.go Normal file
View File

@@ -0,0 +1,252 @@
package imap_test
import (
"bytes"
"reflect"
"testing"
"github.com/emersion/go-imap"
)
func TestResp_WriteTo(t *testing.T) {
var b bytes.Buffer
w := imap.NewWriter(&b)
resp := imap.NewUntaggedResp([]interface{}{imap.RawString("76"), imap.RawString("FETCH"), []interface{}{imap.RawString("UID"), 783}})
if err := resp.WriteTo(w); err != nil {
t.Fatal(err)
}
if b.String() != "* 76 FETCH (UID 783)\r\n" {
t.Error("Invalid response:", b.String())
}
}
func TestContinuationReq_WriteTo(t *testing.T) {
var b bytes.Buffer
w := imap.NewWriter(&b)
resp := &imap.ContinuationReq{}
if err := resp.WriteTo(w); err != nil {
t.Fatal(err)
}
if b.String() != "+\r\n" {
t.Error("Invalid continuation:", b.String())
}
}
func TestContinuationReq_WriteTo_WithInfo(t *testing.T) {
var b bytes.Buffer
w := imap.NewWriter(&b)
resp := &imap.ContinuationReq{Info: "send literal"}
if err := resp.WriteTo(w); err != nil {
t.Fatal(err)
}
if b.String() != "+ send literal\r\n" {
t.Error("Invalid continuation:", b.String())
}
}
func TestReadResp_ContinuationReq(t *testing.T) {
b := bytes.NewBufferString("+ send literal\r\n")
r := imap.NewReader(b)
resp, err := imap.ReadResp(r)
if err != nil {
t.Fatal(err)
}
cont, ok := resp.(*imap.ContinuationReq)
if !ok {
t.Fatal("Response is not a continuation request")
}
if cont.Info != "send literal" {
t.Error("Invalid info:", cont.Info)
}
}
func TestReadResp_ContinuationReq_NoInfo(t *testing.T) {
b := bytes.NewBufferString("+\r\n")
r := imap.NewReader(b)
resp, err := imap.ReadResp(r)
if err != nil {
t.Fatal(err)
}
cont, ok := resp.(*imap.ContinuationReq)
if !ok {
t.Fatal("Response is not a continuation request")
}
if cont.Info != "" {
t.Error("Invalid info:", cont.Info)
}
}
func TestReadResp_Resp(t *testing.T) {
b := bytes.NewBufferString("* 1 EXISTS\r\n")
r := imap.NewReader(b)
resp, err := imap.ReadResp(r)
if err != nil {
t.Fatal(err)
}
data, ok := resp.(*imap.DataResp)
if !ok {
t.Fatal("Invalid response type")
}
if data.Tag != "*" {
t.Error("Invalid tag:", data.Tag)
}
if len(data.Fields) != 2 {
t.Error("Invalid fields:", data.Fields)
}
}
func TestReadResp_Resp_NoArgs(t *testing.T) {
b := bytes.NewBufferString("* SEARCH\r\n")
r := imap.NewReader(b)
resp, err := imap.ReadResp(r)
if err != nil {
t.Fatal(err)
}
data, ok := resp.(*imap.DataResp)
if !ok {
t.Fatal("Invalid response type")
}
if data.Tag != "*" {
t.Error("Invalid tag:", data.Tag)
}
if len(data.Fields) != 1 || data.Fields[0] != "SEARCH" {
t.Error("Invalid fields:", data.Fields)
}
}
func TestReadResp_StatusResp(t *testing.T) {
tests := []struct {
input string
expected *imap.StatusResp
}{
{
input: "* OK IMAP4rev1 Service Ready\r\n",
expected: &imap.StatusResp{
Tag: "*",
Type: imap.StatusRespOk,
Info: "IMAP4rev1 Service Ready",
},
},
{
input: "* PREAUTH Welcome Pauline!\r\n",
expected: &imap.StatusResp{
Tag: "*",
Type: imap.StatusRespPreauth,
Info: "Welcome Pauline!",
},
},
{
input: "a001 OK NOOP completed\r\n",
expected: &imap.StatusResp{
Tag: "a001",
Type: imap.StatusRespOk,
Info: "NOOP completed",
},
},
{
input: "a001 OK [READ-ONLY] SELECT completed\r\n",
expected: &imap.StatusResp{
Tag: "a001",
Type: imap.StatusRespOk,
Code: "READ-ONLY",
Info: "SELECT completed",
},
},
{
input: "a001 OK [CAPABILITY IMAP4rev1 UIDPLUS] LOGIN completed\r\n",
expected: &imap.StatusResp{
Tag: "a001",
Type: imap.StatusRespOk,
Code: "CAPABILITY",
Arguments: []interface{}{"IMAP4rev1", "UIDPLUS"},
Info: "LOGIN completed",
},
},
}
for _, test := range tests {
b := bytes.NewBufferString(test.input)
r := imap.NewReader(b)
resp, err := imap.ReadResp(r)
if err != nil {
t.Fatal(err)
}
status, ok := resp.(*imap.StatusResp)
if !ok {
t.Fatal("Response is not a status:", resp)
}
if status.Tag != test.expected.Tag {
t.Errorf("Invalid tag: expected %v but got %v", status.Tag, test.expected.Tag)
}
if status.Type != test.expected.Type {
t.Errorf("Invalid type: expected %v but got %v", status.Type, test.expected.Type)
}
if status.Code != test.expected.Code {
t.Errorf("Invalid code: expected %v but got %v", status.Code, test.expected.Code)
}
if len(status.Arguments) != len(test.expected.Arguments) {
t.Errorf("Invalid arguments: expected %v but got %v", status.Arguments, test.expected.Arguments)
}
if status.Info != test.expected.Info {
t.Errorf("Invalid info: expected %v but got %v", status.Info, test.expected.Info)
}
}
}
func TestParseNamedResp(t *testing.T) {
tests := []struct {
resp *imap.DataResp
name string
fields []interface{}
}{
{
resp: &imap.DataResp{Fields: []interface{}{"CAPABILITY", "IMAP4rev1"}},
name: "CAPABILITY",
fields: []interface{}{"IMAP4rev1"},
},
{
resp: &imap.DataResp{Fields: []interface{}{"42", "EXISTS"}},
name: "EXISTS",
fields: []interface{}{"42"},
},
{
resp: &imap.DataResp{Fields: []interface{}{"42", "FETCH", "blah"}},
name: "FETCH",
fields: []interface{}{"42", "blah"},
},
}
for _, test := range tests {
name, fields, ok := imap.ParseNamedResp(test.resp)
if !ok {
t.Errorf("ParseNamedResp(%v)[2] = false, want true", test.resp)
} else if name != test.name {
t.Errorf("ParseNamedResp(%v)[0] = %v, want %v", test.resp, name, test.name)
} else if !reflect.DeepEqual(fields, test.fields) {
t.Errorf("ParseNamedResp(%v)[1] = %v, want %v", test.resp, fields, test.fields)
}
}
}

61
responses/authenticate.go Normal file
View File

@@ -0,0 +1,61 @@
package responses
import (
"encoding/base64"
"github.com/emersion/go-imap"
"github.com/emersion/go-sasl"
)
// An AUTHENTICATE response.
type Authenticate struct {
Mechanism sasl.Client
InitialResponse []byte
RepliesCh chan []byte
}
// Implements
func (r *Authenticate) Replies() <-chan []byte {
return r.RepliesCh
}
func (r *Authenticate) writeLine(l string) error {
r.RepliesCh <- []byte(l + "\r\n")
return nil
}
func (r *Authenticate) cancel() error {
return r.writeLine("*")
}
func (r *Authenticate) Handle(resp imap.Resp) error {
cont, ok := resp.(*imap.ContinuationReq)
if !ok {
return ErrUnhandled
}
// Empty challenge, send initial response as stated in RFC 2222 section 5.1
if cont.Info == "" && r.InitialResponse != nil {
encoded := base64.StdEncoding.EncodeToString(r.InitialResponse)
if err := r.writeLine(encoded); err != nil {
return err
}
r.InitialResponse = nil
return nil
}
challenge, err := base64.StdEncoding.DecodeString(cont.Info)
if err != nil {
r.cancel()
return err
}
reply, err := r.Mechanism.Next(challenge)
if err != nil {
r.cancel()
return err
}
encoded := base64.StdEncoding.EncodeToString(reply)
return r.writeLine(encoded)
}

20
responses/capability.go Normal file
View File

@@ -0,0 +1,20 @@
package responses
import (
"github.com/emersion/go-imap"
)
// A CAPABILITY response.
// See RFC 3501 section 7.2.1
type Capability struct {
Caps []string
}
func (r *Capability) WriteTo(w *imap.Writer) error {
fields := []interface{}{imap.RawString("CAPABILITY")}
for _, cap := range r.Caps {
fields = append(fields, imap.RawString(cap))
}
return imap.NewUntaggedResp(fields).WriteTo(w)
}

33
responses/enabled.go Normal file
View File

@@ -0,0 +1,33 @@
package responses
import (
"github.com/emersion/go-imap"
)
// An ENABLED response, defined in RFC 5161 section 3.2.
type Enabled struct {
Caps []string
}
func (r *Enabled) Handle(resp imap.Resp) error {
name, fields, ok := imap.ParseNamedResp(resp)
if !ok || name != "ENABLED" {
return ErrUnhandled
}
if caps, err := imap.ParseStringList(fields); err != nil {
return err
} else {
r.Caps = append(r.Caps, caps...)
}
return nil
}
func (r *Enabled) WriteTo(w *imap.Writer) error {
fields := []interface{}{imap.RawString("ENABLED")}
for _, cap := range r.Caps {
fields = append(fields, imap.RawString(cap))
}
return imap.NewUntaggedResp(fields).WriteTo(w)
}

43
responses/expunge.go Normal file
View File

@@ -0,0 +1,43 @@
package responses
import (
"github.com/emersion/go-imap"
)
const expungeName = "EXPUNGE"
// An EXPUNGE response.
// See RFC 3501 section 7.4.1
type Expunge struct {
SeqNums chan uint32
}
func (r *Expunge) Handle(resp imap.Resp) error {
name, fields, ok := imap.ParseNamedResp(resp)
if !ok || name != expungeName {
return ErrUnhandled
}
if len(fields) == 0 {
return errNotEnoughFields
}
seqNum, err := imap.ParseNumber(fields[0])
if err != nil {
return err
}
r.SeqNums <- seqNum
return nil
}
func (r *Expunge) WriteTo(w *imap.Writer) error {
for seqNum := range r.SeqNums {
resp := imap.NewUntaggedResp([]interface{}{seqNum, imap.RawString(expungeName)})
if err := resp.WriteTo(w); err != nil {
return err
}
}
return nil
}

70
responses/fetch.go Normal file
View File

@@ -0,0 +1,70 @@
package responses
import (
"github.com/emersion/go-imap"
)
const fetchName = "FETCH"
// A FETCH response.
// See RFC 3501 section 7.4.2
type Fetch struct {
Messages chan *imap.Message
SeqSet *imap.SeqSet
Uid bool
}
func (r *Fetch) Handle(resp imap.Resp) error {
name, fields, ok := imap.ParseNamedResp(resp)
if !ok || name != fetchName {
return ErrUnhandled
} else if len(fields) < 1 {
return errNotEnoughFields
}
seqNum, err := imap.ParseNumber(fields[0])
if err != nil {
return err
}
msgFields, _ := fields[1].([]interface{})
msg := &imap.Message{SeqNum: seqNum}
if err := msg.Parse(msgFields); err != nil {
return err
}
if r.Uid && msg.Uid == 0 {
// we requested UIDs and got a message without one --> unilateral update --> ignore
return ErrUnhandled
}
var num uint32
if r.Uid {
num = msg.Uid
} else {
num = seqNum
}
// Check whether we obtained a result we requested with our SeqSet
// If the result is not contained in our SeqSet we have to handle an additional special case:
// In case we requested UIDs with a dynamic sequence (i.e. * or n:*) and the maximum UID of the mailbox
// is less then our n, the server will supply us with the max UID (cf. RFC 3501 §6.4.8 and §9 `seq-range`).
// Thus, such a result is correct and has to be returned by us.
if !r.SeqSet.Contains(num) && (!r.Uid || !r.SeqSet.Dynamic()) {
return ErrUnhandled
}
r.Messages <- msg
return nil
}
func (r *Fetch) WriteTo(w *imap.Writer) error {
var err error
for msg := range r.Messages {
resp := imap.NewUntaggedResp([]interface{}{msg.SeqNum, imap.RawString(fetchName), msg.Format()})
if err == nil {
err = resp.WriteTo(w)
}
}
return err
}

38
responses/idle.go Normal file
View File

@@ -0,0 +1,38 @@
package responses
import (
"github.com/emersion/go-imap"
)
// An IDLE response.
type Idle struct {
RepliesCh chan []byte
Stop <-chan struct{}
gotContinuationReq bool
}
func (r *Idle) Replies() <-chan []byte {
return r.RepliesCh
}
func (r *Idle) stop() {
r.RepliesCh <- []byte("DONE\r\n")
}
func (r *Idle) Handle(resp imap.Resp) error {
// Wait for a continuation request
if _, ok := resp.(*imap.ContinuationReq); ok && !r.gotContinuationReq {
r.gotContinuationReq = true
// We got a continuation request, wait for r.Stop to be closed
go func() {
<-r.Stop
r.stop()
}()
return nil
}
return ErrUnhandled
}

Some files were not shown because too many files have changed in this diff Show More