Forked the emersion/go-imap v1 project.
This commit is contained in:
138
imapclient/acl.go
Normal file
138
imapclient/acl.go
Normal file
@@ -0,0 +1,138 @@
|
||||
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
|
||||
}
|
||||
115
imapclient/acl_test.go
Normal file
115
imapclient/acl_test.go
Normal file
@@ -0,0 +1,115 @@
|
||||
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.Errorf("SetACL().Wait() error: %v", err)
|
||||
}
|
||||
|
||||
// execute GETACL command to reset cache on server
|
||||
getACLData, err := client.GetACL(tc.mailbox).Wait()
|
||||
if err != nil {
|
||||
t.Errorf("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")
|
||||
}
|
||||
})
|
||||
}
|
||||
58
imapclient/append.go
Normal file
58
imapclient/append.go
Normal file
@@ -0,0 +1,58 @@
|
||||
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()
|
||||
}
|
||||
28
imapclient/append_test.go
Normal file
28
imapclient/append_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
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
|
||||
}
|
||||
100
imapclient/authenticate.go
Normal file
100
imapclient/authenticate.go
Normal file
@@ -0,0 +1,100 @@
|
||||
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
|
||||
}
|
||||
24
imapclient/authenticate_test.go
Normal file
24
imapclient/authenticate_test.go
Normal file
@@ -0,0 +1,24 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
55
imapclient/capability.go
Normal file
55
imapclient/capability.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"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() {
|
||||
}
|
||||
|
||||
var name string
|
||||
if !dec.ExpectAtom(&name) {
|
||||
return caps, fmt.Errorf("in capability-data: %v", dec.Err())
|
||||
}
|
||||
caps[imap.Cap(name)] = struct{}{}
|
||||
}
|
||||
return caps, nil
|
||||
}
|
||||
1215
imapclient/client.go
Normal file
1215
imapclient/client.go
Normal file
File diff suppressed because it is too large
Load Diff
277
imapclient/client_test.go
Normal file
277
imapclient/client_test.go
Normal file
@@ -0,0 +1,277 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
37
imapclient/copy.go
Normal file
37
imapclient/copy.go
Normal file
@@ -0,0 +1,37 @@
|
||||
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
|
||||
}
|
||||
21
imapclient/create.go
Normal file
21
imapclient/create.go
Normal file
@@ -0,0 +1,21 @@
|
||||
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
|
||||
}
|
||||
57
imapclient/create_test.go
Normal file
57
imapclient/create_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
64
imapclient/dovecot_test.go
Normal file
64
imapclient/dovecot_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
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 := `log_path = "` + tempDir + `/dovecot.log"
|
||||
ssl = no
|
||||
mail_home = "` + tempDir + `/%u"
|
||||
mail_location = maildir:~/Mail
|
||||
|
||||
namespace inbox {
|
||||
separator = /
|
||||
prefix =
|
||||
inbox = yes
|
||||
}
|
||||
|
||||
mail_plugins = $mail_plugins acl
|
||||
protocol imap {
|
||||
mail_plugins = $mail_plugins imap_acl
|
||||
}
|
||||
plugin {
|
||||
acl = 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()
|
||||
}
|
||||
69
imapclient/enable.go
Normal file
69
imapclient/enable.go
Normal file
@@ -0,0 +1,69 @@
|
||||
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
|
||||
}
|
||||
365
imapclient/example_test.go
Normal file
365
imapclient/example_test.go
Normal file
@@ -0,0 +1,365 @@
|
||||
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)
|
||||
}
|
||||
|
||||
// Wait for 30s to receive updates from the server
|
||||
time.Sleep(30 * time.Second)
|
||||
|
||||
// Stop idling
|
||||
if err := idleCmd.Close(); err != nil {
|
||||
log.Fatalf("failed to stop idling: %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)
|
||||
}
|
||||
}
|
||||
84
imapclient/expunge.go
Normal file
84
imapclient/expunge.go
Normal file
@@ -0,0 +1,84 @@
|
||||
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()
|
||||
}
|
||||
36
imapclient/expunge_test.go
Normal file
36
imapclient/expunge_test.go
Normal file
@@ -0,0 +1,36 @@
|
||||
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
Normal file
1326
imapclient/fetch.go
Normal file
File diff suppressed because it is too large
Load Diff
39
imapclient/fetch_test.go
Normal file
39
imapclient/fetch_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
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
Normal file
163
imapclient/id.go
Normal file
@@ -0,0 +1,163 @@
|
||||
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()
|
||||
}
|
||||
157
imapclient/idle.go
Normal file
157
imapclient/idle.go
Normal file
@@ -0,0 +1,157 @@
|
||||
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()
|
||||
}
|
||||
42
imapclient/idle_test.go
Normal file
42
imapclient/idle_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
259
imapclient/list.go
Normal file
259
imapclient/list.go
Normal file
@@ -0,0 +1,259 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
42
imapclient/list_test.go
Normal file
42
imapclient/list_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
205
imapclient/metadata.go
Normal file
205
imapclient/metadata.go
Normal file
@@ -0,0 +1,205 @@
|
||||
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
|
||||
}
|
||||
74
imapclient/move.go
Normal file
74
imapclient/move.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
// Move sends a MOVE command.
|
||||
//
|
||||
// If the server doesn't support IMAP4rev2 nor the MOVE extension, a fallback
|
||||
// with COPY + STORE + EXPUNGE commands is used.
|
||||
func (c *Client) Move(numSet imap.NumSet, mailbox string) *MoveCommand {
|
||||
// If the server doesn't support MOVE, fallback to [UID] COPY,
|
||||
// [UID] STORE +FLAGS.SILENT \Deleted and [UID] EXPUNGE
|
||||
cmdName := "MOVE"
|
||||
if !c.Caps().Has(imap.CapMove) {
|
||||
cmdName = "COPY"
|
||||
}
|
||||
|
||||
cmd := &MoveCommand{}
|
||||
enc := c.beginCommand(uidCmdName(cmdName, imapwire.NumSetKind(numSet)), cmd)
|
||||
enc.SP().NumSet(numSet).SP().Mailbox(mailbox)
|
||||
enc.end()
|
||||
|
||||
if cmdName == "COPY" {
|
||||
cmd.store = c.Store(numSet, &imap.StoreFlags{
|
||||
Op: imap.StoreFlagsAdd,
|
||||
Silent: true,
|
||||
Flags: []imap.Flag{imap.FlagDeleted},
|
||||
}, nil)
|
||||
if uidSet, ok := numSet.(imap.UIDSet); ok && c.Caps().Has(imap.CapUIDPlus) {
|
||||
cmd.expunge = c.UIDExpunge(uidSet)
|
||||
} else {
|
||||
cmd.expunge = c.Expunge()
|
||||
}
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// MoveCommand is a MOVE command.
|
||||
type MoveCommand struct {
|
||||
commandBase
|
||||
data MoveData
|
||||
|
||||
// Fallback
|
||||
store *FetchCommand
|
||||
expunge *ExpungeCommand
|
||||
}
|
||||
|
||||
func (cmd *MoveCommand) Wait() (*MoveData, error) {
|
||||
if err := cmd.wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cmd.store != nil {
|
||||
if err := cmd.store.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if cmd.expunge != nil {
|
||||
if err := cmd.expunge.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &cmd.data, nil
|
||||
}
|
||||
|
||||
// MoveData contains the data returned by a MOVE command.
|
||||
type MoveData struct {
|
||||
// requires UIDPLUS or IMAP4rev2
|
||||
UIDValidity uint32
|
||||
SourceUIDs imap.NumSet
|
||||
DestUIDs imap.NumSet
|
||||
}
|
||||
110
imapclient/namespace.go
Normal file
110
imapclient/namespace.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
// Namespace sends a NAMESPACE command.
|
||||
//
|
||||
// This command requires support for IMAP4rev2 or the NAMESPACE extension.
|
||||
func (c *Client) Namespace() *NamespaceCommand {
|
||||
cmd := &NamespaceCommand{}
|
||||
c.beginCommand("NAMESPACE", cmd).end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c *Client) handleNamespace() error {
|
||||
data, err := readNamespaceResponse(c.dec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("in namespace-response: %v", err)
|
||||
}
|
||||
if cmd := findPendingCmdByType[*NamespaceCommand](c); cmd != nil {
|
||||
cmd.data = *data
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NamespaceCommand is a NAMESPACE command.
|
||||
type NamespaceCommand struct {
|
||||
commandBase
|
||||
data imap.NamespaceData
|
||||
}
|
||||
|
||||
func (cmd *NamespaceCommand) Wait() (*imap.NamespaceData, error) {
|
||||
return &cmd.data, cmd.wait()
|
||||
}
|
||||
|
||||
func readNamespaceResponse(dec *imapwire.Decoder) (*imap.NamespaceData, error) {
|
||||
var (
|
||||
data imap.NamespaceData
|
||||
err error
|
||||
)
|
||||
|
||||
data.Personal, err = readNamespace(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !dec.ExpectSP() {
|
||||
return nil, dec.Err()
|
||||
}
|
||||
|
||||
data.Other, err = readNamespace(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !dec.ExpectSP() {
|
||||
return nil, dec.Err()
|
||||
}
|
||||
|
||||
data.Shared, err = readNamespace(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
func readNamespace(dec *imapwire.Decoder) ([]imap.NamespaceDescriptor, error) {
|
||||
var l []imap.NamespaceDescriptor
|
||||
err := dec.ExpectNList(func() error {
|
||||
descr, err := readNamespaceDescr(dec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("in namespace-descr: %v", err)
|
||||
}
|
||||
l = append(l, *descr)
|
||||
return nil
|
||||
})
|
||||
return l, err
|
||||
}
|
||||
|
||||
func readNamespaceDescr(dec *imapwire.Decoder) (*imap.NamespaceDescriptor, error) {
|
||||
var descr imap.NamespaceDescriptor
|
||||
|
||||
if !dec.ExpectSpecial('(') || !dec.ExpectString(&descr.Prefix) || !dec.ExpectSP() {
|
||||
return nil, dec.Err()
|
||||
}
|
||||
|
||||
var err error
|
||||
descr.Delim, err = readDelim(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Skip namespace-response-extensions
|
||||
for dec.SP() {
|
||||
if !dec.DiscardValue() {
|
||||
return nil, dec.Err()
|
||||
}
|
||||
}
|
||||
|
||||
if !dec.ExpectSpecial(')') {
|
||||
return nil, dec.Err()
|
||||
}
|
||||
|
||||
return &descr, nil
|
||||
}
|
||||
176
imapclient/quota.go
Normal file
176
imapclient/quota.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
// GetQuota sends a GETQUOTA command.
|
||||
//
|
||||
// This command requires support for the QUOTA extension.
|
||||
func (c *Client) GetQuota(root string) *GetQuotaCommand {
|
||||
cmd := &GetQuotaCommand{root: root}
|
||||
enc := c.beginCommand("GETQUOTA", cmd)
|
||||
enc.SP().String(root)
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
// GetQuotaRoot sends a GETQUOTAROOT command.
|
||||
//
|
||||
// This command requires support for the QUOTA extension.
|
||||
func (c *Client) GetQuotaRoot(mailbox string) *GetQuotaRootCommand {
|
||||
cmd := &GetQuotaRootCommand{mailbox: mailbox}
|
||||
enc := c.beginCommand("GETQUOTAROOT", cmd)
|
||||
enc.SP().Mailbox(mailbox)
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
// SetQuota sends a SETQUOTA command.
|
||||
//
|
||||
// This command requires support for the SETQUOTA extension.
|
||||
func (c *Client) SetQuota(root string, limits map[imap.QuotaResourceType]int64) *Command {
|
||||
// TODO: consider returning the QUOTA response data?
|
||||
cmd := &Command{}
|
||||
enc := c.beginCommand("SETQUOTA", cmd)
|
||||
enc.SP().String(root).SP().Special('(')
|
||||
i := 0
|
||||
for typ, limit := range limits {
|
||||
if i > 0 {
|
||||
enc.SP()
|
||||
}
|
||||
enc.Atom(string(typ)).SP().Number64(limit)
|
||||
i++
|
||||
}
|
||||
enc.Special(')')
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c *Client) handleQuota() error {
|
||||
data, err := readQuotaResponse(c.dec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("in quota-response: %v", err)
|
||||
}
|
||||
|
||||
cmd := c.findPendingCmdFunc(func(cmd command) bool {
|
||||
switch cmd := cmd.(type) {
|
||||
case *GetQuotaCommand:
|
||||
return cmd.root == data.Root
|
||||
case *GetQuotaRootCommand:
|
||||
for _, root := range cmd.roots {
|
||||
if root == data.Root {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})
|
||||
switch cmd := cmd.(type) {
|
||||
case *GetQuotaCommand:
|
||||
cmd.data = data
|
||||
case *GetQuotaRootCommand:
|
||||
cmd.data = append(cmd.data, *data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) handleQuotaRoot() error {
|
||||
mailbox, roots, err := readQuotaRoot(c.dec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("in quotaroot-response: %v", err)
|
||||
}
|
||||
|
||||
cmd := c.findPendingCmdFunc(func(anyCmd command) bool {
|
||||
cmd, ok := anyCmd.(*GetQuotaRootCommand)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return cmd.mailbox == mailbox
|
||||
})
|
||||
if cmd != nil {
|
||||
cmd := cmd.(*GetQuotaRootCommand)
|
||||
cmd.roots = roots
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetQuotaCommand is a GETQUOTA command.
|
||||
type GetQuotaCommand struct {
|
||||
commandBase
|
||||
root string
|
||||
data *QuotaData
|
||||
}
|
||||
|
||||
func (cmd *GetQuotaCommand) Wait() (*QuotaData, error) {
|
||||
if err := cmd.wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cmd.data, nil
|
||||
}
|
||||
|
||||
// GetQuotaRootCommand is a GETQUOTAROOT command.
|
||||
type GetQuotaRootCommand struct {
|
||||
commandBase
|
||||
mailbox string
|
||||
roots []string
|
||||
data []QuotaData
|
||||
}
|
||||
|
||||
func (cmd *GetQuotaRootCommand) Wait() ([]QuotaData, error) {
|
||||
if err := cmd.wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cmd.data, nil
|
||||
}
|
||||
|
||||
// QuotaData is the data returned by a QUOTA response.
|
||||
type QuotaData struct {
|
||||
Root string
|
||||
Resources map[imap.QuotaResourceType]QuotaResourceData
|
||||
}
|
||||
|
||||
// QuotaResourceData contains the usage and limit for a quota resource.
|
||||
type QuotaResourceData struct {
|
||||
Usage int64
|
||||
Limit int64
|
||||
}
|
||||
|
||||
func readQuotaResponse(dec *imapwire.Decoder) (*QuotaData, error) {
|
||||
var data QuotaData
|
||||
if !dec.ExpectAString(&data.Root) || !dec.ExpectSP() {
|
||||
return nil, dec.Err()
|
||||
}
|
||||
data.Resources = make(map[imap.QuotaResourceType]QuotaResourceData)
|
||||
err := dec.ExpectList(func() error {
|
||||
var (
|
||||
name string
|
||||
resData QuotaResourceData
|
||||
)
|
||||
if !dec.ExpectAtom(&name) || !dec.ExpectSP() || !dec.ExpectNumber64(&resData.Usage) || !dec.ExpectSP() || !dec.ExpectNumber64(&resData.Limit) {
|
||||
return fmt.Errorf("in quota-resource: %v", dec.Err())
|
||||
}
|
||||
data.Resources[imap.QuotaResourceType(name)] = resData
|
||||
return nil
|
||||
})
|
||||
return &data, err
|
||||
}
|
||||
|
||||
func readQuotaRoot(dec *imapwire.Decoder) (mailbox string, roots []string, err error) {
|
||||
if !dec.ExpectMailbox(&mailbox) {
|
||||
return "", nil, dec.Err()
|
||||
}
|
||||
for dec.SP() {
|
||||
var root string
|
||||
if !dec.ExpectAString(&root) {
|
||||
return "", nil, dec.Err()
|
||||
}
|
||||
roots = append(roots, root)
|
||||
}
|
||||
return mailbox, roots, nil
|
||||
}
|
||||
401
imapclient/search.go
Normal file
401
imapclient/search.go
Normal file
@@ -0,0 +1,401 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
func returnSearchOptions(options *imap.SearchOptions) []string {
|
||||
if options == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
m := map[string]bool{
|
||||
"MIN": options.ReturnMin,
|
||||
"MAX": options.ReturnMax,
|
||||
"ALL": options.ReturnAll,
|
||||
"COUNT": options.ReturnCount,
|
||||
}
|
||||
|
||||
var l []string
|
||||
for k, ret := range m {
|
||||
if ret {
|
||||
l = append(l, k)
|
||||
}
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
func (c *Client) search(numKind imapwire.NumKind, criteria *imap.SearchCriteria, options *imap.SearchOptions) *SearchCommand {
|
||||
// The IMAP4rev2 SEARCH charset defaults to UTF-8. When UTF8=ACCEPT is
|
||||
// enabled, specifying any CHARSET is invalid. For IMAP4rev1 the default is
|
||||
// undefined and only US-ASCII support is required. What's more, some
|
||||
// servers completely reject the CHARSET keyword. So, let's check if we
|
||||
// actually have UTF-8 strings in the search criteria before using that.
|
||||
// TODO: there might be a benefit in specifying CHARSET UTF-8 for IMAP4rev1
|
||||
// servers even if we only send ASCII characters: the server then must
|
||||
// decode encoded headers and Content-Transfer-Encoding before matching the
|
||||
// criteria.
|
||||
var charset string
|
||||
if !c.Caps().Has(imap.CapIMAP4rev2) && !c.enabled.Has(imap.CapUTF8Accept) && !searchCriteriaIsASCII(criteria) {
|
||||
charset = "UTF-8"
|
||||
}
|
||||
|
||||
var all imap.NumSet
|
||||
switch numKind {
|
||||
case imapwire.NumKindSeq:
|
||||
all = imap.SeqSet(nil)
|
||||
case imapwire.NumKindUID:
|
||||
all = imap.UIDSet(nil)
|
||||
}
|
||||
|
||||
cmd := &SearchCommand{}
|
||||
cmd.data.All = all
|
||||
enc := c.beginCommand(uidCmdName("SEARCH", numKind), cmd)
|
||||
if returnOpts := returnSearchOptions(options); len(returnOpts) > 0 {
|
||||
enc.SP().Atom("RETURN").SP().List(len(returnOpts), func(i int) {
|
||||
enc.Atom(returnOpts[i])
|
||||
})
|
||||
}
|
||||
enc.SP()
|
||||
if charset != "" {
|
||||
enc.Atom("CHARSET").SP().Atom(charset).SP()
|
||||
}
|
||||
writeSearchKey(enc.Encoder, criteria)
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Search sends a SEARCH command.
|
||||
func (c *Client) Search(criteria *imap.SearchCriteria, options *imap.SearchOptions) *SearchCommand {
|
||||
return c.search(imapwire.NumKindSeq, criteria, options)
|
||||
}
|
||||
|
||||
// UIDSearch sends a UID SEARCH command.
|
||||
func (c *Client) UIDSearch(criteria *imap.SearchCriteria, options *imap.SearchOptions) *SearchCommand {
|
||||
return c.search(imapwire.NumKindUID, criteria, options)
|
||||
}
|
||||
|
||||
func (c *Client) handleSearch() error {
|
||||
cmd := findPendingCmdByType[*SearchCommand](c)
|
||||
for c.dec.SP() {
|
||||
if c.dec.Special('(') {
|
||||
var name string
|
||||
if !c.dec.ExpectAtom(&name) || !c.dec.ExpectSP() {
|
||||
return c.dec.Err()
|
||||
} else if strings.ToUpper(name) != "MODSEQ" {
|
||||
return fmt.Errorf("in search-sort-mod-seq: expected %q, got %q", "MODSEQ", name)
|
||||
}
|
||||
var modSeq uint64
|
||||
if !c.dec.ExpectModSeq(&modSeq) || !c.dec.ExpectSpecial(')') {
|
||||
return c.dec.Err()
|
||||
}
|
||||
if cmd != nil {
|
||||
cmd.data.ModSeq = modSeq
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
var num uint32
|
||||
if !c.dec.ExpectNumber(&num) {
|
||||
return c.dec.Err()
|
||||
}
|
||||
if cmd != nil {
|
||||
switch all := cmd.data.All.(type) {
|
||||
case imap.SeqSet:
|
||||
all.AddNum(num)
|
||||
cmd.data.All = all
|
||||
case imap.UIDSet:
|
||||
all.AddNum(imap.UID(num))
|
||||
cmd.data.All = all
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) handleESearch() error {
|
||||
if !c.dec.ExpectSP() {
|
||||
return c.dec.Err()
|
||||
}
|
||||
tag, data, err := readESearchResponse(c.dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := c.findPendingCmdFunc(func(anyCmd command) bool {
|
||||
cmd, ok := anyCmd.(*SearchCommand)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if tag != "" {
|
||||
return cmd.tag == tag
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
})
|
||||
if cmd != nil {
|
||||
cmd := cmd.(*SearchCommand)
|
||||
cmd.data = *data
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SearchCommand is a SEARCH command.
|
||||
type SearchCommand struct {
|
||||
commandBase
|
||||
data imap.SearchData
|
||||
}
|
||||
|
||||
func (cmd *SearchCommand) Wait() (*imap.SearchData, error) {
|
||||
return &cmd.data, cmd.wait()
|
||||
}
|
||||
|
||||
func writeSearchKey(enc *imapwire.Encoder, criteria *imap.SearchCriteria) {
|
||||
firstItem := true
|
||||
encodeItem := func() *imapwire.Encoder {
|
||||
if !firstItem {
|
||||
enc.SP()
|
||||
}
|
||||
firstItem = false
|
||||
return enc
|
||||
}
|
||||
|
||||
for _, seqSet := range criteria.SeqNum {
|
||||
encodeItem().NumSet(seqSet)
|
||||
}
|
||||
for _, uidSet := range criteria.UID {
|
||||
encodeItem().Atom("UID").SP().NumSet(uidSet)
|
||||
}
|
||||
|
||||
if !criteria.Since.IsZero() && !criteria.Before.IsZero() && criteria.Before.Sub(criteria.Since) == 24*time.Hour {
|
||||
encodeItem().Atom("ON").SP().String(criteria.Since.Format(internal.DateLayout))
|
||||
} else {
|
||||
if !criteria.Since.IsZero() {
|
||||
encodeItem().Atom("SINCE").SP().String(criteria.Since.Format(internal.DateLayout))
|
||||
}
|
||||
if !criteria.Before.IsZero() {
|
||||
encodeItem().Atom("BEFORE").SP().String(criteria.Before.Format(internal.DateLayout))
|
||||
}
|
||||
}
|
||||
if !criteria.SentSince.IsZero() && !criteria.SentBefore.IsZero() && criteria.SentBefore.Sub(criteria.SentSince) == 24*time.Hour {
|
||||
encodeItem().Atom("SENTON").SP().String(criteria.SentSince.Format(internal.DateLayout))
|
||||
} else {
|
||||
if !criteria.SentSince.IsZero() {
|
||||
encodeItem().Atom("SENTSINCE").SP().String(criteria.SentSince.Format(internal.DateLayout))
|
||||
}
|
||||
if !criteria.SentBefore.IsZero() {
|
||||
encodeItem().Atom("SENTBEFORE").SP().String(criteria.SentBefore.Format(internal.DateLayout))
|
||||
}
|
||||
}
|
||||
|
||||
for _, kv := range criteria.Header {
|
||||
switch k := strings.ToUpper(kv.Key); k {
|
||||
case "BCC", "CC", "FROM", "SUBJECT", "TO":
|
||||
encodeItem().Atom(k)
|
||||
default:
|
||||
encodeItem().Atom("HEADER").SP().String(kv.Key)
|
||||
}
|
||||
enc.SP().String(kv.Value)
|
||||
}
|
||||
|
||||
for _, s := range criteria.Body {
|
||||
encodeItem().Atom("BODY").SP().String(s)
|
||||
}
|
||||
for _, s := range criteria.Text {
|
||||
encodeItem().Atom("TEXT").SP().String(s)
|
||||
}
|
||||
|
||||
for _, flag := range criteria.Flag {
|
||||
if k := flagSearchKey(flag); k != "" {
|
||||
encodeItem().Atom(k)
|
||||
} else {
|
||||
encodeItem().Atom("KEYWORD").SP().Flag(flag)
|
||||
}
|
||||
}
|
||||
for _, flag := range criteria.NotFlag {
|
||||
if k := flagSearchKey(flag); k != "" {
|
||||
encodeItem().Atom("UN" + k)
|
||||
} else {
|
||||
encodeItem().Atom("UNKEYWORD").SP().Flag(flag)
|
||||
}
|
||||
}
|
||||
|
||||
if criteria.Larger > 0 {
|
||||
encodeItem().Atom("LARGER").SP().Number64(criteria.Larger)
|
||||
}
|
||||
if criteria.Smaller > 0 {
|
||||
encodeItem().Atom("SMALLER").SP().Number64(criteria.Smaller)
|
||||
}
|
||||
|
||||
if modSeq := criteria.ModSeq; modSeq != nil {
|
||||
encodeItem().Atom("MODSEQ")
|
||||
if modSeq.MetadataName != "" && modSeq.MetadataType != "" {
|
||||
enc.SP().Quoted(modSeq.MetadataName).SP().Atom(string(modSeq.MetadataType))
|
||||
}
|
||||
enc.SP()
|
||||
if modSeq.ModSeq != 0 {
|
||||
enc.ModSeq(modSeq.ModSeq)
|
||||
} else {
|
||||
enc.Atom("0")
|
||||
}
|
||||
}
|
||||
|
||||
for _, not := range criteria.Not {
|
||||
encodeItem().Atom("NOT").SP()
|
||||
enc.Special('(')
|
||||
writeSearchKey(enc, ¬)
|
||||
enc.Special(')')
|
||||
}
|
||||
for _, or := range criteria.Or {
|
||||
encodeItem().Atom("OR").SP()
|
||||
enc.Special('(')
|
||||
writeSearchKey(enc, &or[0])
|
||||
enc.Special(')')
|
||||
enc.SP()
|
||||
enc.Special('(')
|
||||
writeSearchKey(enc, &or[1])
|
||||
enc.Special(')')
|
||||
}
|
||||
|
||||
if firstItem {
|
||||
enc.Atom("ALL")
|
||||
}
|
||||
}
|
||||
|
||||
func flagSearchKey(flag imap.Flag) string {
|
||||
switch flag {
|
||||
case imap.FlagAnswered, imap.FlagDeleted, imap.FlagDraft, imap.FlagFlagged, imap.FlagSeen:
|
||||
return strings.ToUpper(strings.TrimPrefix(string(flag), "\\"))
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func readESearchResponse(dec *imapwire.Decoder) (tag string, data *imap.SearchData, err error) {
|
||||
data = &imap.SearchData{}
|
||||
if dec.Special('(') { // search-correlator
|
||||
var correlator string
|
||||
if !dec.ExpectAtom(&correlator) || !dec.ExpectSP() || !dec.ExpectAString(&tag) || !dec.ExpectSpecial(')') {
|
||||
return "", nil, dec.Err()
|
||||
}
|
||||
if correlator != "TAG" {
|
||||
return "", nil, fmt.Errorf("in search-correlator: name must be TAG, but got %q", correlator)
|
||||
}
|
||||
}
|
||||
|
||||
var name string
|
||||
if !dec.SP() {
|
||||
return tag, data, nil
|
||||
} else if !dec.ExpectAtom(&name) {
|
||||
return "", nil, dec.Err()
|
||||
}
|
||||
data.UID = name == "UID"
|
||||
|
||||
if data.UID {
|
||||
if !dec.SP() {
|
||||
return tag, data, nil
|
||||
} else if !dec.ExpectAtom(&name) {
|
||||
return "", nil, dec.Err()
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
if !dec.ExpectSP() {
|
||||
return "", nil, dec.Err()
|
||||
}
|
||||
|
||||
switch strings.ToUpper(name) {
|
||||
case "MIN":
|
||||
var num uint32
|
||||
if !dec.ExpectNumber(&num) {
|
||||
return "", nil, dec.Err()
|
||||
}
|
||||
data.Min = num
|
||||
case "MAX":
|
||||
var num uint32
|
||||
if !dec.ExpectNumber(&num) {
|
||||
return "", nil, dec.Err()
|
||||
}
|
||||
data.Max = num
|
||||
case "ALL":
|
||||
numKind := imapwire.NumKindSeq
|
||||
if data.UID {
|
||||
numKind = imapwire.NumKindUID
|
||||
}
|
||||
if !dec.ExpectNumSet(numKind, &data.All) {
|
||||
return "", nil, dec.Err()
|
||||
}
|
||||
if data.All.Dynamic() {
|
||||
return "", nil, fmt.Errorf("imapclient: server returned a dynamic ALL number set in SEARCH response")
|
||||
}
|
||||
case "COUNT":
|
||||
var num uint32
|
||||
if !dec.ExpectNumber(&num) {
|
||||
return "", nil, dec.Err()
|
||||
}
|
||||
data.Count = num
|
||||
case "MODSEQ":
|
||||
var modSeq uint64
|
||||
if !dec.ExpectModSeq(&modSeq) {
|
||||
return "", nil, dec.Err()
|
||||
}
|
||||
data.ModSeq = modSeq
|
||||
default:
|
||||
if !dec.DiscardValue() {
|
||||
return "", nil, dec.Err()
|
||||
}
|
||||
}
|
||||
|
||||
if !dec.SP() {
|
||||
break
|
||||
} else if !dec.ExpectAtom(&name) {
|
||||
return "", nil, dec.Err()
|
||||
}
|
||||
}
|
||||
|
||||
return tag, data, nil
|
||||
}
|
||||
|
||||
func searchCriteriaIsASCII(criteria *imap.SearchCriteria) bool {
|
||||
for _, kv := range criteria.Header {
|
||||
if !isASCII(kv.Key) || !isASCII(kv.Value) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, s := range criteria.Body {
|
||||
if !isASCII(s) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, s := range criteria.Text {
|
||||
if !isASCII(s) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, not := range criteria.Not {
|
||||
if !searchCriteriaIsASCII(¬) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, or := range criteria.Or {
|
||||
if !searchCriteriaIsASCII(&or[0]) || !searchCriteriaIsASCII(&or[1]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isASCII(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] > unicode.MaxASCII {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
61
imapclient/search_test.go
Normal file
61
imapclient/search_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package imapclient_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
)
|
||||
|
||||
func TestSearch(t *testing.T) {
|
||||
client, server := newClientServerPair(t, imap.ConnStateSelected)
|
||||
defer client.Close()
|
||||
defer server.Close()
|
||||
|
||||
criteria := imap.SearchCriteria{
|
||||
Header: []imap.SearchCriteriaHeaderField{{
|
||||
Key: "Message-Id",
|
||||
Value: "<191101702316132@example.com>",
|
||||
}},
|
||||
}
|
||||
data, err := client.Search(&criteria, nil).Wait()
|
||||
if err != nil {
|
||||
t.Fatalf("Search().Wait() = %v", err)
|
||||
}
|
||||
seqSet, ok := data.All.(imap.SeqSet)
|
||||
if !ok {
|
||||
t.Fatalf("SearchData.All = %T, want SeqSet", data.All)
|
||||
}
|
||||
nums, _ := seqSet.Nums()
|
||||
want := []uint32{1}
|
||||
if !reflect.DeepEqual(nums, want) {
|
||||
t.Errorf("SearchData.All.Nums() = %v, want %v", nums, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestESearch(t *testing.T) {
|
||||
client, server := newClientServerPair(t, imap.ConnStateSelected)
|
||||
defer client.Close()
|
||||
defer server.Close()
|
||||
|
||||
if !client.Caps().Has(imap.CapESearch) {
|
||||
t.Skip("server doesn't support ESEARCH")
|
||||
}
|
||||
|
||||
criteria := imap.SearchCriteria{
|
||||
Header: []imap.SearchCriteriaHeaderField{{
|
||||
Key: "Message-Id",
|
||||
Value: "<191101702316132@example.com>",
|
||||
}},
|
||||
}
|
||||
options := imap.SearchOptions{
|
||||
ReturnCount: true,
|
||||
}
|
||||
data, err := client.Search(&criteria, &options).Wait()
|
||||
if err != nil {
|
||||
t.Fatalf("Search().Wait() = %v", err)
|
||||
}
|
||||
if want := uint32(1); data.Count != want {
|
||||
t.Errorf("Count = %v, want %v", data.Count, want)
|
||||
}
|
||||
}
|
||||
100
imapclient/select.go
Normal file
100
imapclient/select.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal"
|
||||
)
|
||||
|
||||
// Select sends a SELECT or EXAMINE command.
|
||||
//
|
||||
// A nil options pointer is equivalent to a zero options value.
|
||||
func (c *Client) Select(mailbox string, options *imap.SelectOptions) *SelectCommand {
|
||||
cmdName := "SELECT"
|
||||
if options != nil && options.ReadOnly {
|
||||
cmdName = "EXAMINE"
|
||||
}
|
||||
|
||||
cmd := &SelectCommand{mailbox: mailbox}
|
||||
enc := c.beginCommand(cmdName, cmd)
|
||||
enc.SP().Mailbox(mailbox)
|
||||
if options != nil && options.CondStore {
|
||||
enc.SP().Special('(').Atom("CONDSTORE").Special(')')
|
||||
}
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Unselect sends an UNSELECT command.
|
||||
//
|
||||
// This command requires support for IMAP4rev2 or the UNSELECT extension.
|
||||
func (c *Client) Unselect() *Command {
|
||||
cmd := &unselectCommand{}
|
||||
c.beginCommand("UNSELECT", cmd).end()
|
||||
return &cmd.Command
|
||||
}
|
||||
|
||||
// UnselectAndExpunge sends a CLOSE command.
|
||||
//
|
||||
// CLOSE implicitly performs a silent EXPUNGE command.
|
||||
func (c *Client) UnselectAndExpunge() *Command {
|
||||
cmd := &unselectCommand{}
|
||||
c.beginCommand("CLOSE", cmd).end()
|
||||
return &cmd.Command
|
||||
}
|
||||
|
||||
func (c *Client) handleFlags() error {
|
||||
flags, err := internal.ExpectFlagList(c.dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.mutex.Lock()
|
||||
if c.state == imap.ConnStateSelected {
|
||||
c.mailbox = c.mailbox.copy()
|
||||
c.mailbox.PermanentFlags = flags
|
||||
}
|
||||
c.mutex.Unlock()
|
||||
|
||||
cmd := findPendingCmdByType[*SelectCommand](c)
|
||||
if cmd != nil {
|
||||
cmd.data.Flags = flags
|
||||
} else if handler := c.options.unilateralDataHandler().Mailbox; handler != nil {
|
||||
handler(&UnilateralDataMailbox{Flags: flags})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) handleExists(num uint32) error {
|
||||
cmd := findPendingCmdByType[*SelectCommand](c)
|
||||
if cmd != nil {
|
||||
cmd.data.NumMessages = num
|
||||
} else {
|
||||
c.mutex.Lock()
|
||||
if c.state == imap.ConnStateSelected {
|
||||
c.mailbox = c.mailbox.copy()
|
||||
c.mailbox.NumMessages = num
|
||||
}
|
||||
c.mutex.Unlock()
|
||||
|
||||
if handler := c.options.unilateralDataHandler().Mailbox; handler != nil {
|
||||
handler(&UnilateralDataMailbox{NumMessages: &num})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SelectCommand is a SELECT command.
|
||||
type SelectCommand struct {
|
||||
commandBase
|
||||
mailbox string
|
||||
data imap.SelectData
|
||||
}
|
||||
|
||||
func (cmd *SelectCommand) Wait() (*imap.SelectData, error) {
|
||||
return &cmd.data, cmd.wait()
|
||||
}
|
||||
|
||||
type unselectCommand struct {
|
||||
Command
|
||||
}
|
||||
20
imapclient/select_test.go
Normal file
20
imapclient/select_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package imapclient_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
)
|
||||
|
||||
func TestSelect(t *testing.T) {
|
||||
client, server := newClientServerPair(t, imap.ConnStateAuthenticated)
|
||||
defer client.Close()
|
||||
defer server.Close()
|
||||
|
||||
data, err := client.Select("INBOX", nil).Wait()
|
||||
if err != nil {
|
||||
t.Fatalf("Select() = %v", err)
|
||||
} else if data.NumMessages != 1 {
|
||||
t.Errorf("SelectData.NumMessages = %v, want %v", data.NumMessages, 1)
|
||||
}
|
||||
}
|
||||
84
imapclient/sort.go
Normal file
84
imapclient/sort.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
type SortKey string
|
||||
|
||||
const (
|
||||
SortKeyArrival SortKey = "ARRIVAL"
|
||||
SortKeyCc SortKey = "CC"
|
||||
SortKeyDate SortKey = "DATE"
|
||||
SortKeyFrom SortKey = "FROM"
|
||||
SortKeySize SortKey = "SIZE"
|
||||
SortKeySubject SortKey = "SUBJECT"
|
||||
SortKeyTo SortKey = "TO"
|
||||
)
|
||||
|
||||
type SortCriterion struct {
|
||||
Key SortKey
|
||||
Reverse bool
|
||||
}
|
||||
|
||||
// SortOptions contains options for the SORT command.
|
||||
type SortOptions struct {
|
||||
SearchCriteria *imap.SearchCriteria
|
||||
SortCriteria []SortCriterion
|
||||
}
|
||||
|
||||
func (c *Client) sort(numKind imapwire.NumKind, options *SortOptions) *SortCommand {
|
||||
cmd := &SortCommand{}
|
||||
enc := c.beginCommand(uidCmdName("SORT", numKind), cmd)
|
||||
enc.SP().List(len(options.SortCriteria), func(i int) {
|
||||
criterion := options.SortCriteria[i]
|
||||
if criterion.Reverse {
|
||||
enc.Atom("REVERSE").SP()
|
||||
}
|
||||
enc.Atom(string(criterion.Key))
|
||||
})
|
||||
enc.SP().Atom("UTF-8").SP()
|
||||
writeSearchKey(enc.Encoder, options.SearchCriteria)
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c *Client) handleSort() error {
|
||||
cmd := findPendingCmdByType[*SortCommand](c)
|
||||
for c.dec.SP() {
|
||||
var num uint32
|
||||
if !c.dec.ExpectNumber(&num) {
|
||||
return c.dec.Err()
|
||||
}
|
||||
if cmd != nil {
|
||||
cmd.nums = append(cmd.nums, num)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sort sends a SORT command.
|
||||
//
|
||||
// This command requires support for the SORT extension.
|
||||
func (c *Client) Sort(options *SortOptions) *SortCommand {
|
||||
return c.sort(imapwire.NumKindSeq, options)
|
||||
}
|
||||
|
||||
// UIDSort sends a UID SORT command.
|
||||
//
|
||||
// See Sort.
|
||||
func (c *Client) UIDSort(options *SortOptions) *SortCommand {
|
||||
return c.sort(imapwire.NumKindUID, options)
|
||||
}
|
||||
|
||||
// SortCommand is a SORT command.
|
||||
type SortCommand struct {
|
||||
commandBase
|
||||
nums []uint32
|
||||
}
|
||||
|
||||
func (cmd *SortCommand) Wait() ([]uint32, error) {
|
||||
err := cmd.wait()
|
||||
return cmd.nums, err
|
||||
}
|
||||
83
imapclient/starttls.go
Normal file
83
imapclient/starttls.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
)
|
||||
|
||||
// startTLS sends a STARTTLS command.
|
||||
//
|
||||
// Unlike other commands, this method blocks until the command completes.
|
||||
func (c *Client) startTLS(config *tls.Config) error {
|
||||
upgradeDone := make(chan struct{})
|
||||
cmd := &startTLSCommand{
|
||||
tlsConfig: config,
|
||||
upgradeDone: upgradeDone,
|
||||
}
|
||||
enc := c.beginCommand("STARTTLS", cmd)
|
||||
enc.flush()
|
||||
defer enc.end()
|
||||
|
||||
// Once a client issues a STARTTLS command, it MUST NOT issue further
|
||||
// commands until a server response is seen and the TLS negotiation is
|
||||
// complete
|
||||
|
||||
if err := cmd.wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The decoder goroutine will invoke Client.upgradeStartTLS
|
||||
<-upgradeDone
|
||||
|
||||
return cmd.tlsConn.Handshake()
|
||||
}
|
||||
|
||||
// upgradeStartTLS finishes the STARTTLS upgrade after the server has sent an
|
||||
// OK response. It runs in the decoder goroutine.
|
||||
func (c *Client) upgradeStartTLS(startTLS *startTLSCommand) {
|
||||
defer close(startTLS.upgradeDone)
|
||||
|
||||
// Drain buffered data from our bufio.Reader
|
||||
var buf bytes.Buffer
|
||||
if _, err := io.CopyN(&buf, c.br, int64(c.br.Buffered())); err != nil {
|
||||
panic(err) // unreachable
|
||||
}
|
||||
|
||||
var cleartextConn net.Conn
|
||||
if buf.Len() > 0 {
|
||||
r := io.MultiReader(&buf, c.conn)
|
||||
cleartextConn = startTLSConn{c.conn, r}
|
||||
} else {
|
||||
cleartextConn = c.conn
|
||||
}
|
||||
|
||||
tlsConn := tls.Client(cleartextConn, startTLS.tlsConfig)
|
||||
rw := c.options.wrapReadWriter(tlsConn)
|
||||
|
||||
c.br.Reset(rw)
|
||||
// Unfortunately we can't re-use the bufio.Writer here, it races with
|
||||
// Client.StartTLS
|
||||
c.bw = bufio.NewWriter(rw)
|
||||
|
||||
startTLS.tlsConn = tlsConn
|
||||
}
|
||||
|
||||
type startTLSCommand struct {
|
||||
commandBase
|
||||
tlsConfig *tls.Config
|
||||
|
||||
upgradeDone chan<- struct{}
|
||||
tlsConn *tls.Conn
|
||||
}
|
||||
|
||||
type startTLSConn struct {
|
||||
net.Conn
|
||||
r io.Reader
|
||||
}
|
||||
|
||||
func (conn startTLSConn) Read(b []byte) (int, error) {
|
||||
return conn.r.Read(b)
|
||||
}
|
||||
27
imapclient/starttls_test.go
Normal file
27
imapclient/starttls_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package imapclient_test
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"testing"
|
||||
|
||||
"github.com/emersion/go-imap/v2/imapclient"
|
||||
)
|
||||
|
||||
func TestStartTLS(t *testing.T) {
|
||||
conn, server := newMemClientServerPair(t)
|
||||
defer conn.Close()
|
||||
defer server.Close()
|
||||
|
||||
options := imapclient.Options{
|
||||
TLSConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
client, err := imapclient.NewStartTLS(conn, &options)
|
||||
if err != nil {
|
||||
t.Fatalf("NewStartTLS() = %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
if err := client.Noop().Wait(); err != nil {
|
||||
t.Fatalf("Noop().Wait() = %v", err)
|
||||
}
|
||||
}
|
||||
164
imapclient/status.go
Normal file
164
imapclient/status.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
func statusItems(options *imap.StatusOptions) []string {
|
||||
m := map[string]bool{
|
||||
"MESSAGES": options.NumMessages,
|
||||
"UIDNEXT": options.UIDNext,
|
||||
"UIDVALIDITY": options.UIDValidity,
|
||||
"UNSEEN": options.NumUnseen,
|
||||
"DELETED": options.NumDeleted,
|
||||
"SIZE": options.Size,
|
||||
"APPENDLIMIT": options.AppendLimit,
|
||||
"DELETED-STORAGE": options.DeletedStorage,
|
||||
"HIGHESTMODSEQ": options.HighestModSeq,
|
||||
}
|
||||
|
||||
var l []string
|
||||
for k, req := range m {
|
||||
if req {
|
||||
l = append(l, k)
|
||||
}
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// Status sends a STATUS command.
|
||||
//
|
||||
// A nil options pointer is equivalent to a zero options value.
|
||||
func (c *Client) Status(mailbox string, options *imap.StatusOptions) *StatusCommand {
|
||||
if options == nil {
|
||||
options = new(imap.StatusOptions)
|
||||
}
|
||||
if options.NumRecent {
|
||||
panic("StatusOptions.NumRecent is not supported in imapclient")
|
||||
}
|
||||
|
||||
cmd := &StatusCommand{mailbox: mailbox}
|
||||
enc := c.beginCommand("STATUS", cmd)
|
||||
enc.SP().Mailbox(mailbox).SP()
|
||||
items := statusItems(options)
|
||||
enc.List(len(items), func(i int) {
|
||||
enc.Atom(items[i])
|
||||
})
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c *Client) handleStatus() error {
|
||||
data, err := readStatus(c.dec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("in status: %v", err)
|
||||
}
|
||||
|
||||
cmd := c.findPendingCmdFunc(func(cmd command) bool {
|
||||
switch cmd := cmd.(type) {
|
||||
case *StatusCommand:
|
||||
return cmd.mailbox == data.Mailbox
|
||||
case *ListCommand:
|
||||
return cmd.returnStatus && cmd.pendingData != nil && cmd.pendingData.Mailbox == data.Mailbox
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})
|
||||
switch cmd := cmd.(type) {
|
||||
case *StatusCommand:
|
||||
cmd.data = *data
|
||||
case *ListCommand:
|
||||
cmd.pendingData.Status = data
|
||||
cmd.mailboxes <- cmd.pendingData
|
||||
cmd.pendingData = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StatusCommand is a STATUS command.
|
||||
type StatusCommand struct {
|
||||
commandBase
|
||||
mailbox string
|
||||
data imap.StatusData
|
||||
}
|
||||
|
||||
func (cmd *StatusCommand) Wait() (*imap.StatusData, error) {
|
||||
return &cmd.data, cmd.wait()
|
||||
}
|
||||
|
||||
func readStatus(dec *imapwire.Decoder) (*imap.StatusData, error) {
|
||||
var data imap.StatusData
|
||||
|
||||
if !dec.ExpectMailbox(&data.Mailbox) || !dec.ExpectSP() {
|
||||
return nil, dec.Err()
|
||||
}
|
||||
|
||||
err := dec.ExpectList(func() error {
|
||||
if err := readStatusAttVal(dec, &data); err != nil {
|
||||
return fmt.Errorf("in status-att-val: %v", dec.Err())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return &data, err
|
||||
}
|
||||
|
||||
func readStatusAttVal(dec *imapwire.Decoder, data *imap.StatusData) error {
|
||||
var name string
|
||||
if !dec.ExpectAtom(&name) || !dec.ExpectSP() {
|
||||
return dec.Err()
|
||||
}
|
||||
|
||||
var ok bool
|
||||
switch strings.ToUpper(name) {
|
||||
case "MESSAGES":
|
||||
var num uint32
|
||||
ok = dec.ExpectNumber(&num)
|
||||
data.NumMessages = &num
|
||||
case "UIDNEXT":
|
||||
var uidNext imap.UID
|
||||
ok = dec.ExpectUID(&uidNext)
|
||||
data.UIDNext = uidNext
|
||||
case "UIDVALIDITY":
|
||||
ok = dec.ExpectNumber(&data.UIDValidity)
|
||||
case "UNSEEN":
|
||||
var num uint32
|
||||
ok = dec.ExpectNumber(&num)
|
||||
data.NumUnseen = &num
|
||||
case "DELETED":
|
||||
var num uint32
|
||||
ok = dec.ExpectNumber(&num)
|
||||
data.NumDeleted = &num
|
||||
case "SIZE":
|
||||
var size int64
|
||||
ok = dec.ExpectNumber64(&size)
|
||||
data.Size = &size
|
||||
case "APPENDLIMIT":
|
||||
var num uint32
|
||||
if dec.Number(&num) {
|
||||
ok = true
|
||||
} else {
|
||||
ok = dec.ExpectNIL()
|
||||
num = ^uint32(0)
|
||||
}
|
||||
data.AppendLimit = &num
|
||||
case "DELETED-STORAGE":
|
||||
var storage int64
|
||||
ok = dec.ExpectNumber64(&storage)
|
||||
data.DeletedStorage = &storage
|
||||
case "HIGHESTMODSEQ":
|
||||
ok = dec.ExpectModSeq(&data.HighestModSeq)
|
||||
default:
|
||||
if !dec.DiscardValue() {
|
||||
return dec.Err()
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return dec.Err()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
34
imapclient/status_test.go
Normal file
34
imapclient/status_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package imapclient_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
)
|
||||
|
||||
func TestStatus(t *testing.T) {
|
||||
client, server := newClientServerPair(t, imap.ConnStateAuthenticated)
|
||||
defer client.Close()
|
||||
defer server.Close()
|
||||
|
||||
options := imap.StatusOptions{
|
||||
NumMessages: true,
|
||||
NumUnseen: true,
|
||||
}
|
||||
data, err := client.Status("INBOX", &options).Wait()
|
||||
if err != nil {
|
||||
t.Fatalf("Status() = %v", err)
|
||||
}
|
||||
|
||||
wantNumMessages := uint32(1)
|
||||
wantNumUnseen := uint32(1)
|
||||
want := &imap.StatusData{
|
||||
Mailbox: "INBOX",
|
||||
NumMessages: &wantNumMessages,
|
||||
NumUnseen: &wantNumUnseen,
|
||||
}
|
||||
if !reflect.DeepEqual(data, want) {
|
||||
t.Errorf("Status() = %#v but want %#v", data, want)
|
||||
}
|
||||
}
|
||||
44
imapclient/store.go
Normal file
44
imapclient/store.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
// Store sends a STORE command.
|
||||
//
|
||||
// Unless StoreFlags.Silent is set, the server will return the updated values.
|
||||
//
|
||||
// A nil options pointer is equivalent to a zero options value.
|
||||
func (c *Client) Store(numSet imap.NumSet, store *imap.StoreFlags, options *imap.StoreOptions) *FetchCommand {
|
||||
cmd := &FetchCommand{
|
||||
numSet: numSet,
|
||||
msgs: make(chan *FetchMessageData, 128),
|
||||
}
|
||||
enc := c.beginCommand(uidCmdName("STORE", imapwire.NumSetKind(numSet)), cmd)
|
||||
enc.SP().NumSet(numSet).SP()
|
||||
if options != nil && options.UnchangedSince != 0 {
|
||||
enc.Special('(').Atom("UNCHANGEDSINCE").SP().ModSeq(options.UnchangedSince).Special(')').SP()
|
||||
}
|
||||
switch store.Op {
|
||||
case imap.StoreFlagsSet:
|
||||
// nothing to do
|
||||
case imap.StoreFlagsAdd:
|
||||
enc.Special('+')
|
||||
case imap.StoreFlagsDel:
|
||||
enc.Special('-')
|
||||
default:
|
||||
panic(fmt.Errorf("imapclient: unknown store flags op: %v", store.Op))
|
||||
}
|
||||
enc.Atom("FLAGS")
|
||||
if store.Silent {
|
||||
enc.Atom(".SILENT")
|
||||
}
|
||||
enc.SP().List(len(store.Flags), func(i int) {
|
||||
enc.Flag(store.Flags[i])
|
||||
})
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
40
imapclient/store_test.go
Normal file
40
imapclient/store_test.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package imapclient_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
)
|
||||
|
||||
func TestStore(t *testing.T) {
|
||||
client, server := newClientServerPair(t, imap.ConnStateSelected)
|
||||
defer client.Close()
|
||||
defer server.Close()
|
||||
|
||||
seqSet := imap.SeqSetNum(1)
|
||||
storeFlags := imap.StoreFlags{
|
||||
Op: imap.StoreFlagsAdd,
|
||||
Flags: []imap.Flag{imap.FlagDeleted},
|
||||
}
|
||||
msgs, err := client.Store(seqSet, &storeFlags, nil).Collect()
|
||||
if err != nil {
|
||||
t.Fatalf("Store().Collect() = %v", err)
|
||||
} else if len(msgs) != 1 {
|
||||
t.Fatalf("len(msgs) = %v, want %v", len(msgs), 1)
|
||||
}
|
||||
msg := msgs[0]
|
||||
if msg.SeqNum != 1 {
|
||||
t.Errorf("msg.SeqNum = %v, want %v", msg.SeqNum, 1)
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, f := range msg.Flags {
|
||||
if f == imap.FlagDeleted {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("msg.Flags is missing deleted flag: %v", msg.Flags)
|
||||
}
|
||||
}
|
||||
85
imapclient/thread.go
Normal file
85
imapclient/thread.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
// ThreadOptions contains options for the THREAD command.
|
||||
type ThreadOptions struct {
|
||||
Algorithm imap.ThreadAlgorithm
|
||||
SearchCriteria *imap.SearchCriteria
|
||||
}
|
||||
|
||||
func (c *Client) thread(numKind imapwire.NumKind, options *ThreadOptions) *ThreadCommand {
|
||||
cmd := &ThreadCommand{}
|
||||
enc := c.beginCommand(uidCmdName("THREAD", numKind), cmd)
|
||||
enc.SP().Atom(string(options.Algorithm)).SP().Atom("UTF-8").SP()
|
||||
writeSearchKey(enc.Encoder, options.SearchCriteria)
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Thread sends a THREAD command.
|
||||
//
|
||||
// This command requires support for the THREAD extension.
|
||||
func (c *Client) Thread(options *ThreadOptions) *ThreadCommand {
|
||||
return c.thread(imapwire.NumKindSeq, options)
|
||||
}
|
||||
|
||||
// UIDThread sends a UID THREAD command.
|
||||
//
|
||||
// See Thread.
|
||||
func (c *Client) UIDThread(options *ThreadOptions) *ThreadCommand {
|
||||
return c.thread(imapwire.NumKindUID, options)
|
||||
}
|
||||
|
||||
func (c *Client) handleThread() error {
|
||||
cmd := findPendingCmdByType[*ThreadCommand](c)
|
||||
for c.dec.SP() {
|
||||
data, err := readThreadList(c.dec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("in thread-list: %v", err)
|
||||
}
|
||||
if cmd != nil {
|
||||
cmd.data = append(cmd.data, *data)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ThreadCommand is a THREAD command.
|
||||
type ThreadCommand struct {
|
||||
commandBase
|
||||
data []ThreadData
|
||||
}
|
||||
|
||||
func (cmd *ThreadCommand) Wait() ([]ThreadData, error) {
|
||||
err := cmd.wait()
|
||||
return cmd.data, err
|
||||
}
|
||||
|
||||
type ThreadData struct {
|
||||
Chain []uint32
|
||||
SubThreads []ThreadData
|
||||
}
|
||||
|
||||
func readThreadList(dec *imapwire.Decoder) (*ThreadData, error) {
|
||||
var data ThreadData
|
||||
err := dec.ExpectList(func() error {
|
||||
var num uint32
|
||||
if len(data.SubThreads) == 0 && dec.Number(&num) {
|
||||
data.Chain = append(data.Chain, num)
|
||||
} else {
|
||||
sub, err := readThreadList(dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data.SubThreads = append(data.SubThreads, *sub)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return &data, err
|
||||
}
|
||||
Reference in New Issue
Block a user