This is the v1 version, had the v2 before.
This commit is contained in:
2
LICENSE
2
LICENSE
@@ -1,8 +1,8 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 The Go-IMAP Authors
|
||||
Copyright (c) 2016 emersion
|
||||
Copyright (c) 2016 Proton Technologies AG
|
||||
Copyright (c) 2023 Simon Ser
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
181
README.md
181
README.md
@@ -1,29 +1,182 @@
|
||||
# go-imap
|
||||
|
||||
[](https://pkg.go.dev/github.com/emersion/go-imap/v2)
|
||||
[](https://godocs.io/github.com/emersion/go-imap)
|
||||
[](https://builds.sr.ht/~emersion/go-imap/commits/master?)
|
||||
|
||||
An [IMAP4rev2] library for Go.
|
||||
An [IMAP4rev1](https://tools.ietf.org/html/rfc3501) library written in Go. It
|
||||
can be used to build a client and/or a server.
|
||||
|
||||
> **Note**
|
||||
> This is the README for go-imap v2. This new major version is still in
|
||||
> development. For go-imap v1, see the [v1 branch].
|
||||
> This is the README for go-imap v1. go-imap v2 is in development, see the
|
||||
> [v2 branch](https://github.com/emersion/go-imap/tree/v2) for more details.
|
||||
|
||||
## Usage
|
||||
|
||||
To add go-imap to your project, run:
|
||||
### Client [](https://godocs.io/github.com/emersion/go-imap/client)
|
||||
|
||||
go get github.com/emersion/go-imap/v2
|
||||
```go
|
||||
package main
|
||||
|
||||
Documentation and examples for the module are available here:
|
||||
import (
|
||||
"log"
|
||||
|
||||
- [Client docs]
|
||||
- [Server docs]
|
||||
"github.com/emersion/go-imap/client"
|
||||
"github.com/emersion/go-imap"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.Println("Connecting to server...")
|
||||
|
||||
// Connect to server
|
||||
c, err := client.DialTLS("mail.example.org:993", nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Println("Connected")
|
||||
|
||||
// Don't forget to logout
|
||||
defer c.Logout()
|
||||
|
||||
// Login
|
||||
if err := c.Login("username", "password"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Println("Logged in")
|
||||
|
||||
// List mailboxes
|
||||
mailboxes := make(chan *imap.MailboxInfo, 10)
|
||||
done := make(chan error, 1)
|
||||
go func () {
|
||||
done <- c.List("", "*", mailboxes)
|
||||
}()
|
||||
|
||||
log.Println("Mailboxes:")
|
||||
for m := range mailboxes {
|
||||
log.Println("* " + m.Name)
|
||||
}
|
||||
|
||||
if err := <-done; err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Select INBOX
|
||||
mbox, err := c.Select("INBOX", false)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Println("Flags for INBOX:", mbox.Flags)
|
||||
|
||||
// Get the last 4 messages
|
||||
from := uint32(1)
|
||||
to := mbox.Messages
|
||||
if mbox.Messages > 3 {
|
||||
// We're using unsigned integers here, only subtract if the result is > 0
|
||||
from = mbox.Messages - 3
|
||||
}
|
||||
seqset := new(imap.SeqSet)
|
||||
seqset.AddRange(from, to)
|
||||
|
||||
messages := make(chan *imap.Message, 10)
|
||||
done = make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope}, messages)
|
||||
}()
|
||||
|
||||
log.Println("Last 4 messages:")
|
||||
for msg := range messages {
|
||||
log.Println("* " + msg.Envelope.Subject)
|
||||
}
|
||||
|
||||
if err := <-done; err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Println("Done!")
|
||||
}
|
||||
```
|
||||
|
||||
### Server [](https://godocs.io/github.com/emersion/go-imap/server)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/emersion/go-imap/server"
|
||||
"github.com/emersion/go-imap/backend/memory"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create a memory backend
|
||||
be := memory.New()
|
||||
|
||||
// Create a new server
|
||||
s := server.New(be)
|
||||
s.Addr = ":1143"
|
||||
// Since we will use this server for testing only, we can allow plain text
|
||||
// authentication over unencrypted connections
|
||||
s.AllowInsecureAuth = true
|
||||
|
||||
log.Println("Starting IMAP server at localhost:1143")
|
||||
if err := s.ListenAndServe(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can now use `telnet localhost 1143` to manually connect to the server.
|
||||
|
||||
## Extensions
|
||||
|
||||
Support for several IMAP extensions is included in go-imap itself. This
|
||||
includes:
|
||||
|
||||
* [APPENDLIMIT](https://tools.ietf.org/html/rfc7889)
|
||||
* [CHILDREN](https://tools.ietf.org/html/rfc3348)
|
||||
* [ENABLE](https://tools.ietf.org/html/rfc5161)
|
||||
* [IDLE](https://tools.ietf.org/html/rfc2177)
|
||||
* [IMPORTANT](https://tools.ietf.org/html/rfc8457)
|
||||
* [LITERAL+](https://tools.ietf.org/html/rfc7888)
|
||||
* [MOVE](https://tools.ietf.org/html/rfc6851)
|
||||
* [SASL-IR](https://tools.ietf.org/html/rfc4959)
|
||||
* [SPECIAL-USE](https://tools.ietf.org/html/rfc6154)
|
||||
* [UNSELECT](https://tools.ietf.org/html/rfc3691)
|
||||
|
||||
Support for other extensions is provided via separate packages. See below.
|
||||
|
||||
## Extending go-imap
|
||||
|
||||
### Extensions
|
||||
|
||||
Commands defined in IMAP extensions are available in other packages. See [the
|
||||
wiki](https://github.com/emersion/go-imap/wiki/Using-extensions#using-client-extensions)
|
||||
to learn how to use them.
|
||||
|
||||
* [COMPRESS](https://github.com/emersion/go-imap-compress)
|
||||
* [ID](https://github.com/ProtonMail/go-imap-id)
|
||||
* [METADATA](https://github.com/emersion/go-imap-metadata)
|
||||
* [NAMESPACE](https://github.com/foxcpp/go-imap-namespace)
|
||||
* [QUOTA](https://github.com/emersion/go-imap-quota)
|
||||
* [SORT and THREAD](https://github.com/emersion/go-imap-sortthread)
|
||||
* [UIDPLUS](https://github.com/emersion/go-imap-uidplus)
|
||||
|
||||
### Server backends
|
||||
|
||||
* [Memory](https://github.com/emersion/go-imap/tree/master/backend/memory) (for testing)
|
||||
* [Multi](https://github.com/emersion/go-imap-multi)
|
||||
* [PGP](https://github.com/emersion/go-imap-pgp)
|
||||
* [Proxy](https://github.com/emersion/go-imap-proxy)
|
||||
* [Notmuch](https://github.com/stbenjam/go-imap-notmuch) - Experimental gateway for [Notmuch](https://notmuchmail.org/)
|
||||
|
||||
### Related projects
|
||||
|
||||
* [go-message](https://github.com/emersion/go-message) - parsing and formatting MIME and mail messages
|
||||
* [go-msgauth](https://github.com/emersion/go-msgauth) - handle DKIM, DMARC and Authentication-Results
|
||||
* [go-pgpmail](https://github.com/emersion/go-pgpmail) - decrypting and encrypting mails with OpenPGP
|
||||
* [go-sasl](https://github.com/emersion/go-sasl) - sending and receiving SASL authentications
|
||||
* [go-smtp](https://github.com/emersion/go-smtp) - building SMTP clients and servers
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
[IMAP4rev2]: https://www.rfc-editor.org/rfc/rfc9051.html
|
||||
[v1 branch]: https://github.com/emersion/go-imap/tree/v1
|
||||
[Client docs]: https://pkg.go.dev/github.com/emersion/go-imap/v2/imapclient
|
||||
[Server docs]: https://pkg.go.dev/github.com/emersion/go-imap/v2/imapserver
|
||||
|
||||
29
backend/appendlimit.go
Normal file
29
backend/appendlimit.go
Normal 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
20
backend/backend.go
Normal 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)
|
||||
}
|
||||
2
backend/backendutil/backendutil.go
Normal file
2
backend/backendutil/backendutil.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package backendutil provides utility functions to implement IMAP backends.
|
||||
package backendutil
|
||||
79
backend/backendutil/backendutil_test.go
Normal file
79
backend/backendutil/backendutil_test.go
Normal 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
121
backend/backendutil/body.go
Normal 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
|
||||
}
|
||||
196
backend/backendutil/body_test.go
Normal file
196
backend/backendutil/body_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
117
backend/backendutil/bodystructure.go
Normal file
117
backend/backendutil/bodystructure.go
Normal 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
|
||||
}
|
||||
76
backend/backendutil/bodystructure_test.go
Normal file
76
backend/backendutil/bodystructure_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
58
backend/backendutil/envelope.go
Normal file
58
backend/backendutil/envelope.go
Normal 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
|
||||
}
|
||||
40
backend/backendutil/envelope_test.go
Normal file
40
backend/backendutil/envelope_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
73
backend/backendutil/flags.go
Normal file
73
backend/backendutil/flags.go
Normal 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
|
||||
}
|
||||
86
backend/backendutil/flags_test.go
Normal file
86
backend/backendutil/flags_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
230
backend/backendutil/search.go
Normal file
230
backend/backendutil/search.go
Normal 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
|
||||
}
|
||||
432
backend/backendutil/search_test.go
Normal file
432
backend/backendutil/search_test.go
Normal 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
78
backend/mailbox.go
Normal 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
78
backend/memory/backend.go
Normal 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
243
backend/memory/mailbox.go
Normal 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
74
backend/memory/message.go
Normal 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
82
backend/memory/user.go
Normal 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
19
backend/move.go
Normal 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
98
backend/updates.go
Normal 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
92
backend/user.go
Normal 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
695
client/client.go
Normal 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
187
client/client_test.go
Normal 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
88
client/cmd_any.go
Normal 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
80
client/cmd_any_test.go
Normal 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
380
client/cmd_auth.go
Normal 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
499
client/cmd_auth_test.go
Normal 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
174
client/cmd_noauth.go
Normal 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
299
client/cmd_noauth_test.go
Normal 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
372
client/cmd_selected.go
Normal 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
816
client/cmd_selected_test.go
Normal 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
323
client/example_test.go
Normal 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
24
client/tag.go
Normal 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
57
command.go
Normal 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
98
command_test.go
Normal 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
93
commands/append.go
Normal 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
124
commands/authenticate.go
Normal 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
18
commands/capability.go
Normal 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
18
commands/check.go
Normal 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
18
commands/close.go
Normal 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
2
commands/commands.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package commands implements IMAP commands defined in RFC 3501.
|
||||
package commands
|
||||
47
commands/copy.go
Normal file
47
commands/copy.go
Normal 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
38
commands/create.go
Normal 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
38
commands/delete.go
Normal 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
28
commands/enable.go
Normal 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
16
commands/expunge.go
Normal 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
63
commands/fetch.go
Normal 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
17
commands/idle.go
Normal 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
60
commands/list.go
Normal 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
36
commands/login.go
Normal 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
18
commands/logout.go
Normal 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
48
commands/move.go
Normal 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
18
commands/noop.go
Normal 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
51
commands/rename.go
Normal 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
57
commands/search.go
Normal 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
45
commands/select.go
Normal 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
18
commands/starttls.go
Normal 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
58
commands/status.go
Normal 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
50
commands/store.go
Normal 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
63
commands/subscribe.go
Normal 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
44
commands/uid.go
Normal 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
17
commands/unselect.go
Normal 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
284
conn.go
Normal 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
107
conn_test.go
Normal 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
71
date.go
Normal 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
95
date_test.go
Normal 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
BIN
go-imap-1.zip
Normal file
Binary file not shown.
9
go.mod
9
go.mod
@@ -1,8 +1,9 @@
|
||||
module github.com/emersion/go-imap/v2
|
||||
module github.com/emersion/go-imap
|
||||
|
||||
go 1.18
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/emersion/go-message v0.18.1
|
||||
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43
|
||||
github.com/emersion/go-message v0.15.0
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
|
||||
golang.org/x/text v0.3.7
|
||||
)
|
||||
|
||||
41
go.sum
41
go.sum
@@ -1,35 +1,10 @@
|
||||
github.com/emersion/go-message v0.18.1 h1:tfTxIoXFSFRwWaZsgnqS1DSZuGpYGzSmCZD8SK3QA2E=
|
||||
github.com/emersion/go-message v0.18.1/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA=
|
||||
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 h1:hH4PQfOndHDlpzYfLAAfl63E8Le6F2+EL/cdhlkyRJY=
|
||||
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
github.com/emersion/go-message v0.15.0 h1:urgKGqt2JAc9NFJcgncQcohHdiYb803YTH9OQwHBHIY=
|
||||
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
172
imap.go
172
imap.go
@@ -1,105 +1,111 @@
|
||||
// Package imap implements IMAP4rev2.
|
||||
//
|
||||
// IMAP4rev2 is defined in RFC 9051.
|
||||
//
|
||||
// This package contains types and functions common to both the client and
|
||||
// server. See the imapclient and imapserver sub-packages.
|
||||
// Package imap implements IMAP4rev1 (RFC 3501).
|
||||
package imap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ConnState describes the connection state.
|
||||
//
|
||||
// See RFC 9051 section 3.
|
||||
type ConnState int
|
||||
// A StatusItem is a mailbox status data item that can be retrieved with a
|
||||
// STATUS command. See RFC 3501 section 6.3.10.
|
||||
type StatusItem string
|
||||
|
||||
const (
|
||||
ConnStateNone ConnState = iota
|
||||
ConnStateNotAuthenticated
|
||||
ConnStateAuthenticated
|
||||
ConnStateSelected
|
||||
ConnStateLogout
|
||||
StatusMessages StatusItem = "MESSAGES"
|
||||
StatusRecent StatusItem = "RECENT"
|
||||
StatusUidNext StatusItem = "UIDNEXT"
|
||||
StatusUidValidity StatusItem = "UIDVALIDITY"
|
||||
StatusUnseen StatusItem = "UNSEEN"
|
||||
|
||||
StatusAppendLimit StatusItem = "APPENDLIMIT"
|
||||
)
|
||||
|
||||
// String implements fmt.Stringer.
|
||||
func (state ConnState) String() string {
|
||||
switch state {
|
||||
case ConnStateNone:
|
||||
return "none"
|
||||
case ConnStateNotAuthenticated:
|
||||
return "not authenticated"
|
||||
case ConnStateAuthenticated:
|
||||
return "authenticated"
|
||||
case ConnStateSelected:
|
||||
return "selected"
|
||||
case ConnStateLogout:
|
||||
return "logout"
|
||||
// A FetchItem is a message data item that can be fetched.
|
||||
type FetchItem string
|
||||
|
||||
// List of items that can be fetched. See RFC 3501 section 6.4.5.
|
||||
//
|
||||
// Warning: FetchBody will not return the raw message body, instead it will
|
||||
// return a subset of FetchBodyStructure.
|
||||
const (
|
||||
// Macros
|
||||
FetchAll FetchItem = "ALL"
|
||||
FetchFast FetchItem = "FAST"
|
||||
FetchFull FetchItem = "FULL"
|
||||
|
||||
// Items
|
||||
FetchBody FetchItem = "BODY"
|
||||
FetchBodyStructure FetchItem = "BODYSTRUCTURE"
|
||||
FetchEnvelope FetchItem = "ENVELOPE"
|
||||
FetchFlags FetchItem = "FLAGS"
|
||||
FetchInternalDate FetchItem = "INTERNALDATE"
|
||||
FetchRFC822 FetchItem = "RFC822"
|
||||
FetchRFC822Header FetchItem = "RFC822.HEADER"
|
||||
FetchRFC822Size FetchItem = "RFC822.SIZE"
|
||||
FetchRFC822Text FetchItem = "RFC822.TEXT"
|
||||
FetchUid FetchItem = "UID"
|
||||
)
|
||||
|
||||
// Expand expands the item if it's a macro.
|
||||
func (item FetchItem) Expand() []FetchItem {
|
||||
switch item {
|
||||
case FetchAll:
|
||||
return []FetchItem{FetchFlags, FetchInternalDate, FetchRFC822Size, FetchEnvelope}
|
||||
case FetchFast:
|
||||
return []FetchItem{FetchFlags, FetchInternalDate, FetchRFC822Size}
|
||||
case FetchFull:
|
||||
return []FetchItem{FetchFlags, FetchInternalDate, FetchRFC822Size, FetchEnvelope, FetchBody}
|
||||
default:
|
||||
panic(fmt.Errorf("imap: unknown connection state %v", int(state)))
|
||||
return []FetchItem{item}
|
||||
}
|
||||
}
|
||||
|
||||
// MailboxAttr is a mailbox attribute.
|
||||
//
|
||||
// Mailbox attributes are defined in RFC 9051 section 7.3.1.
|
||||
type MailboxAttr string
|
||||
// FlagsOp is an operation that will be applied on message flags.
|
||||
type FlagsOp string
|
||||
|
||||
const (
|
||||
// Base attributes
|
||||
MailboxAttrNonExistent MailboxAttr = "\\NonExistent"
|
||||
MailboxAttrNoInferiors MailboxAttr = "\\Noinferiors"
|
||||
MailboxAttrNoSelect MailboxAttr = "\\Noselect"
|
||||
MailboxAttrHasChildren MailboxAttr = "\\HasChildren"
|
||||
MailboxAttrHasNoChildren MailboxAttr = "\\HasNoChildren"
|
||||
MailboxAttrMarked MailboxAttr = "\\Marked"
|
||||
MailboxAttrUnmarked MailboxAttr = "\\Unmarked"
|
||||
MailboxAttrSubscribed MailboxAttr = "\\Subscribed"
|
||||
MailboxAttrRemote MailboxAttr = "\\Remote"
|
||||
|
||||
// Role (aka. "special-use") attributes
|
||||
MailboxAttrAll MailboxAttr = "\\All"
|
||||
MailboxAttrArchive MailboxAttr = "\\Archive"
|
||||
MailboxAttrDrafts MailboxAttr = "\\Drafts"
|
||||
MailboxAttrFlagged MailboxAttr = "\\Flagged"
|
||||
MailboxAttrJunk MailboxAttr = "\\Junk"
|
||||
MailboxAttrSent MailboxAttr = "\\Sent"
|
||||
MailboxAttrTrash MailboxAttr = "\\Trash"
|
||||
MailboxAttrImportant MailboxAttr = "\\Important" // RFC 8457
|
||||
// SetFlags replaces existing flags by new ones.
|
||||
SetFlags FlagsOp = "FLAGS"
|
||||
// AddFlags adds new flags.
|
||||
AddFlags = "+FLAGS"
|
||||
// RemoveFlags removes existing flags.
|
||||
RemoveFlags = "-FLAGS"
|
||||
)
|
||||
|
||||
// Flag is a message flag.
|
||||
//
|
||||
// Message flags are defined in RFC 9051 section 2.3.2.
|
||||
type Flag string
|
||||
// silentOp can be appended to a FlagsOp to prevent the operation from
|
||||
// triggering unilateral message updates.
|
||||
const silentOp = ".SILENT"
|
||||
|
||||
const (
|
||||
// System flags
|
||||
FlagSeen Flag = "\\Seen"
|
||||
FlagAnswered Flag = "\\Answered"
|
||||
FlagFlagged Flag = "\\Flagged"
|
||||
FlagDeleted Flag = "\\Deleted"
|
||||
FlagDraft Flag = "\\Draft"
|
||||
// A StoreItem is a message data item that can be updated.
|
||||
type StoreItem string
|
||||
|
||||
// Widely used flags
|
||||
FlagForwarded Flag = "$Forwarded"
|
||||
FlagMDNSent Flag = "$MDNSent" // Message Disposition Notification sent
|
||||
FlagJunk Flag = "$Junk"
|
||||
FlagNotJunk Flag = "$NotJunk"
|
||||
FlagPhishing Flag = "$Phishing"
|
||||
FlagImportant Flag = "$Important" // RFC 8457
|
||||
|
||||
// Permanent flags
|
||||
FlagWildcard Flag = "\\*"
|
||||
)
|
||||
|
||||
// LiteralReader is a reader for IMAP literals.
|
||||
type LiteralReader interface {
|
||||
io.Reader
|
||||
Size() int64
|
||||
// FormatFlagsOp returns the StoreItem that executes the flags operation op.
|
||||
func FormatFlagsOp(op FlagsOp, silent bool) StoreItem {
|
||||
s := string(op)
|
||||
if silent {
|
||||
s += silentOp
|
||||
}
|
||||
return StoreItem(s)
|
||||
}
|
||||
|
||||
// UID is a message unique identifier.
|
||||
type UID uint32
|
||||
// ParseFlagsOp parses a flags operation from StoreItem.
|
||||
func ParseFlagsOp(item StoreItem) (op FlagsOp, silent bool, err error) {
|
||||
itemStr := string(item)
|
||||
silent = strings.HasSuffix(itemStr, silentOp)
|
||||
if silent {
|
||||
itemStr = strings.TrimSuffix(itemStr, silentOp)
|
||||
}
|
||||
op = FlagsOp(itemStr)
|
||||
|
||||
if op != SetFlags && op != AddFlags && op != RemoveFlags {
|
||||
err = errors.New("Unsupported STORE operation")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CharsetReader, if non-nil, defines a function to generate charset-conversion
|
||||
// readers, converting from the provided charset into UTF-8. Charsets are always
|
||||
// lower-case. utf-8 and us-ascii charsets are handled by default. One of the
|
||||
// the CharsetReader's result values must be non-nil.
|
||||
var CharsetReader func(charset string, r io.Reader) (io.Reader, error)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
37
internal/testcert.go
Normal 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
16
internal/testutil.go
Normal 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]
|
||||
}
|
||||
@@ -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
13
literal.go
Normal 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
8
logger.go
Normal 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
314
mailbox.go
Normal 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
191
mailbox_test.go
Normal 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
1186
message.go
Normal file
File diff suppressed because it is too large
Load Diff
797
message_test.go
Normal file
797
message_test.go
Normal 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
467
read.go
Normal 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
536
read_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
240
response.go
240
response.go
@@ -1,81 +1,181 @@
|
||||
package imap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// StatusResponseType is a generic status response type.
|
||||
type StatusResponseType string
|
||||
|
||||
const (
|
||||
StatusResponseTypeOK StatusResponseType = "OK"
|
||||
StatusResponseTypeNo StatusResponseType = "NO"
|
||||
StatusResponseTypeBad StatusResponseType = "BAD"
|
||||
StatusResponseTypePreAuth StatusResponseType = "PREAUTH"
|
||||
StatusResponseTypeBye StatusResponseType = "BYE"
|
||||
)
|
||||
|
||||
// ResponseCode is a response code.
|
||||
type ResponseCode string
|
||||
|
||||
const (
|
||||
ResponseCodeAlert ResponseCode = "ALERT"
|
||||
ResponseCodeAlreadyExists ResponseCode = "ALREADYEXISTS"
|
||||
ResponseCodeAuthenticationFailed ResponseCode = "AUTHENTICATIONFAILED"
|
||||
ResponseCodeAuthorizationFailed ResponseCode = "AUTHORIZATIONFAILED"
|
||||
ResponseCodeBadCharset ResponseCode = "BADCHARSET"
|
||||
ResponseCodeCannot ResponseCode = "CANNOT"
|
||||
ResponseCodeClientBug ResponseCode = "CLIENTBUG"
|
||||
ResponseCodeContactAdmin ResponseCode = "CONTACTADMIN"
|
||||
ResponseCodeCorruption ResponseCode = "CORRUPTION"
|
||||
ResponseCodeExpired ResponseCode = "EXPIRED"
|
||||
ResponseCodeHasChildren ResponseCode = "HASCHILDREN"
|
||||
ResponseCodeInUse ResponseCode = "INUSE"
|
||||
ResponseCodeLimit ResponseCode = "LIMIT"
|
||||
ResponseCodeNonExistent ResponseCode = "NONEXISTENT"
|
||||
ResponseCodeNoPerm ResponseCode = "NOPERM"
|
||||
ResponseCodeOverQuota ResponseCode = "OVERQUOTA"
|
||||
ResponseCodeParse ResponseCode = "PARSE"
|
||||
ResponseCodePrivacyRequired ResponseCode = "PRIVACYREQUIRED"
|
||||
ResponseCodeServerBug ResponseCode = "SERVERBUG"
|
||||
ResponseCodeTryCreate ResponseCode = "TRYCREATE"
|
||||
ResponseCodeUnavailable ResponseCode = "UNAVAILABLE"
|
||||
ResponseCodeUnknownCTE ResponseCode = "UNKNOWN-CTE"
|
||||
|
||||
// METADATA
|
||||
ResponseCodeTooMany ResponseCode = "TOOMANY"
|
||||
ResponseCodeNoPrivate ResponseCode = "NOPRIVATE"
|
||||
|
||||
// APPENDLIMIT
|
||||
ResponseCodeTooBig ResponseCode = "TOOBIG"
|
||||
)
|
||||
|
||||
// StatusResponse is a generic status response.
|
||||
//
|
||||
// See RFC 9051 section 7.1.
|
||||
type StatusResponse struct {
|
||||
Type StatusResponseType
|
||||
Code ResponseCode
|
||||
Text string
|
||||
// Resp is an IMAP response. It is either a *DataResp, a
|
||||
// *ContinuationReq or a *StatusResp.
|
||||
type Resp interface {
|
||||
resp()
|
||||
}
|
||||
|
||||
// Error is an IMAP error caused by a status response.
|
||||
type Error StatusResponse
|
||||
|
||||
var _ error = (*Error)(nil)
|
||||
|
||||
// Error implements the error interface.
|
||||
func (err *Error) Error() string {
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "imap: %v", err.Type)
|
||||
if err.Code != "" {
|
||||
fmt.Fprintf(&sb, " [%v]", err.Code)
|
||||
// ReadResp reads a single response from a Reader.
|
||||
func ReadResp(r *Reader) (Resp, error) {
|
||||
atom, err := r.ReadAtom()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
text := err.Text
|
||||
if text == "" {
|
||||
text = "<unknown>"
|
||||
tag, ok := atom.(string)
|
||||
if !ok {
|
||||
return nil, newParseError("response tag is not an atom")
|
||||
}
|
||||
fmt.Fprintf(&sb, " %v", text)
|
||||
return sb.String()
|
||||
|
||||
if tag == "+" {
|
||||
if err := r.ReadSp(); err != nil {
|
||||
r.UnreadRune()
|
||||
}
|
||||
|
||||
resp := &ContinuationReq{}
|
||||
resp.Info, err = r.ReadInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
if err := r.ReadSp(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Can be either data or status
|
||||
// Try to parse a status
|
||||
var fields []interface{}
|
||||
if atom, err := r.ReadAtom(); err == nil {
|
||||
fields = append(fields, atom)
|
||||
|
||||
if err := r.ReadSp(); err == nil {
|
||||
if name, ok := atom.(string); ok {
|
||||
status := StatusRespType(name)
|
||||
switch status {
|
||||
case StatusRespOk, StatusRespNo, StatusRespBad, StatusRespPreauth, StatusRespBye:
|
||||
resp := &StatusResp{
|
||||
Tag: tag,
|
||||
Type: status,
|
||||
}
|
||||
|
||||
char, _, err := r.ReadRune()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.UnreadRune()
|
||||
|
||||
if char == '[' {
|
||||
// Contains code & arguments
|
||||
resp.Code, resp.Arguments, err = r.ReadRespCode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
resp.Info, err = r.ReadInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
r.UnreadRune()
|
||||
}
|
||||
} else {
|
||||
r.UnreadRune()
|
||||
}
|
||||
|
||||
// Not a status so it's data
|
||||
resp := &DataResp{Tag: tag}
|
||||
|
||||
var remaining []interface{}
|
||||
remaining, err = r.ReadLine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Fields = append(fields, remaining...)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// DataResp is an IMAP response containing data.
|
||||
type DataResp struct {
|
||||
// The response tag. Can be either "" for untagged responses, "+" for continuation
|
||||
// requests or a previous command's tag.
|
||||
Tag string
|
||||
// The parsed response fields.
|
||||
Fields []interface{}
|
||||
}
|
||||
|
||||
// NewUntaggedResp creates a new untagged response.
|
||||
func NewUntaggedResp(fields []interface{}) *DataResp {
|
||||
return &DataResp{
|
||||
Tag: "*",
|
||||
Fields: fields,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *DataResp) resp() {}
|
||||
|
||||
func (r *DataResp) WriteTo(w *Writer) error {
|
||||
tag := RawString(r.Tag)
|
||||
if tag == "" {
|
||||
tag = RawString("*")
|
||||
}
|
||||
|
||||
fields := []interface{}{RawString(tag)}
|
||||
fields = append(fields, r.Fields...)
|
||||
return w.writeLine(fields...)
|
||||
}
|
||||
|
||||
// ContinuationReq is a continuation request response.
|
||||
type ContinuationReq struct {
|
||||
// The info message sent with the continuation request.
|
||||
Info string
|
||||
}
|
||||
|
||||
func (r *ContinuationReq) resp() {}
|
||||
|
||||
func (r *ContinuationReq) WriteTo(w *Writer) error {
|
||||
if err := w.writeString("+"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.Info != "" {
|
||||
if err := w.writeString(string(sp) + r.Info); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return w.writeCrlf()
|
||||
}
|
||||
|
||||
// ParseNamedResp attempts to parse a named data response.
|
||||
func ParseNamedResp(resp Resp) (name string, fields []interface{}, ok bool) {
|
||||
data, ok := resp.(*DataResp)
|
||||
if !ok || len(data.Fields) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Some responses (namely EXISTS and RECENT) are formatted like so:
|
||||
// [num] [name] [...]
|
||||
// Which is fucking stupid. But we handle that here by checking if the
|
||||
// response name is a number and then rearranging it.
|
||||
if len(data.Fields) > 1 {
|
||||
name, ok := data.Fields[1].(string)
|
||||
if ok {
|
||||
if _, err := ParseNumber(data.Fields[0]); err == nil {
|
||||
fields := []interface{}{data.Fields[0]}
|
||||
fields = append(fields, data.Fields[2:]...)
|
||||
return strings.ToUpper(name), fields, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IMAP commands are formatted like this:
|
||||
// [name] [...]
|
||||
name, ok = data.Fields[0].(string)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
return strings.ToUpper(name), data.Fields[1:], true
|
||||
}
|
||||
|
||||
252
response_test.go
Normal file
252
response_test.go
Normal 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
61
responses/authenticate.go
Normal 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
20
responses/capability.go
Normal 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
33
responses/enabled.go
Normal 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
43
responses/expunge.go
Normal 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
70
responses/fetch.go
Normal 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
38
responses/idle.go
Normal 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
Reference in New Issue
Block a user