Fixing to have the proper version of go-imap from foxcpp.
This commit is contained in:
2
LICENSE
2
LICENSE
@@ -1,8 +1,8 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2013 The Go-IMAP Authors
|
Copyright (c) 2013 The Go-IMAP Authors
|
||||||
|
Copyright (c) 2016 emersion
|
||||||
Copyright (c) 2016 Proton Technologies AG
|
Copyright (c) 2016 Proton Technologies AG
|
||||||
Copyright (c) 2023 Simon Ser
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
171
README.md
171
README.md
@@ -1,29 +1,170 @@
|
|||||||
# go-imap
|
# go-imap
|
||||||
|
|
||||||
[](https://pkg.go.dev/github.com/emersion/go-imap/v2)
|
[](https://godoc.org/github.com/emersion/go-imap)
|
||||||
|
[](https://travis-ci.org/emersion/go-imap)
|
||||||
|
[](https://codecov.io/gh/emersion/go-imap)
|
||||||
|
[](https://goreportcard.com/report/github.com/emersion/go-imap)
|
||||||
|
[](https://github.com/emersion/stability-badges#unstable)
|
||||||
|
|
||||||
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**
|
```shell
|
||||||
> This is the README for go-imap v2. This new major version is still in
|
go get github.com/emersion/go-imap/...
|
||||||
> development. For go-imap v1, see the [v1 branch].
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
To add go-imap to your project, run:
|
### Client [](https://godoc.org/github.com/emersion/go-imap/client)
|
||||||
|
|
||||||
go get github.com/emersion/go-imap/v2
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
Documentation and examples for the module are available here:
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
- [Client docs]
|
"github.com/emersion/go-imap/client"
|
||||||
- [Server docs]
|
"github.com/emersion/go-imap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.Println("Connecting to server...")
|
||||||
|
|
||||||
|
// Connect to server
|
||||||
|
c, err := client.DialTLS("mail.example.org:993", nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
log.Println("Connected")
|
||||||
|
|
||||||
|
// Don't forget to logout
|
||||||
|
defer c.Logout()
|
||||||
|
|
||||||
|
// Login
|
||||||
|
if err := c.Login("username", "password"); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
log.Println("Logged in")
|
||||||
|
|
||||||
|
// List mailboxes
|
||||||
|
mailboxes := make(chan *imap.MailboxInfo, 10)
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func () {
|
||||||
|
done <- c.List("", "*", mailboxes)
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Println("Mailboxes:")
|
||||||
|
for m := range mailboxes {
|
||||||
|
log.Println("* " + m.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := <-done; err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select INBOX
|
||||||
|
mbox, err := c.Select("INBOX", false)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
log.Println("Flags for INBOX:", mbox.Flags)
|
||||||
|
|
||||||
|
// Get the last 4 messages
|
||||||
|
from := uint32(1)
|
||||||
|
to := mbox.Messages
|
||||||
|
if mbox.Messages > 3 {
|
||||||
|
// We're using unsigned integers here, only substract 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://godoc.org/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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
* [APPENDLIMIT](https://github.com/emersion/go-imap-appendlimit)
|
||||||
|
* [COMPRESS](https://github.com/emersion/go-imap-compress)
|
||||||
|
* [ENABLE](https://github.com/emersion/go-imap-enable)
|
||||||
|
* [ID](https://github.com/ProtonMail/go-imap-id)
|
||||||
|
* [IDLE](https://github.com/emersion/go-imap-idle)
|
||||||
|
* [MOVE](https://github.com/emersion/go-imap-move)
|
||||||
|
* [QUOTA](https://github.com/emersion/go-imap-quota)
|
||||||
|
* [SPECIAL-USE](https://github.com/emersion/go-imap-specialuse)
|
||||||
|
* [UNSELECT](https://github.com/emersion/go-imap-unselect)
|
||||||
|
* [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)
|
||||||
|
|
||||||
|
### Related projects
|
||||||
|
|
||||||
|
* [go-message](https://github.com/emersion/go-message) - parsing and formatting MIME and mail messages
|
||||||
|
* [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
|
||||||
|
* [go-dkim](https://github.com/emersion/go-dkim) - creating and verifying DKIM signatures
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|
||||||
[IMAP4rev2]: https://www.rfc-editor.org/rfc/rfc9051.html
|
|
||||||
[v1 branch]: https://github.com/emersion/go-imap/tree/v1
|
|
||||||
[Client docs]: https://pkg.go.dev/github.com/emersion/go-imap/v2/imapclient
|
|
||||||
[Server docs]: https://pkg.go.dev/github.com/emersion/go-imap/v2/imapserver
|
|
||||||
|
|||||||
104
acl.go
104
acl.go
@@ -1,104 +0,0 @@
|
|||||||
package imap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// IMAP4 ACL extension (RFC 2086)
|
|
||||||
|
|
||||||
// Right describes a set of operations controlled by the IMAP ACL extension.
|
|
||||||
type Right byte
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Standard rights
|
|
||||||
RightLookup = Right('l') // mailbox is visible to LIST/LSUB commands
|
|
||||||
RightRead = Right('r') // SELECT the mailbox, perform CHECK, FETCH, PARTIAL, SEARCH, COPY from mailbox
|
|
||||||
RightSeen = Right('s') // keep seen/unseen information across sessions (STORE SEEN flag)
|
|
||||||
RightWrite = Right('w') // STORE flags other than SEEN and DELETED
|
|
||||||
RightInsert = Right('i') // perform APPEND, COPY into mailbox
|
|
||||||
RightPost = Right('p') // send mail to submission address for mailbox, not enforced by IMAP4 itself
|
|
||||||
RightCreate = Right('c') // CREATE new sub-mailboxes in any implementation-defined hierarchy
|
|
||||||
RightDelete = Right('d') // STORE DELETED flag, perform EXPUNGE
|
|
||||||
RightAdminister = Right('a') // perform SETACL
|
|
||||||
)
|
|
||||||
|
|
||||||
// RightSetAll contains all standard rights.
|
|
||||||
var RightSetAll = RightSet("lrswipcda")
|
|
||||||
|
|
||||||
// RightsIdentifier is an ACL identifier.
|
|
||||||
type RightsIdentifier string
|
|
||||||
|
|
||||||
// RightsIdentifierAnyone is the universal identity (matches everyone).
|
|
||||||
const RightsIdentifierAnyone = RightsIdentifier("anyone")
|
|
||||||
|
|
||||||
// NewRightsIdentifierUsername returns a rights identifier referring to a
|
|
||||||
// username, checking for reserved values.
|
|
||||||
func NewRightsIdentifierUsername(username string) (RightsIdentifier, error) {
|
|
||||||
if username == string(RightsIdentifierAnyone) || strings.HasPrefix(username, "-") {
|
|
||||||
return "", fmt.Errorf("imap: reserved rights identifier")
|
|
||||||
}
|
|
||||||
return RightsIdentifier(username), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RightModification indicates how to mutate a right set.
|
|
||||||
type RightModification byte
|
|
||||||
|
|
||||||
const (
|
|
||||||
RightModificationReplace = RightModification(0)
|
|
||||||
RightModificationAdd = RightModification('+')
|
|
||||||
RightModificationRemove = RightModification('-')
|
|
||||||
)
|
|
||||||
|
|
||||||
// A RightSet is a set of rights.
|
|
||||||
type RightSet []Right
|
|
||||||
|
|
||||||
// String returns a string representation of the right set.
|
|
||||||
func (r RightSet) String() string {
|
|
||||||
return string(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add returns a new right set containing rights from both sets.
|
|
||||||
func (r RightSet) Add(rights RightSet) RightSet {
|
|
||||||
newRights := make(RightSet, len(r), len(r)+len(rights))
|
|
||||||
copy(newRights, r)
|
|
||||||
|
|
||||||
for _, right := range rights {
|
|
||||||
if !strings.ContainsRune(string(r), rune(right)) {
|
|
||||||
newRights = append(newRights, right)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newRights
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove returns a new right set containing all rights in r except these in
|
|
||||||
// the provided set.
|
|
||||||
func (r RightSet) Remove(rights RightSet) RightSet {
|
|
||||||
newRights := make(RightSet, 0, len(r))
|
|
||||||
|
|
||||||
for _, right := range r {
|
|
||||||
if !strings.ContainsRune(string(rights), rune(right)) {
|
|
||||||
newRights = append(newRights, right)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newRights
|
|
||||||
}
|
|
||||||
|
|
||||||
// Equal returns true if both right sets contain exactly the same rights.
|
|
||||||
func (rs1 RightSet) Equal(rs2 RightSet) bool {
|
|
||||||
for _, r := range rs1 {
|
|
||||||
if !strings.ContainsRune(string(rs2), rune(r)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, r := range rs2 {
|
|
||||||
if !strings.ContainsRune(string(rs1), rune(r)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
18
append.go
18
append.go
@@ -1,18 +0,0 @@
|
|||||||
package imap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AppendOptions contains options for the APPEND command.
|
|
||||||
type AppendOptions struct {
|
|
||||||
Flags []Flag
|
|
||||||
Time time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// AppendData is the data returned by an APPEND command.
|
|
||||||
type AppendData struct {
|
|
||||||
// requires UIDPLUS or IMAP4rev2
|
|
||||||
UID UID
|
|
||||||
UIDValidity uint32
|
|
||||||
}
|
|
||||||
16
backend/backend.go
Normal file
16
backend/backend.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// Package backend defines an IMAP server backend interface.
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// 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(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
|
||||||
55
backend/backendutil/backendutil_test.go
Normal file
55
backend/backendutil/backendutil_test.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
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" +
|
||||||
|
"From: Mitsuha Miyamizu <mitsuha.miyamizu@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 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 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</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
|
||||||
75
backend/backendutil/body.go
Normal file
75
backend/backendutil/body.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package backendutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
"github.com/emersion/go-message"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errNoSuchPart = errors.New("backendutil: no such message body part")
|
||||||
|
|
||||||
|
// FetchBodySection extracts a body section from a message.
|
||||||
|
func FetchBodySection(e *message.Entity, 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 := e.MultipartReader()
|
||||||
|
if mr == nil {
|
||||||
|
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 {
|
||||||
|
e = p
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then, write the requested data to a buffer
|
||||||
|
b := new(bytes.Buffer)
|
||||||
|
|
||||||
|
// Write the header
|
||||||
|
mw, err := message.CreateWriter(b, e.Header)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer mw.Close()
|
||||||
|
|
||||||
|
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(mw, e.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
|
||||||
|
}
|
||||||
109
backend/backendutil/body_test.go
Normal file
109
backend/backendutil/body_test.go
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package backendutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
"github.com/emersion/go-message"
|
||||||
|
)
|
||||||
|
|
||||||
|
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[1.1.HEADER]",
|
||||||
|
body: testTextHeaderString,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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) {
|
||||||
|
e, err := message.Read(strings.NewReader(testMailString))
|
||||||
|
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(e, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
60
backend/backendutil/bodystructure.go
Normal file
60
backend/backendutil/bodystructure.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package backendutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
"github.com/emersion/go-message"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FetchBodyStructure computes a message's body structure from its content.
|
||||||
|
func FetchBodyStructure(e *message.Entity, extended bool) (*imap.BodyStructure, error) {
|
||||||
|
bs := new(imap.BodyStructure)
|
||||||
|
|
||||||
|
mediaType, mediaParams, _ := e.Header.ContentType()
|
||||||
|
typeParts := strings.SplitN(mediaType, "/", 2)
|
||||||
|
bs.MIMEType = typeParts[0]
|
||||||
|
if len(typeParts) == 2 {
|
||||||
|
bs.MIMESubType = typeParts[1]
|
||||||
|
}
|
||||||
|
bs.Params = mediaParams
|
||||||
|
|
||||||
|
bs.Id = e.Header.Get("Content-Id")
|
||||||
|
bs.Description = e.Header.Get("Content-Description")
|
||||||
|
bs.Encoding = e.Header.Get("Content-Encoding")
|
||||||
|
// TODO: bs.Size
|
||||||
|
|
||||||
|
if mr := e.MultipartReader(); 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, extended)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
parts = append(parts, pbs)
|
||||||
|
}
|
||||||
|
bs.Parts = parts
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: bs.Envelope, bs.BodyStructure
|
||||||
|
// TODO: bs.Lines
|
||||||
|
|
||||||
|
if extended {
|
||||||
|
bs.Extended = true
|
||||||
|
|
||||||
|
bs.Disposition, bs.DispositionParams, _ = e.Header.ContentDisposition()
|
||||||
|
|
||||||
|
// TODO: bs.Language, bs.Location
|
||||||
|
// TODO: bs.MD5
|
||||||
|
}
|
||||||
|
|
||||||
|
return bs, nil
|
||||||
|
}
|
||||||
67
backend/backendutil/bodystructure_test.go
Normal file
67
backend/backendutil/bodystructure_test.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package backendutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
"github.com/emersion/go-message"
|
||||||
|
)
|
||||||
|
|
||||||
|
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{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MIMEType: "text",
|
||||||
|
MIMESubType: "html",
|
||||||
|
Params: map[string]string{},
|
||||||
|
Extended: true,
|
||||||
|
Disposition: "inline",
|
||||||
|
DispositionParams: map[string]string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MIMEType: "text",
|
||||||
|
MIMESubType: "plain",
|
||||||
|
Params: map[string]string{},
|
||||||
|
Extended: true,
|
||||||
|
Disposition: "attachment",
|
||||||
|
DispositionParams: map[string]string{"filename": "note.txt"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extended: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchBodyStructure(t *testing.T) {
|
||||||
|
e, err := message.Read(strings.NewReader(testMailString))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Expected no error while reading mail, got:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bs, err := FetchBodyStructure(e, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
50
backend/backendutil/envelope.go
Normal file
50
backend/backendutil/envelope.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package backendutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
"github.com/emersion/go-message"
|
||||||
|
"github.com/emersion/go-message/mail"
|
||||||
|
)
|
||||||
|
|
||||||
|
func headerAddressList(h mail.Header, key string) ([]*imap.Address, error) {
|
||||||
|
addrs, err := h.AddressList(key)
|
||||||
|
|
||||||
|
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 message.Header) (*imap.Envelope, error) {
|
||||||
|
mh := mail.Header{h}
|
||||||
|
|
||||||
|
env := new(imap.Envelope)
|
||||||
|
env.Date, _ = mh.Date()
|
||||||
|
env.Subject, _ = mh.Subject()
|
||||||
|
env.From, _ = headerAddressList(mh, "From")
|
||||||
|
env.Sender, _ = headerAddressList(mh, "Sender")
|
||||||
|
env.ReplyTo, _ = headerAddressList(mh, "Reply-To")
|
||||||
|
env.To, _ = headerAddressList(mh, "To")
|
||||||
|
env.Cc, _ = headerAddressList(mh, "Cc")
|
||||||
|
env.Bcc, _ = headerAddressList(mh, "Bcc")
|
||||||
|
env.InReplyTo = mh.Get("In-Reply-To")
|
||||||
|
env.MessageId = mh.Get("Message-Id")
|
||||||
|
|
||||||
|
return env, nil
|
||||||
|
}
|
||||||
39
backend/backendutil/envelope_test.go
Normal file
39
backend/backendutil/envelope_test.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package backendutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
"github.com/emersion/go-message"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testEnvelope = &imap.Envelope{
|
||||||
|
Date: testDate,
|
||||||
|
Subject: "Your Name.",
|
||||||
|
From: []*imap.Address{{PersonalName: "Mitsuha Miyamizu", MailboxName: "mitsuha.miyamizu", HostName: "example.org"}},
|
||||||
|
Sender: []*imap.Address{},
|
||||||
|
ReplyTo: []*imap.Address{},
|
||||||
|
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) {
|
||||||
|
e, err := message.Read(strings.NewReader(testMailString))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Expected no error while reading mail, got:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
env, err := FetchEnvelope(e.Header)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
40
backend/backendutil/flags.go
Normal file
40
backend/backendutil/flags.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
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 {
|
||||||
|
switch op {
|
||||||
|
case imap.SetFlags:
|
||||||
|
// TODO: keep \Recent if it is present
|
||||||
|
return flags
|
||||||
|
case imap.AddFlags:
|
||||||
|
// Check for duplicates
|
||||||
|
for _, flag := range current {
|
||||||
|
for i, addFlag := range flags {
|
||||||
|
if addFlag == flag {
|
||||||
|
flags = append(flags[:i], flags[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return append(current, flags...)
|
||||||
|
case imap.RemoveFlags:
|
||||||
|
// Iterate through flags from the last one to the first one, to be able to
|
||||||
|
// delete some of them.
|
||||||
|
for i := len(current) - 1; i >= 0; i-- {
|
||||||
|
flag := current[i]
|
||||||
|
|
||||||
|
for _, removeFlag := range flags {
|
||||||
|
if removeFlag == flag {
|
||||||
|
current = append(current[:i], current[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
return current
|
||||||
|
}
|
||||||
46
backend/backendutil/flags_test.go
Normal file
46
backend/backendutil/flags_test.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
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"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateFlags(t *testing.T) {
|
||||||
|
current := []string{"a", "b", "c"}
|
||||||
|
for _, test := range updateFlagsTests {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
225
backend/backendutil/search.go
Normal file
225
backend/backendutil/search.go
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
package backendutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
"github.com/emersion/go-message"
|
||||||
|
"github.com/emersion/go-message/mail"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func bodyLen(e *message.Entity) (int, error) {
|
||||||
|
if l, ok := e.Body.(lengther); ok {
|
||||||
|
return l.Len(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := bufferBody(e)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return b.Len(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match returns true if a message matches the provided criteria. Sequence
|
||||||
|
// number, UID, flag and internal date contrainsts are not checked.
|
||||||
|
func Match(e *message.Entity, 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{e.Header}
|
||||||
|
|
||||||
|
if !c.SentBefore.IsZero() || !c.SentSince.IsZero() {
|
||||||
|
t, err := h.Date()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
t = t.Round(24 * time.Hour)
|
||||||
|
|
||||||
|
if !c.SentBefore.IsZero() && !t.Before(c.SentBefore) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if !c.SentSince.IsZero() && !t.After(c.SentSince) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, wantValues := range c.Header {
|
||||||
|
values, ok := e.Header[key]
|
||||||
|
for _, wantValue := range wantValues {
|
||||||
|
if wantValue == "" && !ok {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if wantValue != "" {
|
||||||
|
ok := false
|
||||||
|
for _, v := range values {
|
||||||
|
if matchString(v, 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 {
|
||||||
|
// TODO: also match header fields
|
||||||
|
if ok, err := matchBody(e, text); err != nil || !ok {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, not := range c.Not {
|
||||||
|
ok, err := Match(e, not)
|
||||||
|
if err != nil || ok {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, or := range c.Or {
|
||||||
|
ok1, err := Match(e, or[0])
|
||||||
|
if err != nil {
|
||||||
|
return ok1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ok2, err := Match(e, or[1])
|
||||||
|
if err != nil || (!ok1 && !ok2) {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchFlags(flags map[string]bool, c *imap.SearchCriteria) bool {
|
||||||
|
for _, f := range c.WithFlags {
|
||||||
|
if !flags[f] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, f := range c.WithoutFlags {
|
||||||
|
if flags[f] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, not := range c.Not {
|
||||||
|
if matchFlags(flags, not) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, or := range c.Or {
|
||||||
|
if !matchFlags(flags, or[0]) && !matchFlags(flags, or[1]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchFlags returns true if a flag list matches the provided criteria.
|
||||||
|
func MatchFlags(flags []string, c *imap.SearchCriteria) bool {
|
||||||
|
flagsMap := make(map[string]bool)
|
||||||
|
for _, f := range flags {
|
||||||
|
flagsMap[f] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchFlags(flagsMap, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchSeqNumAndUid returns true if a sequence number and a UID matches the
|
||||||
|
// provided criteria.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, not := range c.Not {
|
||||||
|
if MatchSeqNumAndUid(seqNum, uid, not) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, or := range c.Or {
|
||||||
|
if !MatchSeqNumAndUid(seqNum, uid, or[0]) && !MatchSeqNumAndUid(seqNum, uid, or[1]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchDate returns true if a date matches the provided criteria.
|
||||||
|
func MatchDate(date time.Time, c *imap.SearchCriteria) bool {
|
||||||
|
date = date.Round(24 * time.Hour)
|
||||||
|
if !c.Since.IsZero() && !date.After(c.Since) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !c.Before.IsZero() && !date.Before(c.Before) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, not := range c.Not {
|
||||||
|
if MatchDate(date, not) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, or := range c.Or {
|
||||||
|
if !MatchDate(date, or[0]) && !MatchDate(date, or[1]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
264
backend/backendutil/search_test.go
Normal file
264
backend/backendutil/search_test.go
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
package backendutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/textproto"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
"github.com/emersion/go-message"
|
||||||
|
)
|
||||||
|
|
||||||
|
var matchTests = []struct {
|
||||||
|
criteria *imap.SearchCriteria
|
||||||
|
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{"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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var flagsTests = []struct {
|
||||||
|
flags []string
|
||||||
|
criteria *imap.SearchCriteria
|
||||||
|
res bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMatchFlags(t *testing.T) {
|
||||||
|
for i, test := range flagsTests {
|
||||||
|
ok := MatchFlags(test.flags, test.criteria)
|
||||||
|
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 TestMatchSeqNumAndUid(t *testing.T) {
|
||||||
|
seqNum := uint32(42)
|
||||||
|
uid := uint32(69)
|
||||||
|
|
||||||
|
c := &imap.SearchCriteria{
|
||||||
|
Or: [][2]*imap.SearchCriteria{{
|
||||||
|
{
|
||||||
|
Uid: new(imap.SeqSet),
|
||||||
|
Not: []*imap.SearchCriteria{{SeqNum: new(imap.SeqSet)}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
SeqNum: new(imap.SeqSet),
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
if MatchSeqNumAndUid(seqNum, uid, c) {
|
||||||
|
t.Error("Expected not to match criteria")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Or[0][0].Uid.AddNum(uid)
|
||||||
|
if !MatchSeqNumAndUid(seqNum, uid, c) {
|
||||||
|
t.Error("Expected to match criteria")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Or[0][0].Not[0].SeqNum.AddNum(seqNum)
|
||||||
|
if MatchSeqNumAndUid(seqNum, uid, c) {
|
||||||
|
t.Error("Expected not to match criteria")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Or[0][1].SeqNum.AddNum(seqNum)
|
||||||
|
if !MatchSeqNumAndUid(seqNum, uid, c) {
|
||||||
|
t.Error("Expected to match criteria")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMatchDate(t *testing.T) {
|
||||||
|
date := time.Unix(1483997966, 0)
|
||||||
|
|
||||||
|
c := &imap.SearchCriteria{
|
||||||
|
Or: [][2]*imap.SearchCriteria{{
|
||||||
|
{
|
||||||
|
Since: date.Add(48 * time.Hour),
|
||||||
|
Not: []*imap.SearchCriteria{{
|
||||||
|
Since: date.Add(48 * time.Hour),
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Before: date.Add(-48 * time.Hour),
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
if MatchDate(date, c) {
|
||||||
|
t.Error("Expected not to match criteria")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Or[0][0].Since = date.Add(-48 * time.Hour)
|
||||||
|
if !MatchDate(date, c) {
|
||||||
|
t.Error("Expected to match criteria")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Or[0][0].Not[0].Since = date.Add(-48 * time.Hour)
|
||||||
|
if MatchDate(date, c) {
|
||||||
|
t.Error("Expected not to match criteria")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Or[0][1].Before = date.Add(48 * time.Hour)
|
||||||
|
if !MatchDate(date, c) {
|
||||||
|
t.Error("Expected to match 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
|
||||||
|
}
|
||||||
55
backend/memory/backend.go
Normal file
55
backend/memory/backend.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// A memory backend.
|
||||||
|
package memory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/backend"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Backend struct {
|
||||||
|
users map[string]*User
|
||||||
|
}
|
||||||
|
|
||||||
|
func (be *Backend) Login(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
|
||||||
|
To: contact@example.org
|
||||||
|
Subject: A little message, just for you
|
||||||
|
Date: Wed, 11 May 2016 14:31:59 +0000
|
||||||
|
Message-ID: <0000000@localhost/>
|
||||||
|
Content-Type: text/plain
|
||||||
|
|
||||||
|
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},
|
||||||
|
}
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
70
backend/memory/message.go
Normal file
70
backend/memory/message.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package memory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
"github.com/emersion/go-imap/backend/backendutil"
|
||||||
|
"github.com/emersion/go-message"
|
||||||
|
)
|
||||||
|
|
||||||
|
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) Fetch(seqNum uint32, items []imap.FetchItem) (*imap.Message, error) {
|
||||||
|
fetched := imap.NewMessage(seqNum, items)
|
||||||
|
for _, item := range items {
|
||||||
|
switch item {
|
||||||
|
case imap.FetchEnvelope:
|
||||||
|
e, _ := m.entity()
|
||||||
|
fetched.Envelope, _ = backendutil.FetchEnvelope(e.Header)
|
||||||
|
case imap.FetchBody, imap.FetchBodyStructure:
|
||||||
|
e, _ := m.entity()
|
||||||
|
fetched.BodyStructure, _ = backendutil.FetchBodyStructure(e, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
e, _ := m.entity()
|
||||||
|
l, _ := backendutil.FetchBodySection(e, section)
|
||||||
|
fetched.Body[section] = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetched, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Message) Match(seqNum uint32, c *imap.SearchCriteria) (bool, error) {
|
||||||
|
if !backendutil.MatchSeqNumAndUid(seqNum, m.Uid, c) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if !backendutil.MatchDate(m.Date, c) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if !backendutil.MatchFlags(m.Flags, c) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
e, _ := m.entity()
|
||||||
|
return backendutil.Match(e, 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
|
||||||
|
}
|
||||||
92
backend/updates.go
Normal file
92
backend/updates.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
212
capability.go
212
capability.go
@@ -1,212 +0,0 @@
|
|||||||
package imap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Cap represents an IMAP capability.
|
|
||||||
type Cap string
|
|
||||||
|
|
||||||
// Registered capabilities.
|
|
||||||
//
|
|
||||||
// See: https://www.iana.org/assignments/imap-capabilities/
|
|
||||||
const (
|
|
||||||
CapIMAP4rev1 Cap = "IMAP4rev1" // RFC 3501
|
|
||||||
CapIMAP4rev2 Cap = "IMAP4rev2" // RFC 9051
|
|
||||||
|
|
||||||
CapStartTLS Cap = "STARTTLS"
|
|
||||||
CapLoginDisabled Cap = "LOGINDISABLED"
|
|
||||||
|
|
||||||
// Folded in IMAP4rev2
|
|
||||||
CapNamespace Cap = "NAMESPACE" // RFC 2342
|
|
||||||
CapUnselect Cap = "UNSELECT" // RFC 3691
|
|
||||||
CapUIDPlus Cap = "UIDPLUS" // RFC 4315
|
|
||||||
CapESearch Cap = "ESEARCH" // RFC 4731
|
|
||||||
CapSearchRes Cap = "SEARCHRES" // RFC 5182
|
|
||||||
CapEnable Cap = "ENABLE" // RFC 5161
|
|
||||||
CapIdle Cap = "IDLE" // RFC 2177
|
|
||||||
CapSASLIR Cap = "SASL-IR" // RFC 4959
|
|
||||||
CapListExtended Cap = "LIST-EXTENDED" // RFC 5258
|
|
||||||
CapListStatus Cap = "LIST-STATUS" // RFC 5819
|
|
||||||
CapMove Cap = "MOVE" // RFC 6851
|
|
||||||
CapLiteralMinus Cap = "LITERAL-" // RFC 7888
|
|
||||||
CapStatusSize Cap = "STATUS=SIZE" // RFC 8438
|
|
||||||
CapChildren Cap = "CHILDREN" // RFC 3348
|
|
||||||
|
|
||||||
CapACL Cap = "ACL" // RFC 4314
|
|
||||||
CapAppendLimit Cap = "APPENDLIMIT" // RFC 7889
|
|
||||||
CapBinary Cap = "BINARY" // RFC 3516
|
|
||||||
CapCatenate Cap = "CATENATE" // RFC 4469
|
|
||||||
CapCondStore Cap = "CONDSTORE" // RFC 7162
|
|
||||||
CapConvert Cap = "CONVERT" // RFC 5259
|
|
||||||
CapCreateSpecialUse Cap = "CREATE-SPECIAL-USE" // RFC 6154
|
|
||||||
CapESort Cap = "ESORT" // RFC 5267
|
|
||||||
CapFilters Cap = "FILTERS" // RFC 5466
|
|
||||||
CapID Cap = "ID" // RFC 2971
|
|
||||||
CapLanguage Cap = "LANGUAGE" // RFC 5255
|
|
||||||
CapListMyRights Cap = "LIST-MYRIGHTS" // RFC 8440
|
|
||||||
CapLiteralPlus Cap = "LITERAL+" // RFC 7888
|
|
||||||
CapLoginReferrals Cap = "LOGIN-REFERRALS" // RFC 2221
|
|
||||||
CapMailboxReferrals Cap = "MAILBOX-REFERRALS" // RFC 2193
|
|
||||||
CapMetadata Cap = "METADATA" // RFC 5464
|
|
||||||
CapMetadataServer Cap = "METADATA-SERVER" // RFC 5464
|
|
||||||
CapMultiAppend Cap = "MULTIAPPEND" // RFC 3502
|
|
||||||
CapMultiSearch Cap = "MULTISEARCH" // RFC 7377
|
|
||||||
CapNotify Cap = "NOTIFY" // RFC 5465
|
|
||||||
CapObjectID Cap = "OBJECTID" // RFC 8474
|
|
||||||
CapPreview Cap = "PREVIEW" // RFC 8970
|
|
||||||
CapQResync Cap = "QRESYNC" // RFC 7162
|
|
||||||
CapQuota Cap = "QUOTA" // RFC 9208
|
|
||||||
CapQuotaSet Cap = "QUOTASET" // RFC 9208
|
|
||||||
CapReplace Cap = "REPLACE" // RFC 8508
|
|
||||||
CapSaveDate Cap = "SAVEDATE" // RFC 8514
|
|
||||||
CapSearchFuzzy Cap = "SEARCH=FUZZY" // RFC 6203
|
|
||||||
CapSort Cap = "SORT" // RFC 5256
|
|
||||||
CapSortDisplay Cap = "SORT=DISPLAY" // RFC 5957
|
|
||||||
CapSpecialUse Cap = "SPECIAL-USE" // RFC 6154
|
|
||||||
CapUnauthenticate Cap = "UNAUTHENTICATE" // RFC 8437
|
|
||||||
CapURLPartial Cap = "URL-PARTIAL" // RFC 5550
|
|
||||||
CapURLAuth Cap = "URLAUTH" // RFC 4467
|
|
||||||
CapUTF8Accept Cap = "UTF8=ACCEPT" // RFC 6855
|
|
||||||
CapUTF8Only Cap = "UTF8=ONLY" // RFC 6855
|
|
||||||
CapWithin Cap = "WITHIN" // RFC 5032
|
|
||||||
CapUIDOnly Cap = "UIDONLY" // RFC 9586
|
|
||||||
CapListMetadata Cap = "LIST-METADATA" // RFC 9590
|
|
||||||
CapInProgress Cap = "INPROGRESS" // RFC 9585
|
|
||||||
)
|
|
||||||
|
|
||||||
var imap4rev2Caps = CapSet{
|
|
||||||
CapNamespace: {},
|
|
||||||
CapUnselect: {},
|
|
||||||
CapUIDPlus: {},
|
|
||||||
CapESearch: {},
|
|
||||||
CapSearchRes: {},
|
|
||||||
CapEnable: {},
|
|
||||||
CapIdle: {},
|
|
||||||
CapSASLIR: {},
|
|
||||||
CapListExtended: {},
|
|
||||||
CapListStatus: {},
|
|
||||||
CapMove: {},
|
|
||||||
CapLiteralMinus: {},
|
|
||||||
CapStatusSize: {},
|
|
||||||
CapChildren: {},
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthCap returns the capability name for an SASL authentication mechanism.
|
|
||||||
func AuthCap(mechanism string) Cap {
|
|
||||||
return Cap("AUTH=" + mechanism)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CapSet is a set of capabilities.
|
|
||||||
type CapSet map[Cap]struct{}
|
|
||||||
|
|
||||||
func (set CapSet) has(c Cap) bool {
|
|
||||||
_, ok := set[c]
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (set CapSet) Copy() CapSet {
|
|
||||||
newSet := make(CapSet, len(set))
|
|
||||||
for c := range set {
|
|
||||||
newSet[c] = struct{}{}
|
|
||||||
}
|
|
||||||
return newSet
|
|
||||||
}
|
|
||||||
|
|
||||||
// Has checks whether a capability is supported.
|
|
||||||
//
|
|
||||||
// Some capabilities are implied by others, as such Has may return true even if
|
|
||||||
// the capability is not in the map.
|
|
||||||
func (set CapSet) Has(c Cap) bool {
|
|
||||||
if set.has(c) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if set.has(CapIMAP4rev2) && imap4rev2Caps.has(c) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if c == CapLiteralMinus && set.has(CapLiteralPlus) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if c == CapCondStore && set.has(CapQResync) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if c == CapUTF8Accept && set.has(CapUTF8Only) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if c == CapAppendLimit {
|
|
||||||
_, ok := set.AppendLimit()
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthMechanisms returns the list of supported SASL mechanisms for
|
|
||||||
// authentication.
|
|
||||||
func (set CapSet) AuthMechanisms() []string {
|
|
||||||
var l []string
|
|
||||||
for c := range set {
|
|
||||||
if !strings.HasPrefix(string(c), "AUTH=") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
mech := strings.TrimPrefix(string(c), "AUTH=")
|
|
||||||
l = append(l, mech)
|
|
||||||
}
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
// AppendLimit checks the APPENDLIMIT capability.
|
|
||||||
//
|
|
||||||
// If the server supports APPENDLIMIT, ok is true. If the server doesn't have
|
|
||||||
// the same upload limit for all mailboxes, limit is nil and per-mailbox
|
|
||||||
// limits must be queried via STATUS.
|
|
||||||
func (set CapSet) AppendLimit() (limit *uint32, ok bool) {
|
|
||||||
if set.has(CapAppendLimit) {
|
|
||||||
return nil, true
|
|
||||||
}
|
|
||||||
|
|
||||||
for c := range set {
|
|
||||||
if !strings.HasPrefix(string(c), "APPENDLIMIT=") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
limitStr := strings.TrimPrefix(string(c), "APPENDLIMIT=")
|
|
||||||
limit64, err := strconv.ParseUint(limitStr, 10, 32)
|
|
||||||
if err == nil && limit64 > 0 {
|
|
||||||
limit32 := uint32(limit64)
|
|
||||||
return &limit32, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
limit32 := ^uint32(0)
|
|
||||||
return &limit32, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// QuotaResourceTypes returns the list of supported QUOTA resource types.
|
|
||||||
func (set CapSet) QuotaResourceTypes() []QuotaResourceType {
|
|
||||||
var l []QuotaResourceType
|
|
||||||
for c := range set {
|
|
||||||
if !strings.HasPrefix(string(c), "QUOTA=RES-") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
t := strings.TrimPrefix(string(c), "QUOTA=RES-")
|
|
||||||
l = append(l, QuotaResourceType(t))
|
|
||||||
}
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
// ThreadAlgorithms returns the list of supported threading algorithms.
|
|
||||||
func (set CapSet) ThreadAlgorithms() []ThreadAlgorithm {
|
|
||||||
var l []ThreadAlgorithm
|
|
||||||
for c := range set {
|
|
||||||
if !strings.HasPrefix(string(c), "THREAD=") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
alg := strings.TrimPrefix(string(c), "THREAD=")
|
|
||||||
l = append(l, ThreadAlgorithm(alg))
|
|
||||||
}
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
591
client/client.go
Normal file
591
client/client.go
Normal file
@@ -0,0 +1,591 @@
|
|||||||
|
// Package client provides an IMAP client.
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
"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
|
||||||
|
|
||||||
|
greeted chan struct{}
|
||||||
|
loggedOut chan struct{}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
// 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) read(greeted <-chan struct{}) error {
|
||||||
|
greetedClosed := false
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
// Ensure we close the greeted channel. New may be waiting on an indication
|
||||||
|
// that we've seen the greeting.
|
||||||
|
if !greetedClosed {
|
||||||
|
close(c.greeted)
|
||||||
|
greetedClosed = true
|
||||||
|
}
|
||||||
|
close(c.loggedOut)
|
||||||
|
}()
|
||||||
|
|
||||||
|
first := true
|
||||||
|
for {
|
||||||
|
if c.State() == imap.LogoutState {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.conn.Wait()
|
||||||
|
|
||||||
|
if first {
|
||||||
|
first = false
|
||||||
|
} else {
|
||||||
|
<-greeted
|
||||||
|
if !greetedClosed {
|
||||||
|
close(c.greeted)
|
||||||
|
greetedClosed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := imap.ReadResp(c.conn.Reader)
|
||||||
|
if err == io.EOF || c.State() == imap.LogoutState {
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
c.ErrorLog.Println("error reading response:", err)
|
||||||
|
if imap.IsParseError(err) {
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.handle(resp); err != nil && err != responses.ErrUnhandled {
|
||||||
|
c.ErrorLog.Println("cannot handle response ", resp, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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}
|
||||||
|
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
|
||||||
|
doneWrite := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
doneWrite <- cmd.WriteTo(c.conn.Writer)
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
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 err := <-doneWrite:
|
||||||
|
if err != nil {
|
||||||
|
// Error while sending the command
|
||||||
|
close(unregister)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
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(continues chan<- bool) {
|
||||||
|
c.registerHandler(responses.HandlerFunc(func(resp imap.Resp) error {
|
||||||
|
if _, ok := resp.(*imap.ContinuationReq); ok {
|
||||||
|
go func() {
|
||||||
|
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 {
|
||||||
|
done := make(chan error, 2)
|
||||||
|
greeted := make(chan struct{})
|
||||||
|
|
||||||
|
c.registerHandler(responses.HandlerFunc(func(resp imap.Resp) error {
|
||||||
|
status, ok := resp.(*imap.StatusResp)
|
||||||
|
if !ok {
|
||||||
|
done <- 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()
|
||||||
|
done <- fmt.Errorf("invalid greeting received from server: %v", status.Type)
|
||||||
|
return errUnregisterHandler
|
||||||
|
}
|
||||||
|
c.locker.Unlock()
|
||||||
|
|
||||||
|
if status.Code == imap.CodeCapability {
|
||||||
|
c.gotStatusCaps(status.Arguments)
|
||||||
|
}
|
||||||
|
|
||||||
|
close(greeted)
|
||||||
|
done <- nil
|
||||||
|
return errUnregisterHandler
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Make sure to start reading after we have set up this handler, otherwise
|
||||||
|
// some messages will be lost.
|
||||||
|
go func() {
|
||||||
|
done <- c.read(greeted)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return <-done
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
c.conn.SetDebug(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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),
|
||||||
|
greeted: make(chan struct{}),
|
||||||
|
loggedOut: make(chan struct{}),
|
||||||
|
state: imap.ConnectingState,
|
||||||
|
ErrorLog: log.New(os.Stderr, "imap/client: ", log.LstdFlags),
|
||||||
|
}
|
||||||
|
|
||||||
|
c.handleContinuationReqs(continues)
|
||||||
|
c.handleUnilateral()
|
||||||
|
err := c.handleGreetAndStartReading()
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dial connects to an IMAP server using an unencrypted connection.
|
||||||
|
func Dial(addr string) (c *Client, err error) {
|
||||||
|
conn, err := net.Dial("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err = New(conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 *net.Dialer, address string) (c *Client, err error) {
|
||||||
|
conn, err := dialer.Dial("tcp", address)
|
||||||
|
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 dialer.Timeout > 0 {
|
||||||
|
err = conn.SetDeadline(time.Now().Add(dialer.Timeout))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err = New(conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialTLS connects to an IMAP server using an encrypted connection.
|
||||||
|
func DialTLS(addr string, tlsConfig *tls.Config) (c *Client, err error) {
|
||||||
|
conn, err := tls.Dial("tcp", addr, tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err = New(conn)
|
||||||
|
c.isTLS = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 *net.Dialer, addr string,
|
||||||
|
tlsConfig *tls.Config) (c *Client, err error) {
|
||||||
|
conn, err := tls.DialWithDialer(dialer, "tcp", addr, tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 dialer.Timeout > 0 {
|
||||||
|
err = conn.SetDeadline(time.Now().Add(dialer.Timeout))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err = New(conn)
|
||||||
|
c.isTLS = true
|
||||||
|
return
|
||||||
|
}
|
||||||
173
client/client_test.go
Normal file
173
client/client_test.go
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
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) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
greeting := "* OK [CAPABILITY IMAP4rev1 STARTTLS AUTH=PLAIN] Server ready.\r\n"
|
||||||
|
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
|
||||||
|
c.SetDebug(&b)
|
||||||
|
|
||||||
|
done := make(chan error)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
87
client/cmd_any.go
Normal file
87
client/cmd_any.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
254
client/cmd_auth.go
Normal file
254
client/cmd_auth.go
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
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 {
|
||||||
|
if err := c.ensureAuthenticated(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer close(ch)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if err := c.ensureAuthenticated(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer close(ch)
|
||||||
|
|
||||||
|
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.
|
||||||
|
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()
|
||||||
|
}
|
||||||
359
client/cmd_auth_test.go
Normal file
359
client/cmd_auth_test.go
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
151
client/cmd_noauth.go
Normal file
151
client/cmd_noauth.go
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := new(commands.StartTLS)
|
||||||
|
|
||||||
|
err := c.Upgrade(func(conn net.Conn) (net.Conn, error) {
|
||||||
|
if status, err := c.execute(cmd, nil); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if err := status.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
res := &responses.Authenticate{
|
||||||
|
Mechanism: auth,
|
||||||
|
InitialResponse: ir,
|
||||||
|
Writer: c.Writer(),
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
207
client/cmd_noauth_test.go
Normal file
207
client/cmd_noauth_test.go
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
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_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_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)
|
||||||
|
}
|
||||||
|
}
|
||||||
263
client/cmd_selected.go
Normal file
263
client/cmd_selected.go
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
"github.com/emersion/go-imap/commands"
|
||||||
|
"github.com/emersion/go-imap/responses"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrNoMailboxSelected is returned if a command that requires a mailbox to be
|
||||||
|
// selected is called when there isn't.
|
||||||
|
var ErrNoMailboxSelected = errors.New("No mailbox selected")
|
||||||
|
|
||||||
|
// 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 c.State() != imap.SelectedState {
|
||||||
|
return ErrNoMailboxSelected
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := new(commands.Expunge)
|
||||||
|
|
||||||
|
var h responses.Handler
|
||||||
|
if ch != nil {
|
||||||
|
h = &responses.Expunge{SeqNums: ch}
|
||||||
|
defer close(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
|
||||||
|
cmd = &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) {
|
||||||
|
ids, status, err := c.executeSearch(uid, criteria, "UTF-8")
|
||||||
|
if status != nil && status.Code == imap.CodeBadCharset {
|
||||||
|
// Some servers don't support UTF-8
|
||||||
|
ids, _, err = c.executeSearch(uid, criteria, "US-ASCII")
|
||||||
|
}
|
||||||
|
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.
|
||||||
|
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 {
|
||||||
|
if c.State() != imap.SelectedState {
|
||||||
|
return ErrNoMailboxSelected
|
||||||
|
}
|
||||||
|
|
||||||
|
defer close(ch)
|
||||||
|
|
||||||
|
var cmd imap.Commander
|
||||||
|
cmd = &commands.Fetch{
|
||||||
|
SeqSet: seqset,
|
||||||
|
Items: items,
|
||||||
|
}
|
||||||
|
if uid {
|
||||||
|
cmd = &commands.Uid{Cmd: cmd}
|
||||||
|
}
|
||||||
|
|
||||||
|
res := &responses.Fetch{Messages: ch}
|
||||||
|
|
||||||
|
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 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.Atom(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
|
||||||
|
cmd = &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}
|
||||||
|
defer close(ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
450
client/cmd_selected_test.go
Normal file
450
client/cmd_selected_test.go
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
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_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_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_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_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_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}, updates)
|
||||||
|
}()
|
||||||
|
|
||||||
|
tag, cmd := s.ScanCmd()
|
||||||
|
if cmd != "STORE 2 +FLAGS (\\Seen)" {
|
||||||
|
t.Fatalf("client sent command %v, want %v", cmd, "STORE 2 +FLAGS (\\Seen)")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.WriteString("* 2 FETCH (FLAGS (\\Seen))\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) != 1 || msg.Flags[0] != "\\Seen" {
|
||||||
|
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}, nil)
|
||||||
|
}()
|
||||||
|
|
||||||
|
tag, cmd := s.ScanCmd()
|
||||||
|
if cmd != "STORE 2:3 +FLAGS.SILENT (\\Seen)" {
|
||||||
|
t.Fatalf("client sent command %v, want %v", cmd, "STORE 2:3 +FLAGS.SILENT (\\Seen)")
|
||||||
|
}
|
||||||
|
|
||||||
|
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}, nil)
|
||||||
|
}()
|
||||||
|
|
||||||
|
tag, cmd := s.ScanCmd()
|
||||||
|
if cmd != "UID STORE 27:901 +FLAGS.SILENT (\\Deleted)" {
|
||||||
|
t.Fatalf("client sent command %v, want %v", cmd, "UID STORE 27:901 +FLAGS.SILENT (\\Deleted)")
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
262
client/example_test.go
Normal file
262
client/example_test.go
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
package client_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/mail"
|
||||||
|
|
||||||
|
"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_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!")
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"flag"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/emersion/go-imap/v2"
|
|
||||||
"github.com/emersion/go-imap/v2/imapserver"
|
|
||||||
"github.com/emersion/go-imap/v2/imapserver/imapmemserver"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
listen string
|
|
||||||
tlsCert string
|
|
||||||
tlsKey string
|
|
||||||
username string
|
|
||||||
password string
|
|
||||||
debug bool
|
|
||||||
insecureAuth bool
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.StringVar(&listen, "listen", "localhost:143", "listening address")
|
|
||||||
flag.StringVar(&tlsCert, "tls-cert", "", "TLS certificate")
|
|
||||||
flag.StringVar(&tlsKey, "tls-key", "", "TLS key")
|
|
||||||
flag.StringVar(&username, "username", "user", "Username")
|
|
||||||
flag.StringVar(&password, "password", "user", "Password")
|
|
||||||
flag.BoolVar(&debug, "debug", false, "Print all commands and responses")
|
|
||||||
flag.BoolVar(&insecureAuth, "insecure-auth", false, "Allow authentication without TLS")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
var tlsConfig *tls.Config
|
|
||||||
if tlsCert != "" || tlsKey != "" {
|
|
||||||
cert, err := tls.LoadX509KeyPair(tlsCert, tlsKey)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to load TLS key pair: %v", err)
|
|
||||||
}
|
|
||||||
tlsConfig = &tls.Config{
|
|
||||||
Certificates: []tls.Certificate{cert},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ln, err := net.Listen("tcp", listen)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to listen: %v", err)
|
|
||||||
}
|
|
||||||
log.Printf("IMAP server listening on %v", ln.Addr())
|
|
||||||
|
|
||||||
memServer := imapmemserver.New()
|
|
||||||
|
|
||||||
if username != "" || password != "" {
|
|
||||||
user := imapmemserver.NewUser(username, password)
|
|
||||||
|
|
||||||
// Create standard mailboxes with special-use attributes as per RFC 6154
|
|
||||||
if err := user.Create("INBOX", nil); err != nil {
|
|
||||||
log.Printf("Failed to create INBOX: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := user.Create("Drafts", &imap.CreateOptions{
|
|
||||||
SpecialUse: []imap.MailboxAttr{imap.MailboxAttrDrafts},
|
|
||||||
}); err != nil {
|
|
||||||
log.Printf("Failed to create Drafts mailbox: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := user.Create("Sent", &imap.CreateOptions{
|
|
||||||
SpecialUse: []imap.MailboxAttr{imap.MailboxAttrSent},
|
|
||||||
}); err != nil {
|
|
||||||
log.Printf("Failed to create Sent mailbox: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := user.Create("Archive", &imap.CreateOptions{
|
|
||||||
SpecialUse: []imap.MailboxAttr{imap.MailboxAttrArchive},
|
|
||||||
}); err != nil {
|
|
||||||
log.Printf("Failed to create Archive mailbox: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := user.Create("Junk", &imap.CreateOptions{
|
|
||||||
SpecialUse: []imap.MailboxAttr{imap.MailboxAttrJunk},
|
|
||||||
}); err != nil {
|
|
||||||
log.Printf("Failed to create Junk mailbox: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := user.Create("Trash", &imap.CreateOptions{
|
|
||||||
SpecialUse: []imap.MailboxAttr{imap.MailboxAttrTrash},
|
|
||||||
}); err != nil {
|
|
||||||
log.Printf("Failed to create Trash mailbox: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := user.Create("Flagged", &imap.CreateOptions{
|
|
||||||
SpecialUse: []imap.MailboxAttr{imap.MailboxAttrFlagged},
|
|
||||||
}); err != nil {
|
|
||||||
log.Printf("Failed to create Flagged mailbox: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscribe to the most commonly used mailboxes
|
|
||||||
_ = user.Subscribe("INBOX")
|
|
||||||
_ = user.Subscribe("Drafts")
|
|
||||||
_ = user.Subscribe("Sent")
|
|
||||||
_ = user.Subscribe("Trash")
|
|
||||||
|
|
||||||
memServer.AddUser(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
var debugWriter io.Writer
|
|
||||||
if debug {
|
|
||||||
debugWriter = os.Stdout
|
|
||||||
}
|
|
||||||
|
|
||||||
server := imapserver.New(&imapserver.Options{
|
|
||||||
NewSession: func(conn *imapserver.Conn) (imapserver.Session, *imapserver.GreetingData, error) {
|
|
||||||
return memServer.NewSession(), nil, nil
|
|
||||||
},
|
|
||||||
Caps: imap.CapSet{
|
|
||||||
imap.CapIMAP4rev1: {},
|
|
||||||
imap.CapIMAP4rev2: {},
|
|
||||||
},
|
|
||||||
TLSConfig: tlsConfig,
|
|
||||||
InsecureAuth: insecureAuth,
|
|
||||||
DebugWriter: debugWriter,
|
|
||||||
})
|
|
||||||
if err := server.Serve(ln); err != nil {
|
|
||||||
log.Fatalf("Serve() = %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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{}{tag, 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, mailbox)
|
||||||
|
|
||||||
|
if cmd.Flags != nil {
|
||||||
|
flags := make([]interface{}, len(cmd.Flags))
|
||||||
|
for i, flag := range cmd.Flags {
|
||||||
|
flags[i] = imap.Atom(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
|
||||||
|
}
|
||||||
83
commands/authenticate.go
Normal file
83
commands/authenticate.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Authenticate) Command() *imap.Command {
|
||||||
|
return &imap.Command{
|
||||||
|
Name: "AUTHENTICATE",
|
||||||
|
Arguments: []interface{}{cmd.Mechanism},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
|
||||||
|
var response []byte
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner.Scan()
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded = scanner.Text()
|
||||||
|
if encoded != "" {
|
||||||
|
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, 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{}{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
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
59
commands/fetch.go
Normal file
59
commands/fetch.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
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 {
|
||||||
|
items := make([]interface{}, len(cmd.Items))
|
||||||
|
for i, item := range cmd.Items {
|
||||||
|
if section, err := imap.ParseBodySectionName(item); err == nil {
|
||||||
|
items[i] = section
|
||||||
|
} else {
|
||||||
|
items[i] = string(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
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
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{}{existingName, 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, "CHARSET", 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{}{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] = string(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &imap.Command{
|
||||||
|
Name: "STATUS",
|
||||||
|
Arguments: []interface{}{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
|
||||||
|
}
|
||||||
47
commands/store.go
Normal file
47
commands/store.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
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, string(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))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: could be fields[2:] according to RFC 3501 page 91 "store-att-flags"
|
||||||
|
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{}{mailbox},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Subscribe) Parse(fields []interface{}) error {
|
||||||
|
if len(fields) < 0 {
|
||||||
|
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{}{mailbox},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Unsubscribe) Parse(fields []interface{}) error {
|
||||||
|
if len(fields) < 0 {
|
||||||
|
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{}{inner.Name}
|
||||||
|
args = append(args, inner.Arguments...)
|
||||||
|
|
||||||
|
return &imap.Command{
|
||||||
|
Name: "UID",
|
||||||
|
Arguments: args,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Uid) Parse(fields []interface{}) error {
|
||||||
|
if len(fields) < 0 {
|
||||||
|
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
|
||||||
|
}
|
||||||
194
conn.go
Normal file
194
conn.go
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
package imap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 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}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An IMAP connection.
|
||||||
|
type Conn struct {
|
||||||
|
net.Conn
|
||||||
|
*Reader
|
||||||
|
*Writer
|
||||||
|
|
||||||
|
br *bufio.Reader
|
||||||
|
bw *bufio.Writer
|
||||||
|
|
||||||
|
waits chan struct{}
|
||||||
|
|
||||||
|
// 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) 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 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
// Flush all buffered data
|
||||||
|
if err := c.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block reads and writes during the upgrading process
|
||||||
|
c.waits = make(chan struct{})
|
||||||
|
defer close(c.waits)
|
||||||
|
|
||||||
|
upgraded, err := upgrader(c.Conn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Conn = upgraded
|
||||||
|
c.init()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait waits for the connection to be ready for reads and writes.
|
||||||
|
func (c *Conn) Wait() {
|
||||||
|
if c.waits != nil {
|
||||||
|
<-c.waits
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
106
conn_test.go
Normal file
106
conn_test.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
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{}{}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
9
copy.go
9
copy.go
@@ -1,9 +0,0 @@
|
|||||||
package imap
|
|
||||||
|
|
||||||
// CopyData is the data returned by a COPY command.
|
|
||||||
type CopyData struct {
|
|
||||||
// requires UIDPLUS or IMAP4rev2
|
|
||||||
UIDValidity uint32
|
|
||||||
SourceUIDs UIDSet
|
|
||||||
DestUIDs UIDSet
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package imap
|
|
||||||
|
|
||||||
// CreateOptions contains options for the CREATE command.
|
|
||||||
type CreateOptions struct {
|
|
||||||
SpecialUse []MailboxAttr // requires CREATE-SPECIAL-USE
|
|
||||||
}
|
|
||||||
72
date.go
Normal file
72
date.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package imap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"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:05 -0700 (MST)",
|
||||||
|
"_2 Jan 2006 15:04 -0700",
|
||||||
|
"_2 Jan 2006 15:04 MST",
|
||||||
|
"_2 Jan 2006 15:04 -0700 (MST)",
|
||||||
|
"_2 Jan 06 15:04:05 -0700",
|
||||||
|
"_2 Jan 06 15:04:05 MST",
|
||||||
|
"_2 Jan 06 15:04:05 -0700 (MST)",
|
||||||
|
"_2 Jan 06 15:04 -0700",
|
||||||
|
"_2 Jan 06 15:04 MST",
|
||||||
|
"_2 Jan 06 15:04 -0700 (MST)",
|
||||||
|
"Mon, _2 Jan 2006 15:04:05 -0700",
|
||||||
|
"Mon, _2 Jan 2006 15:04:05 MST",
|
||||||
|
"Mon, _2 Jan 2006 15:04:05 -0700 (MST)",
|
||||||
|
"Mon, _2 Jan 2006 15:04 -0700",
|
||||||
|
"Mon, _2 Jan 2006 15:04 MST",
|
||||||
|
"Mon, _2 Jan 2006 15:04 -0700 (MST)",
|
||||||
|
"Mon, _2 Jan 06 15:04:05 -0700",
|
||||||
|
"Mon, _2 Jan 06 15:04:05 MST",
|
||||||
|
"Mon, _2 Jan 06 15:04:05 -0700 (MST)",
|
||||||
|
"Mon, _2 Jan 06 15:04 -0700",
|
||||||
|
"Mon, _2 Jan 06 15:04 MST",
|
||||||
|
"Mon, _2 Jan 06 15:04 -0700 (MST)",
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
284
fetch.go
284
fetch.go
@@ -1,284 +0,0 @@
|
|||||||
package imap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FetchOptions contains options for the FETCH command.
|
|
||||||
type FetchOptions struct {
|
|
||||||
// Fields to fetch
|
|
||||||
BodyStructure *FetchItemBodyStructure
|
|
||||||
Envelope bool
|
|
||||||
Flags bool
|
|
||||||
InternalDate bool
|
|
||||||
RFC822Size bool
|
|
||||||
UID bool
|
|
||||||
BodySection []*FetchItemBodySection
|
|
||||||
BinarySection []*FetchItemBinarySection // requires IMAP4rev2 or BINARY
|
|
||||||
BinarySectionSize []*FetchItemBinarySectionSize // requires IMAP4rev2 or BINARY
|
|
||||||
ModSeq bool // requires CONDSTORE
|
|
||||||
|
|
||||||
ChangedSince uint64 // requires CONDSTORE
|
|
||||||
}
|
|
||||||
|
|
||||||
// FetchItemBodyStructure contains FETCH options for the body structure.
|
|
||||||
type FetchItemBodyStructure struct {
|
|
||||||
Extended bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// PartSpecifier describes whether to fetch a part's header, body, or both.
|
|
||||||
type PartSpecifier string
|
|
||||||
|
|
||||||
const (
|
|
||||||
PartSpecifierNone PartSpecifier = ""
|
|
||||||
PartSpecifierHeader PartSpecifier = "HEADER"
|
|
||||||
PartSpecifierMIME PartSpecifier = "MIME"
|
|
||||||
PartSpecifierText PartSpecifier = "TEXT"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SectionPartial describes a byte range when fetching a message's payload.
|
|
||||||
type SectionPartial struct {
|
|
||||||
Offset, Size int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// FetchItemBodySection is a FETCH BODY[] data item.
|
|
||||||
//
|
|
||||||
// To fetch the whole body of a message, use the zero FetchItemBodySection:
|
|
||||||
//
|
|
||||||
// imap.FetchItemBodySection{}
|
|
||||||
//
|
|
||||||
// To fetch only a specific part, use the Part field:
|
|
||||||
//
|
|
||||||
// imap.FetchItemBodySection{Part: []int{1, 2, 3}}
|
|
||||||
//
|
|
||||||
// To fetch only the header of the message, use the Specifier field:
|
|
||||||
//
|
|
||||||
// imap.FetchItemBodySection{Specifier: imap.PartSpecifierHeader}
|
|
||||||
type FetchItemBodySection struct {
|
|
||||||
Specifier PartSpecifier
|
|
||||||
Part []int
|
|
||||||
HeaderFields []string
|
|
||||||
HeaderFieldsNot []string
|
|
||||||
Partial *SectionPartial
|
|
||||||
Peek bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// FetchItemBinarySection is a FETCH BINARY[] data item.
|
|
||||||
type FetchItemBinarySection struct {
|
|
||||||
Part []int
|
|
||||||
Partial *SectionPartial
|
|
||||||
Peek bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// FetchItemBinarySectionSize is a FETCH BINARY.SIZE[] data item.
|
|
||||||
type FetchItemBinarySectionSize struct {
|
|
||||||
Part []int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Envelope is the envelope structure of a message.
|
|
||||||
//
|
|
||||||
// The subject and addresses are UTF-8 (ie, not in their encoded form). The
|
|
||||||
// In-Reply-To and Message-ID values contain message identifiers without angle
|
|
||||||
// brackets.
|
|
||||||
type Envelope struct {
|
|
||||||
Date time.Time
|
|
||||||
Subject string
|
|
||||||
From []Address
|
|
||||||
Sender []Address
|
|
||||||
ReplyTo []Address
|
|
||||||
To []Address
|
|
||||||
Cc []Address
|
|
||||||
Bcc []Address
|
|
||||||
InReplyTo []string
|
|
||||||
MessageID string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Address represents a sender or recipient of a message.
|
|
||||||
type Address struct {
|
|
||||||
Name string
|
|
||||||
Mailbox string
|
|
||||||
Host string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Addr returns the e-mail address in the form "foo@example.org".
|
|
||||||
//
|
|
||||||
// If the address is a start or end of group, the empty string is returned.
|
|
||||||
func (addr *Address) Addr() string {
|
|
||||||
if addr.Mailbox == "" || addr.Host == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return addr.Mailbox + "@" + addr.Host
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsGroupStart returns true if this address is a start of group marker.
|
|
||||||
//
|
|
||||||
// In that case, Mailbox contains the group name phrase.
|
|
||||||
func (addr *Address) IsGroupStart() bool {
|
|
||||||
return addr.Host == "" && addr.Mailbox != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsGroupEnd returns true if this address is a end of group marker.
|
|
||||||
func (addr *Address) IsGroupEnd() bool {
|
|
||||||
return addr.Host == "" && addr.Mailbox == ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// BodyStructure describes the body structure of a message.
|
|
||||||
//
|
|
||||||
// A BodyStructure value is either a *BodyStructureSinglePart or a
|
|
||||||
// *BodyStructureMultiPart.
|
|
||||||
type BodyStructure interface {
|
|
||||||
// MediaType returns the MIME type of this body structure, e.g. "text/plain".
|
|
||||||
MediaType() string
|
|
||||||
// Walk walks the body structure tree, calling f for each part in the tree,
|
|
||||||
// including bs itself. The parts are visited in DFS pre-order.
|
|
||||||
Walk(f BodyStructureWalkFunc)
|
|
||||||
// Disposition returns the body structure disposition, if available.
|
|
||||||
Disposition() *BodyStructureDisposition
|
|
||||||
|
|
||||||
bodyStructure()
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ BodyStructure = (*BodyStructureSinglePart)(nil)
|
|
||||||
_ BodyStructure = (*BodyStructureMultiPart)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
// BodyStructureSinglePart is a body structure with a single part.
|
|
||||||
type BodyStructureSinglePart struct {
|
|
||||||
Type, Subtype string
|
|
||||||
Params map[string]string
|
|
||||||
ID string
|
|
||||||
Description string
|
|
||||||
Encoding string
|
|
||||||
Size uint32
|
|
||||||
|
|
||||||
MessageRFC822 *BodyStructureMessageRFC822 // only for "message/rfc822"
|
|
||||||
Text *BodyStructureText // only for "text/*"
|
|
||||||
Extended *BodyStructureSinglePartExt
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bs *BodyStructureSinglePart) MediaType() string {
|
|
||||||
return strings.ToLower(bs.Type) + "/" + strings.ToLower(bs.Subtype)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bs *BodyStructureSinglePart) Walk(f BodyStructureWalkFunc) {
|
|
||||||
f([]int{1}, bs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bs *BodyStructureSinglePart) Disposition() *BodyStructureDisposition {
|
|
||||||
if bs.Extended == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return bs.Extended.Disposition
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filename decodes the body structure's filename, if any.
|
|
||||||
func (bs *BodyStructureSinglePart) Filename() string {
|
|
||||||
var filename string
|
|
||||||
if bs.Extended != nil && bs.Extended.Disposition != nil {
|
|
||||||
filename = bs.Extended.Disposition.Params["filename"]
|
|
||||||
}
|
|
||||||
if filename == "" {
|
|
||||||
// Note: using "name" in Content-Type is discouraged
|
|
||||||
filename = bs.Params["name"]
|
|
||||||
}
|
|
||||||
return filename
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*BodyStructureSinglePart) bodyStructure() {}
|
|
||||||
|
|
||||||
// BodyStructureMessageRFC822 contains metadata specific to RFC 822 parts for
|
|
||||||
// BodyStructureSinglePart.
|
|
||||||
type BodyStructureMessageRFC822 struct {
|
|
||||||
Envelope *Envelope
|
|
||||||
BodyStructure BodyStructure
|
|
||||||
NumLines int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// BodyStructureText contains metadata specific to text parts for
|
|
||||||
// BodyStructureSinglePart.
|
|
||||||
type BodyStructureText struct {
|
|
||||||
NumLines int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// BodyStructureSinglePartExt contains extended body structure data for
|
|
||||||
// BodyStructureSinglePart.
|
|
||||||
type BodyStructureSinglePartExt struct {
|
|
||||||
Disposition *BodyStructureDisposition
|
|
||||||
Language []string
|
|
||||||
Location string
|
|
||||||
}
|
|
||||||
|
|
||||||
// BodyStructureMultiPart is a body structure with multiple parts.
|
|
||||||
type BodyStructureMultiPart struct {
|
|
||||||
Children []BodyStructure
|
|
||||||
Subtype string
|
|
||||||
|
|
||||||
Extended *BodyStructureMultiPartExt
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bs *BodyStructureMultiPart) MediaType() string {
|
|
||||||
return "multipart/" + strings.ToLower(bs.Subtype)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bs *BodyStructureMultiPart) Walk(f BodyStructureWalkFunc) {
|
|
||||||
bs.walk(f, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bs *BodyStructureMultiPart) walk(f BodyStructureWalkFunc, path []int) {
|
|
||||||
if !f(path, bs) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pathBuf := make([]int, len(path))
|
|
||||||
copy(pathBuf, path)
|
|
||||||
for i, part := range bs.Children {
|
|
||||||
num := i + 1
|
|
||||||
partPath := append(pathBuf, num)
|
|
||||||
|
|
||||||
switch part := part.(type) {
|
|
||||||
case *BodyStructureSinglePart:
|
|
||||||
f(partPath, part)
|
|
||||||
case *BodyStructureMultiPart:
|
|
||||||
part.walk(f, partPath)
|
|
||||||
default:
|
|
||||||
panic(fmt.Errorf("unsupported body structure type %T", part))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bs *BodyStructureMultiPart) Disposition() *BodyStructureDisposition {
|
|
||||||
if bs.Extended == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return bs.Extended.Disposition
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*BodyStructureMultiPart) bodyStructure() {}
|
|
||||||
|
|
||||||
// BodyStructureMultiPartExt contains extended body structure data for
|
|
||||||
// BodyStructureMultiPart.
|
|
||||||
type BodyStructureMultiPartExt struct {
|
|
||||||
Params map[string]string
|
|
||||||
Disposition *BodyStructureDisposition
|
|
||||||
Language []string
|
|
||||||
Location string
|
|
||||||
}
|
|
||||||
|
|
||||||
// BodyStructureDisposition describes the content disposition of a part
|
|
||||||
// (specified in the Content-Disposition header field).
|
|
||||||
type BodyStructureDisposition struct {
|
|
||||||
Value string
|
|
||||||
Params map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
// BodyStructureWalkFunc is a function called for each body structure visited
|
|
||||||
// by BodyStructure.Walk.
|
|
||||||
//
|
|
||||||
// The path argument contains the IMAP part path.
|
|
||||||
//
|
|
||||||
// The function should return true to visit all of the part's children or false
|
|
||||||
// to skip them.
|
|
||||||
type BodyStructureWalkFunc func(path []int, part BodyStructure) (walkChildren bool)
|
|
||||||
10
go.mod
10
go.mod
@@ -1,8 +1,8 @@
|
|||||||
module github.com/emersion/go-imap/v2
|
module github.com/emersion/go-imap
|
||||||
|
|
||||||
go 1.18
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/emersion/go-message v0.18.2
|
github.com/emersion/go-message v0.9.1
|
||||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6
|
github.com/emersion/go-sasl v0.0.0-20161116183048-7e096a0a6197
|
||||||
|
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe // indirect
|
||||||
|
golang.org/x/text v0.3.0
|
||||||
)
|
)
|
||||||
|
|||||||
35
go.sum
35
go.sum
@@ -1,35 +0,0 @@
|
|||||||
github.com/emersion/go-message v0.18.2 h1:rl55SQdjd9oJcIoQNhubD2Acs1E6IzlZISRTK7x/Lpg=
|
|
||||||
github.com/emersion/go-message v0.18.2/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA=
|
|
||||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
|
|
||||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/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=
|
|
||||||
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=
|
|
||||||
15
id.go
15
id.go
@@ -1,15 +0,0 @@
|
|||||||
package imap
|
|
||||||
|
|
||||||
type IDData struct {
|
|
||||||
Name string
|
|
||||||
Version string
|
|
||||||
OS string
|
|
||||||
OSVersion string
|
|
||||||
Vendor string
|
|
||||||
SupportURL string
|
|
||||||
Address string
|
|
||||||
Date string
|
|
||||||
Command string
|
|
||||||
Arguments string
|
|
||||||
Environment string
|
|
||||||
}
|
|
||||||
167
imap.go
167
imap.go
@@ -1,105 +1,106 @@
|
|||||||
// Package imap implements IMAP4rev2.
|
// Package imap implements IMAP4rev1 (RFC 3501).
|
||||||
//
|
|
||||||
// IMAP4rev2 is defined in RFC 9051.
|
|
||||||
//
|
|
||||||
// This package contains types and functions common to both the client and
|
|
||||||
// server. See the imapclient and imapserver sub-packages.
|
|
||||||
package imap
|
package imap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConnState describes the connection state.
|
// A StatusItem is a mailbox status data item that can be retrieved with a
|
||||||
//
|
// STATUS command. See RFC 3501 section 6.3.10.
|
||||||
// See RFC 9051 section 3.
|
type StatusItem string
|
||||||
type ConnState int
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ConnStateNone ConnState = iota
|
StatusMessages StatusItem = "MESSAGES"
|
||||||
ConnStateNotAuthenticated
|
StatusRecent = "RECENT"
|
||||||
ConnStateAuthenticated
|
StatusUidNext = "UIDNEXT"
|
||||||
ConnStateSelected
|
StatusUidValidity = "UIDVALIDITY"
|
||||||
ConnStateLogout
|
StatusUnseen = "UNSEEN"
|
||||||
)
|
)
|
||||||
|
|
||||||
// String implements fmt.Stringer.
|
// A FetchItem is a message data item that can be fetched.
|
||||||
func (state ConnState) String() string {
|
type FetchItem string
|
||||||
switch state {
|
|
||||||
case ConnStateNone:
|
// List of items that can be fetched.
|
||||||
return "none"
|
const (
|
||||||
case ConnStateNotAuthenticated:
|
// Macros
|
||||||
return "not authenticated"
|
FetchAll FetchItem = "ALL"
|
||||||
case ConnStateAuthenticated:
|
FetchFast = "FAST"
|
||||||
return "authenticated"
|
FetchFull = "FULL"
|
||||||
case ConnStateSelected:
|
|
||||||
return "selected"
|
// Items
|
||||||
case ConnStateLogout:
|
FetchBody = "BODY"
|
||||||
return "logout"
|
FetchBodyStructure = "BODYSTRUCTURE"
|
||||||
|
FetchEnvelope = "ENVELOPE"
|
||||||
|
FetchFlags = "FLAGS"
|
||||||
|
FetchInternalDate = "INTERNALDATE"
|
||||||
|
FetchRFC822 = "RFC822"
|
||||||
|
FetchRFC822Header = "RFC822.HEADER"
|
||||||
|
FetchRFC822Size = "RFC822.SIZE"
|
||||||
|
FetchRFC822Text = "RFC822.TEXT"
|
||||||
|
FetchUid = "UID"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Expand expands the item if it's a macro.
|
||||||
|
func (item FetchItem) Expand() []FetchItem {
|
||||||
|
switch item {
|
||||||
|
case FetchAll:
|
||||||
|
return []FetchItem{FetchFlags, FetchInternalDate, FetchRFC822Size, FetchEnvelope}
|
||||||
|
case FetchFast:
|
||||||
|
return []FetchItem{FetchFlags, FetchInternalDate, FetchRFC822Size}
|
||||||
|
case FetchFull:
|
||||||
|
return []FetchItem{FetchFlags, FetchInternalDate, FetchRFC822Size, FetchEnvelope, FetchBody}
|
||||||
default:
|
default:
|
||||||
panic(fmt.Errorf("imap: unknown connection state %v", int(state)))
|
return []FetchItem{item}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MailboxAttr is a mailbox attribute.
|
// FlagsOp is an operation that will be applied on message flags.
|
||||||
//
|
type FlagsOp string
|
||||||
// Mailbox attributes are defined in RFC 9051 section 7.3.1.
|
|
||||||
type MailboxAttr string
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Base attributes
|
// SetFlags replaces existing flags by new ones.
|
||||||
MailboxAttrNonExistent MailboxAttr = "\\NonExistent"
|
SetFlags FlagsOp = "FLAGS"
|
||||||
MailboxAttrNoInferiors MailboxAttr = "\\Noinferiors"
|
// AddFlags adds new flags.
|
||||||
MailboxAttrNoSelect MailboxAttr = "\\Noselect"
|
AddFlags = "+FLAGS"
|
||||||
MailboxAttrHasChildren MailboxAttr = "\\HasChildren"
|
// RemoveFlags removes existing flags.
|
||||||
MailboxAttrHasNoChildren MailboxAttr = "\\HasNoChildren"
|
RemoveFlags = "-FLAGS"
|
||||||
MailboxAttrMarked MailboxAttr = "\\Marked"
|
|
||||||
MailboxAttrUnmarked MailboxAttr = "\\Unmarked"
|
|
||||||
MailboxAttrSubscribed MailboxAttr = "\\Subscribed"
|
|
||||||
MailboxAttrRemote MailboxAttr = "\\Remote"
|
|
||||||
|
|
||||||
// Role (aka. "special-use") attributes
|
|
||||||
MailboxAttrAll MailboxAttr = "\\All"
|
|
||||||
MailboxAttrArchive MailboxAttr = "\\Archive"
|
|
||||||
MailboxAttrDrafts MailboxAttr = "\\Drafts"
|
|
||||||
MailboxAttrFlagged MailboxAttr = "\\Flagged"
|
|
||||||
MailboxAttrJunk MailboxAttr = "\\Junk"
|
|
||||||
MailboxAttrSent MailboxAttr = "\\Sent"
|
|
||||||
MailboxAttrTrash MailboxAttr = "\\Trash"
|
|
||||||
MailboxAttrImportant MailboxAttr = "\\Important" // RFC 8457
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Flag is a message flag.
|
// silentOp can be appended to a FlagsOp to prevent the operation from
|
||||||
//
|
// triggering unilateral message updates.
|
||||||
// Message flags are defined in RFC 9051 section 2.3.2.
|
const silentOp = ".SILENT"
|
||||||
type Flag string
|
|
||||||
|
|
||||||
const (
|
// A StoreItem is a message data item that can be updated.
|
||||||
// System flags
|
type StoreItem string
|
||||||
FlagSeen Flag = "\\Seen"
|
|
||||||
FlagAnswered Flag = "\\Answered"
|
|
||||||
FlagFlagged Flag = "\\Flagged"
|
|
||||||
FlagDeleted Flag = "\\Deleted"
|
|
||||||
FlagDraft Flag = "\\Draft"
|
|
||||||
|
|
||||||
// Widely used flags
|
// FormatFlagsOp returns the StoreItem that executes the flags operation op.
|
||||||
FlagForwarded Flag = "$Forwarded"
|
func FormatFlagsOp(op FlagsOp, silent bool) StoreItem {
|
||||||
FlagMDNSent Flag = "$MDNSent" // Message Disposition Notification sent
|
s := string(op)
|
||||||
FlagJunk Flag = "$Junk"
|
if silent {
|
||||||
FlagNotJunk Flag = "$NotJunk"
|
s += silentOp
|
||||||
FlagPhishing Flag = "$Phishing"
|
}
|
||||||
FlagImportant Flag = "$Important" // RFC 8457
|
return StoreItem(s)
|
||||||
|
|
||||||
// Permanent flags
|
|
||||||
FlagWildcard Flag = "\\*"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LiteralReader is a reader for IMAP literals.
|
|
||||||
type LiteralReader interface {
|
|
||||||
io.Reader
|
|
||||||
Size() int64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UID is a message unique identifier.
|
// ParseFlagsOp parses a flags operation from StoreItem.
|
||||||
type UID uint32
|
func ParseFlagsOp(item StoreItem) (op FlagsOp, silent bool, err error) {
|
||||||
|
itemStr := string(item)
|
||||||
|
silent = strings.HasSuffix(itemStr, silentOp)
|
||||||
|
if silent {
|
||||||
|
itemStr = strings.TrimSuffix(itemStr, silentOp)
|
||||||
|
}
|
||||||
|
op = FlagsOp(itemStr)
|
||||||
|
|
||||||
|
if op != SetFlags && op != AddFlags && op != RemoveFlags {
|
||||||
|
err = errors.New("Unsupported STORE operation")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CharsetReader, if non-nil, defines a function to generate charset-conversion
|
||||||
|
// readers, converting from the provided charset into UTF-8. Charsets are always
|
||||||
|
// lower-case. utf-8 and us-ascii charsets are handled by default. One of the
|
||||||
|
// the CharsetReader's result values must be non-nil.
|
||||||
|
var CharsetReader func(charset string, r io.Reader) (io.Reader, error)
|
||||||
|
|||||||
@@ -1,138 +0,0 @@
|
|||||||
package imapclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/emersion/go-imap/v2"
|
|
||||||
"github.com/emersion/go-imap/v2/internal"
|
|
||||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MyRights sends a MYRIGHTS command.
|
|
||||||
//
|
|
||||||
// This command requires support for the ACL extension.
|
|
||||||
func (c *Client) MyRights(mailbox string) *MyRightsCommand {
|
|
||||||
cmd := &MyRightsCommand{}
|
|
||||||
enc := c.beginCommand("MYRIGHTS", cmd)
|
|
||||||
enc.SP().Mailbox(mailbox)
|
|
||||||
enc.end()
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetACL sends a SETACL command.
|
|
||||||
//
|
|
||||||
// This command requires support for the ACL extension.
|
|
||||||
func (c *Client) SetACL(mailbox string, ri imap.RightsIdentifier, rm imap.RightModification, rs imap.RightSet) *SetACLCommand {
|
|
||||||
cmd := &SetACLCommand{}
|
|
||||||
enc := c.beginCommand("SETACL", cmd)
|
|
||||||
enc.SP().Mailbox(mailbox).SP().String(string(ri)).SP()
|
|
||||||
enc.String(internal.FormatRights(rm, rs))
|
|
||||||
enc.end()
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetACLCommand is a SETACL command.
|
|
||||||
type SetACLCommand struct {
|
|
||||||
commandBase
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cmd *SetACLCommand) Wait() error {
|
|
||||||
return cmd.wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetACL sends a GETACL command.
|
|
||||||
//
|
|
||||||
// This command requires support for the ACL extension.
|
|
||||||
func (c *Client) GetACL(mailbox string) *GetACLCommand {
|
|
||||||
cmd := &GetACLCommand{}
|
|
||||||
enc := c.beginCommand("GETACL", cmd)
|
|
||||||
enc.SP().Mailbox(mailbox)
|
|
||||||
enc.end()
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetACLCommand is a GETACL command.
|
|
||||||
type GetACLCommand struct {
|
|
||||||
commandBase
|
|
||||||
data GetACLData
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cmd *GetACLCommand) Wait() (*GetACLData, error) {
|
|
||||||
return &cmd.data, cmd.wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) handleMyRights() error {
|
|
||||||
data, err := readMyRights(c.dec)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("in myrights-response: %v", err)
|
|
||||||
}
|
|
||||||
if cmd := findPendingCmdByType[*MyRightsCommand](c); cmd != nil {
|
|
||||||
cmd.data = *data
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) handleGetACL() error {
|
|
||||||
data, err := readGetACL(c.dec)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("in getacl-response: %v", err)
|
|
||||||
}
|
|
||||||
if cmd := findPendingCmdByType[*GetACLCommand](c); cmd != nil {
|
|
||||||
cmd.data = *data
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MyRightsCommand is a MYRIGHTS command.
|
|
||||||
type MyRightsCommand struct {
|
|
||||||
commandBase
|
|
||||||
data MyRightsData
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cmd *MyRightsCommand) Wait() (*MyRightsData, error) {
|
|
||||||
return &cmd.data, cmd.wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MyRightsData is the data returned by the MYRIGHTS command.
|
|
||||||
type MyRightsData struct {
|
|
||||||
Mailbox string
|
|
||||||
Rights imap.RightSet
|
|
||||||
}
|
|
||||||
|
|
||||||
func readMyRights(dec *imapwire.Decoder) (*MyRightsData, error) {
|
|
||||||
var (
|
|
||||||
rights string
|
|
||||||
data MyRightsData
|
|
||||||
)
|
|
||||||
if !dec.ExpectMailbox(&data.Mailbox) || !dec.ExpectSP() || !dec.ExpectAString(&rights) {
|
|
||||||
return nil, dec.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
data.Rights = imap.RightSet(rights)
|
|
||||||
return &data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetACLData is the data returned by the GETACL command.
|
|
||||||
type GetACLData struct {
|
|
||||||
Mailbox string
|
|
||||||
Rights map[imap.RightsIdentifier]imap.RightSet
|
|
||||||
}
|
|
||||||
|
|
||||||
func readGetACL(dec *imapwire.Decoder) (*GetACLData, error) {
|
|
||||||
data := &GetACLData{Rights: make(map[imap.RightsIdentifier]imap.RightSet)}
|
|
||||||
|
|
||||||
if !dec.ExpectMailbox(&data.Mailbox) {
|
|
||||||
return nil, dec.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
for dec.SP() {
|
|
||||||
var rsStr, riStr string
|
|
||||||
if !dec.ExpectAString(&riStr) || !dec.ExpectSP() || !dec.ExpectAString(&rsStr) {
|
|
||||||
return nil, dec.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
data.Rights[imap.RightsIdentifier(riStr)] = imap.RightSet(rsStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
package imapclient_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/emersion/go-imap/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// order matters
|
|
||||||
var testCases = []struct {
|
|
||||||
name string
|
|
||||||
mailbox string
|
|
||||||
setRightsModification imap.RightModification
|
|
||||||
setRights imap.RightSet
|
|
||||||
expectedRights imap.RightSet
|
|
||||||
execStatusCmd bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "inbox",
|
|
||||||
mailbox: "INBOX",
|
|
||||||
setRightsModification: imap.RightModificationReplace,
|
|
||||||
setRights: imap.RightSet("akxeilprwtscd"),
|
|
||||||
expectedRights: imap.RightSet("akxeilprwtscd"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "custom_folder",
|
|
||||||
mailbox: "MyFolder",
|
|
||||||
setRightsModification: imap.RightModificationReplace,
|
|
||||||
setRights: imap.RightSet("ailw"),
|
|
||||||
expectedRights: imap.RightSet("ailw"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "custom_child_folder",
|
|
||||||
mailbox: "MyFolder/Child",
|
|
||||||
setRightsModification: imap.RightModificationReplace,
|
|
||||||
setRights: imap.RightSet("aelrwtd"),
|
|
||||||
expectedRights: imap.RightSet("aelrwtd"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "add_rights",
|
|
||||||
mailbox: "MyFolder",
|
|
||||||
setRightsModification: imap.RightModificationAdd,
|
|
||||||
setRights: imap.RightSet("rwi"),
|
|
||||||
expectedRights: imap.RightSet("ailwr"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "remove_rights",
|
|
||||||
mailbox: "MyFolder",
|
|
||||||
setRightsModification: imap.RightModificationRemove,
|
|
||||||
setRights: imap.RightSet("iwc"),
|
|
||||||
expectedRights: imap.RightSet("alr"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty_rights",
|
|
||||||
mailbox: "MyFolder/Child",
|
|
||||||
setRightsModification: imap.RightModificationReplace,
|
|
||||||
setRights: imap.RightSet("a"),
|
|
||||||
expectedRights: imap.RightSet("a"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestACL runs tests on SetACL, GetACL and MyRights commands.
|
|
||||||
func TestACL(t *testing.T) {
|
|
||||||
client, server := newClientServerPair(t, imap.ConnStateAuthenticated)
|
|
||||||
defer client.Close()
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
if !client.Caps().Has(imap.CapACL) {
|
|
||||||
t.Skipf("server doesn't support ACL")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := client.Create("MyFolder", nil).Wait(); err != nil {
|
|
||||||
t.Fatalf("create MyFolder error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := client.Create("MyFolder/Child", nil).Wait(); err != nil {
|
|
||||||
t.Fatalf("create MyFolder/Child error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
// execute SETACL command
|
|
||||||
err := client.SetACL(tc.mailbox, testUsername, tc.setRightsModification, tc.setRights).Wait()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("SetACL().Wait() error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// execute GETACL command to reset cache on server
|
|
||||||
getACLData, err := client.GetACL(tc.mailbox).Wait()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("GetACL().Wait() error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tc.expectedRights.Equal(getACLData.Rights[testUsername]) {
|
|
||||||
t.Errorf("GETACL returned wrong rights; expected: %s, got: %s", tc.expectedRights, getACLData.Rights[testUsername])
|
|
||||||
}
|
|
||||||
|
|
||||||
// execute MYRIGHTS command
|
|
||||||
myRightsData, err := client.MyRights(tc.mailbox).Wait()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("MyRights().Wait() error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tc.expectedRights.Equal(myRightsData.Rights) {
|
|
||||||
t.Errorf("MYRIGHTS returned wrong rights; expected: %s, got: %s", tc.expectedRights, myRightsData.Rights)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("nonexistent_mailbox", func(t *testing.T) {
|
|
||||||
if client.SetACL("BibiMailbox", testUsername, imap.RightModificationReplace, nil).Wait() == nil {
|
|
||||||
t.Errorf("expected error")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
package imapclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/emersion/go-imap/v2"
|
|
||||||
"github.com/emersion/go-imap/v2/internal"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Append sends an APPEND command.
|
|
||||||
//
|
|
||||||
// The caller must call AppendCommand.Close.
|
|
||||||
//
|
|
||||||
// The options are optional.
|
|
||||||
func (c *Client) Append(mailbox string, size int64, options *imap.AppendOptions) *AppendCommand {
|
|
||||||
cmd := &AppendCommand{}
|
|
||||||
cmd.enc = c.beginCommand("APPEND", cmd)
|
|
||||||
cmd.enc.SP().Mailbox(mailbox).SP()
|
|
||||||
if options != nil && len(options.Flags) > 0 {
|
|
||||||
cmd.enc.List(len(options.Flags), func(i int) {
|
|
||||||
cmd.enc.Flag(options.Flags[i])
|
|
||||||
}).SP()
|
|
||||||
}
|
|
||||||
if options != nil && !options.Time.IsZero() {
|
|
||||||
cmd.enc.String(options.Time.Format(internal.DateTimeLayout)).SP()
|
|
||||||
}
|
|
||||||
// TODO: literal8 for BINARY
|
|
||||||
// TODO: UTF8 data ext for UTF8=ACCEPT, with literal8
|
|
||||||
cmd.wc = cmd.enc.Literal(size)
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// AppendCommand is an APPEND command.
|
|
||||||
//
|
|
||||||
// Callers must write the message contents, then call Close.
|
|
||||||
type AppendCommand struct {
|
|
||||||
commandBase
|
|
||||||
enc *commandEncoder
|
|
||||||
wc io.WriteCloser
|
|
||||||
data imap.AppendData
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cmd *AppendCommand) Write(b []byte) (int, error) {
|
|
||||||
return cmd.wc.Write(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cmd *AppendCommand) Close() error {
|
|
||||||
err := cmd.wc.Close()
|
|
||||||
if cmd.enc != nil {
|
|
||||||
cmd.enc.end()
|
|
||||||
cmd.enc = nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cmd *AppendCommand) Wait() (*imap.AppendData, error) {
|
|
||||||
return &cmd.data, cmd.wait()
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package imapclient_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/emersion/go-imap/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestAppend(t *testing.T) {
|
|
||||||
client, server := newClientServerPair(t, imap.ConnStateSelected)
|
|
||||||
defer client.Close()
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
body := "This is a test message."
|
|
||||||
|
|
||||||
appendCmd := client.Append("INBOX", int64(len(body)), nil)
|
|
||||||
if _, err := appendCmd.Write([]byte(body)); err != nil {
|
|
||||||
t.Fatalf("AppendCommand.Write() = %v", err)
|
|
||||||
}
|
|
||||||
if err := appendCmd.Close(); err != nil {
|
|
||||||
t.Fatalf("AppendCommand.Close() = %v", err)
|
|
||||||
}
|
|
||||||
if _, err := appendCmd.Wait(); err != nil {
|
|
||||||
t.Fatalf("AppendCommand.Wait() = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: fetch back message and check body
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
package imapclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/emersion/go-sasl"
|
|
||||||
|
|
||||||
"github.com/emersion/go-imap/v2"
|
|
||||||
"github.com/emersion/go-imap/v2/internal"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Authenticate sends an AUTHENTICATE command.
|
|
||||||
//
|
|
||||||
// Unlike other commands, this method blocks until the SASL exchange completes.
|
|
||||||
func (c *Client) Authenticate(saslClient sasl.Client) error {
|
|
||||||
mech, initialResp, err := saslClient.Start()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// c.Caps may send a CAPABILITY command, so check it before c.beginCommand
|
|
||||||
var hasSASLIR bool
|
|
||||||
if initialResp != nil {
|
|
||||||
hasSASLIR = c.Caps().Has(imap.CapSASLIR)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := &authenticateCommand{}
|
|
||||||
contReq := c.registerContReq(cmd)
|
|
||||||
enc := c.beginCommand("AUTHENTICATE", cmd)
|
|
||||||
enc.SP().Atom(mech)
|
|
||||||
if initialResp != nil && hasSASLIR {
|
|
||||||
enc.SP().Atom(internal.EncodeSASL(initialResp))
|
|
||||||
initialResp = nil
|
|
||||||
}
|
|
||||||
enc.flush()
|
|
||||||
defer enc.end()
|
|
||||||
|
|
||||||
for {
|
|
||||||
challengeStr, err := contReq.Wait()
|
|
||||||
if err != nil {
|
|
||||||
return cmd.wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
if challengeStr == "" {
|
|
||||||
if initialResp == nil {
|
|
||||||
return fmt.Errorf("imapclient: server requested SASL initial response, but we don't have one")
|
|
||||||
}
|
|
||||||
|
|
||||||
contReq = c.registerContReq(cmd)
|
|
||||||
if err := c.writeSASLResp(initialResp); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
initialResp = nil
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
challenge, err := internal.DecodeSASL(challengeStr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := saslClient.Next(challenge)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
contReq = c.registerContReq(cmd)
|
|
||||||
if err := c.writeSASLResp(resp); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type authenticateCommand struct {
|
|
||||||
commandBase
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) writeSASLResp(resp []byte) error {
|
|
||||||
respStr := internal.EncodeSASL(resp)
|
|
||||||
if _, err := c.bw.WriteString(respStr + "\r\n"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := c.bw.Flush(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unauthenticate sends an UNAUTHENTICATE command.
|
|
||||||
//
|
|
||||||
// This command requires support for the UNAUTHENTICATE extension.
|
|
||||||
func (c *Client) Unauthenticate() *Command {
|
|
||||||
cmd := &unauthenticateCommand{}
|
|
||||||
c.beginCommand("UNAUTHENTICATE", cmd).end()
|
|
||||||
return &cmd.Command
|
|
||||||
}
|
|
||||||
|
|
||||||
type unauthenticateCommand struct {
|
|
||||||
Command
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package imapclient_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/emersion/go-sasl"
|
|
||||||
|
|
||||||
"github.com/emersion/go-imap/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestClient_Authenticate(t *testing.T) {
|
|
||||||
client, server := newClientServerPair(t, imap.ConnStateNotAuthenticated)
|
|
||||||
defer client.Close()
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
saslClient := sasl.NewPlainClient("", testUsername, testPassword)
|
|
||||||
if err := client.Authenticate(saslClient); err != nil {
|
|
||||||
t.Fatalf("Authenticate() = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if state := client.State(); state != imap.ConnStateAuthenticated {
|
|
||||||
t.Errorf("State() = %v, want %v", state, imap.ConnStateAuthenticated)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package imapclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/emersion/go-imap/v2"
|
|
||||||
"github.com/emersion/go-imap/v2/internal"
|
|
||||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Capability sends a CAPABILITY command.
|
|
||||||
func (c *Client) Capability() *CapabilityCommand {
|
|
||||||
cmd := &CapabilityCommand{}
|
|
||||||
c.beginCommand("CAPABILITY", cmd).end()
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) handleCapability() error {
|
|
||||||
caps, err := readCapabilities(c.dec)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.setCaps(caps)
|
|
||||||
if cmd := findPendingCmdByType[*CapabilityCommand](c); cmd != nil {
|
|
||||||
cmd.caps = caps
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CapabilityCommand is a CAPABILITY command.
|
|
||||||
type CapabilityCommand struct {
|
|
||||||
commandBase
|
|
||||||
caps imap.CapSet
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cmd *CapabilityCommand) Wait() (imap.CapSet, error) {
|
|
||||||
err := cmd.wait()
|
|
||||||
return cmd.caps, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func readCapabilities(dec *imapwire.Decoder) (imap.CapSet, error) {
|
|
||||||
caps := make(imap.CapSet)
|
|
||||||
for dec.SP() {
|
|
||||||
// Some IMAP servers send multiple SP between caps:
|
|
||||||
// https://github.com/emersion/go-imap/pull/652
|
|
||||||
for dec.SP() {
|
|
||||||
}
|
|
||||||
|
|
||||||
cap, err := internal.ExpectCap(dec)
|
|
||||||
if err != nil {
|
|
||||||
return caps, fmt.Errorf("in capability-data: %w", err)
|
|
||||||
}
|
|
||||||
caps[cap] = struct{}{}
|
|
||||||
}
|
|
||||||
return caps, nil
|
|
||||||
}
|
|
||||||
1243
imapclient/client.go
1243
imapclient/client.go
File diff suppressed because it is too large
Load Diff
@@ -1,277 +0,0 @@
|
|||||||
package imapclient_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/emersion/go-imap/v2"
|
|
||||||
"github.com/emersion/go-imap/v2/imapclient"
|
|
||||||
"github.com/emersion/go-imap/v2/imapserver"
|
|
||||||
"github.com/emersion/go-imap/v2/imapserver/imapmemserver"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
testUsername = "test-user"
|
|
||||||
testPassword = "test-password"
|
|
||||||
)
|
|
||||||
|
|
||||||
const simpleRawMessage = `MIME-Version: 1.0
|
|
||||||
Message-Id: <191101702316132@example.com>
|
|
||||||
Content-Transfer-Encoding: 8bit
|
|
||||||
Content-Type: text/plain; charset=utf-8
|
|
||||||
|
|
||||||
This is my letter!`
|
|
||||||
|
|
||||||
var rsaCertPEM = `-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDOTCCAiGgAwIBAgIQSRJrEpBGFc7tNb1fb5pKFzANBgkqhkiG9w0BAQsFADAS
|
|
||||||
MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw
|
|
||||||
MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
|
|
||||||
MIIBCgKCAQEA6Gba5tHV1dAKouAaXO3/ebDUU4rvwCUg/CNaJ2PT5xLD4N1Vcb8r
|
|
||||||
bFSW2HXKq+MPfVdwIKR/1DczEoAGf/JWQTW7EgzlXrCd3rlajEX2D73faWJekD0U
|
|
||||||
aUgz5vtrTXZ90BQL7WvRICd7FlEZ6FPOcPlumiyNmzUqtwGhO+9ad1W5BqJaRI6P
|
|
||||||
YfouNkwR6Na4TzSj5BrqUfP0FwDizKSJ0XXmh8g8G9mtwxOSN3Ru1QFc61Xyeluk
|
|
||||||
POGKBV/q6RBNklTNe0gI8usUMlYyoC7ytppNMW7X2vodAelSu25jgx2anj9fDVZu
|
|
||||||
h7AXF5+4nJS4AAt0n1lNY7nGSsdZas8PbQIDAQABo4GIMIGFMA4GA1UdDwEB/wQE
|
|
||||||
AwICpDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud
|
|
||||||
DgQWBBStsdjh3/JCXXYlQryOrL4Sh7BW5TAuBgNVHREEJzAlggtleGFtcGxlLmNv
|
|
||||||
bYcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG9w0BAQsFAAOCAQEAxWGI
|
|
||||||
5NhpF3nwwy/4yB4i/CwwSpLrWUa70NyhvprUBC50PxiXav1TeDzwzLx/o5HyNwsv
|
|
||||||
cxv3HdkLW59i/0SlJSrNnWdfZ19oTcS+6PtLoVyISgtyN6DpkKpdG1cOkW3Cy2P2
|
|
||||||
+tK/tKHRP1Y/Ra0RiDpOAmqn0gCOFGz8+lqDIor/T7MTpibL3IxqWfPrvfVRHL3B
|
|
||||||
grw/ZQTTIVjjh4JBSW3WyWgNo/ikC1lrVxzl4iPUGptxT36Cr7Zk2Bsg0XqwbOvK
|
|
||||||
5d+NTDREkSnUbie4GeutujmX3Dsx88UiV6UY/4lHJa6I5leHUNOHahRbpbWeOfs/
|
|
||||||
WkBKOclmOV2xlTVuPw==
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
`
|
|
||||||
|
|
||||||
var rsaKeyPEM = `-----BEGIN RSA PRIVATE KEY-----
|
|
||||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDoZtrm0dXV0Aqi
|
|
||||||
4Bpc7f95sNRTiu/AJSD8I1onY9PnEsPg3VVxvytsVJbYdcqr4w99V3AgpH/UNzMS
|
|
||||||
gAZ/8lZBNbsSDOVesJ3euVqMRfYPvd9pYl6QPRRpSDPm+2tNdn3QFAvta9EgJ3sW
|
|
||||||
URnoU85w+W6aLI2bNSq3AaE771p3VbkGolpEjo9h+i42TBHo1rhPNKPkGupR8/QX
|
|
||||||
AOLMpInRdeaHyDwb2a3DE5I3dG7VAVzrVfJ6W6Q84YoFX+rpEE2SVM17SAjy6xQy
|
|
||||||
VjKgLvK2mk0xbtfa+h0B6VK7bmODHZqeP18NVm6HsBcXn7iclLgAC3SfWU1jucZK
|
|
||||||
x1lqzw9tAgMBAAECggEABWzxS1Y2wckblnXY57Z+sl6YdmLV+gxj2r8Qib7g4ZIk
|
|
||||||
lIlWR1OJNfw7kU4eryib4fc6nOh6O4AWZyYqAK6tqNQSS/eVG0LQTLTTEldHyVJL
|
|
||||||
dvBe+MsUQOj4nTndZW+QvFzbcm2D8lY5n2nBSxU5ypVoKZ1EqQzytFcLZpTN7d89
|
|
||||||
EPj0qDyrV4NZlWAwL1AygCwnlwhMQjXEalVF1ylXwU3QzyZ/6MgvF6d3SSUlh+sq
|
|
||||||
XefuyigXw484cQQgbzopv6niMOmGP3of+yV4JQqUSb3IDmmT68XjGd2Dkxl4iPki
|
|
||||||
6ZwXf3CCi+c+i/zVEcufgZ3SLf8D99kUGE7v7fZ6AQKBgQD1ZX3RAla9hIhxCf+O
|
|
||||||
3D+I1j2LMrdjAh0ZKKqwMR4JnHX3mjQI6LwqIctPWTU8wYFECSh9klEclSdCa64s
|
|
||||||
uI/GNpcqPXejd0cAAdqHEEeG5sHMDt0oFSurL4lyud0GtZvwlzLuwEweuDtvT9cJ
|
|
||||||
Wfvl86uyO36IW8JdvUprYDctrQKBgQDycZ697qutBieZlGkHpnYWUAeImVA878sJ
|
|
||||||
w44NuXHvMxBPz+lbJGAg8Cn8fcxNAPqHIraK+kx3po8cZGQywKHUWsxi23ozHoxo
|
|
||||||
+bGqeQb9U661TnfdDspIXia+xilZt3mm5BPzOUuRqlh4Y9SOBpSWRmEhyw76w4ZP
|
|
||||||
OPxjWYAgwQKBgA/FehSYxeJgRjSdo+MWnK66tjHgDJE8bYpUZsP0JC4R9DL5oiaA
|
|
||||||
brd2fI6Y+SbyeNBallObt8LSgzdtnEAbjIH8uDJqyOmknNePRvAvR6mP4xyuR+Bv
|
|
||||||
m+Lgp0DMWTw5J9CKpydZDItc49T/mJ5tPhdFVd+am0NAQnmr1MCZ6nHxAoGABS3Y
|
|
||||||
LkaC9FdFUUqSU8+Chkd/YbOkuyiENdkvl6t2e52jo5DVc1T7mLiIrRQi4SI8N9bN
|
|
||||||
/3oJWCT+uaSLX2ouCtNFunblzWHBrhxnZzTeqVq4SLc8aESAnbslKL4i8/+vYZlN
|
|
||||||
s8xtiNcSvL+lMsOBORSXzpj/4Ot8WwTkn1qyGgECgYBKNTypzAHeLE6yVadFp3nQ
|
|
||||||
Ckq9yzvP/ib05rvgbvrne00YeOxqJ9gtTrzgh7koqJyX1L4NwdkEza4ilDWpucn0
|
|
||||||
xiUZS4SoaJq6ZvcBYS62Yr1t8n09iG47YL8ibgtmH3L+svaotvpVxVK+d7BLevA/
|
|
||||||
ZboOWVe3icTy64BT3OQhmg==
|
|
||||||
-----END RSA PRIVATE KEY-----
|
|
||||||
`
|
|
||||||
|
|
||||||
func newMemClientServerPair(t *testing.T) (net.Conn, io.Closer) {
|
|
||||||
memServer := imapmemserver.New()
|
|
||||||
|
|
||||||
user := imapmemserver.NewUser(testUsername, testPassword)
|
|
||||||
user.Create("INBOX", nil)
|
|
||||||
|
|
||||||
memServer.AddUser(user)
|
|
||||||
|
|
||||||
cert, err := tls.X509KeyPair([]byte(rsaCertPEM), []byte(rsaKeyPEM))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("tls.X509KeyPair() = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
server := imapserver.New(&imapserver.Options{
|
|
||||||
NewSession: func(conn *imapserver.Conn) (imapserver.Session, *imapserver.GreetingData, error) {
|
|
||||||
return memServer.NewSession(), nil, nil
|
|
||||||
},
|
|
||||||
TLSConfig: &tls.Config{
|
|
||||||
Certificates: []tls.Certificate{cert},
|
|
||||||
},
|
|
||||||
InsecureAuth: true,
|
|
||||||
Caps: imap.CapSet{
|
|
||||||
imap.CapIMAP4rev1: {},
|
|
||||||
imap.CapIMAP4rev2: {},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
ln, err := net.Listen("tcp", "localhost:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("net.Listen() = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
if err := server.Serve(ln); err != nil {
|
|
||||||
t.Errorf("Serve() = %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
conn, err := net.Dial("tcp", ln.Addr().String())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("net.Dial() = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return conn, server
|
|
||||||
}
|
|
||||||
|
|
||||||
func newClientServerPair(t *testing.T, initialState imap.ConnState) (*imapclient.Client, io.Closer) {
|
|
||||||
var useDovecot bool
|
|
||||||
switch os.Getenv("GOIMAP_TEST_DOVECOT") {
|
|
||||||
case "0", "":
|
|
||||||
// ok
|
|
||||||
case "1":
|
|
||||||
useDovecot = true
|
|
||||||
default:
|
|
||||||
t.Fatalf("invalid GOIMAP_TEST_DOVECOT env var")
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
conn net.Conn
|
|
||||||
server io.Closer
|
|
||||||
)
|
|
||||||
if useDovecot {
|
|
||||||
if initialState < imap.ConnStateAuthenticated {
|
|
||||||
t.Skip("Dovecot connections are pre-authenticated")
|
|
||||||
}
|
|
||||||
conn, server = newDovecotClientServerPair(t)
|
|
||||||
} else {
|
|
||||||
conn, server = newMemClientServerPair(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
var debugWriter swapWriter
|
|
||||||
debugWriter.Swap(io.Discard)
|
|
||||||
|
|
||||||
var options imapclient.Options
|
|
||||||
if testing.Verbose() {
|
|
||||||
options.DebugWriter = &debugWriter
|
|
||||||
}
|
|
||||||
client := imapclient.New(conn, &options)
|
|
||||||
|
|
||||||
if initialState >= imap.ConnStateAuthenticated {
|
|
||||||
// Dovecot connections are pre-authenticated
|
|
||||||
if !useDovecot {
|
|
||||||
if err := client.Login(testUsername, testPassword).Wait(); err != nil {
|
|
||||||
t.Fatalf("Login().Wait() = %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
appendCmd := client.Append("INBOX", int64(len(simpleRawMessage)), nil)
|
|
||||||
appendCmd.Write([]byte(simpleRawMessage))
|
|
||||||
appendCmd.Close()
|
|
||||||
if _, err := appendCmd.Wait(); err != nil {
|
|
||||||
t.Fatalf("AppendCommand.Wait() = %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if initialState >= imap.ConnStateSelected {
|
|
||||||
if _, err := client.Select("INBOX", nil).Wait(); err != nil {
|
|
||||||
t.Fatalf("Select().Wait() = %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Turn on debug logs after we're done initializing the test
|
|
||||||
debugWriter.Swap(os.Stderr)
|
|
||||||
|
|
||||||
return client, server
|
|
||||||
}
|
|
||||||
|
|
||||||
// swapWriter is an io.Writer which can be swapped at runtime.
|
|
||||||
type swapWriter struct {
|
|
||||||
w io.Writer
|
|
||||||
mutex sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sw *swapWriter) Write(b []byte) (int, error) {
|
|
||||||
sw.mutex.Lock()
|
|
||||||
w := sw.w
|
|
||||||
sw.mutex.Unlock()
|
|
||||||
|
|
||||||
return w.Write(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sw *swapWriter) Swap(w io.Writer) {
|
|
||||||
sw.mutex.Lock()
|
|
||||||
sw.w = w
|
|
||||||
sw.mutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLogin(t *testing.T) {
|
|
||||||
client, server := newClientServerPair(t, imap.ConnStateNotAuthenticated)
|
|
||||||
defer client.Close()
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
if err := client.Login(testUsername, testPassword).Wait(); err != nil {
|
|
||||||
t.Errorf("Login().Wait() = %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLogout(t *testing.T) {
|
|
||||||
client, server := newClientServerPair(t, imap.ConnStateAuthenticated)
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
if _, ok := server.(*dovecotServer); ok {
|
|
||||||
t.Skip("Dovecot connections don't reply to LOGOUT")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := client.Logout().Wait(); err != nil {
|
|
||||||
t.Errorf("Logout().Wait() = %v", err)
|
|
||||||
}
|
|
||||||
if err := client.Close(); err != nil {
|
|
||||||
t.Errorf("Close() = %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/emersion/go-imap/issues/562
|
|
||||||
func TestFetch_invalid(t *testing.T) {
|
|
||||||
client, server := newClientServerPair(t, imap.ConnStateSelected)
|
|
||||||
defer client.Close()
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
_, err := client.Fetch(imap.UIDSet(nil), nil).Collect()
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("UIDFetch().Collect() = %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFetch_closeUnreadBody(t *testing.T) {
|
|
||||||
client, server := newClientServerPair(t, imap.ConnStateSelected)
|
|
||||||
defer client.Close()
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
fetchCmd := client.Fetch(imap.SeqSetNum(1), &imap.FetchOptions{
|
|
||||||
BodySection: []*imap.FetchItemBodySection{
|
|
||||||
{
|
|
||||||
Specifier: imap.PartSpecifierNone,
|
|
||||||
Peek: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err := fetchCmd.Close(); err != nil {
|
|
||||||
t.Fatalf("UIDFetch().Close() = %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWaitGreeting_eof(t *testing.T) {
|
|
||||||
// bad server: connected but without greeting
|
|
||||||
clientConn, serverConn := net.Pipe()
|
|
||||||
|
|
||||||
client := imapclient.New(clientConn, nil)
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
if err := serverConn.Close(); err != nil {
|
|
||||||
t.Fatalf("serverConn.Close() = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := client.WaitGreeting(); err == nil {
|
|
||||||
t.Fatalf("WaitGreeting() should fail")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package imapclient_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/emersion/go-imap/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestClient_Closed tests that the Closed() channel is closed when the
|
|
||||||
// connection is explicitly closed via Close().
|
|
||||||
func TestClient_Closed(t *testing.T) {
|
|
||||||
client, server := newClientServerPair(t, imap.ConnStateAuthenticated)
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
closedCh := client.Closed()
|
|
||||||
if closedCh == nil {
|
|
||||||
t.Fatal("Closed() returned nil channel")
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-closedCh:
|
|
||||||
t.Fatal("Closed() channel closed before calling Close()")
|
|
||||||
default: // Expected
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := client.Close(); err != nil {
|
|
||||||
t.Fatalf("Close() = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-closedCh:
|
|
||||||
t.Log("Closed() channel properly closed after Close()")
|
|
||||||
case <-time.After(2 * time.Second):
|
|
||||||
t.Fatal("Closed() channel not closed after Close()")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package imapclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/emersion/go-imap/v2"
|
|
||||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Copy sends a COPY command.
|
|
||||||
func (c *Client) Copy(numSet imap.NumSet, mailbox string) *CopyCommand {
|
|
||||||
cmd := &CopyCommand{}
|
|
||||||
enc := c.beginCommand(uidCmdName("COPY", imapwire.NumSetKind(numSet)), cmd)
|
|
||||||
enc.SP().NumSet(numSet).SP().Mailbox(mailbox)
|
|
||||||
enc.end()
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// CopyCommand is a COPY command.
|
|
||||||
type CopyCommand struct {
|
|
||||||
commandBase
|
|
||||||
data imap.CopyData
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cmd *CopyCommand) Wait() (*imap.CopyData, error) {
|
|
||||||
return &cmd.data, cmd.wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func readRespCodeCopyUID(dec *imapwire.Decoder) (uidValidity uint32, srcUIDs, dstUIDs imap.UIDSet, err error) {
|
|
||||||
if !dec.ExpectNumber(&uidValidity) || !dec.ExpectSP() || !dec.ExpectUIDSet(&srcUIDs) || !dec.ExpectSP() || !dec.ExpectUIDSet(&dstUIDs) {
|
|
||||||
return 0, nil, nil, dec.Err()
|
|
||||||
}
|
|
||||||
if srcUIDs.Dynamic() || dstUIDs.Dynamic() {
|
|
||||||
return 0, nil, nil, fmt.Errorf("imapclient: server returned dynamic number set in COPYUID response")
|
|
||||||
}
|
|
||||||
return uidValidity, srcUIDs, dstUIDs, nil
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package imapclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/emersion/go-imap/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create sends a CREATE command.
|
|
||||||
//
|
|
||||||
// A nil options pointer is equivalent to a zero options value.
|
|
||||||
func (c *Client) Create(mailbox string, options *imap.CreateOptions) *Command {
|
|
||||||
cmd := &Command{}
|
|
||||||
enc := c.beginCommand("CREATE", cmd)
|
|
||||||
enc.SP().Mailbox(mailbox)
|
|
||||||
if options != nil && len(options.SpecialUse) > 0 {
|
|
||||||
enc.SP().Special('(').Atom("USE").SP().List(len(options.SpecialUse), func(i int) {
|
|
||||||
enc.MailboxAttr(options.SpecialUse[i])
|
|
||||||
}).Special(')')
|
|
||||||
}
|
|
||||||
enc.end()
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
package imapclient_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/emersion/go-imap/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func testCreate(t *testing.T, name string, utf8Accept bool) {
|
|
||||||
client, server := newClientServerPair(t, imap.ConnStateAuthenticated)
|
|
||||||
defer client.Close()
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
if utf8Accept {
|
|
||||||
if !client.Caps().Has(imap.CapUTF8Accept) {
|
|
||||||
t.Skipf("missing UTF8=ACCEPT support")
|
|
||||||
}
|
|
||||||
if data, err := client.Enable(imap.CapUTF8Accept).Wait(); err != nil {
|
|
||||||
t.Fatalf("Enable(CapUTF8Accept) = %v", err)
|
|
||||||
} else if !data.Caps.Has(imap.CapUTF8Accept) {
|
|
||||||
t.Fatalf("server refused to enable UTF8=ACCEPT")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := client.Create(name, nil).Wait(); err != nil {
|
|
||||||
t.Fatalf("Create() = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
listCmd := client.List("", name, nil)
|
|
||||||
mailboxes, err := listCmd.Collect()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("List() = %v", err)
|
|
||||||
} else if len(mailboxes) != 1 || mailboxes[0].Mailbox != name {
|
|
||||||
t.Errorf("List() = %v, want exactly one entry with correct name", mailboxes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreate(t *testing.T) {
|
|
||||||
t.Run("basic", func(t *testing.T) {
|
|
||||||
testCreate(t, "Test mailbox", false)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("unicode_utf7", func(t *testing.T) {
|
|
||||||
testCreate(t, "Cafè", false)
|
|
||||||
})
|
|
||||||
t.Run("unicode_utf8", func(t *testing.T) {
|
|
||||||
testCreate(t, "Cafè", true)
|
|
||||||
})
|
|
||||||
|
|
||||||
// '&' is the UTF-7 escape character
|
|
||||||
t.Run("ampersand_utf7", func(t *testing.T) {
|
|
||||||
testCreate(t, "Angus & Julia", false)
|
|
||||||
})
|
|
||||||
t.Run("ampersand_utf8", func(t *testing.T) {
|
|
||||||
testCreate(t, "Angus & Julia", true)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
package imapclient_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newDovecotClientServerPair(t *testing.T) (net.Conn, io.Closer) {
|
|
||||||
tempDir := t.TempDir()
|
|
||||||
|
|
||||||
cfgFilename := filepath.Join(tempDir, "dovecot.conf")
|
|
||||||
cfg := `dovecot_config_version = 2.4.0
|
|
||||||
dovecot_storage_version = 2.4.0
|
|
||||||
|
|
||||||
log_path = "` + tempDir + `/dovecot.log"
|
|
||||||
ssl = no
|
|
||||||
mail_home = "` + tempDir + `/%{user}"
|
|
||||||
mail_driver = maildir
|
|
||||||
mail_path = "~/Mail"
|
|
||||||
|
|
||||||
namespace inbox {
|
|
||||||
separator = /
|
|
||||||
prefix =
|
|
||||||
inbox = yes
|
|
||||||
}
|
|
||||||
|
|
||||||
mail_plugins {
|
|
||||||
acl = yes
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol imap {
|
|
||||||
mail_plugins {
|
|
||||||
imap_acl = yes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
acl_driver = vfile
|
|
||||||
`
|
|
||||||
if err := os.WriteFile(cfgFilename, []byte(cfg), 0666); err != nil {
|
|
||||||
t.Fatalf("failed to write Dovecot config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
clientConn, serverConn := net.Pipe()
|
|
||||||
|
|
||||||
cmd := exec.Command("doveadm", "-c", cfgFilename, "exec", "imap")
|
|
||||||
cmd.Env = []string{"USER=" + testUsername, "PATH=" + os.Getenv("PATH")}
|
|
||||||
cmd.Dir = tempDir
|
|
||||||
cmd.Stdin = serverConn
|
|
||||||
cmd.Stdout = serverConn
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
t.Fatalf("failed to start Dovecot: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return clientConn, &dovecotServer{cmd, serverConn}
|
|
||||||
}
|
|
||||||
|
|
||||||
type dovecotServer struct {
|
|
||||||
cmd *exec.Cmd
|
|
||||||
conn net.Conn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *dovecotServer) Close() error {
|
|
||||||
if err := srv.conn.Close(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return srv.cmd.Wait()
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
package imapclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/emersion/go-imap/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Enable sends an ENABLE command.
|
|
||||||
//
|
|
||||||
// This command requires support for IMAP4rev2 or the ENABLE extension.
|
|
||||||
func (c *Client) Enable(caps ...imap.Cap) *EnableCommand {
|
|
||||||
// Enabling an extension may change the IMAP syntax, so only allow the
|
|
||||||
// extensions we support here
|
|
||||||
for _, name := range caps {
|
|
||||||
switch name {
|
|
||||||
case imap.CapIMAP4rev2, imap.CapUTF8Accept, imap.CapMetadata, imap.CapMetadataServer:
|
|
||||||
// ok
|
|
||||||
default:
|
|
||||||
done := make(chan error)
|
|
||||||
close(done)
|
|
||||||
err := fmt.Errorf("imapclient: cannot enable %q: not supported", name)
|
|
||||||
return &EnableCommand{commandBase: commandBase{done: done, err: err}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := &EnableCommand{}
|
|
||||||
enc := c.beginCommand("ENABLE", cmd)
|
|
||||||
for _, c := range caps {
|
|
||||||
enc.SP().Atom(string(c))
|
|
||||||
}
|
|
||||||
enc.end()
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) handleEnabled() error {
|
|
||||||
caps, err := readCapabilities(c.dec)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.mutex.Lock()
|
|
||||||
for name := range caps {
|
|
||||||
c.enabled[name] = struct{}{}
|
|
||||||
}
|
|
||||||
c.mutex.Unlock()
|
|
||||||
|
|
||||||
if cmd := findPendingCmdByType[*EnableCommand](c); cmd != nil {
|
|
||||||
cmd.data.Caps = caps
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// EnableCommand is an ENABLE command.
|
|
||||||
type EnableCommand struct {
|
|
||||||
commandBase
|
|
||||||
data EnableData
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cmd *EnableCommand) Wait() (*EnableData, error) {
|
|
||||||
return &cmd.data, cmd.wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
// EnableData is the data returned by the ENABLE command.
|
|
||||||
type EnableData struct {
|
|
||||||
// Capabilities that were successfully enabled
|
|
||||||
Caps imap.CapSet
|
|
||||||
}
|
|
||||||
@@ -1,411 +0,0 @@
|
|||||||
package imapclient_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/emersion/go-message/mail"
|
|
||||||
"github.com/emersion/go-sasl"
|
|
||||||
|
|
||||||
"github.com/emersion/go-imap/v2"
|
|
||||||
"github.com/emersion/go-imap/v2/imapclient"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ExampleClient() {
|
|
||||||
c, err := imapclient.DialTLS("mail.example.org:993", nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed to dial IMAP server: %v", err)
|
|
||||||
}
|
|
||||||
defer c.Close()
|
|
||||||
|
|
||||||
if err := c.Login("root", "asdf").Wait(); err != nil {
|
|
||||||
log.Fatalf("failed to login: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mailboxes, err := c.List("", "%", nil).Collect()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed to list mailboxes: %v", err)
|
|
||||||
}
|
|
||||||
log.Printf("Found %v mailboxes", len(mailboxes))
|
|
||||||
for _, mbox := range mailboxes {
|
|
||||||
log.Printf(" - %v", mbox.Mailbox)
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedMbox, err := c.Select("INBOX", nil).Wait()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed to select INBOX: %v", err)
|
|
||||||
}
|
|
||||||
log.Printf("INBOX contains %v messages", selectedMbox.NumMessages)
|
|
||||||
|
|
||||||
if selectedMbox.NumMessages > 0 {
|
|
||||||
seqSet := imap.SeqSetNum(1)
|
|
||||||
fetchOptions := &imap.FetchOptions{Envelope: true}
|
|
||||||
messages, err := c.Fetch(seqSet, fetchOptions).Collect()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed to fetch first message in INBOX: %v", err)
|
|
||||||
}
|
|
||||||
log.Printf("subject of first message in INBOX: %v", messages[0].Envelope.Subject)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.Logout().Wait(); err != nil {
|
|
||||||
log.Fatalf("failed to logout: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleClient_pipelining() {
|
|
||||||
var c *imapclient.Client
|
|
||||||
|
|
||||||
uid := imap.UID(42)
|
|
||||||
fetchOptions := &imap.FetchOptions{Envelope: true}
|
|
||||||
|
|
||||||
// Login, select and fetch a message in a single roundtrip
|
|
||||||
loginCmd := c.Login("root", "root")
|
|
||||||
selectCmd := c.Select("INBOX", nil)
|
|
||||||
fetchCmd := c.Fetch(imap.UIDSetNum(uid), fetchOptions)
|
|
||||||
|
|
||||||
if err := loginCmd.Wait(); err != nil {
|
|
||||||
log.Fatalf("failed to login: %v", err)
|
|
||||||
}
|
|
||||||
if _, err := selectCmd.Wait(); err != nil {
|
|
||||||
log.Fatalf("failed to select INBOX: %v", err)
|
|
||||||
}
|
|
||||||
if messages, err := fetchCmd.Collect(); err != nil {
|
|
||||||
log.Fatalf("failed to fetch message: %v", err)
|
|
||||||
} else {
|
|
||||||
log.Printf("Subject: %v", messages[0].Envelope.Subject)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleClient_Append() {
|
|
||||||
var c *imapclient.Client
|
|
||||||
|
|
||||||
buf := []byte("From: <root@nsa.gov>\r\n\r\nHi <3")
|
|
||||||
size := int64(len(buf))
|
|
||||||
appendCmd := c.Append("INBOX", size, nil)
|
|
||||||
if _, err := appendCmd.Write(buf); err != nil {
|
|
||||||
log.Fatalf("failed to write message: %v", err)
|
|
||||||
}
|
|
||||||
if err := appendCmd.Close(); err != nil {
|
|
||||||
log.Fatalf("failed to close message: %v", err)
|
|
||||||
}
|
|
||||||
if _, err := appendCmd.Wait(); err != nil {
|
|
||||||
log.Fatalf("APPEND command failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleClient_Status() {
|
|
||||||
var c *imapclient.Client
|
|
||||||
|
|
||||||
options := imap.StatusOptions{NumMessages: true}
|
|
||||||
if data, err := c.Status("INBOX", &options).Wait(); err != nil {
|
|
||||||
log.Fatalf("STATUS command failed: %v", err)
|
|
||||||
} else {
|
|
||||||
log.Printf("INBOX contains %v messages", *data.NumMessages)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleClient_List_stream() {
|
|
||||||
var c *imapclient.Client
|
|
||||||
|
|
||||||
// ReturnStatus requires server support for IMAP4rev2 or LIST-STATUS
|
|
||||||
listCmd := c.List("", "%", &imap.ListOptions{
|
|
||||||
ReturnStatus: &imap.StatusOptions{
|
|
||||||
NumMessages: true,
|
|
||||||
NumUnseen: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
for {
|
|
||||||
mbox := listCmd.Next()
|
|
||||||
if mbox == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
log.Printf("Mailbox %q contains %v messages (%v unseen)", mbox.Mailbox, mbox.Status.NumMessages, mbox.Status.NumUnseen)
|
|
||||||
}
|
|
||||||
if err := listCmd.Close(); err != nil {
|
|
||||||
log.Fatalf("LIST command failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleClient_Store() {
|
|
||||||
var c *imapclient.Client
|
|
||||||
|
|
||||||
seqSet := imap.SeqSetNum(1)
|
|
||||||
storeFlags := imap.StoreFlags{
|
|
||||||
Op: imap.StoreFlagsAdd,
|
|
||||||
Flags: []imap.Flag{imap.FlagFlagged},
|
|
||||||
Silent: true,
|
|
||||||
}
|
|
||||||
if err := c.Store(seqSet, &storeFlags, nil).Close(); err != nil {
|
|
||||||
log.Fatalf("STORE command failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleClient_Fetch() {
|
|
||||||
var c *imapclient.Client
|
|
||||||
|
|
||||||
seqSet := imap.SeqSetNum(1)
|
|
||||||
bodySection := &imap.FetchItemBodySection{Specifier: imap.PartSpecifierHeader}
|
|
||||||
fetchOptions := &imap.FetchOptions{
|
|
||||||
Flags: true,
|
|
||||||
Envelope: true,
|
|
||||||
BodySection: []*imap.FetchItemBodySection{bodySection},
|
|
||||||
}
|
|
||||||
messages, err := c.Fetch(seqSet, fetchOptions).Collect()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("FETCH command failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
msg := messages[0]
|
|
||||||
header := msg.FindBodySection(bodySection)
|
|
||||||
|
|
||||||
log.Printf("Flags: %v", msg.Flags)
|
|
||||||
log.Printf("Subject: %v", msg.Envelope.Subject)
|
|
||||||
log.Printf("Header:\n%v", string(header))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleClient_Fetch_streamBody() {
|
|
||||||
var c *imapclient.Client
|
|
||||||
|
|
||||||
seqSet := imap.SeqSetNum(1)
|
|
||||||
bodySection := &imap.FetchItemBodySection{}
|
|
||||||
fetchOptions := &imap.FetchOptions{
|
|
||||||
UID: true,
|
|
||||||
BodySection: []*imap.FetchItemBodySection{bodySection},
|
|
||||||
}
|
|
||||||
fetchCmd := c.Fetch(seqSet, fetchOptions)
|
|
||||||
defer fetchCmd.Close()
|
|
||||||
|
|
||||||
for {
|
|
||||||
msg := fetchCmd.Next()
|
|
||||||
if msg == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
item := msg.Next()
|
|
||||||
if item == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
switch item := item.(type) {
|
|
||||||
case imapclient.FetchItemDataUID:
|
|
||||||
log.Printf("UID: %v", item.UID)
|
|
||||||
case imapclient.FetchItemDataBodySection:
|
|
||||||
b, err := io.ReadAll(item.Literal)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed to read body section: %v", err)
|
|
||||||
}
|
|
||||||
log.Printf("Body:\n%v", string(b))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := fetchCmd.Close(); err != nil {
|
|
||||||
log.Fatalf("FETCH command failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleClient_Fetch_parseBody() {
|
|
||||||
var c *imapclient.Client
|
|
||||||
|
|
||||||
// Send a FETCH command to fetch the message body
|
|
||||||
seqSet := imap.SeqSetNum(1)
|
|
||||||
bodySection := &imap.FetchItemBodySection{}
|
|
||||||
fetchOptions := &imap.FetchOptions{
|
|
||||||
BodySection: []*imap.FetchItemBodySection{bodySection},
|
|
||||||
}
|
|
||||||
fetchCmd := c.Fetch(seqSet, fetchOptions)
|
|
||||||
defer fetchCmd.Close()
|
|
||||||
|
|
||||||
msg := fetchCmd.Next()
|
|
||||||
if msg == nil {
|
|
||||||
log.Fatalf("FETCH command did not return any message")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the body section in the response
|
|
||||||
var bodySectionData imapclient.FetchItemDataBodySection
|
|
||||||
ok := false
|
|
||||||
for {
|
|
||||||
item := msg.Next()
|
|
||||||
if item == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
bodySectionData, ok = item.(imapclient.FetchItemDataBodySection)
|
|
||||||
if ok {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
log.Fatalf("FETCH command did not return body section")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the message via the go-message library
|
|
||||||
mr, err := mail.CreateReader(bodySectionData.Literal)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed to create mail reader: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print a few header fields
|
|
||||||
h := mr.Header
|
|
||||||
if date, err := h.Date(); err != nil {
|
|
||||||
log.Printf("failed to parse Date header field: %v", err)
|
|
||||||
} else {
|
|
||||||
log.Printf("Date: %v", date)
|
|
||||||
}
|
|
||||||
if to, err := h.AddressList("To"); err != nil {
|
|
||||||
log.Printf("failed to parse To header field: %v", err)
|
|
||||||
} else {
|
|
||||||
log.Printf("To: %v", to)
|
|
||||||
}
|
|
||||||
if subject, err := h.Text("Subject"); err != nil {
|
|
||||||
log.Printf("failed to parse Subject header field: %v", err)
|
|
||||||
} else {
|
|
||||||
log.Printf("Subject: %v", subject)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process the message's parts
|
|
||||||
for {
|
|
||||||
p, err := mr.NextPart()
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
} else if err != nil {
|
|
||||||
log.Fatalf("failed to read message part: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch h := p.Header.(type) {
|
|
||||||
case *mail.InlineHeader:
|
|
||||||
// This is the message's text (can be plain-text or HTML)
|
|
||||||
b, _ := io.ReadAll(p.Body)
|
|
||||||
log.Printf("Inline text: %v", string(b))
|
|
||||||
case *mail.AttachmentHeader:
|
|
||||||
// This is an attachment
|
|
||||||
filename, _ := h.Filename()
|
|
||||||
log.Printf("Attachment: %v", filename)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := fetchCmd.Close(); err != nil {
|
|
||||||
log.Fatalf("FETCH command failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleClient_Search() {
|
|
||||||
var c *imapclient.Client
|
|
||||||
|
|
||||||
data, err := c.UIDSearch(&imap.SearchCriteria{
|
|
||||||
Body: []string{"Hello world"},
|
|
||||||
}, nil).Wait()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("UID SEARCH command failed: %v", err)
|
|
||||||
}
|
|
||||||
log.Fatalf("UIDs matching the search criteria: %v", data.AllUIDs())
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleClient_Idle() {
|
|
||||||
options := imapclient.Options{
|
|
||||||
UnilateralDataHandler: &imapclient.UnilateralDataHandler{
|
|
||||||
Expunge: func(seqNum uint32) {
|
|
||||||
log.Printf("message %v has been expunged", seqNum)
|
|
||||||
},
|
|
||||||
Mailbox: func(data *imapclient.UnilateralDataMailbox) {
|
|
||||||
if data.NumMessages != nil {
|
|
||||||
log.Printf("a new message has been received")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := imapclient.DialTLS("mail.example.org:993", &options)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed to dial IMAP server: %v", err)
|
|
||||||
}
|
|
||||||
defer c.Close()
|
|
||||||
|
|
||||||
if err := c.Login("root", "asdf").Wait(); err != nil {
|
|
||||||
log.Fatalf("failed to login: %v", err)
|
|
||||||
}
|
|
||||||
if _, err := c.Select("INBOX", nil).Wait(); err != nil {
|
|
||||||
log.Fatalf("failed to select INBOX: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start idling
|
|
||||||
idleCmd, err := c.Idle()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("IDLE command failed: %v", err)
|
|
||||||
}
|
|
||||||
defer idleCmd.Close()
|
|
||||||
|
|
||||||
done := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
done <- idleCmd.Wait()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Wait for 30s to receive updates from the server, then stop idling
|
|
||||||
t := time.NewTimer(30 * time.Second)
|
|
||||||
defer t.Stop()
|
|
||||||
select {
|
|
||||||
case <-t.C:
|
|
||||||
if err := idleCmd.Close(); err != nil {
|
|
||||||
log.Fatalf("failed to stop idling: %v", err)
|
|
||||||
}
|
|
||||||
if err := <-done; err != nil {
|
|
||||||
log.Fatalf("IDLE command failed: %v", err)
|
|
||||||
}
|
|
||||||
case err := <-done:
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("IDLE command failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleClient_Authenticate_oauth() {
|
|
||||||
var (
|
|
||||||
c *imapclient.Client
|
|
||||||
username string
|
|
||||||
token string
|
|
||||||
)
|
|
||||||
|
|
||||||
if !c.Caps().Has(imap.AuthCap(sasl.OAuthBearer)) {
|
|
||||||
log.Fatal("OAUTHBEARER not supported by the server")
|
|
||||||
}
|
|
||||||
|
|
||||||
saslClient := sasl.NewOAuthBearerClient(&sasl.OAuthBearerOptions{
|
|
||||||
Username: username,
|
|
||||||
Token: token,
|
|
||||||
})
|
|
||||||
if err := c.Authenticate(saslClient); err != nil {
|
|
||||||
log.Fatalf("authentication failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleClient_Closed() {
|
|
||||||
c, err := imapclient.DialTLS("mail.example.org:993", nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed to dial IMAP server: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
selected := false
|
|
||||||
|
|
||||||
go func(c *imapclient.Client) {
|
|
||||||
if err := c.Login("root", "asdf").Wait(); err != nil {
|
|
||||||
log.Fatalf("failed to login: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := c.Select("INBOX", nil).Wait(); err != nil {
|
|
||||||
log.Fatalf("failed to select INBOX: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
selected = true
|
|
||||||
|
|
||||||
c.Close()
|
|
||||||
}(c)
|
|
||||||
|
|
||||||
// This channel shall be closed when the connection is closed.
|
|
||||||
<-c.Closed()
|
|
||||||
log.Println("Connection has been closed")
|
|
||||||
|
|
||||||
if !selected {
|
|
||||||
log.Fatalf("Connection was closed before selecting mailbox")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
package imapclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/emersion/go-imap/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Expunge sends an EXPUNGE command.
|
|
||||||
func (c *Client) Expunge() *ExpungeCommand {
|
|
||||||
cmd := &ExpungeCommand{seqNums: make(chan uint32, 128)}
|
|
||||||
c.beginCommand("EXPUNGE", cmd).end()
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// UIDExpunge sends a UID EXPUNGE command.
|
|
||||||
//
|
|
||||||
// This command requires support for IMAP4rev2 or the UIDPLUS extension.
|
|
||||||
func (c *Client) UIDExpunge(uids imap.UIDSet) *ExpungeCommand {
|
|
||||||
cmd := &ExpungeCommand{seqNums: make(chan uint32, 128)}
|
|
||||||
enc := c.beginCommand("UID EXPUNGE", cmd)
|
|
||||||
enc.SP().NumSet(uids)
|
|
||||||
enc.end()
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) handleExpunge(seqNum uint32) error {
|
|
||||||
c.mutex.Lock()
|
|
||||||
if c.state == imap.ConnStateSelected && c.mailbox.NumMessages > 0 {
|
|
||||||
c.mailbox = c.mailbox.copy()
|
|
||||||
c.mailbox.NumMessages--
|
|
||||||
}
|
|
||||||
c.mutex.Unlock()
|
|
||||||
|
|
||||||
cmd := findPendingCmdByType[*ExpungeCommand](c)
|
|
||||||
if cmd != nil {
|
|
||||||
cmd.seqNums <- seqNum
|
|
||||||
} else if handler := c.options.unilateralDataHandler().Expunge; handler != nil {
|
|
||||||
handler(seqNum)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExpungeCommand is an EXPUNGE command.
|
|
||||||
//
|
|
||||||
// The caller must fully consume the ExpungeCommand. A simple way to do so is
|
|
||||||
// to defer a call to FetchCommand.Close.
|
|
||||||
type ExpungeCommand struct {
|
|
||||||
commandBase
|
|
||||||
seqNums chan uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next advances to the next expunged message sequence number.
|
|
||||||
//
|
|
||||||
// On success, the message sequence number is returned. On error or if there
|
|
||||||
// are no more messages, 0 is returned. To check the error value, use Close.
|
|
||||||
func (cmd *ExpungeCommand) Next() uint32 {
|
|
||||||
return <-cmd.seqNums
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close releases the command.
|
|
||||||
//
|
|
||||||
// Calling Close unblocks the IMAP client decoder and lets it read the next
|
|
||||||
// responses. Next will always return nil after Close.
|
|
||||||
func (cmd *ExpungeCommand) Close() error {
|
|
||||||
for cmd.Next() != 0 {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
return cmd.wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect accumulates expunged sequence numbers into a list.
|
|
||||||
//
|
|
||||||
// This is equivalent to calling Next repeatedly and then Close.
|
|
||||||
func (cmd *ExpungeCommand) Collect() ([]uint32, error) {
|
|
||||||
var l []uint32
|
|
||||||
for {
|
|
||||||
seqNum := cmd.Next()
|
|
||||||
if seqNum == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
l = append(l, seqNum)
|
|
||||||
}
|
|
||||||
return l, cmd.Close()
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package imapclient_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/emersion/go-imap/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestExpunge(t *testing.T) {
|
|
||||||
client, server := newClientServerPair(t, imap.ConnStateSelected)
|
|
||||||
defer client.Close()
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
seqNums, err := client.Expunge().Collect()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Expunge() = %v", err)
|
|
||||||
} else if len(seqNums) != 0 {
|
|
||||||
t.Errorf("Expunge().Collect() = %v, want []", seqNums)
|
|
||||||
}
|
|
||||||
|
|
||||||
seqSet := imap.SeqSetNum(1)
|
|
||||||
storeFlags := imap.StoreFlags{
|
|
||||||
Op: imap.StoreFlagsAdd,
|
|
||||||
Flags: []imap.Flag{imap.FlagDeleted},
|
|
||||||
}
|
|
||||||
if err := client.Store(seqSet, &storeFlags, nil).Close(); err != nil {
|
|
||||||
t.Fatalf("Store() = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
seqNums, err = client.Expunge().Collect()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Expunge() = %v", err)
|
|
||||||
} else if len(seqNums) != 1 || seqNums[0] != 1 {
|
|
||||||
t.Errorf("Expunge().Collect() = %v, want [1]", seqNums)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1326
imapclient/fetch.go
1326
imapclient/fetch.go
File diff suppressed because it is too large
Load Diff
@@ -1,39 +0,0 @@
|
|||||||
package imapclient_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/emersion/go-imap/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFetch(t *testing.T) {
|
|
||||||
client, server := newClientServerPair(t, imap.ConnStateSelected)
|
|
||||||
defer client.Close()
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
seqSet := imap.SeqSetNum(1)
|
|
||||||
bodySection := &imap.FetchItemBodySection{}
|
|
||||||
fetchOptions := &imap.FetchOptions{
|
|
||||||
BodySection: []*imap.FetchItemBodySection{bodySection},
|
|
||||||
}
|
|
||||||
messages, err := client.Fetch(seqSet, fetchOptions).Collect()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to fetch first message: %v", err)
|
|
||||||
} else if len(messages) != 1 {
|
|
||||||
t.Fatalf("len(messages) = %v, want 1", len(messages))
|
|
||||||
}
|
|
||||||
|
|
||||||
msg := messages[0]
|
|
||||||
if len(msg.BodySection) != 1 {
|
|
||||||
t.Fatalf("len(msg.BodySection) = %v, want 1", len(msg.BodySection))
|
|
||||||
}
|
|
||||||
b := msg.FindBodySection(bodySection)
|
|
||||||
if b == nil {
|
|
||||||
t.Fatalf("FindBodySection() = nil")
|
|
||||||
}
|
|
||||||
body := strings.ReplaceAll(string(b), "\r\n", "\n")
|
|
||||||
if body != simpleRawMessage {
|
|
||||||
t.Errorf("body mismatch: got \n%v\n but want \n%v", body, simpleRawMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
163
imapclient/id.go
163
imapclient/id.go
@@ -1,163 +0,0 @@
|
|||||||
package imapclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/emersion/go-imap/v2"
|
|
||||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ID sends an ID command.
|
|
||||||
//
|
|
||||||
// The ID command is introduced in RFC 2971. It requires support for the ID
|
|
||||||
// extension.
|
|
||||||
//
|
|
||||||
// An example ID command:
|
|
||||||
//
|
|
||||||
// ID ("name" "go-imap" "version" "1.0" "os" "Linux" "os-version" "7.9.4" "vendor" "Yahoo")
|
|
||||||
func (c *Client) ID(idData *imap.IDData) *IDCommand {
|
|
||||||
cmd := &IDCommand{}
|
|
||||||
enc := c.beginCommand("ID", cmd)
|
|
||||||
|
|
||||||
if idData == nil {
|
|
||||||
enc.SP().NIL()
|
|
||||||
enc.end()
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
enc.SP().Special('(')
|
|
||||||
isFirstKey := true
|
|
||||||
if idData.Name != "" {
|
|
||||||
addIDKeyValue(enc, &isFirstKey, "name", idData.Name)
|
|
||||||
}
|
|
||||||
if idData.Version != "" {
|
|
||||||
addIDKeyValue(enc, &isFirstKey, "version", idData.Version)
|
|
||||||
}
|
|
||||||
if idData.OS != "" {
|
|
||||||
addIDKeyValue(enc, &isFirstKey, "os", idData.OS)
|
|
||||||
}
|
|
||||||
if idData.OSVersion != "" {
|
|
||||||
addIDKeyValue(enc, &isFirstKey, "os-version", idData.OSVersion)
|
|
||||||
}
|
|
||||||
if idData.Vendor != "" {
|
|
||||||
addIDKeyValue(enc, &isFirstKey, "vendor", idData.Vendor)
|
|
||||||
}
|
|
||||||
if idData.SupportURL != "" {
|
|
||||||
addIDKeyValue(enc, &isFirstKey, "support-url", idData.SupportURL)
|
|
||||||
}
|
|
||||||
if idData.Address != "" {
|
|
||||||
addIDKeyValue(enc, &isFirstKey, "address", idData.Address)
|
|
||||||
}
|
|
||||||
if idData.Date != "" {
|
|
||||||
addIDKeyValue(enc, &isFirstKey, "date", idData.Date)
|
|
||||||
}
|
|
||||||
if idData.Command != "" {
|
|
||||||
addIDKeyValue(enc, &isFirstKey, "command", idData.Command)
|
|
||||||
}
|
|
||||||
if idData.Arguments != "" {
|
|
||||||
addIDKeyValue(enc, &isFirstKey, "arguments", idData.Arguments)
|
|
||||||
}
|
|
||||||
if idData.Environment != "" {
|
|
||||||
addIDKeyValue(enc, &isFirstKey, "environment", idData.Environment)
|
|
||||||
}
|
|
||||||
|
|
||||||
enc.Special(')')
|
|
||||||
enc.end()
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func addIDKeyValue(enc *commandEncoder, isFirstKey *bool, key, value string) {
|
|
||||||
if isFirstKey == nil {
|
|
||||||
panic("isFirstKey cannot be nil")
|
|
||||||
} else if !*isFirstKey {
|
|
||||||
enc.SP().Quoted(key).SP().Quoted(value)
|
|
||||||
} else {
|
|
||||||
enc.Quoted(key).SP().Quoted(value)
|
|
||||||
}
|
|
||||||
*isFirstKey = false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) handleID() error {
|
|
||||||
data, err := c.readID(c.dec)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("in id: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd := findPendingCmdByType[*IDCommand](c); cmd != nil {
|
|
||||||
cmd.data = *data
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) readID(dec *imapwire.Decoder) (*imap.IDData, error) {
|
|
||||||
var data = imap.IDData{}
|
|
||||||
|
|
||||||
if !dec.ExpectSP() {
|
|
||||||
return nil, dec.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
if dec.ExpectNIL() {
|
|
||||||
return &data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
currKey := ""
|
|
||||||
err := dec.ExpectList(func() error {
|
|
||||||
var keyOrValue string
|
|
||||||
if !dec.String(&keyOrValue) {
|
|
||||||
return fmt.Errorf("in id key-val list: %v", dec.Err())
|
|
||||||
}
|
|
||||||
|
|
||||||
if currKey == "" {
|
|
||||||
currKey = keyOrValue
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch currKey {
|
|
||||||
case "name":
|
|
||||||
data.Name = keyOrValue
|
|
||||||
case "version":
|
|
||||||
data.Version = keyOrValue
|
|
||||||
case "os":
|
|
||||||
data.OS = keyOrValue
|
|
||||||
case "os-version":
|
|
||||||
data.OSVersion = keyOrValue
|
|
||||||
case "vendor":
|
|
||||||
data.Vendor = keyOrValue
|
|
||||||
case "support-url":
|
|
||||||
data.SupportURL = keyOrValue
|
|
||||||
case "address":
|
|
||||||
data.Address = keyOrValue
|
|
||||||
case "date":
|
|
||||||
data.Date = keyOrValue
|
|
||||||
case "command":
|
|
||||||
data.Command = keyOrValue
|
|
||||||
case "arguments":
|
|
||||||
data.Arguments = keyOrValue
|
|
||||||
case "environment":
|
|
||||||
data.Environment = keyOrValue
|
|
||||||
default:
|
|
||||||
// Ignore unknown key
|
|
||||||
// Yahoo server sends "host" and "remote-host" keys
|
|
||||||
// which are not defined in RFC 2971
|
|
||||||
}
|
|
||||||
currKey = ""
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type IDCommand struct {
|
|
||||||
commandBase
|
|
||||||
data imap.IDData
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *IDCommand) Wait() (*imap.IDData, error) {
|
|
||||||
return &r.data, r.wait()
|
|
||||||
}
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
package imapclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const idleRestartInterval = 28 * time.Minute
|
|
||||||
|
|
||||||
// Idle sends an IDLE command.
|
|
||||||
//
|
|
||||||
// Unlike other commands, this method blocks until the server acknowledges it.
|
|
||||||
// On success, the IDLE command is running and other commands cannot be sent.
|
|
||||||
// The caller must invoke IdleCommand.Close to stop IDLE and unblock the
|
|
||||||
// client.
|
|
||||||
//
|
|
||||||
// This command requires support for IMAP4rev2 or the IDLE extension. The IDLE
|
|
||||||
// command is restarted automatically to avoid getting disconnected due to
|
|
||||||
// inactivity timeouts.
|
|
||||||
func (c *Client) Idle() (*IdleCommand, error) {
|
|
||||||
child, err := c.idle()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := &IdleCommand{
|
|
||||||
stop: make(chan struct{}),
|
|
||||||
done: make(chan struct{}),
|
|
||||||
}
|
|
||||||
go cmd.run(c, child)
|
|
||||||
return cmd, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IdleCommand is an IDLE command.
|
|
||||||
//
|
|
||||||
// Initially, the IDLE command is running. The server may send unilateral
|
|
||||||
// data. The client cannot send any command while IDLE is running.
|
|
||||||
//
|
|
||||||
// Close must be called to stop the IDLE command.
|
|
||||||
type IdleCommand struct {
|
|
||||||
stopped atomic.Bool
|
|
||||||
stop chan struct{}
|
|
||||||
done chan struct{}
|
|
||||||
|
|
||||||
err error
|
|
||||||
lastChild *idleCommand
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cmd *IdleCommand) run(c *Client, child *idleCommand) {
|
|
||||||
defer close(cmd.done)
|
|
||||||
|
|
||||||
timer := time.NewTimer(idleRestartInterval)
|
|
||||||
defer timer.Stop()
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if child != nil {
|
|
||||||
if err := child.Close(); err != nil && cmd.err == nil {
|
|
||||||
cmd.err = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-timer.C:
|
|
||||||
timer.Reset(idleRestartInterval)
|
|
||||||
|
|
||||||
if cmd.err = child.Close(); cmd.err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if child, cmd.err = c.idle(); cmd.err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case <-c.decCh:
|
|
||||||
cmd.lastChild = child
|
|
||||||
return
|
|
||||||
case <-cmd.stop:
|
|
||||||
cmd.lastChild = child
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close stops the IDLE command.
|
|
||||||
//
|
|
||||||
// This method blocks until the command to stop IDLE is written, but doesn't
|
|
||||||
// wait for the server to respond. Callers can use Wait for this purpose.
|
|
||||||
func (cmd *IdleCommand) Close() error {
|
|
||||||
if cmd.stopped.Swap(true) {
|
|
||||||
return fmt.Errorf("imapclient: IDLE already closed")
|
|
||||||
}
|
|
||||||
close(cmd.stop)
|
|
||||||
<-cmd.done
|
|
||||||
return cmd.err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait blocks until the IDLE command has completed.
|
|
||||||
func (cmd *IdleCommand) Wait() error {
|
|
||||||
<-cmd.done
|
|
||||||
if cmd.err != nil {
|
|
||||||
return cmd.err
|
|
||||||
}
|
|
||||||
return cmd.lastChild.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) idle() (*idleCommand, error) {
|
|
||||||
cmd := &idleCommand{}
|
|
||||||
contReq := c.registerContReq(cmd)
|
|
||||||
cmd.enc = c.beginCommand("IDLE", cmd)
|
|
||||||
cmd.enc.flush()
|
|
||||||
|
|
||||||
_, err := contReq.Wait()
|
|
||||||
if err != nil {
|
|
||||||
cmd.enc.end()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return cmd, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// idleCommand represents a singular IDLE command, without the restart logic.
|
|
||||||
type idleCommand struct {
|
|
||||||
commandBase
|
|
||||||
enc *commandEncoder
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close stops the IDLE command.
|
|
||||||
//
|
|
||||||
// This method blocks until the command to stop IDLE is written, but doesn't
|
|
||||||
// wait for the server to respond. Callers can use Wait for this purpose.
|
|
||||||
func (cmd *idleCommand) Close() error {
|
|
||||||
if cmd.err != nil {
|
|
||||||
return cmd.err
|
|
||||||
}
|
|
||||||
if cmd.enc == nil {
|
|
||||||
return fmt.Errorf("imapclient: IDLE command closed twice")
|
|
||||||
}
|
|
||||||
cmd.enc.client.setWriteTimeout(cmdWriteTimeout)
|
|
||||||
_, err := cmd.enc.client.bw.WriteString("DONE\r\n")
|
|
||||||
if err == nil {
|
|
||||||
err = cmd.enc.client.bw.Flush()
|
|
||||||
}
|
|
||||||
cmd.enc.end()
|
|
||||||
cmd.enc = nil
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait blocks until the IDLE command has completed.
|
|
||||||
//
|
|
||||||
// Wait can only be called after Close.
|
|
||||||
func (cmd *idleCommand) Wait() error {
|
|
||||||
if cmd.enc != nil {
|
|
||||||
panic("imapclient: idleCommand.Close must be called before Wait")
|
|
||||||
}
|
|
||||||
return cmd.wait()
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package imapclient_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/emersion/go-imap/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestIdle(t *testing.T) {
|
|
||||||
client, server := newClientServerPair(t, imap.ConnStateSelected)
|
|
||||||
defer client.Close()
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
idleCmd, err := client.Idle()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Idle() = %v", err)
|
|
||||||
}
|
|
||||||
// TODO: test unilateral updates
|
|
||||||
if err := idleCmd.Close(); err != nil {
|
|
||||||
t.Errorf("Close() = %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIdle_closedConn(t *testing.T) {
|
|
||||||
client, server := newClientServerPair(t, imap.ConnStateSelected)
|
|
||||||
defer client.Close()
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
idleCmd, err := client.Idle()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Idle() = %v", err)
|
|
||||||
}
|
|
||||||
defer idleCmd.Close()
|
|
||||||
|
|
||||||
if err := client.Close(); err != nil {
|
|
||||||
t.Fatalf("client.Close() = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := idleCmd.Wait(); err == nil {
|
|
||||||
t.Errorf("IdleCommand.Wait() = nil, want an error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,259 +0,0 @@
|
|||||||
package imapclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"unicode/utf8"
|
|
||||||
|
|
||||||
"github.com/emersion/go-imap/v2"
|
|
||||||
"github.com/emersion/go-imap/v2/internal"
|
|
||||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
|
||||||
)
|
|
||||||
|
|
||||||
func getSelectOpts(options *imap.ListOptions) []string {
|
|
||||||
if options == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var l []string
|
|
||||||
if options.SelectSubscribed {
|
|
||||||
l = append(l, "SUBSCRIBED")
|
|
||||||
}
|
|
||||||
if options.SelectRemote {
|
|
||||||
l = append(l, "REMOTE")
|
|
||||||
}
|
|
||||||
if options.SelectRecursiveMatch {
|
|
||||||
l = append(l, "RECURSIVEMATCH")
|
|
||||||
}
|
|
||||||
if options.SelectSpecialUse {
|
|
||||||
l = append(l, "SPECIAL-USE")
|
|
||||||
}
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
func getReturnOpts(options *imap.ListOptions) []string {
|
|
||||||
if options == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var l []string
|
|
||||||
if options.ReturnSubscribed {
|
|
||||||
l = append(l, "SUBSCRIBED")
|
|
||||||
}
|
|
||||||
if options.ReturnChildren {
|
|
||||||
l = append(l, "CHILDREN")
|
|
||||||
}
|
|
||||||
if options.ReturnStatus != nil {
|
|
||||||
l = append(l, "STATUS")
|
|
||||||
}
|
|
||||||
if options.ReturnSpecialUse {
|
|
||||||
l = append(l, "SPECIAL-USE")
|
|
||||||
}
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
// List sends a LIST command.
|
|
||||||
//
|
|
||||||
// The caller must fully consume the ListCommand. A simple way to do so is to
|
|
||||||
// defer a call to ListCommand.Close.
|
|
||||||
//
|
|
||||||
// A nil options pointer is equivalent to a zero options value.
|
|
||||||
//
|
|
||||||
// A non-zero options value requires support for IMAP4rev2 or the LIST-EXTENDED
|
|
||||||
// extension.
|
|
||||||
func (c *Client) List(ref, pattern string, options *imap.ListOptions) *ListCommand {
|
|
||||||
cmd := &ListCommand{
|
|
||||||
mailboxes: make(chan *imap.ListData, 64),
|
|
||||||
returnStatus: options != nil && options.ReturnStatus != nil,
|
|
||||||
}
|
|
||||||
enc := c.beginCommand("LIST", cmd)
|
|
||||||
if selectOpts := getSelectOpts(options); len(selectOpts) > 0 {
|
|
||||||
enc.SP().List(len(selectOpts), func(i int) {
|
|
||||||
enc.Atom(selectOpts[i])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
enc.SP().Mailbox(ref).SP().Mailbox(pattern)
|
|
||||||
if returnOpts := getReturnOpts(options); len(returnOpts) > 0 {
|
|
||||||
enc.SP().Atom("RETURN").SP().List(len(returnOpts), func(i int) {
|
|
||||||
opt := returnOpts[i]
|
|
||||||
enc.Atom(opt)
|
|
||||||
if opt == "STATUS" {
|
|
||||||
returnStatus := statusItems(options.ReturnStatus)
|
|
||||||
enc.SP().List(len(returnStatus), func(j int) {
|
|
||||||
enc.Atom(returnStatus[j])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
enc.end()
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) handleList() error {
|
|
||||||
data, err := readList(c.dec)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("in LIST: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := c.findPendingCmdFunc(func(cmd command) bool {
|
|
||||||
switch cmd := cmd.(type) {
|
|
||||||
case *ListCommand:
|
|
||||||
return true // TODO: match pattern, check if already handled
|
|
||||||
case *SelectCommand:
|
|
||||||
return cmd.mailbox == data.Mailbox && cmd.data.List == nil
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
switch cmd := cmd.(type) {
|
|
||||||
case *ListCommand:
|
|
||||||
if cmd.returnStatus {
|
|
||||||
if cmd.pendingData != nil {
|
|
||||||
cmd.mailboxes <- cmd.pendingData
|
|
||||||
}
|
|
||||||
cmd.pendingData = data
|
|
||||||
} else {
|
|
||||||
cmd.mailboxes <- data
|
|
||||||
}
|
|
||||||
case *SelectCommand:
|
|
||||||
cmd.data.List = data
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListCommand is a LIST command.
|
|
||||||
type ListCommand struct {
|
|
||||||
commandBase
|
|
||||||
mailboxes chan *imap.ListData
|
|
||||||
|
|
||||||
returnStatus bool
|
|
||||||
pendingData *imap.ListData
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next advances to the next mailbox.
|
|
||||||
//
|
|
||||||
// On success, the mailbox LIST data is returned. On error or if there are no
|
|
||||||
// more mailboxes, nil is returned.
|
|
||||||
func (cmd *ListCommand) Next() *imap.ListData {
|
|
||||||
return <-cmd.mailboxes
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close releases the command.
|
|
||||||
//
|
|
||||||
// Calling Close unblocks the IMAP client decoder and lets it read the next
|
|
||||||
// responses. Next will always return nil after Close.
|
|
||||||
func (cmd *ListCommand) Close() error {
|
|
||||||
for cmd.Next() != nil {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
return cmd.wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect accumulates mailboxes into a list.
|
|
||||||
//
|
|
||||||
// This is equivalent to calling Next repeatedly and then Close.
|
|
||||||
func (cmd *ListCommand) Collect() ([]*imap.ListData, error) {
|
|
||||||
var l []*imap.ListData
|
|
||||||
for {
|
|
||||||
data := cmd.Next()
|
|
||||||
if data == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
l = append(l, data)
|
|
||||||
}
|
|
||||||
return l, cmd.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func readList(dec *imapwire.Decoder) (*imap.ListData, error) {
|
|
||||||
var data imap.ListData
|
|
||||||
|
|
||||||
var err error
|
|
||||||
data.Attrs, err = internal.ExpectMailboxAttrList(dec)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("in mbx-list-flags: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !dec.ExpectSP() {
|
|
||||||
return nil, dec.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
data.Delim, err = readDelim(dec)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !dec.ExpectSP() || !dec.ExpectMailbox(&data.Mailbox) {
|
|
||||||
return nil, dec.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
if dec.SP() {
|
|
||||||
err := dec.ExpectList(func() error {
|
|
||||||
var tag string
|
|
||||||
if !dec.ExpectAString(&tag) || !dec.ExpectSP() {
|
|
||||||
return dec.Err()
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
switch strings.ToUpper(tag) {
|
|
||||||
case "CHILDINFO":
|
|
||||||
data.ChildInfo, err = readChildInfoExtendedItem(dec)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("in childinfo-extended-item: %v", err)
|
|
||||||
}
|
|
||||||
case "OLDNAME":
|
|
||||||
data.OldName, err = readOldNameExtendedItem(dec)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("in oldname-extended-item: %v", err)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if !dec.DiscardValue() {
|
|
||||||
return fmt.Errorf("in tagged-ext-val: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("in mbox-list-extended: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readChildInfoExtendedItem(dec *imapwire.Decoder) (*imap.ListDataChildInfo, error) {
|
|
||||||
var childInfo imap.ListDataChildInfo
|
|
||||||
err := dec.ExpectList(func() error {
|
|
||||||
var opt string
|
|
||||||
if !dec.ExpectAString(&opt) {
|
|
||||||
return dec.Err()
|
|
||||||
}
|
|
||||||
if strings.ToUpper(opt) == "SUBSCRIBED" {
|
|
||||||
childInfo.Subscribed = true
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return &childInfo, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func readOldNameExtendedItem(dec *imapwire.Decoder) (string, error) {
|
|
||||||
var name string
|
|
||||||
if !dec.ExpectSpecial('(') || !dec.ExpectMailbox(&name) || !dec.ExpectSpecial(')') {
|
|
||||||
return "", dec.Err()
|
|
||||||
}
|
|
||||||
return name, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readDelim(dec *imapwire.Decoder) (rune, error) {
|
|
||||||
var delimStr string
|
|
||||||
if dec.Quoted(&delimStr) {
|
|
||||||
delim, size := utf8.DecodeRuneInString(delimStr)
|
|
||||||
if delim == utf8.RuneError || size != len(delimStr) {
|
|
||||||
return 0, fmt.Errorf("mailbox delimiter must be a single rune")
|
|
||||||
}
|
|
||||||
return delim, nil
|
|
||||||
} else if !dec.ExpectNIL() {
|
|
||||||
return 0, dec.Err()
|
|
||||||
} else {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package imapclient_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/emersion/go-imap/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestList(t *testing.T) {
|
|
||||||
client, server := newClientServerPair(t, imap.ConnStateAuthenticated)
|
|
||||||
defer client.Close()
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
options := imap.ListOptions{
|
|
||||||
ReturnStatus: &imap.StatusOptions{
|
|
||||||
NumMessages: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
mailboxes, err := client.List("", "%", &options).Collect()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("List() = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(mailboxes) != 1 {
|
|
||||||
t.Fatalf("List() returned %v mailboxes, want 1", len(mailboxes))
|
|
||||||
}
|
|
||||||
mbox := mailboxes[0]
|
|
||||||
|
|
||||||
wantNumMessages := uint32(1)
|
|
||||||
want := &imap.ListData{
|
|
||||||
Delim: '/',
|
|
||||||
Mailbox: "INBOX",
|
|
||||||
Status: &imap.StatusData{
|
|
||||||
Mailbox: "INBOX",
|
|
||||||
NumMessages: &wantNumMessages,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(mbox, want) {
|
|
||||||
t.Errorf("got %#v but want %#v", mbox, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
package imapclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GetMetadataDepth int
|
|
||||||
|
|
||||||
const (
|
|
||||||
GetMetadataDepthZero GetMetadataDepth = 0
|
|
||||||
GetMetadataDepthOne GetMetadataDepth = 1
|
|
||||||
GetMetadataDepthInfinity GetMetadataDepth = -1
|
|
||||||
)
|
|
||||||
|
|
||||||
func (depth GetMetadataDepth) String() string {
|
|
||||||
switch depth {
|
|
||||||
case GetMetadataDepthZero:
|
|
||||||
return "0"
|
|
||||||
case GetMetadataDepthOne:
|
|
||||||
return "1"
|
|
||||||
case GetMetadataDepthInfinity:
|
|
||||||
return "infinity"
|
|
||||||
default:
|
|
||||||
panic(fmt.Errorf("imapclient: unknown GETMETADATA depth %d", depth))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMetadataOptions contains options for the GETMETADATA command.
|
|
||||||
type GetMetadataOptions struct {
|
|
||||||
MaxSize *uint32
|
|
||||||
Depth GetMetadataDepth
|
|
||||||
}
|
|
||||||
|
|
||||||
func (options *GetMetadataOptions) names() []string {
|
|
||||||
if options == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var l []string
|
|
||||||
if options.MaxSize != nil {
|
|
||||||
l = append(l, "MAXSIZE")
|
|
||||||
}
|
|
||||||
if options.Depth != GetMetadataDepthZero {
|
|
||||||
l = append(l, "DEPTH")
|
|
||||||
}
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMetadata sends a GETMETADATA command.
|
|
||||||
//
|
|
||||||
// This command requires support for the METADATA or METADATA-SERVER extension.
|
|
||||||
func (c *Client) GetMetadata(mailbox string, entries []string, options *GetMetadataOptions) *GetMetadataCommand {
|
|
||||||
cmd := &GetMetadataCommand{mailbox: mailbox}
|
|
||||||
enc := c.beginCommand("GETMETADATA", cmd)
|
|
||||||
enc.SP().Mailbox(mailbox)
|
|
||||||
if opts := options.names(); len(opts) > 0 {
|
|
||||||
enc.SP().List(len(opts), func(i int) {
|
|
||||||
opt := opts[i]
|
|
||||||
enc.Atom(opt).SP()
|
|
||||||
switch opt {
|
|
||||||
case "MAXSIZE":
|
|
||||||
enc.Number(*options.MaxSize)
|
|
||||||
case "DEPTH":
|
|
||||||
enc.Atom(options.Depth.String())
|
|
||||||
default:
|
|
||||||
panic(fmt.Errorf("imapclient: unknown GETMETADATA option %q", opt))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
enc.SP().List(len(entries), func(i int) {
|
|
||||||
enc.String(entries[i])
|
|
||||||
})
|
|
||||||
enc.end()
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetMetadata sends a SETMETADATA command.
|
|
||||||
//
|
|
||||||
// To remove an entry, set it to nil.
|
|
||||||
//
|
|
||||||
// This command requires support for the METADATA or METADATA-SERVER extension.
|
|
||||||
func (c *Client) SetMetadata(mailbox string, entries map[string]*[]byte) *Command {
|
|
||||||
cmd := &Command{}
|
|
||||||
enc := c.beginCommand("SETMETADATA", cmd)
|
|
||||||
enc.SP().Mailbox(mailbox).SP().Special('(')
|
|
||||||
i := 0
|
|
||||||
for k, v := range entries {
|
|
||||||
if i > 0 {
|
|
||||||
enc.SP()
|
|
||||||
}
|
|
||||||
enc.String(k).SP()
|
|
||||||
if v == nil {
|
|
||||||
enc.NIL()
|
|
||||||
} else {
|
|
||||||
enc.String(string(*v)) // TODO: use literals if required
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
enc.Special(')')
|
|
||||||
enc.end()
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) handleMetadata() error {
|
|
||||||
data, err := readMetadataResp(c.dec)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("in metadata-resp: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := c.findPendingCmdFunc(func(anyCmd command) bool {
|
|
||||||
cmd, ok := anyCmd.(*GetMetadataCommand)
|
|
||||||
return ok && cmd.mailbox == data.Mailbox
|
|
||||||
})
|
|
||||||
if cmd != nil && len(data.EntryValues) > 0 {
|
|
||||||
cmd := cmd.(*GetMetadataCommand)
|
|
||||||
cmd.data.Mailbox = data.Mailbox
|
|
||||||
if cmd.data.Entries == nil {
|
|
||||||
cmd.data.Entries = make(map[string]*[]byte)
|
|
||||||
}
|
|
||||||
// The server might send multiple METADATA responses for a single
|
|
||||||
// METADATA command
|
|
||||||
for k, v := range data.EntryValues {
|
|
||||||
cmd.data.Entries[k] = v
|
|
||||||
}
|
|
||||||
} else if handler := c.options.unilateralDataHandler().Metadata; handler != nil && len(data.EntryList) > 0 {
|
|
||||||
handler(data.Mailbox, data.EntryList)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMetadataCommand is a GETMETADATA command.
|
|
||||||
type GetMetadataCommand struct {
|
|
||||||
commandBase
|
|
||||||
mailbox string
|
|
||||||
data GetMetadataData
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cmd *GetMetadataCommand) Wait() (*GetMetadataData, error) {
|
|
||||||
return &cmd.data, cmd.wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMetadataData is the data returned by the GETMETADATA command.
|
|
||||||
type GetMetadataData struct {
|
|
||||||
Mailbox string
|
|
||||||
Entries map[string]*[]byte
|
|
||||||
}
|
|
||||||
|
|
||||||
type metadataResp struct {
|
|
||||||
Mailbox string
|
|
||||||
EntryList []string
|
|
||||||
EntryValues map[string]*[]byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func readMetadataResp(dec *imapwire.Decoder) (*metadataResp, error) {
|
|
||||||
var data metadataResp
|
|
||||||
|
|
||||||
if !dec.ExpectMailbox(&data.Mailbox) || !dec.ExpectSP() {
|
|
||||||
return nil, dec.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
isList, err := dec.List(func() error {
|
|
||||||
var name string
|
|
||||||
if !dec.ExpectAString(&name) || !dec.ExpectSP() {
|
|
||||||
return dec.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: decode as []byte
|
|
||||||
var (
|
|
||||||
value *[]byte
|
|
||||||
s string
|
|
||||||
)
|
|
||||||
if dec.String(&s) || dec.Literal(&s) {
|
|
||||||
b := []byte(s)
|
|
||||||
value = &b
|
|
||||||
} else if !dec.ExpectNIL() {
|
|
||||||
return dec.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
if data.EntryValues == nil {
|
|
||||||
data.EntryValues = make(map[string]*[]byte)
|
|
||||||
}
|
|
||||||
data.EntryValues[name] = value
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if !isList {
|
|
||||||
var name string
|
|
||||||
if !dec.ExpectAString(&name) {
|
|
||||||
return nil, dec.Err()
|
|
||||||
}
|
|
||||||
data.EntryList = append(data.EntryList, name)
|
|
||||||
|
|
||||||
for dec.SP() {
|
|
||||||
if !dec.ExpectAString(&name) {
|
|
||||||
return nil, dec.Err()
|
|
||||||
}
|
|
||||||
data.EntryList = append(data.EntryList, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &data, nil
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user