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

181
README.md
View File

@@ -1,29 +1,182 @@
# go-imap # 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** > **Note**
> This is the README for go-imap v2. This new major version is still in > This is the README for go-imap v1. go-imap v2 is in development, see the
> development. For go-imap v1, see the [v1 branch]. > [v2 branch](https://github.com/emersion/go-imap/tree/v2) for more details.
## Usage ## 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] "github.com/emersion/go-imap/client"
- [Server docs] "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 ## License
MIT 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 ( require (
github.com/emersion/go-message v0.18.1 github.com/emersion/go-message v0.15.0
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 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.15.0 h1:urgKGqt2JAc9NFJcgncQcohHdiYb803YTH9OQwHBHIY=
github.com/emersion/go-message v0.18.1/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA= github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 h1:hH4PQfOndHDlpzYfLAAfl63E8Le6F2+EL/cdhlkyRJY= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
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=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 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-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. // Package imap implements IMAP4rev1 (RFC 3501).
//
// 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 package imap
import ( import (
"fmt" "errors"
"io" "io"
"strings"
) )
// ConnState describes the connection state. // A StatusItem is a mailbox status data item that can be retrieved with a
// // STATUS command. See RFC 3501 section 6.3.10.
// See RFC 9051 section 3. type StatusItem string
type ConnState int
const ( const (
ConnStateNone ConnState = iota StatusMessages StatusItem = "MESSAGES"
ConnStateNotAuthenticated StatusRecent StatusItem = "RECENT"
ConnStateAuthenticated StatusUidNext StatusItem = "UIDNEXT"
ConnStateSelected StatusUidValidity StatusItem = "UIDVALIDITY"
ConnStateLogout StatusUnseen StatusItem = "UNSEEN"
StatusAppendLimit StatusItem = "APPENDLIMIT"
) )
// String implements fmt.Stringer. // A FetchItem is a message data item that can be fetched.
func (state ConnState) String() string { type FetchItem string
switch state {
case ConnStateNone: // List of items that can be fetched. See RFC 3501 section 6.4.5.
return "none" //
case ConnStateNotAuthenticated: // Warning: FetchBody will not return the raw message body, instead it will
return "not authenticated" // return a subset of FetchBodyStructure.
case ConnStateAuthenticated: const (
return "authenticated" // Macros
case ConnStateSelected: FetchAll FetchItem = "ALL"
return "selected" FetchFast FetchItem = "FAST"
case ConnStateLogout: FetchFull FetchItem = "FULL"
return "logout"
// 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: default:
panic(fmt.Errorf("imap: unknown connection state %v", int(state))) return []FetchItem{item}
} }
} }
// MailboxAttr is a mailbox attribute. // FlagsOp is an operation that will be applied on message flags.
// type FlagsOp string
// Mailbox attributes are defined in RFC 9051 section 7.3.1.
type MailboxAttr string
const ( const (
// Base attributes // SetFlags replaces existing flags by new ones.
MailboxAttrNonExistent MailboxAttr = "\\NonExistent" SetFlags FlagsOp = "FLAGS"
MailboxAttrNoInferiors MailboxAttr = "\\Noinferiors" // AddFlags adds new flags.
MailboxAttrNoSelect MailboxAttr = "\\Noselect" AddFlags = "+FLAGS"
MailboxAttrHasChildren MailboxAttr = "\\HasChildren" // RemoveFlags removes existing flags.
MailboxAttrHasNoChildren MailboxAttr = "\\HasNoChildren" RemoveFlags = "-FLAGS"
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
) )
// Flag is a message flag. // silentOp can be appended to a FlagsOp to prevent the operation from
// // triggering unilateral message updates.
// Message flags are defined in RFC 9051 section 2.3.2. const silentOp = ".SILENT"
type Flag string
const ( // A StoreItem is a message data item that can be updated.
// System flags type StoreItem string
FlagSeen Flag = "\\Seen"
FlagAnswered Flag = "\\Answered"
FlagFlagged Flag = "\\Flagged"
FlagDeleted Flag = "\\Deleted"
FlagDraft Flag = "\\Draft"
// Widely used flags // FormatFlagsOp returns the StoreItem that executes the flags operation op.
FlagForwarded Flag = "$Forwarded" func FormatFlagsOp(op FlagsOp, silent bool) StoreItem {
FlagMDNSent Flag = "$MDNSent" // Message Disposition Notification sent s := string(op)
FlagJunk Flag = "$Junk" if silent {
FlagNotJunk Flag = "$NotJunk" s += silentOp
FlagPhishing Flag = "$Phishing" }
FlagImportant Flag = "$Important" // RFC 8457 return StoreItem(s)
// Permanent flags
FlagWildcard Flag = "\\*"
)
// LiteralReader is a reader for IMAP literals.
type LiteralReader interface {
io.Reader
Size() int64
} }
// UID is a message unique identifier. // ParseFlagsOp parses a flags operation from StoreItem.
type UID uint32 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 package imap
import ( import (
"fmt"
"strings" "strings"
) )
// StatusResponseType is a generic status response type. // Resp is an IMAP response. It is either a *DataResp, a
type StatusResponseType string // *ContinuationReq or a *StatusResp.
type Resp interface {
const ( resp()
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
} }
// Error is an IMAP error caused by a status response. // ReadResp reads a single response from a Reader.
type Error StatusResponse func ReadResp(r *Reader) (Resp, error) {
atom, err := r.ReadAtom()
if err != nil {
return nil, err
}
tag, ok := atom.(string)
if !ok {
return nil, newParseError("response tag is not an atom")
}
var _ error = (*Error)(nil) if tag == "+" {
if err := r.ReadSp(); err != nil {
r.UnreadRune()
}
// Error implements the error interface. resp := &ContinuationReq{}
func (err *Error) Error() string { resp.Info, err = r.ReadInfo()
var sb strings.Builder if err != nil {
fmt.Fprintf(&sb, "imap: %v", err.Type) return nil, err
if err.Code != "" {
fmt.Fprintf(&sb, " [%v]", err.Code)
} }
text := err.Text
if text == "" { return resp, nil
text = "<unknown>"
} }
fmt.Fprintf(&sb, " %v", text)
return sb.String() 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