Merge pull request 'Fixing to have the proper version of go-imap from foxcpp.' (#1) from main into v1

Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
ace
2025-12-08 20:53:18 +00:00
207 changed files with 15166 additions and 15437 deletions

View File

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

171
README.md
View File

@@ -1,29 +1,170 @@
# go-imap # go-imap
[![Go Reference](https://pkg.go.dev/badge/github.com/emersion/go-imap/v2.svg)](https://pkg.go.dev/github.com/emersion/go-imap/v2) [![GoDoc](https://godoc.org/github.com/emersion/go-imap?status.svg)](https://godoc.org/github.com/emersion/go-imap)
[![Build Status](https://travis-ci.org/emersion/go-imap.svg?branch=master)](https://travis-ci.org/emersion/go-imap)
[![Codecov](https://codecov.io/gh/emersion/go-imap/branch/master/graph/badge.svg)](https://codecov.io/gh/emersion/go-imap)
[![Go Report
Card](https://goreportcard.com/badge/github.com/emersion/go-imap)](https://goreportcard.com/report/github.com/emersion/go-imap)
[![Unstable](https://img.shields.io/badge/stability-unstable-yellow.svg)](https://github.com/emersion/stability-badges#unstable)
An [IMAP4rev2] library for Go. An [IMAP4rev1](https://tools.ietf.org/html/rfc3501) library written in Go. It
can be used to build a client and/or a server.
> **Note** ```shell
> This is the README for go-imap v2. This new major version is still in go get github.com/emersion/go-imap/...
> development. For go-imap v1, see the [v1 branch]. ```
## Usage ## Usage
To add go-imap to your project, run: ### Client [![GoDoc](https://godoc.org/github.com/emersion/go-imap/client?status.svg)](https://godoc.org/github.com/emersion/go-imap/client)
go get github.com/emersion/go-imap/v2 ```go
package main
Documentation and examples for the module are available here: import (
"log"
- [Client docs] "github.com/emersion/go-imap/client"
- [Server docs] "github.com/emersion/go-imap"
)
func main() {
log.Println("Connecting to server...")
// Connect to server
c, err := client.DialTLS("mail.example.org:993", nil)
if err != nil {
log.Fatal(err)
}
log.Println("Connected")
// Don't forget to logout
defer c.Logout()
// Login
if err := c.Login("username", "password"); err != nil {
log.Fatal(err)
}
log.Println("Logged in")
// List mailboxes
mailboxes := make(chan *imap.MailboxInfo, 10)
done := make(chan error, 1)
go func () {
done <- c.List("", "*", mailboxes)
}()
log.Println("Mailboxes:")
for m := range mailboxes {
log.Println("* " + m.Name)
}
if err := <-done; err != nil {
log.Fatal(err)
}
// Select INBOX
mbox, err := c.Select("INBOX", false)
if err != nil {
log.Fatal(err)
}
log.Println("Flags for INBOX:", mbox.Flags)
// Get the last 4 messages
from := uint32(1)
to := mbox.Messages
if mbox.Messages > 3 {
// We're using unsigned integers here, only substract if the result is > 0
from = mbox.Messages - 3
}
seqset := new(imap.SeqSet)
seqset.AddRange(from, to)
messages := make(chan *imap.Message, 10)
done = make(chan error, 1)
go func() {
done <- c.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope}, messages)
}()
log.Println("Last 4 messages:")
for msg := range messages {
log.Println("* " + msg.Envelope.Subject)
}
if err := <-done; err != nil {
log.Fatal(err)
}
log.Println("Done!")
}
```
### Server [![GoDoc](https://godoc.org/github.com/emersion/go-imap/server?status.svg)](https://godoc.org/github.com/emersion/go-imap/server)
```go
package main
import (
"log"
"github.com/emersion/go-imap/server"
"github.com/emersion/go-imap/backend/memory"
)
func main() {
// Create a memory backend
be := memory.New()
// Create a new server
s := server.New(be)
s.Addr = ":1143"
// Since we will use this server for testing only, we can allow plain text
// authentication over unencrypted connections
s.AllowInsecureAuth = true
log.Println("Starting IMAP server at localhost:1143")
if err := s.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
```
You can now use `telnet localhost 1143` to manually connect to the server.
## Extending go-imap
### Extensions
Commands defined in IMAP extensions are available in other packages. See [the
wiki](https://github.com/emersion/go-imap/wiki/Using-extensions#using-client-extensions)
to learn how to use them.
* [APPENDLIMIT](https://github.com/emersion/go-imap-appendlimit)
* [COMPRESS](https://github.com/emersion/go-imap-compress)
* [ENABLE](https://github.com/emersion/go-imap-enable)
* [ID](https://github.com/ProtonMail/go-imap-id)
* [IDLE](https://github.com/emersion/go-imap-idle)
* [MOVE](https://github.com/emersion/go-imap-move)
* [QUOTA](https://github.com/emersion/go-imap-quota)
* [SPECIAL-USE](https://github.com/emersion/go-imap-specialuse)
* [UNSELECT](https://github.com/emersion/go-imap-unselect)
* [UIDPLUS](https://github.com/emersion/go-imap-uidplus)
### Server backends
* [Memory](https://github.com/emersion/go-imap/tree/master/backend/memory) (for testing)
* [Multi](https://github.com/emersion/go-imap-multi)
* [PGP](https://github.com/emersion/go-imap-pgp)
* [Proxy](https://github.com/emersion/go-imap-proxy)
### Related projects
* [go-message](https://github.com/emersion/go-message) - parsing and formatting MIME and mail messages
* [go-pgpmail](https://github.com/emersion/go-pgpmail) - decrypting and encrypting mails with OpenPGP
* [go-sasl](https://github.com/emersion/go-sasl) - sending and receiving SASL authentications
* [go-smtp](https://github.com/emersion/go-smtp) - building SMTP clients and servers
* [go-dkim](https://github.com/emersion/go-dkim) - creating and verifying DKIM signatures
## License ## License
MIT MIT
[IMAP4rev2]: https://www.rfc-editor.org/rfc/rfc9051.html
[v1 branch]: https://github.com/emersion/go-imap/tree/v1
[Client docs]: https://pkg.go.dev/github.com/emersion/go-imap/v2/imapclient
[Server docs]: https://pkg.go.dev/github.com/emersion/go-imap/v2/imapserver

104
acl.go
View File

@@ -1,104 +0,0 @@
package imap
import (
"fmt"
"strings"
)
// IMAP4 ACL extension (RFC 2086)
// Right describes a set of operations controlled by the IMAP ACL extension.
type Right byte
const (
// Standard rights
RightLookup = Right('l') // mailbox is visible to LIST/LSUB commands
RightRead = Right('r') // SELECT the mailbox, perform CHECK, FETCH, PARTIAL, SEARCH, COPY from mailbox
RightSeen = Right('s') // keep seen/unseen information across sessions (STORE SEEN flag)
RightWrite = Right('w') // STORE flags other than SEEN and DELETED
RightInsert = Right('i') // perform APPEND, COPY into mailbox
RightPost = Right('p') // send mail to submission address for mailbox, not enforced by IMAP4 itself
RightCreate = Right('c') // CREATE new sub-mailboxes in any implementation-defined hierarchy
RightDelete = Right('d') // STORE DELETED flag, perform EXPUNGE
RightAdminister = Right('a') // perform SETACL
)
// RightSetAll contains all standard rights.
var RightSetAll = RightSet("lrswipcda")
// RightsIdentifier is an ACL identifier.
type RightsIdentifier string
// RightsIdentifierAnyone is the universal identity (matches everyone).
const RightsIdentifierAnyone = RightsIdentifier("anyone")
// NewRightsIdentifierUsername returns a rights identifier referring to a
// username, checking for reserved values.
func NewRightsIdentifierUsername(username string) (RightsIdentifier, error) {
if username == string(RightsIdentifierAnyone) || strings.HasPrefix(username, "-") {
return "", fmt.Errorf("imap: reserved rights identifier")
}
return RightsIdentifier(username), nil
}
// RightModification indicates how to mutate a right set.
type RightModification byte
const (
RightModificationReplace = RightModification(0)
RightModificationAdd = RightModification('+')
RightModificationRemove = RightModification('-')
)
// A RightSet is a set of rights.
type RightSet []Right
// String returns a string representation of the right set.
func (r RightSet) String() string {
return string(r)
}
// Add returns a new right set containing rights from both sets.
func (r RightSet) Add(rights RightSet) RightSet {
newRights := make(RightSet, len(r), len(r)+len(rights))
copy(newRights, r)
for _, right := range rights {
if !strings.ContainsRune(string(r), rune(right)) {
newRights = append(newRights, right)
}
}
return newRights
}
// Remove returns a new right set containing all rights in r except these in
// the provided set.
func (r RightSet) Remove(rights RightSet) RightSet {
newRights := make(RightSet, 0, len(r))
for _, right := range r {
if !strings.ContainsRune(string(rights), rune(right)) {
newRights = append(newRights, right)
}
}
return newRights
}
// Equal returns true if both right sets contain exactly the same rights.
func (rs1 RightSet) Equal(rs2 RightSet) bool {
for _, r := range rs1 {
if !strings.ContainsRune(string(rs2), rune(r)) {
return false
}
}
for _, r := range rs2 {
if !strings.ContainsRune(string(rs1), rune(r)) {
return false
}
}
return true
}

View File

@@ -1,18 +0,0 @@
package imap
import (
"time"
)
// AppendOptions contains options for the APPEND command.
type AppendOptions struct {
Flags []Flag
Time time.Time
}
// AppendData is the data returned by an APPEND command.
type AppendData struct {
// requires UIDPLUS or IMAP4rev2
UID UID
UIDValidity uint32
}

16
backend/backend.go Normal file
View File

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

View File

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

View File

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

View File

@@ -0,0 +1,75 @@
package backendutil
import (
"bytes"
"errors"
"io"
"github.com/emersion/go-imap"
"github.com/emersion/go-message"
)
var errNoSuchPart = errors.New("backendutil: no such message body part")
// FetchBodySection extracts a body section from a message.
func FetchBodySection(e *message.Entity, section *imap.BodySectionName) (imap.Literal, error) {
// First, find the requested part using the provided path
for i := 0; i < len(section.Path); i++ {
n := section.Path[i]
mr := e.MultipartReader()
if mr == nil {
return nil, errNoSuchPart
}
for j := 1; j <= n; j++ {
p, err := mr.NextPart()
if err == io.EOF {
return nil, errNoSuchPart
} else if err != nil {
return nil, err
}
if j == n {
e = p
break
}
}
}
// Then, write the requested data to a buffer
b := new(bytes.Buffer)
// Write the header
mw, err := message.CreateWriter(b, e.Header)
if err != nil {
return nil, err
}
defer mw.Close()
switch section.Specifier {
case imap.TextSpecifier:
// The header hasn't been requested. Discard it.
b.Reset()
case imap.EntireSpecifier:
if len(section.Path) > 0 {
// When selecting a specific part by index, IMAP servers
// return only the text, not the associated MIME header.
b.Reset()
}
}
// Write the body, if requested
switch section.Specifier {
case imap.EntireSpecifier, imap.TextSpecifier:
if _, err := io.Copy(mw, e.Body); err != nil {
return nil, err
}
}
var l imap.Literal = b
if section.Partial != nil {
l = bytes.NewReader(section.ExtractPartial(b.Bytes()))
}
return l, nil
}

View File

@@ -0,0 +1,109 @@
package backendutil
import (
"io/ioutil"
"strings"
"testing"
"github.com/emersion/go-imap"
"github.com/emersion/go-message"
)
var bodyTests = []struct {
section string
body string
}{
{
section: "BODY[]",
body: testMailString,
},
{
section: "BODY[1.1]",
body: testTextBodyString,
},
{
section: "BODY[1.2]",
body: testHTMLBodyString,
},
{
section: "BODY[2]",
body: testAttachmentBodyString,
},
{
section: "BODY[HEADER]",
body: testHeaderString,
},
{
section: "BODY[1.1.HEADER]",
body: testTextHeaderString,
},
{
section: "BODY[2.HEADER]",
body: testAttachmentHeaderString,
},
{
section: "BODY[2.MIME]",
body: testAttachmentHeaderString,
},
{
section: "BODY[TEXT]",
body: testBodyString,
},
{
section: "BODY[1.1.TEXT]",
body: testTextBodyString,
},
{
section: "BODY[2.TEXT]",
body: testAttachmentBodyString,
},
{
section: "BODY[2.1]",
body: "",
},
{
section: "BODY[3]",
body: "",
},
{
section: "BODY[2.TEXT]<0.9>",
body: testAttachmentBodyString[:9],
},
}
func TestFetchBodySection(t *testing.T) {
for _, test := range bodyTests {
test := test
t.Run(test.section, func(t *testing.T) {
e, err := message.Read(strings.NewReader(testMailString))
if err != nil {
t.Fatal("Expected no error while reading mail, got:", err)
}
section, err := imap.ParseBodySectionName(imap.FetchItem(test.section))
if err != nil {
t.Fatal("Expected no error while parsing body section name, got:", err)
}
r, err := FetchBodySection(e, section)
if test.body == "" {
if err == nil {
t.Error("Expected an error while extracting non-existing body section")
}
} else {
if err != nil {
t.Fatal("Expected no error while extracting body section, got:", err)
}
b, err := ioutil.ReadAll(r)
if err != nil {
t.Fatal("Expected no error while reading body section, got:", err)
}
if s := string(b); s != test.body {
t.Errorf("Expected body section %q to be \n%s\n but got \n%s", test.section, test.body, s)
}
}
})
}
}

View File

@@ -0,0 +1,60 @@
package backendutil
import (
"io"
"strings"
"github.com/emersion/go-imap"
"github.com/emersion/go-message"
)
// FetchBodyStructure computes a message's body structure from its content.
func FetchBodyStructure(e *message.Entity, extended bool) (*imap.BodyStructure, error) {
bs := new(imap.BodyStructure)
mediaType, mediaParams, _ := e.Header.ContentType()
typeParts := strings.SplitN(mediaType, "/", 2)
bs.MIMEType = typeParts[0]
if len(typeParts) == 2 {
bs.MIMESubType = typeParts[1]
}
bs.Params = mediaParams
bs.Id = e.Header.Get("Content-Id")
bs.Description = e.Header.Get("Content-Description")
bs.Encoding = e.Header.Get("Content-Encoding")
// TODO: bs.Size
if mr := e.MultipartReader(); mr != nil {
var parts []*imap.BodyStructure
for {
p, err := mr.NextPart()
if err == io.EOF {
break
} else if err != nil {
return nil, err
}
pbs, err := FetchBodyStructure(p, extended)
if err != nil {
return nil, err
}
parts = append(parts, pbs)
}
bs.Parts = parts
}
// TODO: bs.Envelope, bs.BodyStructure
// TODO: bs.Lines
if extended {
bs.Extended = true
bs.Disposition, bs.DispositionParams, _ = e.Header.ContentDisposition()
// TODO: bs.Language, bs.Location
// TODO: bs.MD5
}
return bs, nil
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,40 @@
package backendutil
import (
"github.com/emersion/go-imap"
)
// UpdateFlags executes a flag operation on the flag set current.
func UpdateFlags(current []string, op imap.FlagsOp, flags []string) []string {
switch op {
case imap.SetFlags:
// TODO: keep \Recent if it is present
return flags
case imap.AddFlags:
// Check for duplicates
for _, flag := range current {
for i, addFlag := range flags {
if addFlag == flag {
flags = append(flags[:i], flags[i+1:]...)
break
}
}
}
return append(current, flags...)
case imap.RemoveFlags:
// Iterate through flags from the last one to the first one, to be able to
// delete some of them.
for i := len(current) - 1; i >= 0; i-- {
flag := current[i]
for _, removeFlag := range flags {
if removeFlag == flag {
current = append(current[:i], current[i+1:]...)
break
}
}
}
return current
}
return current
}

View File

@@ -0,0 +1,46 @@
package backendutil
import (
"reflect"
"testing"
"github.com/emersion/go-imap"
)
var updateFlagsTests = []struct {
op imap.FlagsOp
flags []string
res []string
}{
{
op: imap.AddFlags,
flags: []string{"d", "e"},
res: []string{"a", "b", "c", "d", "e"},
},
{
op: imap.AddFlags,
flags: []string{"a", "d", "b"},
res: []string{"a", "b", "c", "d"},
},
{
op: imap.RemoveFlags,
flags: []string{"b", "v", "e", "a"},
res: []string{"c"},
},
{
op: imap.SetFlags,
flags: []string{"a", "d", "e"},
res: []string{"a", "d", "e"},
},
}
func TestUpdateFlags(t *testing.T) {
current := []string{"a", "b", "c"}
for _, test := range updateFlagsTests {
got := UpdateFlags(current[:], test.op, test.flags)
if !reflect.DeepEqual(got, test.res) {
t.Errorf("Expected result to be \n%v\n but got \n%v", test.res, got)
}
}
}

View File

@@ -0,0 +1,225 @@
package backendutil
import (
"bytes"
"fmt"
"io"
"strings"
"time"
"github.com/emersion/go-imap"
"github.com/emersion/go-message"
"github.com/emersion/go-message/mail"
)
func matchString(s, substr string) bool {
return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
}
func bufferBody(e *message.Entity) (*bytes.Buffer, error) {
b := new(bytes.Buffer)
if _, err := io.Copy(b, e.Body); err != nil {
return nil, err
}
e.Body = b
return b, nil
}
func matchBody(e *message.Entity, substr string) (bool, error) {
if s, ok := e.Body.(fmt.Stringer); ok {
return matchString(s.String(), substr), nil
}
b, err := bufferBody(e)
if err != nil {
return false, err
}
return matchString(b.String(), substr), nil
}
type lengther interface {
Len() int
}
func bodyLen(e *message.Entity) (int, error) {
if l, ok := e.Body.(lengther); ok {
return l.Len(), nil
}
b, err := bufferBody(e)
if err != nil {
return 0, err
}
return b.Len(), nil
}
// Match returns true if a message matches the provided criteria. Sequence
// number, UID, flag and internal date contrainsts are not checked.
func Match(e *message.Entity, c *imap.SearchCriteria) (bool, error) {
// TODO: support encoded header fields for Bcc, Cc, From, To
// TODO: add header size for Larger and Smaller
h := mail.Header{e.Header}
if !c.SentBefore.IsZero() || !c.SentSince.IsZero() {
t, err := h.Date()
if err != nil {
return false, err
}
t = t.Round(24 * time.Hour)
if !c.SentBefore.IsZero() && !t.Before(c.SentBefore) {
return false, nil
}
if !c.SentSince.IsZero() && !t.After(c.SentSince) {
return false, nil
}
}
for key, wantValues := range c.Header {
values, ok := e.Header[key]
for _, wantValue := range wantValues {
if wantValue == "" && !ok {
return false, nil
}
if wantValue != "" {
ok := false
for _, v := range values {
if matchString(v, wantValue) {
ok = true
break
}
}
if !ok {
return false, nil
}
}
}
}
for _, body := range c.Body {
if ok, err := matchBody(e, body); err != nil || !ok {
return false, err
}
}
for _, text := range c.Text {
// TODO: also match header fields
if ok, err := matchBody(e, text); err != nil || !ok {
return false, err
}
}
if c.Larger > 0 || c.Smaller > 0 {
n, err := bodyLen(e)
if err != nil {
return false, err
}
if c.Larger > 0 && uint32(n) < c.Larger {
return false, nil
}
if c.Smaller > 0 && uint32(n) > c.Smaller {
return false, nil
}
}
for _, not := range c.Not {
ok, err := Match(e, not)
if err != nil || ok {
return false, err
}
}
for _, or := range c.Or {
ok1, err := Match(e, or[0])
if err != nil {
return ok1, err
}
ok2, err := Match(e, or[1])
if err != nil || (!ok1 && !ok2) {
return false, err
}
}
return true, nil
}
func matchFlags(flags map[string]bool, c *imap.SearchCriteria) bool {
for _, f := range c.WithFlags {
if !flags[f] {
return false
}
}
for _, f := range c.WithoutFlags {
if flags[f] {
return false
}
}
for _, not := range c.Not {
if matchFlags(flags, not) {
return false
}
}
for _, or := range c.Or {
if !matchFlags(flags, or[0]) && !matchFlags(flags, or[1]) {
return false
}
}
return true
}
// MatchFlags returns true if a flag list matches the provided criteria.
func MatchFlags(flags []string, c *imap.SearchCriteria) bool {
flagsMap := make(map[string]bool)
for _, f := range flags {
flagsMap[f] = true
}
return matchFlags(flagsMap, c)
}
// MatchSeqNumAndUid returns true if a sequence number and a UID matches the
// provided criteria.
func MatchSeqNumAndUid(seqNum uint32, uid uint32, c *imap.SearchCriteria) bool {
if c.SeqNum != nil && !c.SeqNum.Contains(seqNum) {
return false
}
if c.Uid != nil && !c.Uid.Contains(uid) {
return false
}
for _, not := range c.Not {
if MatchSeqNumAndUid(seqNum, uid, not) {
return false
}
}
for _, or := range c.Or {
if !MatchSeqNumAndUid(seqNum, uid, or[0]) && !MatchSeqNumAndUid(seqNum, uid, or[1]) {
return false
}
}
return true
}
// MatchDate returns true if a date matches the provided criteria.
func MatchDate(date time.Time, c *imap.SearchCriteria) bool {
date = date.Round(24 * time.Hour)
if !c.Since.IsZero() && !date.After(c.Since) {
return false
}
if !c.Before.IsZero() && !date.Before(c.Before) {
return false
}
for _, not := range c.Not {
if MatchDate(date, not) {
return false
}
}
for _, or := range c.Or {
if !MatchDate(date, or[0]) && !MatchDate(date, or[1]) {
return false
}
}
return true
}

View File

@@ -0,0 +1,264 @@
package backendutil
import (
"net/textproto"
"strings"
"testing"
"time"
"github.com/emersion/go-imap"
"github.com/emersion/go-message"
)
var matchTests = []struct {
criteria *imap.SearchCriteria
res bool
}{
{
criteria: &imap.SearchCriteria{
Header: textproto.MIMEHeader{"From": {"Mitsuha"}},
},
res: true,
},
{
criteria: &imap.SearchCriteria{
Header: textproto.MIMEHeader{"To": {"Mitsuha"}},
},
res: false,
},
{
criteria: &imap.SearchCriteria{SentBefore: testDate.Add(48 * time.Hour)},
res: true,
},
{
criteria: &imap.SearchCriteria{
Not: []*imap.SearchCriteria{{SentSince: testDate.Add(48 * time.Hour)}},
},
res: true,
},
{
criteria: &imap.SearchCriteria{
Not: []*imap.SearchCriteria{{Body: []string{"name"}}},
},
res: false,
},
{
criteria: &imap.SearchCriteria{
Text: []string{"name"},
},
res: true,
},
{
criteria: &imap.SearchCriteria{
Or: [][2]*imap.SearchCriteria{{
{Text: []string{"i'm not in the text"}},
{Body: []string{"i'm not in the body"}},
}},
},
res: false,
},
{
criteria: &imap.SearchCriteria{
Header: textproto.MIMEHeader{"Message-Id": {"42@example.org"}},
},
res: true,
},
{
criteria: &imap.SearchCriteria{
Header: textproto.MIMEHeader{"Message-Id": {"43@example.org"}},
},
res: false,
},
{
criteria: &imap.SearchCriteria{
Header: textproto.MIMEHeader{"Message-Id": {""}},
},
res: true,
},
{
criteria: &imap.SearchCriteria{
Header: textproto.MIMEHeader{"Reply-To": {""}},
},
res: false,
},
{
criteria: &imap.SearchCriteria{
Larger: 10,
},
res: true,
},
{
criteria: &imap.SearchCriteria{
Smaller: 10,
},
res: false,
},
{
criteria: &imap.SearchCriteria{
Header: textproto.MIMEHeader{"Subject": {"your"}},
},
res: true,
},
{
criteria: &imap.SearchCriteria{
Header: textproto.MIMEHeader{"Subject": {"Taki"}},
},
res: false,
},
}
func TestMatch(t *testing.T) {
for i, test := range matchTests {
e, err := message.Read(strings.NewReader(testMailString))
if err != nil {
t.Fatal("Expected no error while reading entity, got:", err)
}
ok, err := Match(e, test.criteria)
if err != nil {
t.Fatal("Expected no error while matching entity, got:", err)
}
if test.res && !ok {
t.Errorf("Expected #%v to match search criteria", i+1)
}
if !test.res && ok {
t.Errorf("Expected #%v not to match search criteria", i+1)
}
}
}
var flagsTests = []struct {
flags []string
criteria *imap.SearchCriteria
res bool
}{
{
flags: []string{imap.SeenFlag},
criteria: &imap.SearchCriteria{
WithFlags: []string{imap.SeenFlag},
WithoutFlags: []string{imap.FlaggedFlag},
},
res: true,
},
{
flags: []string{imap.SeenFlag},
criteria: &imap.SearchCriteria{
WithFlags: []string{imap.DraftFlag},
WithoutFlags: []string{imap.FlaggedFlag},
},
res: false,
},
{
flags: []string{imap.SeenFlag, imap.FlaggedFlag},
criteria: &imap.SearchCriteria{
WithFlags: []string{imap.SeenFlag},
WithoutFlags: []string{imap.FlaggedFlag},
},
res: false,
},
{
flags: []string{imap.SeenFlag, imap.FlaggedFlag},
criteria: &imap.SearchCriteria{
Or: [][2]*imap.SearchCriteria{{
{WithFlags: []string{imap.DraftFlag}},
{WithoutFlags: []string{imap.SeenFlag}},
}},
},
res: false,
},
{
flags: []string{imap.SeenFlag, imap.FlaggedFlag},
criteria: &imap.SearchCriteria{
Not: []*imap.SearchCriteria{
{WithFlags: []string{imap.SeenFlag}},
},
},
res: false,
},
}
func TestMatchFlags(t *testing.T) {
for i, test := range flagsTests {
ok := MatchFlags(test.flags, test.criteria)
if test.res && !ok {
t.Errorf("Expected #%v to match search criteria", i+1)
}
if !test.res && ok {
t.Errorf("Expected #%v not to match search criteria", i+1)
}
}
}
func TestMatchSeqNumAndUid(t *testing.T) {
seqNum := uint32(42)
uid := uint32(69)
c := &imap.SearchCriteria{
Or: [][2]*imap.SearchCriteria{{
{
Uid: new(imap.SeqSet),
Not: []*imap.SearchCriteria{{SeqNum: new(imap.SeqSet)}},
},
{
SeqNum: new(imap.SeqSet),
},
}},
}
if MatchSeqNumAndUid(seqNum, uid, c) {
t.Error("Expected not to match criteria")
}
c.Or[0][0].Uid.AddNum(uid)
if !MatchSeqNumAndUid(seqNum, uid, c) {
t.Error("Expected to match criteria")
}
c.Or[0][0].Not[0].SeqNum.AddNum(seqNum)
if MatchSeqNumAndUid(seqNum, uid, c) {
t.Error("Expected not to match criteria")
}
c.Or[0][1].SeqNum.AddNum(seqNum)
if !MatchSeqNumAndUid(seqNum, uid, c) {
t.Error("Expected to match criteria")
}
}
func TestMatchDate(t *testing.T) {
date := time.Unix(1483997966, 0)
c := &imap.SearchCriteria{
Or: [][2]*imap.SearchCriteria{{
{
Since: date.Add(48 * time.Hour),
Not: []*imap.SearchCriteria{{
Since: date.Add(48 * time.Hour),
}},
},
{
Before: date.Add(-48 * time.Hour),
},
}},
}
if MatchDate(date, c) {
t.Error("Expected not to match criteria")
}
c.Or[0][0].Since = date.Add(-48 * time.Hour)
if !MatchDate(date, c) {
t.Error("Expected to match criteria")
}
c.Or[0][0].Not[0].Since = date.Add(-48 * time.Hour)
if MatchDate(date, c) {
t.Error("Expected not to match criteria")
}
c.Or[0][1].Before = date.Add(48 * time.Hour)
if !MatchDate(date, c) {
t.Error("Expected to match criteria")
}
}

78
backend/mailbox.go Normal file
View File

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

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

@@ -0,0 +1,55 @@
// A memory backend.
package memory
import (
"errors"
"time"
"github.com/emersion/go-imap/backend"
)
type Backend struct {
users map[string]*User
}
func (be *Backend) Login(username, password string) (backend.User, error) {
user, ok := be.users[username]
if ok && user.password == password {
return user, nil
}
return nil, errors.New("Bad username or password")
}
func New() *Backend {
user := &User{username: "username", password: "password"}
body := `From: contact@example.org
To: contact@example.org
Subject: A little message, just for you
Date: Wed, 11 May 2016 14:31:59 +0000
Message-ID: <0000000@localhost/>
Content-Type: text/plain
Hi there :)`
user.mailboxes = map[string]*Mailbox{
"INBOX": {
name: "INBOX",
user: user,
Messages: []*Message{
{
Uid: 6,
Date: time.Now(),
Flags: []string{"\\Seen"},
Size: uint32(len(body)),
Body: []byte(body),
},
},
},
}
return &Backend{
users: map[string]*User{user.username: user},
}
}

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

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

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

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

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

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

92
backend/updates.go Normal file
View File

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

92
backend/user.go Normal file
View File

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

View File

@@ -1,212 +0,0 @@
package imap
import (
"strconv"
"strings"
)
// Cap represents an IMAP capability.
type Cap string
// Registered capabilities.
//
// See: https://www.iana.org/assignments/imap-capabilities/
const (
CapIMAP4rev1 Cap = "IMAP4rev1" // RFC 3501
CapIMAP4rev2 Cap = "IMAP4rev2" // RFC 9051
CapStartTLS Cap = "STARTTLS"
CapLoginDisabled Cap = "LOGINDISABLED"
// Folded in IMAP4rev2
CapNamespace Cap = "NAMESPACE" // RFC 2342
CapUnselect Cap = "UNSELECT" // RFC 3691
CapUIDPlus Cap = "UIDPLUS" // RFC 4315
CapESearch Cap = "ESEARCH" // RFC 4731
CapSearchRes Cap = "SEARCHRES" // RFC 5182
CapEnable Cap = "ENABLE" // RFC 5161
CapIdle Cap = "IDLE" // RFC 2177
CapSASLIR Cap = "SASL-IR" // RFC 4959
CapListExtended Cap = "LIST-EXTENDED" // RFC 5258
CapListStatus Cap = "LIST-STATUS" // RFC 5819
CapMove Cap = "MOVE" // RFC 6851
CapLiteralMinus Cap = "LITERAL-" // RFC 7888
CapStatusSize Cap = "STATUS=SIZE" // RFC 8438
CapChildren Cap = "CHILDREN" // RFC 3348
CapACL Cap = "ACL" // RFC 4314
CapAppendLimit Cap = "APPENDLIMIT" // RFC 7889
CapBinary Cap = "BINARY" // RFC 3516
CapCatenate Cap = "CATENATE" // RFC 4469
CapCondStore Cap = "CONDSTORE" // RFC 7162
CapConvert Cap = "CONVERT" // RFC 5259
CapCreateSpecialUse Cap = "CREATE-SPECIAL-USE" // RFC 6154
CapESort Cap = "ESORT" // RFC 5267
CapFilters Cap = "FILTERS" // RFC 5466
CapID Cap = "ID" // RFC 2971
CapLanguage Cap = "LANGUAGE" // RFC 5255
CapListMyRights Cap = "LIST-MYRIGHTS" // RFC 8440
CapLiteralPlus Cap = "LITERAL+" // RFC 7888
CapLoginReferrals Cap = "LOGIN-REFERRALS" // RFC 2221
CapMailboxReferrals Cap = "MAILBOX-REFERRALS" // RFC 2193
CapMetadata Cap = "METADATA" // RFC 5464
CapMetadataServer Cap = "METADATA-SERVER" // RFC 5464
CapMultiAppend Cap = "MULTIAPPEND" // RFC 3502
CapMultiSearch Cap = "MULTISEARCH" // RFC 7377
CapNotify Cap = "NOTIFY" // RFC 5465
CapObjectID Cap = "OBJECTID" // RFC 8474
CapPreview Cap = "PREVIEW" // RFC 8970
CapQResync Cap = "QRESYNC" // RFC 7162
CapQuota Cap = "QUOTA" // RFC 9208
CapQuotaSet Cap = "QUOTASET" // RFC 9208
CapReplace Cap = "REPLACE" // RFC 8508
CapSaveDate Cap = "SAVEDATE" // RFC 8514
CapSearchFuzzy Cap = "SEARCH=FUZZY" // RFC 6203
CapSort Cap = "SORT" // RFC 5256
CapSortDisplay Cap = "SORT=DISPLAY" // RFC 5957
CapSpecialUse Cap = "SPECIAL-USE" // RFC 6154
CapUnauthenticate Cap = "UNAUTHENTICATE" // RFC 8437
CapURLPartial Cap = "URL-PARTIAL" // RFC 5550
CapURLAuth Cap = "URLAUTH" // RFC 4467
CapUTF8Accept Cap = "UTF8=ACCEPT" // RFC 6855
CapUTF8Only Cap = "UTF8=ONLY" // RFC 6855
CapWithin Cap = "WITHIN" // RFC 5032
CapUIDOnly Cap = "UIDONLY" // RFC 9586
CapListMetadata Cap = "LIST-METADATA" // RFC 9590
CapInProgress Cap = "INPROGRESS" // RFC 9585
)
var imap4rev2Caps = CapSet{
CapNamespace: {},
CapUnselect: {},
CapUIDPlus: {},
CapESearch: {},
CapSearchRes: {},
CapEnable: {},
CapIdle: {},
CapSASLIR: {},
CapListExtended: {},
CapListStatus: {},
CapMove: {},
CapLiteralMinus: {},
CapStatusSize: {},
CapChildren: {},
}
// AuthCap returns the capability name for an SASL authentication mechanism.
func AuthCap(mechanism string) Cap {
return Cap("AUTH=" + mechanism)
}
// CapSet is a set of capabilities.
type CapSet map[Cap]struct{}
func (set CapSet) has(c Cap) bool {
_, ok := set[c]
return ok
}
func (set CapSet) Copy() CapSet {
newSet := make(CapSet, len(set))
for c := range set {
newSet[c] = struct{}{}
}
return newSet
}
// Has checks whether a capability is supported.
//
// Some capabilities are implied by others, as such Has may return true even if
// the capability is not in the map.
func (set CapSet) Has(c Cap) bool {
if set.has(c) {
return true
}
if set.has(CapIMAP4rev2) && imap4rev2Caps.has(c) {
return true
}
if c == CapLiteralMinus && set.has(CapLiteralPlus) {
return true
}
if c == CapCondStore && set.has(CapQResync) {
return true
}
if c == CapUTF8Accept && set.has(CapUTF8Only) {
return true
}
if c == CapAppendLimit {
_, ok := set.AppendLimit()
return ok
}
return false
}
// AuthMechanisms returns the list of supported SASL mechanisms for
// authentication.
func (set CapSet) AuthMechanisms() []string {
var l []string
for c := range set {
if !strings.HasPrefix(string(c), "AUTH=") {
continue
}
mech := strings.TrimPrefix(string(c), "AUTH=")
l = append(l, mech)
}
return l
}
// AppendLimit checks the APPENDLIMIT capability.
//
// If the server supports APPENDLIMIT, ok is true. If the server doesn't have
// the same upload limit for all mailboxes, limit is nil and per-mailbox
// limits must be queried via STATUS.
func (set CapSet) AppendLimit() (limit *uint32, ok bool) {
if set.has(CapAppendLimit) {
return nil, true
}
for c := range set {
if !strings.HasPrefix(string(c), "APPENDLIMIT=") {
continue
}
limitStr := strings.TrimPrefix(string(c), "APPENDLIMIT=")
limit64, err := strconv.ParseUint(limitStr, 10, 32)
if err == nil && limit64 > 0 {
limit32 := uint32(limit64)
return &limit32, true
}
}
limit32 := ^uint32(0)
return &limit32, false
}
// QuotaResourceTypes returns the list of supported QUOTA resource types.
func (set CapSet) QuotaResourceTypes() []QuotaResourceType {
var l []QuotaResourceType
for c := range set {
if !strings.HasPrefix(string(c), "QUOTA=RES-") {
continue
}
t := strings.TrimPrefix(string(c), "QUOTA=RES-")
l = append(l, QuotaResourceType(t))
}
return l
}
// ThreadAlgorithms returns the list of supported threading algorithms.
func (set CapSet) ThreadAlgorithms() []ThreadAlgorithm {
var l []ThreadAlgorithm
for c := range set {
if !strings.HasPrefix(string(c), "THREAD=") {
continue
}
alg := strings.TrimPrefix(string(c), "THREAD=")
l = append(l, ThreadAlgorithm(alg))
}
return l
}

591
client/client.go Normal file
View File

@@ -0,0 +1,591 @@
// Package client provides an IMAP client.
package client
import (
"crypto/tls"
"fmt"
"io"
"log"
"net"
"os"
"sync"
"time"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/responses"
)
// errClosed is used when a connection is closed while waiting for a command
// response.
var errClosed = fmt.Errorf("imap: connection closed")
// errUnregisterHandler is returned by a response handler to unregister itself.
var errUnregisterHandler = fmt.Errorf("imap: unregister handler")
// Update is an unilateral server update.
type Update interface {
update()
}
// StatusUpdate is delivered when a status update is received.
type StatusUpdate struct {
Status *imap.StatusResp
}
func (u *StatusUpdate) update() {}
// MailboxUpdate is delivered when a mailbox status changes.
type MailboxUpdate struct {
Mailbox *imap.MailboxStatus
}
func (u *MailboxUpdate) update() {}
// ExpungeUpdate is delivered when a message is deleted.
type ExpungeUpdate struct {
SeqNum uint32
}
func (u *ExpungeUpdate) update() {}
// MessageUpdate is delivered when a message attribute changes.
type MessageUpdate struct {
Message *imap.Message
}
func (u *MessageUpdate) update() {}
// Client is an IMAP client.
type Client struct {
conn *imap.Conn
isTLS bool
greeted chan struct{}
loggedOut chan struct{}
handlers []responses.Handler
handlersLocker sync.Mutex
// The current connection state.
state imap.ConnState
// The selected mailbox, if there is one.
mailbox *imap.MailboxStatus
// The cached server capabilities.
caps map[string]bool
// state, mailbox and caps may be accessed in different goroutines. Protect
// access.
locker sync.Mutex
// A channel to which unilateral updates from the server will be sent. An
// update can be one of: *StatusUpdate, *MailboxUpdate, *MessageUpdate,
// *ExpungeUpdate. Note that blocking this channel blocks the whole client,
// so it's recommended to use a separate goroutine and a buffered channel to
// prevent deadlocks.
Updates chan<- Update
// ErrorLog specifies an optional logger for errors accepting connections and
// unexpected behavior from handlers. By default, logging goes to os.Stderr
// via the log package's standard logger. The logger must be safe to use
// simultaneously from multiple goroutines.
ErrorLog imap.Logger
// Timeout specifies a maximum amount of time to wait on a command.
//
// A Timeout of zero means no timeout. This is the default.
Timeout time.Duration
}
func (c *Client) registerHandler(h responses.Handler) {
if h == nil {
return
}
c.handlersLocker.Lock()
c.handlers = append(c.handlers, h)
c.handlersLocker.Unlock()
}
func (c *Client) handle(resp imap.Resp) error {
c.handlersLocker.Lock()
for i := len(c.handlers) - 1; i >= 0; i-- {
if err := c.handlers[i].Handle(resp); err != responses.ErrUnhandled {
if err == errUnregisterHandler {
c.handlers = append(c.handlers[:i], c.handlers[i+1:]...)
err = nil
}
c.handlersLocker.Unlock()
return err
}
}
c.handlersLocker.Unlock()
return responses.ErrUnhandled
}
func (c *Client) read(greeted <-chan struct{}) error {
greetedClosed := false
defer func() {
// Ensure we close the greeted channel. New may be waiting on an indication
// that we've seen the greeting.
if !greetedClosed {
close(c.greeted)
greetedClosed = true
}
close(c.loggedOut)
}()
first := true
for {
if c.State() == imap.LogoutState {
return nil
}
c.conn.Wait()
if first {
first = false
} else {
<-greeted
if !greetedClosed {
close(c.greeted)
greetedClosed = true
}
}
resp, err := imap.ReadResp(c.conn.Reader)
if err == io.EOF || c.State() == imap.LogoutState {
return nil
} else if err != nil {
c.ErrorLog.Println("error reading response:", err)
if imap.IsParseError(err) {
continue
} else {
return err
}
}
if err := c.handle(resp); err != nil && err != responses.ErrUnhandled {
c.ErrorLog.Println("cannot handle response ", resp, err)
}
}
}
type handleResult struct {
status *imap.StatusResp
err error
}
func (c *Client) execute(cmdr imap.Commander, h responses.Handler) (*imap.StatusResp, error) {
cmd := cmdr.Command()
cmd.Tag = generateTag()
if c.Timeout > 0 {
err := c.conn.SetDeadline(time.Now().Add(c.Timeout))
if err != nil {
return nil, err
}
} else {
// It's possible the client had a timeout set from a previous command, but no
// longer does. Ensure we respect that. The zero time means no deadline.
if err := c.conn.SetDeadline(time.Time{}); err != nil {
return nil, err
}
}
// Add handler before sending command, to be sure to get the response in time
// (in tests, the response is sent right after our command is received, so
// sometimes the response was received before the setup of this handler)
doneHandle := make(chan handleResult, 1)
unregister := make(chan struct{})
c.registerHandler(responses.HandlerFunc(func(resp imap.Resp) error {
select {
case <-unregister:
// If an error occured while sending the command, abort
return errUnregisterHandler
default:
}
if s, ok := resp.(*imap.StatusResp); ok && s.Tag == cmd.Tag {
// This is the command's status response, we're done
doneHandle <- handleResult{s, nil}
return errUnregisterHandler
}
if h != nil {
// Pass the response to the response handler
if err := h.Handle(resp); err != nil && err != responses.ErrUnhandled {
// If the response handler returns an error, abort
doneHandle <- handleResult{nil, err}
return errUnregisterHandler
} else {
return err
}
}
return responses.ErrUnhandled
}))
// Send the command to the server
doneWrite := make(chan error, 1)
go func() {
doneWrite <- cmd.WriteTo(c.conn.Writer)
}()
for {
select {
case <-c.loggedOut:
// If the connection is closed (such as from an I/O error), ensure we
// realize this and don't block waiting on a response that will never
// come. loggedOut is a channel that closes when the reader goroutine
// ends.
close(unregister)
return nil, errClosed
case err := <-doneWrite:
if err != nil {
// Error while sending the command
close(unregister)
return nil, err
}
case result := <-doneHandle:
return result.status, result.err
}
}
}
// State returns the current connection state.
func (c *Client) State() imap.ConnState {
c.locker.Lock()
state := c.state
c.locker.Unlock()
return state
}
// Mailbox returns the selected mailbox. It returns nil if there isn't one.
func (c *Client) Mailbox() *imap.MailboxStatus {
// c.Mailbox fields are not supposed to change, so we can return the pointer.
c.locker.Lock()
mbox := c.mailbox
c.locker.Unlock()
return mbox
}
// SetState sets this connection's internal state.
//
// This function should not be called directly, it must only be used by
// libraries implementing extensions of the IMAP protocol.
func (c *Client) SetState(state imap.ConnState, mailbox *imap.MailboxStatus) {
c.locker.Lock()
c.state = state
c.mailbox = mailbox
c.locker.Unlock()
}
// Execute executes a generic command. cmdr is a value that can be converted to
// a raw command and h is a response handler. The function returns when the
// command has completed or failed, in this case err is nil. A non-nil err value
// indicates a network error.
//
// This function should not be called directly, it must only be used by
// libraries implementing extensions of the IMAP protocol.
func (c *Client) Execute(cmdr imap.Commander, h responses.Handler) (*imap.StatusResp, error) {
return c.execute(cmdr, h)
}
func (c *Client) handleContinuationReqs(continues chan<- bool) {
c.registerHandler(responses.HandlerFunc(func(resp imap.Resp) error {
if _, ok := resp.(*imap.ContinuationReq); ok {
go func() {
continues <- true
}()
return nil
}
return responses.ErrUnhandled
}))
}
func (c *Client) gotStatusCaps(args []interface{}) {
c.locker.Lock()
c.caps = make(map[string]bool)
for _, cap := range args {
if cap, ok := cap.(string); ok {
c.caps[cap] = true
}
}
c.locker.Unlock()
}
// The server can send unilateral data. This function handles it.
func (c *Client) handleUnilateral() {
c.registerHandler(responses.HandlerFunc(func(resp imap.Resp) error {
switch resp := resp.(type) {
case *imap.StatusResp:
if resp.Tag != "*" {
return responses.ErrUnhandled
}
switch resp.Type {
case imap.StatusRespOk, imap.StatusRespNo, imap.StatusRespBad:
if c.Updates != nil {
c.Updates <- &StatusUpdate{resp}
}
case imap.StatusRespBye:
c.locker.Lock()
c.state = imap.LogoutState
c.mailbox = nil
c.locker.Unlock()
c.conn.Close()
if c.Updates != nil {
c.Updates <- &StatusUpdate{resp}
}
default:
return responses.ErrUnhandled
}
case *imap.DataResp:
name, fields, ok := imap.ParseNamedResp(resp)
if !ok {
return responses.ErrUnhandled
}
switch name {
case "CAPABILITY":
c.gotStatusCaps(fields)
case "EXISTS":
if c.Mailbox() == nil {
break
}
if messages, err := imap.ParseNumber(fields[0]); err == nil {
c.locker.Lock()
c.mailbox.Messages = messages
c.locker.Unlock()
c.mailbox.ItemsLocker.Lock()
c.mailbox.Items[imap.StatusMessages] = nil
c.mailbox.ItemsLocker.Unlock()
}
if c.Updates != nil {
c.Updates <- &MailboxUpdate{c.Mailbox()}
}
case "RECENT":
if c.Mailbox() == nil {
break
}
if recent, err := imap.ParseNumber(fields[0]); err == nil {
c.locker.Lock()
c.mailbox.Recent = recent
c.locker.Unlock()
c.mailbox.ItemsLocker.Lock()
c.mailbox.Items[imap.StatusRecent] = nil
c.mailbox.ItemsLocker.Unlock()
}
if c.Updates != nil {
c.Updates <- &MailboxUpdate{c.Mailbox()}
}
case "EXPUNGE":
seqNum, _ := imap.ParseNumber(fields[0])
if c.Updates != nil {
c.Updates <- &ExpungeUpdate{seqNum}
}
case "FETCH":
seqNum, _ := imap.ParseNumber(fields[0])
fields, _ := fields[1].([]interface{})
msg := &imap.Message{SeqNum: seqNum}
if err := msg.Parse(fields); err != nil {
break
}
if c.Updates != nil {
c.Updates <- &MessageUpdate{msg}
}
default:
return responses.ErrUnhandled
}
default:
return responses.ErrUnhandled
}
return nil
}))
}
func (c *Client) handleGreetAndStartReading() error {
done := make(chan error, 2)
greeted := make(chan struct{})
c.registerHandler(responses.HandlerFunc(func(resp imap.Resp) error {
status, ok := resp.(*imap.StatusResp)
if !ok {
done <- fmt.Errorf("invalid greeting received from server: not a status response")
return errUnregisterHandler
}
c.locker.Lock()
switch status.Type {
case imap.StatusRespPreauth:
c.state = imap.AuthenticatedState
case imap.StatusRespBye:
c.state = imap.LogoutState
case imap.StatusRespOk:
c.state = imap.NotAuthenticatedState
default:
c.state = imap.LogoutState
c.locker.Unlock()
done <- fmt.Errorf("invalid greeting received from server: %v", status.Type)
return errUnregisterHandler
}
c.locker.Unlock()
if status.Code == imap.CodeCapability {
c.gotStatusCaps(status.Arguments)
}
close(greeted)
done <- nil
return errUnregisterHandler
}))
// Make sure to start reading after we have set up this handler, otherwise
// some messages will be lost.
go func() {
done <- c.read(greeted)
}()
return <-done
}
// Upgrade a connection, e.g. wrap an unencrypted connection with an encrypted
// tunnel.
//
// This function should not be called directly, it must only be used by
// libraries implementing extensions of the IMAP protocol.
func (c *Client) Upgrade(upgrader imap.ConnUpgrader) error {
return c.conn.Upgrade(upgrader)
}
// Writer returns the imap.Writer for this client's connection.
//
// This function should not be called directly, it must only be used by
// libraries implementing extensions of the IMAP protocol.
func (c *Client) Writer() *imap.Writer {
return c.conn.Writer
}
// IsTLS checks if this client's connection has TLS enabled.
func (c *Client) IsTLS() bool {
return c.isTLS
}
// LoggedOut returns a channel which is closed when the connection to the server
// is closed.
func (c *Client) LoggedOut() <-chan struct{} {
return c.loggedOut
}
// SetDebug defines an io.Writer to which all network activity will be logged.
// If nil is provided, network activity will not be logged.
func (c *Client) SetDebug(w io.Writer) {
c.conn.SetDebug(w)
}
// New creates a new client from an existing connection.
func New(conn net.Conn) (*Client, error) {
continues := make(chan bool)
w := imap.NewClientWriter(nil, continues)
r := imap.NewReader(nil)
c := &Client{
conn: imap.NewConn(conn, r, w),
greeted: make(chan struct{}),
loggedOut: make(chan struct{}),
state: imap.ConnectingState,
ErrorLog: log.New(os.Stderr, "imap/client: ", log.LstdFlags),
}
c.handleContinuationReqs(continues)
c.handleUnilateral()
err := c.handleGreetAndStartReading()
return c, err
}
// Dial connects to an IMAP server using an unencrypted connection.
func Dial(addr string) (c *Client, err error) {
conn, err := net.Dial("tcp", addr)
if err != nil {
return
}
c, err = New(conn)
return
}
// DialWithDialer connects to an IMAP server using an unencrypted connection
// using dialer.Dial.
//
// Among other uses, this allows to apply a dial timeout.
func DialWithDialer(dialer *net.Dialer, address string) (c *Client, err error) {
conn, err := dialer.Dial("tcp", address)
if err != nil {
return nil, err
}
// We don't return to the caller until we try to receive a greeting. As such,
// there is no way to set the client's Timeout for that action. As a
// workaround, if the dialer has a timeout set, use that for the connection's
// deadline.
if dialer.Timeout > 0 {
err = conn.SetDeadline(time.Now().Add(dialer.Timeout))
if err != nil {
return
}
}
c, err = New(conn)
return
}
// DialTLS connects to an IMAP server using an encrypted connection.
func DialTLS(addr string, tlsConfig *tls.Config) (c *Client, err error) {
conn, err := tls.Dial("tcp", addr, tlsConfig)
if err != nil {
return
}
c, err = New(conn)
c.isTLS = true
return
}
// DialWithDialerTLS connects to an IMAP server using an encrypted connection
// using dialer.Dial.
//
// Among other uses, this allows to apply a dial timeout.
func DialWithDialerTLS(dialer *net.Dialer, addr string,
tlsConfig *tls.Config) (c *Client, err error) {
conn, err := tls.DialWithDialer(dialer, "tcp", addr, tlsConfig)
if err != nil {
return
}
// We don't return to the caller until we try to receive a greeting. As such,
// there is no way to set the client's Timeout for that action. As a
// workaround, if the dialer has a timeout set, use that for the connection's
// deadline.
if dialer.Timeout > 0 {
err = conn.SetDeadline(time.Now().Add(dialer.Timeout))
if err != nil {
return
}
}
c, err = New(conn)
c.isTLS = true
return
}

173
client/client_test.go Normal file
View File

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

87
client/cmd_any.go Normal file
View File

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

80
client/cmd_any_test.go Normal file
View File

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

254
client/cmd_auth.go Normal file
View File

@@ -0,0 +1,254 @@
package client
import (
"errors"
"time"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/commands"
"github.com/emersion/go-imap/responses"
)
// ErrNotLoggedIn is returned if a function that requires the client to be
// logged in is called then the client isn't.
var ErrNotLoggedIn = errors.New("Not logged in")
func (c *Client) ensureAuthenticated() error {
state := c.State()
if state != imap.AuthenticatedState && state != imap.SelectedState {
return ErrNotLoggedIn
}
return nil
}
// Select selects a mailbox so that messages in the mailbox can be accessed. Any
// currently selected mailbox is deselected before attempting the new selection.
// Even if the readOnly parameter is set to false, the server can decide to open
// the mailbox in read-only mode.
func (c *Client) Select(name string, readOnly bool) (*imap.MailboxStatus, error) {
if err := c.ensureAuthenticated(); err != nil {
return nil, err
}
cmd := &commands.Select{
Mailbox: name,
ReadOnly: readOnly,
}
mbox := &imap.MailboxStatus{Name: name, Items: make(map[imap.StatusItem]interface{})}
res := &responses.Select{
Mailbox: mbox,
}
c.locker.Lock()
c.mailbox = mbox
c.locker.Unlock()
status, err := c.execute(cmd, res)
if err != nil {
c.locker.Lock()
c.mailbox = nil
c.locker.Unlock()
return nil, err
}
if err := status.Err(); err != nil {
c.locker.Lock()
c.mailbox = nil
c.locker.Unlock()
return nil, err
}
c.locker.Lock()
mbox.ReadOnly = (status.Code == imap.CodeReadOnly)
c.state = imap.SelectedState
c.locker.Unlock()
return mbox, nil
}
// Create creates a mailbox with the given name.
func (c *Client) Create(name string) error {
if err := c.ensureAuthenticated(); err != nil {
return err
}
cmd := &commands.Create{
Mailbox: name,
}
status, err := c.execute(cmd, nil)
if err != nil {
return err
}
return status.Err()
}
// Delete permanently removes the mailbox with the given name.
func (c *Client) Delete(name string) error {
if err := c.ensureAuthenticated(); err != nil {
return err
}
cmd := &commands.Delete{
Mailbox: name,
}
status, err := c.execute(cmd, nil)
if err != nil {
return err
}
return status.Err()
}
// Rename changes the name of a mailbox.
func (c *Client) Rename(existingName, newName string) error {
if err := c.ensureAuthenticated(); err != nil {
return err
}
cmd := &commands.Rename{
Existing: existingName,
New: newName,
}
status, err := c.execute(cmd, nil)
if err != nil {
return err
}
return status.Err()
}
// Subscribe adds the specified mailbox name to the server's set of "active" or
// "subscribed" mailboxes.
func (c *Client) Subscribe(name string) error {
if err := c.ensureAuthenticated(); err != nil {
return err
}
cmd := &commands.Subscribe{
Mailbox: name,
}
status, err := c.execute(cmd, nil)
if err != nil {
return err
}
return status.Err()
}
// Unsubscribe removes the specified mailbox name from the server's set of
// "active" or "subscribed" mailboxes.
func (c *Client) Unsubscribe(name string) error {
if err := c.ensureAuthenticated(); err != nil {
return err
}
cmd := &commands.Unsubscribe{
Mailbox: name,
}
status, err := c.execute(cmd, nil)
if err != nil {
return err
}
return status.Err()
}
// List returns a subset of names from the complete set of all names available
// to the client.
//
// An empty name argument is a special request to return the hierarchy delimiter
// and the root name of the name given in the reference. The character "*" is a
// wildcard, and matches zero or more characters at this position. The
// character "%" is similar to "*", but it does not match a hierarchy delimiter.
func (c *Client) List(ref, name string, ch chan *imap.MailboxInfo) error {
if err := c.ensureAuthenticated(); err != nil {
return err
}
defer close(ch)
cmd := &commands.List{
Reference: ref,
Mailbox: name,
}
res := &responses.List{Mailboxes: ch}
status, err := c.execute(cmd, res)
if err != nil {
return err
}
return status.Err()
}
// Lsub returns a subset of names from the set of names that the user has
// declared as being "active" or "subscribed".
func (c *Client) Lsub(ref, name string, ch chan *imap.MailboxInfo) error {
if err := c.ensureAuthenticated(); err != nil {
return err
}
defer close(ch)
cmd := &commands.List{
Reference: ref,
Mailbox: name,
Subscribed: true,
}
res := &responses.List{
Mailboxes: ch,
Subscribed: true,
}
status, err := c.execute(cmd, res)
if err != nil {
return err
}
return status.Err()
}
// Status requests the status of the indicated mailbox. It does not change the
// currently selected mailbox, nor does it affect the state of any messages in
// the queried mailbox.
//
// See RFC 3501 section 6.3.10 for a list of items that can be requested.
func (c *Client) Status(name string, items []imap.StatusItem) (*imap.MailboxStatus, error) {
if err := c.ensureAuthenticated(); err != nil {
return nil, err
}
cmd := &commands.Status{
Mailbox: name,
Items: items,
}
res := &responses.Status{
Mailbox: new(imap.MailboxStatus),
}
status, err := c.execute(cmd, res)
if err != nil {
return nil, err
}
return res.Mailbox, status.Err()
}
// Append appends the literal argument as a new message to the end of the
// specified destination mailbox. This argument SHOULD be in the format of an
// RFC 2822 message. flags and date are optional arguments and can be set to
// nil.
func (c *Client) Append(mbox string, flags []string, date time.Time, msg imap.Literal) error {
if err := c.ensureAuthenticated(); err != nil {
return err
}
cmd := &commands.Append{
Mailbox: mbox,
Flags: flags,
Date: date,
Message: msg,
}
status, err := c.execute(cmd, nil)
if err != nil {
return err
}
return status.Err()
}

359
client/cmd_auth_test.go Normal file
View File

@@ -0,0 +1,359 @@
package client
import (
"bytes"
"io"
"reflect"
"testing"
"time"
"github.com/emersion/go-imap"
)
func TestClient_Select(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.AuthenticatedState, nil)
var mbox *imap.MailboxStatus
done := make(chan error, 1)
go func() {
var err error
mbox, err = c.Select("INBOX", false)
done <- err
}()
tag, cmd := s.ScanCmd()
if cmd != "SELECT INBOX" {
t.Fatalf("client sent command %v, want SELECT INBOX", cmd)
}
s.WriteString("* 172 EXISTS\r\n")
s.WriteString("* 1 RECENT\r\n")
s.WriteString("* OK [UNSEEN 12] Message 12 is first unseen\r\n")
s.WriteString("* OK [UIDVALIDITY 3857529045] UIDs valid\r\n")
s.WriteString("* OK [UIDNEXT 4392] Predicted next UID\r\n")
s.WriteString("* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n")
s.WriteString("* OK [PERMANENTFLAGS (\\Deleted \\Seen \\*)] Limited\r\n")
s.WriteString(tag + " OK SELECT completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Select() = %v", err)
}
want := &imap.MailboxStatus{
Name: "INBOX",
ReadOnly: false,
Flags: []string{imap.AnsweredFlag, imap.FlaggedFlag, imap.DeletedFlag, imap.SeenFlag, imap.DraftFlag},
PermanentFlags: []string{imap.DeletedFlag, imap.SeenFlag, "\\*"},
UnseenSeqNum: 12,
Messages: 172,
Recent: 1,
UidNext: 4392,
UidValidity: 3857529045,
}
mbox.Items = nil
if !reflect.DeepEqual(mbox, want) {
t.Errorf("c.Select() = \n%+v\n want \n%+v", mbox, want)
}
}
func TestClient_Select_ReadOnly(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.AuthenticatedState, nil)
var mbox *imap.MailboxStatus
done := make(chan error, 1)
go func() {
var err error
mbox, err = c.Select("INBOX", true)
done <- err
}()
tag, cmd := s.ScanCmd()
if cmd != "EXAMINE INBOX" {
t.Fatalf("client sent command %v, want EXAMINE INBOX", cmd)
}
s.WriteString(tag + " OK [READ-ONLY] EXAMINE completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Select() = %v", err)
}
if !mbox.ReadOnly {
t.Errorf("c.Select().ReadOnly = false, want true")
}
}
func TestClient_Create(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.AuthenticatedState, nil)
done := make(chan error, 1)
go func() {
done <- c.Create("New Mailbox")
}()
tag, cmd := s.ScanCmd()
if cmd != "CREATE \"New Mailbox\"" {
t.Fatalf("client sent command %v, want %v", cmd, "CREATE \"New Mailbox\"")
}
s.WriteString(tag + " OK CREATE completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Create() = %v", err)
}
}
func TestClient_Delete(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.AuthenticatedState, nil)
done := make(chan error, 1)
go func() {
done <- c.Delete("Old Mailbox")
}()
tag, cmd := s.ScanCmd()
if cmd != "DELETE \"Old Mailbox\"" {
t.Fatalf("client sent command %v, want %v", cmd, "DELETE \"Old Mailbox\"")
}
s.WriteString(tag + " OK DELETE completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Delete() = %v", err)
}
}
func TestClient_Rename(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.AuthenticatedState, nil)
done := make(chan error, 1)
go func() {
done <- c.Rename("Old Mailbox", "New Mailbox")
}()
tag, cmd := s.ScanCmd()
if cmd != "RENAME \"Old Mailbox\" \"New Mailbox\"" {
t.Fatalf("client sent command %v, want %v", cmd, "RENAME \"Old Mailbox\" \"New Mailbox\"")
}
s.WriteString(tag + " OK RENAME completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Rename() = %v", err)
}
}
func TestClient_Subscribe(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.AuthenticatedState, nil)
done := make(chan error, 1)
go func() {
done <- c.Subscribe("Mailbox")
}()
tag, cmd := s.ScanCmd()
if cmd != "SUBSCRIBE Mailbox" {
t.Fatalf("client sent command %v, want %v", cmd, "SUBSCRIBE Mailbox")
}
s.WriteString(tag + " OK SUBSCRIBE completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Subscribe() = %v", err)
}
}
func TestClient_Unsubscribe(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.AuthenticatedState, nil)
done := make(chan error, 1)
go func() {
done <- c.Unsubscribe("Mailbox")
}()
tag, cmd := s.ScanCmd()
if cmd != "UNSUBSCRIBE Mailbox" {
t.Fatalf("client sent command %v, want %v", cmd, "UNSUBSCRIBE Mailbox")
}
s.WriteString(tag + " OK UNSUBSCRIBE completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Unsubscribe() = %v", err)
}
}
func TestClient_List(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.AuthenticatedState, nil)
done := make(chan error, 1)
mailboxes := make(chan *imap.MailboxInfo, 3)
go func() {
done <- c.List("", "%", mailboxes)
}()
tag, cmd := s.ScanCmd()
if cmd != "LIST \"\" \"%\"" {
t.Fatalf("client sent command %v, want %v", cmd, "LIST \"\" \"%\"")
}
s.WriteString("* LIST (flag1) \"/\" INBOX\r\n")
s.WriteString("* LIST (flag2 flag3) \"/\" Drafts\r\n")
s.WriteString("* LIST () \"/\" Sent\r\n")
s.WriteString(tag + " OK LIST completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.List() = %v", err)
}
want := []struct {
name string
attributes []string
}{
{"INBOX", []string{"flag1"}},
{"Drafts", []string{"flag2", "flag3"}},
{"Sent", []string{}},
}
i := 0
for mbox := range mailboxes {
if mbox.Name != want[i].name {
t.Errorf("Bad mailbox name for %v: %v, want %v", i, mbox.Name, want[i].name)
}
if !reflect.DeepEqual(mbox.Attributes, want[i].attributes) {
t.Errorf("Bad mailbox attributes for %v: %v, want %v", i, mbox.Attributes, want[i].attributes)
}
i++
}
}
func TestClient_Lsub(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.AuthenticatedState, nil)
done := make(chan error, 1)
mailboxes := make(chan *imap.MailboxInfo, 1)
go func() {
done <- c.Lsub("", "%", mailboxes)
}()
tag, cmd := s.ScanCmd()
if cmd != "LSUB \"\" \"%\"" {
t.Fatalf("client sent command %v, want %v", cmd, "LSUB \"\" \"%\"")
}
s.WriteString("* LSUB () \"/\" INBOX\r\n")
s.WriteString(tag + " OK LSUB completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Lsub() = %v", err)
}
mbox := <-mailboxes
if mbox.Name != "INBOX" {
t.Errorf("Bad mailbox name: %v", mbox.Name)
}
if len(mbox.Attributes) != 0 {
t.Errorf("Bad mailbox flags: %v", mbox.Attributes)
}
}
func TestClient_Status(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.AuthenticatedState, nil)
done := make(chan error, 1)
var mbox *imap.MailboxStatus
go func() {
var err error
mbox, err = c.Status("INBOX", []imap.StatusItem{imap.StatusMessages, imap.StatusRecent})
done <- err
}()
tag, cmd := s.ScanCmd()
if cmd != "STATUS INBOX (MESSAGES RECENT)" {
t.Fatalf("client sent command %v, want %v", cmd, "STATUS INBOX (MESSAGES RECENT)")
}
s.WriteString("* STATUS INBOX (MESSAGES 42 RECENT 1)\r\n")
s.WriteString(tag + " OK STATUS completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Status() = %v", err)
}
if mbox.Messages != 42 {
t.Errorf("Bad mailbox messages: %v", mbox.Messages)
}
if mbox.Recent != 1 {
t.Errorf("Bad mailbox recent: %v", mbox.Recent)
}
}
func TestClient_Append(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.AuthenticatedState, nil)
msg := "Hello World!\r\nHello Gophers!\r\n"
date := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
flags := []string{imap.SeenFlag, imap.DraftFlag}
done := make(chan error, 1)
go func() {
done <- c.Append("INBOX", flags, date, bytes.NewBufferString(msg))
}()
tag, cmd := s.ScanCmd()
if cmd != "APPEND INBOX (\\Seen \\Draft) \"10-Nov-2009 23:00:00 +0000\" {30}" {
t.Fatalf("client sent command %v, want %v", cmd, "APPEND INBOX (\\Seen \\Draft) \"10-Nov-2009 23:00:00 +0000\" {30}")
}
s.WriteString("+ send literal\r\n")
b := make([]byte, 30)
if _, err := io.ReadFull(s, b); err != nil {
t.Fatal(err)
}
if string(b) != msg {
t.Fatal("Bad literal:", string(b))
}
s.WriteString(tag + " OK APPEND completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Append() = %v", err)
}
}

151
client/cmd_noauth.go Normal file
View File

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

207
client/cmd_noauth_test.go Normal file
View File

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

263
client/cmd_selected.go Normal file
View File

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

450
client/cmd_selected_test.go Normal file
View File

@@ -0,0 +1,450 @@
package client
import (
"io/ioutil"
"net/textproto"
"reflect"
"testing"
"time"
"github.com/emersion/go-imap"
)
func TestClient_Check(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
done := make(chan error, 1)
go func() {
done <- c.Check()
}()
tag, cmd := s.ScanCmd()
if cmd != "CHECK" {
t.Fatalf("client sent command %v, want %v", cmd, "CHECK")
}
s.WriteString(tag + " OK CHECK completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Check() = %v", err)
}
}
func TestClient_Close(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, &imap.MailboxStatus{Name: "INBOX"})
done := make(chan error, 1)
go func() {
done <- c.Close()
}()
tag, cmd := s.ScanCmd()
if cmd != "CLOSE" {
t.Fatalf("client sent command %v, want %v", cmd, "CLOSE")
}
s.WriteString(tag + " OK CLOSE completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Check() = %v", err)
}
if state := c.State(); state != imap.AuthenticatedState {
t.Errorf("Bad state: %v", state)
}
if mailbox := c.Mailbox(); mailbox != nil {
t.Errorf("Client selected mailbox is not nil: %v", mailbox)
}
}
func TestClient_Expunge(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
done := make(chan error, 1)
expunged := make(chan uint32, 4)
go func() {
done <- c.Expunge(expunged)
}()
tag, cmd := s.ScanCmd()
if cmd != "EXPUNGE" {
t.Fatalf("client sent command %v, want %v", cmd, "EXPUNGE")
}
s.WriteString("* 3 EXPUNGE\r\n")
s.WriteString("* 3 EXPUNGE\r\n")
s.WriteString("* 5 EXPUNGE\r\n")
s.WriteString("* 8 EXPUNGE\r\n")
s.WriteString(tag + " OK EXPUNGE completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Expunge() = %v", err)
}
expected := []uint32{3, 3, 5, 8}
i := 0
for id := range expunged {
if id != expected[i] {
t.Errorf("Bad expunged sequence number: got %v instead of %v", id, expected[i])
}
i++
}
}
func TestClient_Search(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
date, _ := time.Parse(imap.DateLayout, "1-Feb-1994")
criteria := &imap.SearchCriteria{
WithFlags: []string{imap.DeletedFlag},
Header: textproto.MIMEHeader{"From": {"Smith"}},
Since: date,
Not: []*imap.SearchCriteria{{
Header: textproto.MIMEHeader{"To": {"Pauline"}},
}},
}
done := make(chan error, 1)
var results []uint32
go func() {
var err error
results, err = c.Search(criteria)
done <- err
}()
wantCmd := `SEARCH CHARSET UTF-8 SINCE "1-Feb-1994" FROM Smith DELETED NOT (TO Pauline)`
tag, cmd := s.ScanCmd()
if cmd != wantCmd {
t.Fatalf("client sent command %v, want %v", cmd, wantCmd)
}
s.WriteString("* SEARCH 2 84 882\r\n")
s.WriteString(tag + " OK SEARCH completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Search() = %v", err)
}
want := []uint32{2, 84, 882}
if !reflect.DeepEqual(results, want) {
t.Errorf("c.Search() = %v, want %v", results, want)
}
}
func TestClient_Search_Uid(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
criteria := &imap.SearchCriteria{
WithoutFlags: []string{imap.DeletedFlag},
}
done := make(chan error, 1)
var results []uint32
go func() {
var err error
results, err = c.UidSearch(criteria)
done <- err
}()
wantCmd := "UID SEARCH CHARSET UTF-8 UNDELETED"
tag, cmd := s.ScanCmd()
if cmd != wantCmd {
t.Fatalf("client sent command %v, want %v", cmd, wantCmd)
}
s.WriteString("* SEARCH 1 78 2010\r\n")
s.WriteString(tag + " OK UID SEARCH completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Search() = %v", err)
}
want := []uint32{1, 78, 2010}
if !reflect.DeepEqual(results, want) {
t.Errorf("c.Search() = %v, want %v", results, want)
}
}
func TestClient_Fetch(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
seqset, _ := imap.ParseSeqSet("2:3")
fields := []imap.FetchItem{imap.FetchUid, imap.FetchItem("BODY[]")}
done := make(chan error, 1)
messages := make(chan *imap.Message, 2)
go func() {
done <- c.Fetch(seqset, fields, messages)
}()
tag, cmd := s.ScanCmd()
if cmd != "FETCH 2:3 (UID BODY[])" {
t.Fatalf("client sent command %v, want %v", cmd, "FETCH 2:3 (UID BODY[])")
}
s.WriteString("* 2 FETCH (UID 42 BODY[] {16}\r\n")
s.WriteString("I love potatoes.")
s.WriteString(")\r\n")
s.WriteString("* 3 FETCH (UID 28 BODY[] {12}\r\n")
s.WriteString("Hello World!")
s.WriteString(")\r\n")
s.WriteString(tag + " OK FETCH completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Fetch() = %v", err)
}
section, _ := imap.ParseBodySectionName("BODY[]")
msg := <-messages
if msg.SeqNum != 2 {
t.Errorf("First message has bad sequence number: %v", msg.SeqNum)
}
if msg.Uid != 42 {
t.Errorf("First message has bad UID: %v", msg.Uid)
}
if body, _ := ioutil.ReadAll(msg.GetBody(section)); string(body) != "I love potatoes." {
t.Errorf("First message has bad body: %q", body)
}
msg = <-messages
if msg.SeqNum != 3 {
t.Errorf("First message has bad sequence number: %v", msg.SeqNum)
}
if msg.Uid != 28 {
t.Errorf("Second message has bad UID: %v", msg.Uid)
}
if body, _ := ioutil.ReadAll(msg.GetBody(section)); string(body) != "Hello World!" {
t.Errorf("Second message has bad body: %q", body)
}
}
func TestClient_Fetch_Partial(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
seqset, _ := imap.ParseSeqSet("1")
fields := []imap.FetchItem{imap.FetchItem("BODY.PEEK[]<0.10>")}
done := make(chan error, 1)
messages := make(chan *imap.Message, 1)
go func() {
done <- c.Fetch(seqset, fields, messages)
}()
tag, cmd := s.ScanCmd()
if cmd != "FETCH 1 (BODY.PEEK[]<0.10>)" {
t.Fatalf("client sent command %v, want %v", cmd, "FETCH 1 (BODY.PEEK[]<0.10>)")
}
s.WriteString("* 1 FETCH (BODY[]<0> {10}\r\n")
s.WriteString("I love pot")
s.WriteString(")\r\n")
s.WriteString(tag + " OK FETCH completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Fetch() = %v", err)
}
section, _ := imap.ParseBodySectionName("BODY.PEEK[]<0.10>")
msg := <-messages
if body, _ := ioutil.ReadAll(msg.GetBody(section)); string(body) != "I love pot" {
t.Errorf("Message has bad body: %q", body)
}
}
func TestClient_Fetch_Uid(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
seqset, _ := imap.ParseSeqSet("1:867")
fields := []imap.FetchItem{imap.FetchFlags}
done := make(chan error, 1)
messages := make(chan *imap.Message, 1)
go func() {
done <- c.UidFetch(seqset, fields, messages)
}()
tag, cmd := s.ScanCmd()
if cmd != "UID FETCH 1:867 (FLAGS)" {
t.Fatalf("client sent command %v, want %v", cmd, "UID FETCH 1:867 (FLAGS)")
}
s.WriteString("* 23 FETCH (UID 42 FLAGS (\\Seen))\r\n")
s.WriteString(tag + " OK UID FETCH completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.UidFetch() = %v", err)
}
msg := <-messages
if msg.SeqNum != 23 {
t.Errorf("First message has bad sequence number: %v", msg.SeqNum)
}
if msg.Uid != 42 {
t.Errorf("Message has bad UID: %v", msg.Uid)
}
if len(msg.Flags) != 1 || msg.Flags[0] != "\\Seen" {
t.Errorf("Message has bad flags: %v", msg.Flags)
}
}
func TestClient_Store(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
seqset, _ := imap.ParseSeqSet("2")
done := make(chan error, 1)
updates := make(chan *imap.Message, 1)
go func() {
done <- c.Store(seqset, imap.AddFlags, []interface{}{imap.SeenFlag}, updates)
}()
tag, cmd := s.ScanCmd()
if cmd != "STORE 2 +FLAGS (\\Seen)" {
t.Fatalf("client sent command %v, want %v", cmd, "STORE 2 +FLAGS (\\Seen)")
}
s.WriteString("* 2 FETCH (FLAGS (\\Seen))\r\n")
s.WriteString(tag + " OK STORE completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Store() = %v", err)
}
msg := <-updates
if len(msg.Flags) != 1 || msg.Flags[0] != "\\Seen" {
t.Errorf("Bad message flags: %v", msg.Flags)
}
}
func TestClient_Store_Silent(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
seqset, _ := imap.ParseSeqSet("2:3")
done := make(chan error, 1)
go func() {
done <- c.Store(seqset, imap.AddFlags, []interface{}{imap.SeenFlag}, nil)
}()
tag, cmd := s.ScanCmd()
if cmd != "STORE 2:3 +FLAGS.SILENT (\\Seen)" {
t.Fatalf("client sent command %v, want %v", cmd, "STORE 2:3 +FLAGS.SILENT (\\Seen)")
}
s.WriteString(tag + " OK STORE completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Store() = %v", err)
}
}
func TestClient_Store_Uid(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
seqset, _ := imap.ParseSeqSet("27:901")
done := make(chan error, 1)
go func() {
done <- c.UidStore(seqset, imap.AddFlags, []interface{}{imap.DeletedFlag}, nil)
}()
tag, cmd := s.ScanCmd()
if cmd != "UID STORE 27:901 +FLAGS.SILENT (\\Deleted)" {
t.Fatalf("client sent command %v, want %v", cmd, "UID STORE 27:901 +FLAGS.SILENT (\\Deleted)")
}
s.WriteString(tag + " OK STORE completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.UidStore() = %v", err)
}
}
func TestClient_Copy(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
seqset, _ := imap.ParseSeqSet("2:4")
done := make(chan error, 1)
go func() {
done <- c.Copy(seqset, "Sent")
}()
tag, cmd := s.ScanCmd()
if cmd != "COPY 2:4 Sent" {
t.Fatalf("client sent command %v, want %v", cmd, "COPY 2:4 Sent")
}
s.WriteString(tag + " OK COPY completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.Copy() = %v", err)
}
}
func TestClient_Copy_Uid(t *testing.T) {
c, s := newTestClient(t)
defer s.Close()
setClientState(c, imap.SelectedState, nil)
seqset, _ := imap.ParseSeqSet("78:102")
done := make(chan error, 1)
go func() {
done <- c.UidCopy(seqset, "Drafts")
}()
tag, cmd := s.ScanCmd()
if cmd != "UID COPY 78:102 Drafts" {
t.Fatalf("client sent command %v, want %v", cmd, "UID COPY 78:102 Drafts")
}
s.WriteString(tag + " OK UID COPY completed\r\n")
if err := <-done; err != nil {
t.Fatalf("c.UidCopy() = %v", err)
}
}

262
client/example_test.go Normal file
View File

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

24
client/tag.go Normal file
View File

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

View File

@@ -1,128 +0,0 @@
package main
import (
"crypto/tls"
"flag"
"io"
"log"
"net"
"os"
"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/imapserver"
"github.com/emersion/go-imap/v2/imapserver/imapmemserver"
)
var (
listen string
tlsCert string
tlsKey string
username string
password string
debug bool
insecureAuth bool
)
func main() {
flag.StringVar(&listen, "listen", "localhost:143", "listening address")
flag.StringVar(&tlsCert, "tls-cert", "", "TLS certificate")
flag.StringVar(&tlsKey, "tls-key", "", "TLS key")
flag.StringVar(&username, "username", "user", "Username")
flag.StringVar(&password, "password", "user", "Password")
flag.BoolVar(&debug, "debug", false, "Print all commands and responses")
flag.BoolVar(&insecureAuth, "insecure-auth", false, "Allow authentication without TLS")
flag.Parse()
var tlsConfig *tls.Config
if tlsCert != "" || tlsKey != "" {
cert, err := tls.LoadX509KeyPair(tlsCert, tlsKey)
if err != nil {
log.Fatalf("Failed to load TLS key pair: %v", err)
}
tlsConfig = &tls.Config{
Certificates: []tls.Certificate{cert},
}
}
ln, err := net.Listen("tcp", listen)
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
log.Printf("IMAP server listening on %v", ln.Addr())
memServer := imapmemserver.New()
if username != "" || password != "" {
user := imapmemserver.NewUser(username, password)
// Create standard mailboxes with special-use attributes as per RFC 6154
if err := user.Create("INBOX", nil); err != nil {
log.Printf("Failed to create INBOX: %v", err)
}
if err := user.Create("Drafts", &imap.CreateOptions{
SpecialUse: []imap.MailboxAttr{imap.MailboxAttrDrafts},
}); err != nil {
log.Printf("Failed to create Drafts mailbox: %v", err)
}
if err := user.Create("Sent", &imap.CreateOptions{
SpecialUse: []imap.MailboxAttr{imap.MailboxAttrSent},
}); err != nil {
log.Printf("Failed to create Sent mailbox: %v", err)
}
if err := user.Create("Archive", &imap.CreateOptions{
SpecialUse: []imap.MailboxAttr{imap.MailboxAttrArchive},
}); err != nil {
log.Printf("Failed to create Archive mailbox: %v", err)
}
if err := user.Create("Junk", &imap.CreateOptions{
SpecialUse: []imap.MailboxAttr{imap.MailboxAttrJunk},
}); err != nil {
log.Printf("Failed to create Junk mailbox: %v", err)
}
if err := user.Create("Trash", &imap.CreateOptions{
SpecialUse: []imap.MailboxAttr{imap.MailboxAttrTrash},
}); err != nil {
log.Printf("Failed to create Trash mailbox: %v", err)
}
if err := user.Create("Flagged", &imap.CreateOptions{
SpecialUse: []imap.MailboxAttr{imap.MailboxAttrFlagged},
}); err != nil {
log.Printf("Failed to create Flagged mailbox: %v", err)
}
// Subscribe to the most commonly used mailboxes
_ = user.Subscribe("INBOX")
_ = user.Subscribe("Drafts")
_ = user.Subscribe("Sent")
_ = user.Subscribe("Trash")
memServer.AddUser(user)
}
var debugWriter io.Writer
if debug {
debugWriter = os.Stdout
}
server := imapserver.New(&imapserver.Options{
NewSession: func(conn *imapserver.Conn) (imapserver.Session, *imapserver.GreetingData, error) {
return memServer.NewSession(), nil, nil
},
Caps: imap.CapSet{
imap.CapIMAP4rev1: {},
imap.CapIMAP4rev2: {},
},
TLSConfig: tlsConfig,
InsecureAuth: insecureAuth,
DebugWriter: debugWriter,
})
if err := server.Serve(ln); err != nil {
log.Fatalf("Serve() = %v", err)
}
}

57
command.go Normal file
View File

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

98
command_test.go Normal file
View File

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

93
commands/append.go Normal file
View File

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

83
commands/authenticate.go Normal file
View File

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

18
commands/capability.go Normal file
View File

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

18
commands/check.go Normal file
View File

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

18
commands/close.go Normal file
View File

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

2
commands/commands.go Normal file
View File

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

47
commands/copy.go Normal file
View File

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

38
commands/create.go Normal file
View File

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

38
commands/delete.go Normal file
View File

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

16
commands/expunge.go Normal file
View File

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

59
commands/fetch.go Normal file
View File

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

60
commands/list.go Normal file
View File

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

36
commands/login.go Normal file
View File

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

18
commands/logout.go Normal file
View File

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

18
commands/noop.go Normal file
View File

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

51
commands/rename.go Normal file
View File

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

57
commands/search.go Normal file
View File

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

45
commands/select.go Normal file
View File

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

18
commands/starttls.go Normal file
View File

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

58
commands/status.go Normal file
View File

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

47
commands/store.go Normal file
View File

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

63
commands/subscribe.go Normal file
View File

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

44
commands/uid.go Normal file
View File

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

194
conn.go Normal file
View File

@@ -0,0 +1,194 @@
package imap
import (
"bufio"
"io"
"net"
)
// A connection state.
// See RFC 3501 section 3.
type ConnState int
const (
// In the connecting state, the server has not yet sent a greeting and no
// command can be issued.
ConnectingState = 0
// In the not authenticated state, the client MUST supply
// authentication credentials before most commands will be
// permitted. This state is entered when a connection starts
// unless the connection has been pre-authenticated.
NotAuthenticatedState ConnState = 1 << 0
// In the authenticated state, the client is authenticated and MUST
// select a mailbox to access before commands that affect messages
// will be permitted. This state is entered when a
// pre-authenticated connection starts, when acceptable
// authentication credentials have been provided, after an error in
// selecting a mailbox, or after a successful CLOSE command.
AuthenticatedState = 1 << 1
// In a selected state, a mailbox has been selected to access.
// This state is entered when a mailbox has been successfully
// selected.
SelectedState = AuthenticatedState + 1<<2
// In the logout state, the connection is being terminated. This
// state can be entered as a result of a client request (via the
// LOGOUT command) or by unilateral action on the part of either
// the client or server.
LogoutState = 1 << 3
// ConnectedState is either NotAuthenticatedState, AuthenticatedState or
// SelectedState.
ConnectedState = NotAuthenticatedState | AuthenticatedState | SelectedState
)
// A function that upgrades a connection.
//
// This should only be used by libraries implementing an IMAP extension (e.g.
// COMPRESS).
type ConnUpgrader func(conn net.Conn) (net.Conn, error)
type debugWriter struct {
io.Writer
local io.Writer
remote io.Writer
}
// NewDebugWriter creates a new io.Writer that will write local network activity
// to local and remote network activity to remote.
func NewDebugWriter(local, remote io.Writer) io.Writer {
return &debugWriter{Writer: local, local: local, remote: remote}
}
type multiFlusher struct {
flushers []flusher
}
func (mf *multiFlusher) Flush() error {
for _, f := range mf.flushers {
if err := f.Flush(); err != nil {
return err
}
}
return nil
}
func newMultiFlusher(flushers ...flusher) flusher {
return &multiFlusher{flushers}
}
// An IMAP connection.
type Conn struct {
net.Conn
*Reader
*Writer
br *bufio.Reader
bw *bufio.Writer
waits chan struct{}
// Print all commands and responses to this io.Writer.
debug io.Writer
}
// NewConn creates a new IMAP connection.
func NewConn(conn net.Conn, r *Reader, w *Writer) *Conn {
c := &Conn{Conn: conn, Reader: r, Writer: w}
c.init()
return c
}
func (c *Conn) init() {
r := io.Reader(c.Conn)
w := io.Writer(c.Conn)
if c.debug != nil {
localDebug, remoteDebug := c.debug, c.debug
if debug, ok := c.debug.(*debugWriter); ok {
localDebug, remoteDebug = debug.local, debug.remote
}
if localDebug != nil {
w = io.MultiWriter(c.Conn, localDebug)
}
if remoteDebug != nil {
r = io.TeeReader(c.Conn, remoteDebug)
}
}
if c.br == nil {
c.br = bufio.NewReader(r)
c.Reader.reader = c.br
} else {
c.br.Reset(r)
}
if c.bw == nil {
c.bw = bufio.NewWriter(w)
c.Writer.Writer = c.bw
} else {
c.bw.Reset(w)
}
if f, ok := c.Conn.(flusher); ok {
c.Writer.Writer = struct {
io.Writer
flusher
}{
c.bw,
newMultiFlusher(c.bw, f),
}
}
}
// Write implements io.Writer.
func (c *Conn) Write(b []byte) (n int, err error) {
return c.Writer.Write(b)
}
// Flush writes any buffered data to the underlying connection.
func (c *Conn) Flush() error {
return c.Writer.Flush()
}
// Upgrade a connection, e.g. wrap an unencrypted connection with an encrypted
// tunnel.
func (c *Conn) Upgrade(upgrader ConnUpgrader) error {
// Flush all buffered data
if err := c.Flush(); err != nil {
return err
}
// Block reads and writes during the upgrading process
c.waits = make(chan struct{})
defer close(c.waits)
upgraded, err := upgrader(c.Conn)
if err != nil {
return err
}
c.Conn = upgraded
c.init()
return nil
}
// Wait waits for the connection to be ready for reads and writes.
func (c *Conn) Wait() {
if c.waits != nil {
<-c.waits
}
}
// SetDebug defines an io.Writer to which all network activity will be logged.
// If nil is provided, network activity will not be logged.
func (c *Conn) SetDebug(w io.Writer) {
c.debug = w
c.init()
}

106
conn_test.go Normal file
View File

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

View File

@@ -1,9 +0,0 @@
package imap
// CopyData is the data returned by a COPY command.
type CopyData struct {
// requires UIDPLUS or IMAP4rev2
UIDValidity uint32
SourceUIDs UIDSet
DestUIDs UIDSet
}

View File

@@ -1,6 +0,0 @@
package imap
// CreateOptions contains options for the CREATE command.
type CreateOptions struct {
SpecialUse []MailboxAttr // requires CREATE-SPECIAL-USE
}

72
date.go Normal file
View File

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

95
date_test.go Normal file
View File

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

284
fetch.go
View File

@@ -1,284 +0,0 @@
package imap
import (
"fmt"
"strings"
"time"
)
// FetchOptions contains options for the FETCH command.
type FetchOptions struct {
// Fields to fetch
BodyStructure *FetchItemBodyStructure
Envelope bool
Flags bool
InternalDate bool
RFC822Size bool
UID bool
BodySection []*FetchItemBodySection
BinarySection []*FetchItemBinarySection // requires IMAP4rev2 or BINARY
BinarySectionSize []*FetchItemBinarySectionSize // requires IMAP4rev2 or BINARY
ModSeq bool // requires CONDSTORE
ChangedSince uint64 // requires CONDSTORE
}
// FetchItemBodyStructure contains FETCH options for the body structure.
type FetchItemBodyStructure struct {
Extended bool
}
// PartSpecifier describes whether to fetch a part's header, body, or both.
type PartSpecifier string
const (
PartSpecifierNone PartSpecifier = ""
PartSpecifierHeader PartSpecifier = "HEADER"
PartSpecifierMIME PartSpecifier = "MIME"
PartSpecifierText PartSpecifier = "TEXT"
)
// SectionPartial describes a byte range when fetching a message's payload.
type SectionPartial struct {
Offset, Size int64
}
// FetchItemBodySection is a FETCH BODY[] data item.
//
// To fetch the whole body of a message, use the zero FetchItemBodySection:
//
// imap.FetchItemBodySection{}
//
// To fetch only a specific part, use the Part field:
//
// imap.FetchItemBodySection{Part: []int{1, 2, 3}}
//
// To fetch only the header of the message, use the Specifier field:
//
// imap.FetchItemBodySection{Specifier: imap.PartSpecifierHeader}
type FetchItemBodySection struct {
Specifier PartSpecifier
Part []int
HeaderFields []string
HeaderFieldsNot []string
Partial *SectionPartial
Peek bool
}
// FetchItemBinarySection is a FETCH BINARY[] data item.
type FetchItemBinarySection struct {
Part []int
Partial *SectionPartial
Peek bool
}
// FetchItemBinarySectionSize is a FETCH BINARY.SIZE[] data item.
type FetchItemBinarySectionSize struct {
Part []int
}
// Envelope is the envelope structure of a message.
//
// The subject and addresses are UTF-8 (ie, not in their encoded form). The
// In-Reply-To and Message-ID values contain message identifiers without angle
// brackets.
type Envelope struct {
Date time.Time
Subject string
From []Address
Sender []Address
ReplyTo []Address
To []Address
Cc []Address
Bcc []Address
InReplyTo []string
MessageID string
}
// Address represents a sender or recipient of a message.
type Address struct {
Name string
Mailbox string
Host string
}
// Addr returns the e-mail address in the form "foo@example.org".
//
// If the address is a start or end of group, the empty string is returned.
func (addr *Address) Addr() string {
if addr.Mailbox == "" || addr.Host == "" {
return ""
}
return addr.Mailbox + "@" + addr.Host
}
// IsGroupStart returns true if this address is a start of group marker.
//
// In that case, Mailbox contains the group name phrase.
func (addr *Address) IsGroupStart() bool {
return addr.Host == "" && addr.Mailbox != ""
}
// IsGroupEnd returns true if this address is a end of group marker.
func (addr *Address) IsGroupEnd() bool {
return addr.Host == "" && addr.Mailbox == ""
}
// BodyStructure describes the body structure of a message.
//
// A BodyStructure value is either a *BodyStructureSinglePart or a
// *BodyStructureMultiPart.
type BodyStructure interface {
// MediaType returns the MIME type of this body structure, e.g. "text/plain".
MediaType() string
// Walk walks the body structure tree, calling f for each part in the tree,
// including bs itself. The parts are visited in DFS pre-order.
Walk(f BodyStructureWalkFunc)
// Disposition returns the body structure disposition, if available.
Disposition() *BodyStructureDisposition
bodyStructure()
}
var (
_ BodyStructure = (*BodyStructureSinglePart)(nil)
_ BodyStructure = (*BodyStructureMultiPart)(nil)
)
// BodyStructureSinglePart is a body structure with a single part.
type BodyStructureSinglePart struct {
Type, Subtype string
Params map[string]string
ID string
Description string
Encoding string
Size uint32
MessageRFC822 *BodyStructureMessageRFC822 // only for "message/rfc822"
Text *BodyStructureText // only for "text/*"
Extended *BodyStructureSinglePartExt
}
func (bs *BodyStructureSinglePart) MediaType() string {
return strings.ToLower(bs.Type) + "/" + strings.ToLower(bs.Subtype)
}
func (bs *BodyStructureSinglePart) Walk(f BodyStructureWalkFunc) {
f([]int{1}, bs)
}
func (bs *BodyStructureSinglePart) Disposition() *BodyStructureDisposition {
if bs.Extended == nil {
return nil
}
return bs.Extended.Disposition
}
// Filename decodes the body structure's filename, if any.
func (bs *BodyStructureSinglePart) Filename() string {
var filename string
if bs.Extended != nil && bs.Extended.Disposition != nil {
filename = bs.Extended.Disposition.Params["filename"]
}
if filename == "" {
// Note: using "name" in Content-Type is discouraged
filename = bs.Params["name"]
}
return filename
}
func (*BodyStructureSinglePart) bodyStructure() {}
// BodyStructureMessageRFC822 contains metadata specific to RFC 822 parts for
// BodyStructureSinglePart.
type BodyStructureMessageRFC822 struct {
Envelope *Envelope
BodyStructure BodyStructure
NumLines int64
}
// BodyStructureText contains metadata specific to text parts for
// BodyStructureSinglePart.
type BodyStructureText struct {
NumLines int64
}
// BodyStructureSinglePartExt contains extended body structure data for
// BodyStructureSinglePart.
type BodyStructureSinglePartExt struct {
Disposition *BodyStructureDisposition
Language []string
Location string
}
// BodyStructureMultiPart is a body structure with multiple parts.
type BodyStructureMultiPart struct {
Children []BodyStructure
Subtype string
Extended *BodyStructureMultiPartExt
}
func (bs *BodyStructureMultiPart) MediaType() string {
return "multipart/" + strings.ToLower(bs.Subtype)
}
func (bs *BodyStructureMultiPart) Walk(f BodyStructureWalkFunc) {
bs.walk(f, nil)
}
func (bs *BodyStructureMultiPart) walk(f BodyStructureWalkFunc, path []int) {
if !f(path, bs) {
return
}
pathBuf := make([]int, len(path))
copy(pathBuf, path)
for i, part := range bs.Children {
num := i + 1
partPath := append(pathBuf, num)
switch part := part.(type) {
case *BodyStructureSinglePart:
f(partPath, part)
case *BodyStructureMultiPart:
part.walk(f, partPath)
default:
panic(fmt.Errorf("unsupported body structure type %T", part))
}
}
}
func (bs *BodyStructureMultiPart) Disposition() *BodyStructureDisposition {
if bs.Extended == nil {
return nil
}
return bs.Extended.Disposition
}
func (*BodyStructureMultiPart) bodyStructure() {}
// BodyStructureMultiPartExt contains extended body structure data for
// BodyStructureMultiPart.
type BodyStructureMultiPartExt struct {
Params map[string]string
Disposition *BodyStructureDisposition
Language []string
Location string
}
// BodyStructureDisposition describes the content disposition of a part
// (specified in the Content-Disposition header field).
type BodyStructureDisposition struct {
Value string
Params map[string]string
}
// BodyStructureWalkFunc is a function called for each body structure visited
// by BodyStructure.Walk.
//
// The path argument contains the IMAP part path.
//
// The function should return true to visit all of the part's children or false
// to skip them.
type BodyStructureWalkFunc func(path []int, part BodyStructure) (walkChildren bool)

10
go.mod
View File

@@ -1,8 +1,8 @@
module github.com/emersion/go-imap/v2 module github.com/emersion/go-imap
go 1.18
require ( require (
github.com/emersion/go-message v0.18.2 github.com/emersion/go-message v0.9.1
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 github.com/emersion/go-sasl v0.0.0-20161116183048-7e096a0a6197
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe // indirect
golang.org/x/text v0.3.0
) )

35
go.sum
View File

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

15
id.go
View File

@@ -1,15 +0,0 @@
package imap
type IDData struct {
Name string
Version string
OS string
OSVersion string
Vendor string
SupportURL string
Address string
Date string
Command string
Arguments string
Environment string
}

167
imap.go
View File

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

View File

@@ -1,138 +0,0 @@
package imapclient
import (
"fmt"
"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/internal"
"github.com/emersion/go-imap/v2/internal/imapwire"
)
// MyRights sends a MYRIGHTS command.
//
// This command requires support for the ACL extension.
func (c *Client) MyRights(mailbox string) *MyRightsCommand {
cmd := &MyRightsCommand{}
enc := c.beginCommand("MYRIGHTS", cmd)
enc.SP().Mailbox(mailbox)
enc.end()
return cmd
}
// SetACL sends a SETACL command.
//
// This command requires support for the ACL extension.
func (c *Client) SetACL(mailbox string, ri imap.RightsIdentifier, rm imap.RightModification, rs imap.RightSet) *SetACLCommand {
cmd := &SetACLCommand{}
enc := c.beginCommand("SETACL", cmd)
enc.SP().Mailbox(mailbox).SP().String(string(ri)).SP()
enc.String(internal.FormatRights(rm, rs))
enc.end()
return cmd
}
// SetACLCommand is a SETACL command.
type SetACLCommand struct {
commandBase
}
func (cmd *SetACLCommand) Wait() error {
return cmd.wait()
}
// GetACL sends a GETACL command.
//
// This command requires support for the ACL extension.
func (c *Client) GetACL(mailbox string) *GetACLCommand {
cmd := &GetACLCommand{}
enc := c.beginCommand("GETACL", cmd)
enc.SP().Mailbox(mailbox)
enc.end()
return cmd
}
// GetACLCommand is a GETACL command.
type GetACLCommand struct {
commandBase
data GetACLData
}
func (cmd *GetACLCommand) Wait() (*GetACLData, error) {
return &cmd.data, cmd.wait()
}
func (c *Client) handleMyRights() error {
data, err := readMyRights(c.dec)
if err != nil {
return fmt.Errorf("in myrights-response: %v", err)
}
if cmd := findPendingCmdByType[*MyRightsCommand](c); cmd != nil {
cmd.data = *data
}
return nil
}
func (c *Client) handleGetACL() error {
data, err := readGetACL(c.dec)
if err != nil {
return fmt.Errorf("in getacl-response: %v", err)
}
if cmd := findPendingCmdByType[*GetACLCommand](c); cmd != nil {
cmd.data = *data
}
return nil
}
// MyRightsCommand is a MYRIGHTS command.
type MyRightsCommand struct {
commandBase
data MyRightsData
}
func (cmd *MyRightsCommand) Wait() (*MyRightsData, error) {
return &cmd.data, cmd.wait()
}
// MyRightsData is the data returned by the MYRIGHTS command.
type MyRightsData struct {
Mailbox string
Rights imap.RightSet
}
func readMyRights(dec *imapwire.Decoder) (*MyRightsData, error) {
var (
rights string
data MyRightsData
)
if !dec.ExpectMailbox(&data.Mailbox) || !dec.ExpectSP() || !dec.ExpectAString(&rights) {
return nil, dec.Err()
}
data.Rights = imap.RightSet(rights)
return &data, nil
}
// GetACLData is the data returned by the GETACL command.
type GetACLData struct {
Mailbox string
Rights map[imap.RightsIdentifier]imap.RightSet
}
func readGetACL(dec *imapwire.Decoder) (*GetACLData, error) {
data := &GetACLData{Rights: make(map[imap.RightsIdentifier]imap.RightSet)}
if !dec.ExpectMailbox(&data.Mailbox) {
return nil, dec.Err()
}
for dec.SP() {
var rsStr, riStr string
if !dec.ExpectAString(&riStr) || !dec.ExpectSP() || !dec.ExpectAString(&rsStr) {
return nil, dec.Err()
}
data.Rights[imap.RightsIdentifier(riStr)] = imap.RightSet(rsStr)
}
return data, nil
}

View File

@@ -1,115 +0,0 @@
package imapclient_test
import (
"testing"
"github.com/emersion/go-imap/v2"
)
// order matters
var testCases = []struct {
name string
mailbox string
setRightsModification imap.RightModification
setRights imap.RightSet
expectedRights imap.RightSet
execStatusCmd bool
}{
{
name: "inbox",
mailbox: "INBOX",
setRightsModification: imap.RightModificationReplace,
setRights: imap.RightSet("akxeilprwtscd"),
expectedRights: imap.RightSet("akxeilprwtscd"),
},
{
name: "custom_folder",
mailbox: "MyFolder",
setRightsModification: imap.RightModificationReplace,
setRights: imap.RightSet("ailw"),
expectedRights: imap.RightSet("ailw"),
},
{
name: "custom_child_folder",
mailbox: "MyFolder/Child",
setRightsModification: imap.RightModificationReplace,
setRights: imap.RightSet("aelrwtd"),
expectedRights: imap.RightSet("aelrwtd"),
},
{
name: "add_rights",
mailbox: "MyFolder",
setRightsModification: imap.RightModificationAdd,
setRights: imap.RightSet("rwi"),
expectedRights: imap.RightSet("ailwr"),
},
{
name: "remove_rights",
mailbox: "MyFolder",
setRightsModification: imap.RightModificationRemove,
setRights: imap.RightSet("iwc"),
expectedRights: imap.RightSet("alr"),
},
{
name: "empty_rights",
mailbox: "MyFolder/Child",
setRightsModification: imap.RightModificationReplace,
setRights: imap.RightSet("a"),
expectedRights: imap.RightSet("a"),
},
}
// TestACL runs tests on SetACL, GetACL and MyRights commands.
func TestACL(t *testing.T) {
client, server := newClientServerPair(t, imap.ConnStateAuthenticated)
defer client.Close()
defer server.Close()
if !client.Caps().Has(imap.CapACL) {
t.Skipf("server doesn't support ACL")
}
if err := client.Create("MyFolder", nil).Wait(); err != nil {
t.Fatalf("create MyFolder error: %v", err)
}
if err := client.Create("MyFolder/Child", nil).Wait(); err != nil {
t.Fatalf("create MyFolder/Child error: %v", err)
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// execute SETACL command
err := client.SetACL(tc.mailbox, testUsername, tc.setRightsModification, tc.setRights).Wait()
if err != nil {
t.Fatalf("SetACL().Wait() error: %v", err)
}
// execute GETACL command to reset cache on server
getACLData, err := client.GetACL(tc.mailbox).Wait()
if err != nil {
t.Fatalf("GetACL().Wait() error: %v", err)
}
if !tc.expectedRights.Equal(getACLData.Rights[testUsername]) {
t.Errorf("GETACL returned wrong rights; expected: %s, got: %s", tc.expectedRights, getACLData.Rights[testUsername])
}
// execute MYRIGHTS command
myRightsData, err := client.MyRights(tc.mailbox).Wait()
if err != nil {
t.Errorf("MyRights().Wait() error: %v", err)
}
if !tc.expectedRights.Equal(myRightsData.Rights) {
t.Errorf("MYRIGHTS returned wrong rights; expected: %s, got: %s", tc.expectedRights, myRightsData.Rights)
}
})
}
t.Run("nonexistent_mailbox", func(t *testing.T) {
if client.SetACL("BibiMailbox", testUsername, imap.RightModificationReplace, nil).Wait() == nil {
t.Errorf("expected error")
}
})
}

View File

@@ -1,58 +0,0 @@
package imapclient
import (
"io"
"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/internal"
)
// Append sends an APPEND command.
//
// The caller must call AppendCommand.Close.
//
// The options are optional.
func (c *Client) Append(mailbox string, size int64, options *imap.AppendOptions) *AppendCommand {
cmd := &AppendCommand{}
cmd.enc = c.beginCommand("APPEND", cmd)
cmd.enc.SP().Mailbox(mailbox).SP()
if options != nil && len(options.Flags) > 0 {
cmd.enc.List(len(options.Flags), func(i int) {
cmd.enc.Flag(options.Flags[i])
}).SP()
}
if options != nil && !options.Time.IsZero() {
cmd.enc.String(options.Time.Format(internal.DateTimeLayout)).SP()
}
// TODO: literal8 for BINARY
// TODO: UTF8 data ext for UTF8=ACCEPT, with literal8
cmd.wc = cmd.enc.Literal(size)
return cmd
}
// AppendCommand is an APPEND command.
//
// Callers must write the message contents, then call Close.
type AppendCommand struct {
commandBase
enc *commandEncoder
wc io.WriteCloser
data imap.AppendData
}
func (cmd *AppendCommand) Write(b []byte) (int, error) {
return cmd.wc.Write(b)
}
func (cmd *AppendCommand) Close() error {
err := cmd.wc.Close()
if cmd.enc != nil {
cmd.enc.end()
cmd.enc = nil
}
return err
}
func (cmd *AppendCommand) Wait() (*imap.AppendData, error) {
return &cmd.data, cmd.wait()
}

View File

@@ -1,28 +0,0 @@
package imapclient_test
import (
"testing"
"github.com/emersion/go-imap/v2"
)
func TestAppend(t *testing.T) {
client, server := newClientServerPair(t, imap.ConnStateSelected)
defer client.Close()
defer server.Close()
body := "This is a test message."
appendCmd := client.Append("INBOX", int64(len(body)), nil)
if _, err := appendCmd.Write([]byte(body)); err != nil {
t.Fatalf("AppendCommand.Write() = %v", err)
}
if err := appendCmd.Close(); err != nil {
t.Fatalf("AppendCommand.Close() = %v", err)
}
if _, err := appendCmd.Wait(); err != nil {
t.Fatalf("AppendCommand.Wait() = %v", err)
}
// TODO: fetch back message and check body
}

View File

@@ -1,100 +0,0 @@
package imapclient
import (
"fmt"
"github.com/emersion/go-sasl"
"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/internal"
)
// Authenticate sends an AUTHENTICATE command.
//
// Unlike other commands, this method blocks until the SASL exchange completes.
func (c *Client) Authenticate(saslClient sasl.Client) error {
mech, initialResp, err := saslClient.Start()
if err != nil {
return err
}
// c.Caps may send a CAPABILITY command, so check it before c.beginCommand
var hasSASLIR bool
if initialResp != nil {
hasSASLIR = c.Caps().Has(imap.CapSASLIR)
}
cmd := &authenticateCommand{}
contReq := c.registerContReq(cmd)
enc := c.beginCommand("AUTHENTICATE", cmd)
enc.SP().Atom(mech)
if initialResp != nil && hasSASLIR {
enc.SP().Atom(internal.EncodeSASL(initialResp))
initialResp = nil
}
enc.flush()
defer enc.end()
for {
challengeStr, err := contReq.Wait()
if err != nil {
return cmd.wait()
}
if challengeStr == "" {
if initialResp == nil {
return fmt.Errorf("imapclient: server requested SASL initial response, but we don't have one")
}
contReq = c.registerContReq(cmd)
if err := c.writeSASLResp(initialResp); err != nil {
return err
}
initialResp = nil
continue
}
challenge, err := internal.DecodeSASL(challengeStr)
if err != nil {
return err
}
resp, err := saslClient.Next(challenge)
if err != nil {
return err
}
contReq = c.registerContReq(cmd)
if err := c.writeSASLResp(resp); err != nil {
return err
}
}
}
type authenticateCommand struct {
commandBase
}
func (c *Client) writeSASLResp(resp []byte) error {
respStr := internal.EncodeSASL(resp)
if _, err := c.bw.WriteString(respStr + "\r\n"); err != nil {
return err
}
if err := c.bw.Flush(); err != nil {
return err
}
return nil
}
// Unauthenticate sends an UNAUTHENTICATE command.
//
// This command requires support for the UNAUTHENTICATE extension.
func (c *Client) Unauthenticate() *Command {
cmd := &unauthenticateCommand{}
c.beginCommand("UNAUTHENTICATE", cmd).end()
return &cmd.Command
}
type unauthenticateCommand struct {
Command
}

View File

@@ -1,24 +0,0 @@
package imapclient_test
import (
"testing"
"github.com/emersion/go-sasl"
"github.com/emersion/go-imap/v2"
)
func TestClient_Authenticate(t *testing.T) {
client, server := newClientServerPair(t, imap.ConnStateNotAuthenticated)
defer client.Close()
defer server.Close()
saslClient := sasl.NewPlainClient("", testUsername, testPassword)
if err := client.Authenticate(saslClient); err != nil {
t.Fatalf("Authenticate() = %v", err)
}
if state := client.State(); state != imap.ConnStateAuthenticated {
t.Errorf("State() = %v, want %v", state, imap.ConnStateAuthenticated)
}
}

View File

@@ -1,56 +0,0 @@
package imapclient
import (
"fmt"
"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/internal"
"github.com/emersion/go-imap/v2/internal/imapwire"
)
// Capability sends a CAPABILITY command.
func (c *Client) Capability() *CapabilityCommand {
cmd := &CapabilityCommand{}
c.beginCommand("CAPABILITY", cmd).end()
return cmd
}
func (c *Client) handleCapability() error {
caps, err := readCapabilities(c.dec)
if err != nil {
return err
}
c.setCaps(caps)
if cmd := findPendingCmdByType[*CapabilityCommand](c); cmd != nil {
cmd.caps = caps
}
return nil
}
// CapabilityCommand is a CAPABILITY command.
type CapabilityCommand struct {
commandBase
caps imap.CapSet
}
func (cmd *CapabilityCommand) Wait() (imap.CapSet, error) {
err := cmd.wait()
return cmd.caps, err
}
func readCapabilities(dec *imapwire.Decoder) (imap.CapSet, error) {
caps := make(imap.CapSet)
for dec.SP() {
// Some IMAP servers send multiple SP between caps:
// https://github.com/emersion/go-imap/pull/652
for dec.SP() {
}
cap, err := internal.ExpectCap(dec)
if err != nil {
return caps, fmt.Errorf("in capability-data: %w", err)
}
caps[cap] = struct{}{}
}
return caps, nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,277 +0,0 @@
package imapclient_test
import (
"crypto/tls"
"io"
"net"
"os"
"sync"
"testing"
"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/imapclient"
"github.com/emersion/go-imap/v2/imapserver"
"github.com/emersion/go-imap/v2/imapserver/imapmemserver"
)
const (
testUsername = "test-user"
testPassword = "test-password"
)
const simpleRawMessage = `MIME-Version: 1.0
Message-Id: <191101702316132@example.com>
Content-Transfer-Encoding: 8bit
Content-Type: text/plain; charset=utf-8
This is my letter!`
var rsaCertPEM = `-----BEGIN CERTIFICATE-----
MIIDOTCCAiGgAwIBAgIQSRJrEpBGFc7tNb1fb5pKFzANBgkqhkiG9w0BAQsFADAS
MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw
MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEA6Gba5tHV1dAKouAaXO3/ebDUU4rvwCUg/CNaJ2PT5xLD4N1Vcb8r
bFSW2HXKq+MPfVdwIKR/1DczEoAGf/JWQTW7EgzlXrCd3rlajEX2D73faWJekD0U
aUgz5vtrTXZ90BQL7WvRICd7FlEZ6FPOcPlumiyNmzUqtwGhO+9ad1W5BqJaRI6P
YfouNkwR6Na4TzSj5BrqUfP0FwDizKSJ0XXmh8g8G9mtwxOSN3Ru1QFc61Xyeluk
POGKBV/q6RBNklTNe0gI8usUMlYyoC7ytppNMW7X2vodAelSu25jgx2anj9fDVZu
h7AXF5+4nJS4AAt0n1lNY7nGSsdZas8PbQIDAQABo4GIMIGFMA4GA1UdDwEB/wQE
AwICpDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud
DgQWBBStsdjh3/JCXXYlQryOrL4Sh7BW5TAuBgNVHREEJzAlggtleGFtcGxlLmNv
bYcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG9w0BAQsFAAOCAQEAxWGI
5NhpF3nwwy/4yB4i/CwwSpLrWUa70NyhvprUBC50PxiXav1TeDzwzLx/o5HyNwsv
cxv3HdkLW59i/0SlJSrNnWdfZ19oTcS+6PtLoVyISgtyN6DpkKpdG1cOkW3Cy2P2
+tK/tKHRP1Y/Ra0RiDpOAmqn0gCOFGz8+lqDIor/T7MTpibL3IxqWfPrvfVRHL3B
grw/ZQTTIVjjh4JBSW3WyWgNo/ikC1lrVxzl4iPUGptxT36Cr7Zk2Bsg0XqwbOvK
5d+NTDREkSnUbie4GeutujmX3Dsx88UiV6UY/4lHJa6I5leHUNOHahRbpbWeOfs/
WkBKOclmOV2xlTVuPw==
-----END CERTIFICATE-----
`
var rsaKeyPEM = `-----BEGIN RSA PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDoZtrm0dXV0Aqi
4Bpc7f95sNRTiu/AJSD8I1onY9PnEsPg3VVxvytsVJbYdcqr4w99V3AgpH/UNzMS
gAZ/8lZBNbsSDOVesJ3euVqMRfYPvd9pYl6QPRRpSDPm+2tNdn3QFAvta9EgJ3sW
URnoU85w+W6aLI2bNSq3AaE771p3VbkGolpEjo9h+i42TBHo1rhPNKPkGupR8/QX
AOLMpInRdeaHyDwb2a3DE5I3dG7VAVzrVfJ6W6Q84YoFX+rpEE2SVM17SAjy6xQy
VjKgLvK2mk0xbtfa+h0B6VK7bmODHZqeP18NVm6HsBcXn7iclLgAC3SfWU1jucZK
x1lqzw9tAgMBAAECggEABWzxS1Y2wckblnXY57Z+sl6YdmLV+gxj2r8Qib7g4ZIk
lIlWR1OJNfw7kU4eryib4fc6nOh6O4AWZyYqAK6tqNQSS/eVG0LQTLTTEldHyVJL
dvBe+MsUQOj4nTndZW+QvFzbcm2D8lY5n2nBSxU5ypVoKZ1EqQzytFcLZpTN7d89
EPj0qDyrV4NZlWAwL1AygCwnlwhMQjXEalVF1ylXwU3QzyZ/6MgvF6d3SSUlh+sq
XefuyigXw484cQQgbzopv6niMOmGP3of+yV4JQqUSb3IDmmT68XjGd2Dkxl4iPki
6ZwXf3CCi+c+i/zVEcufgZ3SLf8D99kUGE7v7fZ6AQKBgQD1ZX3RAla9hIhxCf+O
3D+I1j2LMrdjAh0ZKKqwMR4JnHX3mjQI6LwqIctPWTU8wYFECSh9klEclSdCa64s
uI/GNpcqPXejd0cAAdqHEEeG5sHMDt0oFSurL4lyud0GtZvwlzLuwEweuDtvT9cJ
Wfvl86uyO36IW8JdvUprYDctrQKBgQDycZ697qutBieZlGkHpnYWUAeImVA878sJ
w44NuXHvMxBPz+lbJGAg8Cn8fcxNAPqHIraK+kx3po8cZGQywKHUWsxi23ozHoxo
+bGqeQb9U661TnfdDspIXia+xilZt3mm5BPzOUuRqlh4Y9SOBpSWRmEhyw76w4ZP
OPxjWYAgwQKBgA/FehSYxeJgRjSdo+MWnK66tjHgDJE8bYpUZsP0JC4R9DL5oiaA
brd2fI6Y+SbyeNBallObt8LSgzdtnEAbjIH8uDJqyOmknNePRvAvR6mP4xyuR+Bv
m+Lgp0DMWTw5J9CKpydZDItc49T/mJ5tPhdFVd+am0NAQnmr1MCZ6nHxAoGABS3Y
LkaC9FdFUUqSU8+Chkd/YbOkuyiENdkvl6t2e52jo5DVc1T7mLiIrRQi4SI8N9bN
/3oJWCT+uaSLX2ouCtNFunblzWHBrhxnZzTeqVq4SLc8aESAnbslKL4i8/+vYZlN
s8xtiNcSvL+lMsOBORSXzpj/4Ot8WwTkn1qyGgECgYBKNTypzAHeLE6yVadFp3nQ
Ckq9yzvP/ib05rvgbvrne00YeOxqJ9gtTrzgh7koqJyX1L4NwdkEza4ilDWpucn0
xiUZS4SoaJq6ZvcBYS62Yr1t8n09iG47YL8ibgtmH3L+svaotvpVxVK+d7BLevA/
ZboOWVe3icTy64BT3OQhmg==
-----END RSA PRIVATE KEY-----
`
func newMemClientServerPair(t *testing.T) (net.Conn, io.Closer) {
memServer := imapmemserver.New()
user := imapmemserver.NewUser(testUsername, testPassword)
user.Create("INBOX", nil)
memServer.AddUser(user)
cert, err := tls.X509KeyPair([]byte(rsaCertPEM), []byte(rsaKeyPEM))
if err != nil {
t.Fatalf("tls.X509KeyPair() = %v", err)
}
server := imapserver.New(&imapserver.Options{
NewSession: func(conn *imapserver.Conn) (imapserver.Session, *imapserver.GreetingData, error) {
return memServer.NewSession(), nil, nil
},
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
},
InsecureAuth: true,
Caps: imap.CapSet{
imap.CapIMAP4rev1: {},
imap.CapIMAP4rev2: {},
},
})
ln, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatalf("net.Listen() = %v", err)
}
go func() {
if err := server.Serve(ln); err != nil {
t.Errorf("Serve() = %v", err)
}
}()
conn, err := net.Dial("tcp", ln.Addr().String())
if err != nil {
t.Fatalf("net.Dial() = %v", err)
}
return conn, server
}
func newClientServerPair(t *testing.T, initialState imap.ConnState) (*imapclient.Client, io.Closer) {
var useDovecot bool
switch os.Getenv("GOIMAP_TEST_DOVECOT") {
case "0", "":
// ok
case "1":
useDovecot = true
default:
t.Fatalf("invalid GOIMAP_TEST_DOVECOT env var")
}
var (
conn net.Conn
server io.Closer
)
if useDovecot {
if initialState < imap.ConnStateAuthenticated {
t.Skip("Dovecot connections are pre-authenticated")
}
conn, server = newDovecotClientServerPair(t)
} else {
conn, server = newMemClientServerPair(t)
}
var debugWriter swapWriter
debugWriter.Swap(io.Discard)
var options imapclient.Options
if testing.Verbose() {
options.DebugWriter = &debugWriter
}
client := imapclient.New(conn, &options)
if initialState >= imap.ConnStateAuthenticated {
// Dovecot connections are pre-authenticated
if !useDovecot {
if err := client.Login(testUsername, testPassword).Wait(); err != nil {
t.Fatalf("Login().Wait() = %v", err)
}
}
appendCmd := client.Append("INBOX", int64(len(simpleRawMessage)), nil)
appendCmd.Write([]byte(simpleRawMessage))
appendCmd.Close()
if _, err := appendCmd.Wait(); err != nil {
t.Fatalf("AppendCommand.Wait() = %v", err)
}
}
if initialState >= imap.ConnStateSelected {
if _, err := client.Select("INBOX", nil).Wait(); err != nil {
t.Fatalf("Select().Wait() = %v", err)
}
}
// Turn on debug logs after we're done initializing the test
debugWriter.Swap(os.Stderr)
return client, server
}
// swapWriter is an io.Writer which can be swapped at runtime.
type swapWriter struct {
w io.Writer
mutex sync.Mutex
}
func (sw *swapWriter) Write(b []byte) (int, error) {
sw.mutex.Lock()
w := sw.w
sw.mutex.Unlock()
return w.Write(b)
}
func (sw *swapWriter) Swap(w io.Writer) {
sw.mutex.Lock()
sw.w = w
sw.mutex.Unlock()
}
func TestLogin(t *testing.T) {
client, server := newClientServerPair(t, imap.ConnStateNotAuthenticated)
defer client.Close()
defer server.Close()
if err := client.Login(testUsername, testPassword).Wait(); err != nil {
t.Errorf("Login().Wait() = %v", err)
}
}
func TestLogout(t *testing.T) {
client, server := newClientServerPair(t, imap.ConnStateAuthenticated)
defer server.Close()
if _, ok := server.(*dovecotServer); ok {
t.Skip("Dovecot connections don't reply to LOGOUT")
}
if err := client.Logout().Wait(); err != nil {
t.Errorf("Logout().Wait() = %v", err)
}
if err := client.Close(); err != nil {
t.Errorf("Close() = %v", err)
}
}
// https://github.com/emersion/go-imap/issues/562
func TestFetch_invalid(t *testing.T) {
client, server := newClientServerPair(t, imap.ConnStateSelected)
defer client.Close()
defer server.Close()
_, err := client.Fetch(imap.UIDSet(nil), nil).Collect()
if err == nil {
t.Fatalf("UIDFetch().Collect() = %v", err)
}
}
func TestFetch_closeUnreadBody(t *testing.T) {
client, server := newClientServerPair(t, imap.ConnStateSelected)
defer client.Close()
defer server.Close()
fetchCmd := client.Fetch(imap.SeqSetNum(1), &imap.FetchOptions{
BodySection: []*imap.FetchItemBodySection{
{
Specifier: imap.PartSpecifierNone,
Peek: true,
},
},
})
if err := fetchCmd.Close(); err != nil {
t.Fatalf("UIDFetch().Close() = %v", err)
}
}
func TestWaitGreeting_eof(t *testing.T) {
// bad server: connected but without greeting
clientConn, serverConn := net.Pipe()
client := imapclient.New(clientConn, nil)
defer client.Close()
if err := serverConn.Close(); err != nil {
t.Fatalf("serverConn.Close() = %v", err)
}
if err := client.WaitGreeting(); err == nil {
t.Fatalf("WaitGreeting() should fail")
}
}

View File

@@ -1,37 +0,0 @@
package imapclient_test
import (
"testing"
"time"
"github.com/emersion/go-imap/v2"
)
// TestClient_Closed tests that the Closed() channel is closed when the
// connection is explicitly closed via Close().
func TestClient_Closed(t *testing.T) {
client, server := newClientServerPair(t, imap.ConnStateAuthenticated)
defer server.Close()
closedCh := client.Closed()
if closedCh == nil {
t.Fatal("Closed() returned nil channel")
}
select {
case <-closedCh:
t.Fatal("Closed() channel closed before calling Close()")
default: // Expected
}
if err := client.Close(); err != nil {
t.Fatalf("Close() = %v", err)
}
select {
case <-closedCh:
t.Log("Closed() channel properly closed after Close()")
case <-time.After(2 * time.Second):
t.Fatal("Closed() channel not closed after Close()")
}
}

View File

@@ -1,37 +0,0 @@
package imapclient
import (
"fmt"
"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/internal/imapwire"
)
// Copy sends a COPY command.
func (c *Client) Copy(numSet imap.NumSet, mailbox string) *CopyCommand {
cmd := &CopyCommand{}
enc := c.beginCommand(uidCmdName("COPY", imapwire.NumSetKind(numSet)), cmd)
enc.SP().NumSet(numSet).SP().Mailbox(mailbox)
enc.end()
return cmd
}
// CopyCommand is a COPY command.
type CopyCommand struct {
commandBase
data imap.CopyData
}
func (cmd *CopyCommand) Wait() (*imap.CopyData, error) {
return &cmd.data, cmd.wait()
}
func readRespCodeCopyUID(dec *imapwire.Decoder) (uidValidity uint32, srcUIDs, dstUIDs imap.UIDSet, err error) {
if !dec.ExpectNumber(&uidValidity) || !dec.ExpectSP() || !dec.ExpectUIDSet(&srcUIDs) || !dec.ExpectSP() || !dec.ExpectUIDSet(&dstUIDs) {
return 0, nil, nil, dec.Err()
}
if srcUIDs.Dynamic() || dstUIDs.Dynamic() {
return 0, nil, nil, fmt.Errorf("imapclient: server returned dynamic number set in COPYUID response")
}
return uidValidity, srcUIDs, dstUIDs, nil
}

View File

@@ -1,21 +0,0 @@
package imapclient
import (
"github.com/emersion/go-imap/v2"
)
// Create sends a CREATE command.
//
// A nil options pointer is equivalent to a zero options value.
func (c *Client) Create(mailbox string, options *imap.CreateOptions) *Command {
cmd := &Command{}
enc := c.beginCommand("CREATE", cmd)
enc.SP().Mailbox(mailbox)
if options != nil && len(options.SpecialUse) > 0 {
enc.SP().Special('(').Atom("USE").SP().List(len(options.SpecialUse), func(i int) {
enc.MailboxAttr(options.SpecialUse[i])
}).Special(')')
}
enc.end()
return cmd
}

View File

@@ -1,57 +0,0 @@
package imapclient_test
import (
"testing"
"github.com/emersion/go-imap/v2"
)
func testCreate(t *testing.T, name string, utf8Accept bool) {
client, server := newClientServerPair(t, imap.ConnStateAuthenticated)
defer client.Close()
defer server.Close()
if utf8Accept {
if !client.Caps().Has(imap.CapUTF8Accept) {
t.Skipf("missing UTF8=ACCEPT support")
}
if data, err := client.Enable(imap.CapUTF8Accept).Wait(); err != nil {
t.Fatalf("Enable(CapUTF8Accept) = %v", err)
} else if !data.Caps.Has(imap.CapUTF8Accept) {
t.Fatalf("server refused to enable UTF8=ACCEPT")
}
}
if err := client.Create(name, nil).Wait(); err != nil {
t.Fatalf("Create() = %v", err)
}
listCmd := client.List("", name, nil)
mailboxes, err := listCmd.Collect()
if err != nil {
t.Errorf("List() = %v", err)
} else if len(mailboxes) != 1 || mailboxes[0].Mailbox != name {
t.Errorf("List() = %v, want exactly one entry with correct name", mailboxes)
}
}
func TestCreate(t *testing.T) {
t.Run("basic", func(t *testing.T) {
testCreate(t, "Test mailbox", false)
})
t.Run("unicode_utf7", func(t *testing.T) {
testCreate(t, "Cafè", false)
})
t.Run("unicode_utf8", func(t *testing.T) {
testCreate(t, "Cafè", true)
})
// '&' is the UTF-7 escape character
t.Run("ampersand_utf7", func(t *testing.T) {
testCreate(t, "Angus & Julia", false)
})
t.Run("ampersand_utf8", func(t *testing.T) {
testCreate(t, "Angus & Julia", true)
})
}

View File

@@ -1,72 +0,0 @@
package imapclient_test
import (
"io"
"net"
"os"
"os/exec"
"path/filepath"
"testing"
)
func newDovecotClientServerPair(t *testing.T) (net.Conn, io.Closer) {
tempDir := t.TempDir()
cfgFilename := filepath.Join(tempDir, "dovecot.conf")
cfg := `dovecot_config_version = 2.4.0
dovecot_storage_version = 2.4.0
log_path = "` + tempDir + `/dovecot.log"
ssl = no
mail_home = "` + tempDir + `/%{user}"
mail_driver = maildir
mail_path = "~/Mail"
namespace inbox {
separator = /
prefix =
inbox = yes
}
mail_plugins {
acl = yes
}
protocol imap {
mail_plugins {
imap_acl = yes
}
}
acl_driver = vfile
`
if err := os.WriteFile(cfgFilename, []byte(cfg), 0666); err != nil {
t.Fatalf("failed to write Dovecot config: %v", err)
}
clientConn, serverConn := net.Pipe()
cmd := exec.Command("doveadm", "-c", cfgFilename, "exec", "imap")
cmd.Env = []string{"USER=" + testUsername, "PATH=" + os.Getenv("PATH")}
cmd.Dir = tempDir
cmd.Stdin = serverConn
cmd.Stdout = serverConn
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
t.Fatalf("failed to start Dovecot: %v", err)
}
return clientConn, &dovecotServer{cmd, serverConn}
}
type dovecotServer struct {
cmd *exec.Cmd
conn net.Conn
}
func (srv *dovecotServer) Close() error {
if err := srv.conn.Close(); err != nil {
return err
}
return srv.cmd.Wait()
}

View File

@@ -1,69 +0,0 @@
package imapclient
import (
"fmt"
"github.com/emersion/go-imap/v2"
)
// Enable sends an ENABLE command.
//
// This command requires support for IMAP4rev2 or the ENABLE extension.
func (c *Client) Enable(caps ...imap.Cap) *EnableCommand {
// Enabling an extension may change the IMAP syntax, so only allow the
// extensions we support here
for _, name := range caps {
switch name {
case imap.CapIMAP4rev2, imap.CapUTF8Accept, imap.CapMetadata, imap.CapMetadataServer:
// ok
default:
done := make(chan error)
close(done)
err := fmt.Errorf("imapclient: cannot enable %q: not supported", name)
return &EnableCommand{commandBase: commandBase{done: done, err: err}}
}
}
cmd := &EnableCommand{}
enc := c.beginCommand("ENABLE", cmd)
for _, c := range caps {
enc.SP().Atom(string(c))
}
enc.end()
return cmd
}
func (c *Client) handleEnabled() error {
caps, err := readCapabilities(c.dec)
if err != nil {
return err
}
c.mutex.Lock()
for name := range caps {
c.enabled[name] = struct{}{}
}
c.mutex.Unlock()
if cmd := findPendingCmdByType[*EnableCommand](c); cmd != nil {
cmd.data.Caps = caps
}
return nil
}
// EnableCommand is an ENABLE command.
type EnableCommand struct {
commandBase
data EnableData
}
func (cmd *EnableCommand) Wait() (*EnableData, error) {
return &cmd.data, cmd.wait()
}
// EnableData is the data returned by the ENABLE command.
type EnableData struct {
// Capabilities that were successfully enabled
Caps imap.CapSet
}

View File

@@ -1,411 +0,0 @@
package imapclient_test
import (
"io"
"log"
"time"
"github.com/emersion/go-message/mail"
"github.com/emersion/go-sasl"
"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/imapclient"
)
func ExampleClient() {
c, err := imapclient.DialTLS("mail.example.org:993", nil)
if err != nil {
log.Fatalf("failed to dial IMAP server: %v", err)
}
defer c.Close()
if err := c.Login("root", "asdf").Wait(); err != nil {
log.Fatalf("failed to login: %v", err)
}
mailboxes, err := c.List("", "%", nil).Collect()
if err != nil {
log.Fatalf("failed to list mailboxes: %v", err)
}
log.Printf("Found %v mailboxes", len(mailboxes))
for _, mbox := range mailboxes {
log.Printf(" - %v", mbox.Mailbox)
}
selectedMbox, err := c.Select("INBOX", nil).Wait()
if err != nil {
log.Fatalf("failed to select INBOX: %v", err)
}
log.Printf("INBOX contains %v messages", selectedMbox.NumMessages)
if selectedMbox.NumMessages > 0 {
seqSet := imap.SeqSetNum(1)
fetchOptions := &imap.FetchOptions{Envelope: true}
messages, err := c.Fetch(seqSet, fetchOptions).Collect()
if err != nil {
log.Fatalf("failed to fetch first message in INBOX: %v", err)
}
log.Printf("subject of first message in INBOX: %v", messages[0].Envelope.Subject)
}
if err := c.Logout().Wait(); err != nil {
log.Fatalf("failed to logout: %v", err)
}
}
func ExampleClient_pipelining() {
var c *imapclient.Client
uid := imap.UID(42)
fetchOptions := &imap.FetchOptions{Envelope: true}
// Login, select and fetch a message in a single roundtrip
loginCmd := c.Login("root", "root")
selectCmd := c.Select("INBOX", nil)
fetchCmd := c.Fetch(imap.UIDSetNum(uid), fetchOptions)
if err := loginCmd.Wait(); err != nil {
log.Fatalf("failed to login: %v", err)
}
if _, err := selectCmd.Wait(); err != nil {
log.Fatalf("failed to select INBOX: %v", err)
}
if messages, err := fetchCmd.Collect(); err != nil {
log.Fatalf("failed to fetch message: %v", err)
} else {
log.Printf("Subject: %v", messages[0].Envelope.Subject)
}
}
func ExampleClient_Append() {
var c *imapclient.Client
buf := []byte("From: <root@nsa.gov>\r\n\r\nHi <3")
size := int64(len(buf))
appendCmd := c.Append("INBOX", size, nil)
if _, err := appendCmd.Write(buf); err != nil {
log.Fatalf("failed to write message: %v", err)
}
if err := appendCmd.Close(); err != nil {
log.Fatalf("failed to close message: %v", err)
}
if _, err := appendCmd.Wait(); err != nil {
log.Fatalf("APPEND command failed: %v", err)
}
}
func ExampleClient_Status() {
var c *imapclient.Client
options := imap.StatusOptions{NumMessages: true}
if data, err := c.Status("INBOX", &options).Wait(); err != nil {
log.Fatalf("STATUS command failed: %v", err)
} else {
log.Printf("INBOX contains %v messages", *data.NumMessages)
}
}
func ExampleClient_List_stream() {
var c *imapclient.Client
// ReturnStatus requires server support for IMAP4rev2 or LIST-STATUS
listCmd := c.List("", "%", &imap.ListOptions{
ReturnStatus: &imap.StatusOptions{
NumMessages: true,
NumUnseen: true,
},
})
for {
mbox := listCmd.Next()
if mbox == nil {
break
}
log.Printf("Mailbox %q contains %v messages (%v unseen)", mbox.Mailbox, mbox.Status.NumMessages, mbox.Status.NumUnseen)
}
if err := listCmd.Close(); err != nil {
log.Fatalf("LIST command failed: %v", err)
}
}
func ExampleClient_Store() {
var c *imapclient.Client
seqSet := imap.SeqSetNum(1)
storeFlags := imap.StoreFlags{
Op: imap.StoreFlagsAdd,
Flags: []imap.Flag{imap.FlagFlagged},
Silent: true,
}
if err := c.Store(seqSet, &storeFlags, nil).Close(); err != nil {
log.Fatalf("STORE command failed: %v", err)
}
}
func ExampleClient_Fetch() {
var c *imapclient.Client
seqSet := imap.SeqSetNum(1)
bodySection := &imap.FetchItemBodySection{Specifier: imap.PartSpecifierHeader}
fetchOptions := &imap.FetchOptions{
Flags: true,
Envelope: true,
BodySection: []*imap.FetchItemBodySection{bodySection},
}
messages, err := c.Fetch(seqSet, fetchOptions).Collect()
if err != nil {
log.Fatalf("FETCH command failed: %v", err)
}
msg := messages[0]
header := msg.FindBodySection(bodySection)
log.Printf("Flags: %v", msg.Flags)
log.Printf("Subject: %v", msg.Envelope.Subject)
log.Printf("Header:\n%v", string(header))
}
func ExampleClient_Fetch_streamBody() {
var c *imapclient.Client
seqSet := imap.SeqSetNum(1)
bodySection := &imap.FetchItemBodySection{}
fetchOptions := &imap.FetchOptions{
UID: true,
BodySection: []*imap.FetchItemBodySection{bodySection},
}
fetchCmd := c.Fetch(seqSet, fetchOptions)
defer fetchCmd.Close()
for {
msg := fetchCmd.Next()
if msg == nil {
break
}
for {
item := msg.Next()
if item == nil {
break
}
switch item := item.(type) {
case imapclient.FetchItemDataUID:
log.Printf("UID: %v", item.UID)
case imapclient.FetchItemDataBodySection:
b, err := io.ReadAll(item.Literal)
if err != nil {
log.Fatalf("failed to read body section: %v", err)
}
log.Printf("Body:\n%v", string(b))
}
}
}
if err := fetchCmd.Close(); err != nil {
log.Fatalf("FETCH command failed: %v", err)
}
}
func ExampleClient_Fetch_parseBody() {
var c *imapclient.Client
// Send a FETCH command to fetch the message body
seqSet := imap.SeqSetNum(1)
bodySection := &imap.FetchItemBodySection{}
fetchOptions := &imap.FetchOptions{
BodySection: []*imap.FetchItemBodySection{bodySection},
}
fetchCmd := c.Fetch(seqSet, fetchOptions)
defer fetchCmd.Close()
msg := fetchCmd.Next()
if msg == nil {
log.Fatalf("FETCH command did not return any message")
}
// Find the body section in the response
var bodySectionData imapclient.FetchItemDataBodySection
ok := false
for {
item := msg.Next()
if item == nil {
break
}
bodySectionData, ok = item.(imapclient.FetchItemDataBodySection)
if ok {
break
}
}
if !ok {
log.Fatalf("FETCH command did not return body section")
}
// Read the message via the go-message library
mr, err := mail.CreateReader(bodySectionData.Literal)
if err != nil {
log.Fatalf("failed to create mail reader: %v", err)
}
// Print a few header fields
h := mr.Header
if date, err := h.Date(); err != nil {
log.Printf("failed to parse Date header field: %v", err)
} else {
log.Printf("Date: %v", date)
}
if to, err := h.AddressList("To"); err != nil {
log.Printf("failed to parse To header field: %v", err)
} else {
log.Printf("To: %v", to)
}
if subject, err := h.Text("Subject"); err != nil {
log.Printf("failed to parse Subject header field: %v", err)
} else {
log.Printf("Subject: %v", subject)
}
// Process the message's parts
for {
p, err := mr.NextPart()
if err == io.EOF {
break
} else if err != nil {
log.Fatalf("failed to read message part: %v", err)
}
switch h := p.Header.(type) {
case *mail.InlineHeader:
// This is the message's text (can be plain-text or HTML)
b, _ := io.ReadAll(p.Body)
log.Printf("Inline text: %v", string(b))
case *mail.AttachmentHeader:
// This is an attachment
filename, _ := h.Filename()
log.Printf("Attachment: %v", filename)
}
}
if err := fetchCmd.Close(); err != nil {
log.Fatalf("FETCH command failed: %v", err)
}
}
func ExampleClient_Search() {
var c *imapclient.Client
data, err := c.UIDSearch(&imap.SearchCriteria{
Body: []string{"Hello world"},
}, nil).Wait()
if err != nil {
log.Fatalf("UID SEARCH command failed: %v", err)
}
log.Fatalf("UIDs matching the search criteria: %v", data.AllUIDs())
}
func ExampleClient_Idle() {
options := imapclient.Options{
UnilateralDataHandler: &imapclient.UnilateralDataHandler{
Expunge: func(seqNum uint32) {
log.Printf("message %v has been expunged", seqNum)
},
Mailbox: func(data *imapclient.UnilateralDataMailbox) {
if data.NumMessages != nil {
log.Printf("a new message has been received")
}
},
},
}
c, err := imapclient.DialTLS("mail.example.org:993", &options)
if err != nil {
log.Fatalf("failed to dial IMAP server: %v", err)
}
defer c.Close()
if err := c.Login("root", "asdf").Wait(); err != nil {
log.Fatalf("failed to login: %v", err)
}
if _, err := c.Select("INBOX", nil).Wait(); err != nil {
log.Fatalf("failed to select INBOX: %v", err)
}
// Start idling
idleCmd, err := c.Idle()
if err != nil {
log.Fatalf("IDLE command failed: %v", err)
}
defer idleCmd.Close()
done := make(chan error, 1)
go func() {
done <- idleCmd.Wait()
}()
// Wait for 30s to receive updates from the server, then stop idling
t := time.NewTimer(30 * time.Second)
defer t.Stop()
select {
case <-t.C:
if err := idleCmd.Close(); err != nil {
log.Fatalf("failed to stop idling: %v", err)
}
if err := <-done; err != nil {
log.Fatalf("IDLE command failed: %v", err)
}
case err := <-done:
if err != nil {
log.Fatalf("IDLE command failed: %v", err)
}
}
}
func ExampleClient_Authenticate_oauth() {
var (
c *imapclient.Client
username string
token string
)
if !c.Caps().Has(imap.AuthCap(sasl.OAuthBearer)) {
log.Fatal("OAUTHBEARER not supported by the server")
}
saslClient := sasl.NewOAuthBearerClient(&sasl.OAuthBearerOptions{
Username: username,
Token: token,
})
if err := c.Authenticate(saslClient); err != nil {
log.Fatalf("authentication failed: %v", err)
}
}
func ExampleClient_Closed() {
c, err := imapclient.DialTLS("mail.example.org:993", nil)
if err != nil {
log.Fatalf("failed to dial IMAP server: %v", err)
}
selected := false
go func(c *imapclient.Client) {
if err := c.Login("root", "asdf").Wait(); err != nil {
log.Fatalf("failed to login: %v", err)
}
if _, err := c.Select("INBOX", nil).Wait(); err != nil {
log.Fatalf("failed to select INBOX: %v", err)
}
selected = true
c.Close()
}(c)
// This channel shall be closed when the connection is closed.
<-c.Closed()
log.Println("Connection has been closed")
if !selected {
log.Fatalf("Connection was closed before selecting mailbox")
}
}

View File

@@ -1,84 +0,0 @@
package imapclient
import (
"github.com/emersion/go-imap/v2"
)
// Expunge sends an EXPUNGE command.
func (c *Client) Expunge() *ExpungeCommand {
cmd := &ExpungeCommand{seqNums: make(chan uint32, 128)}
c.beginCommand("EXPUNGE", cmd).end()
return cmd
}
// UIDExpunge sends a UID EXPUNGE command.
//
// This command requires support for IMAP4rev2 or the UIDPLUS extension.
func (c *Client) UIDExpunge(uids imap.UIDSet) *ExpungeCommand {
cmd := &ExpungeCommand{seqNums: make(chan uint32, 128)}
enc := c.beginCommand("UID EXPUNGE", cmd)
enc.SP().NumSet(uids)
enc.end()
return cmd
}
func (c *Client) handleExpunge(seqNum uint32) error {
c.mutex.Lock()
if c.state == imap.ConnStateSelected && c.mailbox.NumMessages > 0 {
c.mailbox = c.mailbox.copy()
c.mailbox.NumMessages--
}
c.mutex.Unlock()
cmd := findPendingCmdByType[*ExpungeCommand](c)
if cmd != nil {
cmd.seqNums <- seqNum
} else if handler := c.options.unilateralDataHandler().Expunge; handler != nil {
handler(seqNum)
}
return nil
}
// ExpungeCommand is an EXPUNGE command.
//
// The caller must fully consume the ExpungeCommand. A simple way to do so is
// to defer a call to FetchCommand.Close.
type ExpungeCommand struct {
commandBase
seqNums chan uint32
}
// Next advances to the next expunged message sequence number.
//
// On success, the message sequence number is returned. On error or if there
// are no more messages, 0 is returned. To check the error value, use Close.
func (cmd *ExpungeCommand) Next() uint32 {
return <-cmd.seqNums
}
// Close releases the command.
//
// Calling Close unblocks the IMAP client decoder and lets it read the next
// responses. Next will always return nil after Close.
func (cmd *ExpungeCommand) Close() error {
for cmd.Next() != 0 {
// ignore
}
return cmd.wait()
}
// Collect accumulates expunged sequence numbers into a list.
//
// This is equivalent to calling Next repeatedly and then Close.
func (cmd *ExpungeCommand) Collect() ([]uint32, error) {
var l []uint32
for {
seqNum := cmd.Next()
if seqNum == 0 {
break
}
l = append(l, seqNum)
}
return l, cmd.Close()
}

View File

@@ -1,36 +0,0 @@
package imapclient_test
import (
"testing"
"github.com/emersion/go-imap/v2"
)
func TestExpunge(t *testing.T) {
client, server := newClientServerPair(t, imap.ConnStateSelected)
defer client.Close()
defer server.Close()
seqNums, err := client.Expunge().Collect()
if err != nil {
t.Fatalf("Expunge() = %v", err)
} else if len(seqNums) != 0 {
t.Errorf("Expunge().Collect() = %v, want []", seqNums)
}
seqSet := imap.SeqSetNum(1)
storeFlags := imap.StoreFlags{
Op: imap.StoreFlagsAdd,
Flags: []imap.Flag{imap.FlagDeleted},
}
if err := client.Store(seqSet, &storeFlags, nil).Close(); err != nil {
t.Fatalf("Store() = %v", err)
}
seqNums, err = client.Expunge().Collect()
if err != nil {
t.Fatalf("Expunge() = %v", err)
} else if len(seqNums) != 1 || seqNums[0] != 1 {
t.Errorf("Expunge().Collect() = %v, want [1]", seqNums)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,39 +0,0 @@
package imapclient_test
import (
"strings"
"testing"
"github.com/emersion/go-imap/v2"
)
func TestFetch(t *testing.T) {
client, server := newClientServerPair(t, imap.ConnStateSelected)
defer client.Close()
defer server.Close()
seqSet := imap.SeqSetNum(1)
bodySection := &imap.FetchItemBodySection{}
fetchOptions := &imap.FetchOptions{
BodySection: []*imap.FetchItemBodySection{bodySection},
}
messages, err := client.Fetch(seqSet, fetchOptions).Collect()
if err != nil {
t.Fatalf("failed to fetch first message: %v", err)
} else if len(messages) != 1 {
t.Fatalf("len(messages) = %v, want 1", len(messages))
}
msg := messages[0]
if len(msg.BodySection) != 1 {
t.Fatalf("len(msg.BodySection) = %v, want 1", len(msg.BodySection))
}
b := msg.FindBodySection(bodySection)
if b == nil {
t.Fatalf("FindBodySection() = nil")
}
body := strings.ReplaceAll(string(b), "\r\n", "\n")
if body != simpleRawMessage {
t.Errorf("body mismatch: got \n%v\n but want \n%v", body, simpleRawMessage)
}
}

View File

@@ -1,163 +0,0 @@
package imapclient
import (
"fmt"
"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/internal/imapwire"
)
// ID sends an ID command.
//
// The ID command is introduced in RFC 2971. It requires support for the ID
// extension.
//
// An example ID command:
//
// ID ("name" "go-imap" "version" "1.0" "os" "Linux" "os-version" "7.9.4" "vendor" "Yahoo")
func (c *Client) ID(idData *imap.IDData) *IDCommand {
cmd := &IDCommand{}
enc := c.beginCommand("ID", cmd)
if idData == nil {
enc.SP().NIL()
enc.end()
return cmd
}
enc.SP().Special('(')
isFirstKey := true
if idData.Name != "" {
addIDKeyValue(enc, &isFirstKey, "name", idData.Name)
}
if idData.Version != "" {
addIDKeyValue(enc, &isFirstKey, "version", idData.Version)
}
if idData.OS != "" {
addIDKeyValue(enc, &isFirstKey, "os", idData.OS)
}
if idData.OSVersion != "" {
addIDKeyValue(enc, &isFirstKey, "os-version", idData.OSVersion)
}
if idData.Vendor != "" {
addIDKeyValue(enc, &isFirstKey, "vendor", idData.Vendor)
}
if idData.SupportURL != "" {
addIDKeyValue(enc, &isFirstKey, "support-url", idData.SupportURL)
}
if idData.Address != "" {
addIDKeyValue(enc, &isFirstKey, "address", idData.Address)
}
if idData.Date != "" {
addIDKeyValue(enc, &isFirstKey, "date", idData.Date)
}
if idData.Command != "" {
addIDKeyValue(enc, &isFirstKey, "command", idData.Command)
}
if idData.Arguments != "" {
addIDKeyValue(enc, &isFirstKey, "arguments", idData.Arguments)
}
if idData.Environment != "" {
addIDKeyValue(enc, &isFirstKey, "environment", idData.Environment)
}
enc.Special(')')
enc.end()
return cmd
}
func addIDKeyValue(enc *commandEncoder, isFirstKey *bool, key, value string) {
if isFirstKey == nil {
panic("isFirstKey cannot be nil")
} else if !*isFirstKey {
enc.SP().Quoted(key).SP().Quoted(value)
} else {
enc.Quoted(key).SP().Quoted(value)
}
*isFirstKey = false
}
func (c *Client) handleID() error {
data, err := c.readID(c.dec)
if err != nil {
return fmt.Errorf("in id: %v", err)
}
if cmd := findPendingCmdByType[*IDCommand](c); cmd != nil {
cmd.data = *data
}
return nil
}
func (c *Client) readID(dec *imapwire.Decoder) (*imap.IDData, error) {
var data = imap.IDData{}
if !dec.ExpectSP() {
return nil, dec.Err()
}
if dec.ExpectNIL() {
return &data, nil
}
currKey := ""
err := dec.ExpectList(func() error {
var keyOrValue string
if !dec.String(&keyOrValue) {
return fmt.Errorf("in id key-val list: %v", dec.Err())
}
if currKey == "" {
currKey = keyOrValue
return nil
}
switch currKey {
case "name":
data.Name = keyOrValue
case "version":
data.Version = keyOrValue
case "os":
data.OS = keyOrValue
case "os-version":
data.OSVersion = keyOrValue
case "vendor":
data.Vendor = keyOrValue
case "support-url":
data.SupportURL = keyOrValue
case "address":
data.Address = keyOrValue
case "date":
data.Date = keyOrValue
case "command":
data.Command = keyOrValue
case "arguments":
data.Arguments = keyOrValue
case "environment":
data.Environment = keyOrValue
default:
// Ignore unknown key
// Yahoo server sends "host" and "remote-host" keys
// which are not defined in RFC 2971
}
currKey = ""
return nil
})
if err != nil {
return nil, err
}
return &data, nil
}
type IDCommand struct {
commandBase
data imap.IDData
}
func (r *IDCommand) Wait() (*imap.IDData, error) {
return &r.data, r.wait()
}

View File

@@ -1,157 +0,0 @@
package imapclient
import (
"fmt"
"sync/atomic"
"time"
)
const idleRestartInterval = 28 * time.Minute
// Idle sends an IDLE command.
//
// Unlike other commands, this method blocks until the server acknowledges it.
// On success, the IDLE command is running and other commands cannot be sent.
// The caller must invoke IdleCommand.Close to stop IDLE and unblock the
// client.
//
// This command requires support for IMAP4rev2 or the IDLE extension. The IDLE
// command is restarted automatically to avoid getting disconnected due to
// inactivity timeouts.
func (c *Client) Idle() (*IdleCommand, error) {
child, err := c.idle()
if err != nil {
return nil, err
}
cmd := &IdleCommand{
stop: make(chan struct{}),
done: make(chan struct{}),
}
go cmd.run(c, child)
return cmd, nil
}
// IdleCommand is an IDLE command.
//
// Initially, the IDLE command is running. The server may send unilateral
// data. The client cannot send any command while IDLE is running.
//
// Close must be called to stop the IDLE command.
type IdleCommand struct {
stopped atomic.Bool
stop chan struct{}
done chan struct{}
err error
lastChild *idleCommand
}
func (cmd *IdleCommand) run(c *Client, child *idleCommand) {
defer close(cmd.done)
timer := time.NewTimer(idleRestartInterval)
defer timer.Stop()
defer func() {
if child != nil {
if err := child.Close(); err != nil && cmd.err == nil {
cmd.err = err
}
}
}()
for {
select {
case <-timer.C:
timer.Reset(idleRestartInterval)
if cmd.err = child.Close(); cmd.err != nil {
return
}
if child, cmd.err = c.idle(); cmd.err != nil {
return
}
case <-c.decCh:
cmd.lastChild = child
return
case <-cmd.stop:
cmd.lastChild = child
return
}
}
}
// Close stops the IDLE command.
//
// This method blocks until the command to stop IDLE is written, but doesn't
// wait for the server to respond. Callers can use Wait for this purpose.
func (cmd *IdleCommand) Close() error {
if cmd.stopped.Swap(true) {
return fmt.Errorf("imapclient: IDLE already closed")
}
close(cmd.stop)
<-cmd.done
return cmd.err
}
// Wait blocks until the IDLE command has completed.
func (cmd *IdleCommand) Wait() error {
<-cmd.done
if cmd.err != nil {
return cmd.err
}
return cmd.lastChild.Wait()
}
func (c *Client) idle() (*idleCommand, error) {
cmd := &idleCommand{}
contReq := c.registerContReq(cmd)
cmd.enc = c.beginCommand("IDLE", cmd)
cmd.enc.flush()
_, err := contReq.Wait()
if err != nil {
cmd.enc.end()
return nil, err
}
return cmd, nil
}
// idleCommand represents a singular IDLE command, without the restart logic.
type idleCommand struct {
commandBase
enc *commandEncoder
}
// Close stops the IDLE command.
//
// This method blocks until the command to stop IDLE is written, but doesn't
// wait for the server to respond. Callers can use Wait for this purpose.
func (cmd *idleCommand) Close() error {
if cmd.err != nil {
return cmd.err
}
if cmd.enc == nil {
return fmt.Errorf("imapclient: IDLE command closed twice")
}
cmd.enc.client.setWriteTimeout(cmdWriteTimeout)
_, err := cmd.enc.client.bw.WriteString("DONE\r\n")
if err == nil {
err = cmd.enc.client.bw.Flush()
}
cmd.enc.end()
cmd.enc = nil
return err
}
// Wait blocks until the IDLE command has completed.
//
// Wait can only be called after Close.
func (cmd *idleCommand) Wait() error {
if cmd.enc != nil {
panic("imapclient: idleCommand.Close must be called before Wait")
}
return cmd.wait()
}

View File

@@ -1,42 +0,0 @@
package imapclient_test
import (
"testing"
"github.com/emersion/go-imap/v2"
)
func TestIdle(t *testing.T) {
client, server := newClientServerPair(t, imap.ConnStateSelected)
defer client.Close()
defer server.Close()
idleCmd, err := client.Idle()
if err != nil {
t.Fatalf("Idle() = %v", err)
}
// TODO: test unilateral updates
if err := idleCmd.Close(); err != nil {
t.Errorf("Close() = %v", err)
}
}
func TestIdle_closedConn(t *testing.T) {
client, server := newClientServerPair(t, imap.ConnStateSelected)
defer client.Close()
defer server.Close()
idleCmd, err := client.Idle()
if err != nil {
t.Fatalf("Idle() = %v", err)
}
defer idleCmd.Close()
if err := client.Close(); err != nil {
t.Fatalf("client.Close() = %v", err)
}
if err := idleCmd.Wait(); err == nil {
t.Errorf("IdleCommand.Wait() = nil, want an error")
}
}

View File

@@ -1,259 +0,0 @@
package imapclient
import (
"fmt"
"strings"
"unicode/utf8"
"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/internal"
"github.com/emersion/go-imap/v2/internal/imapwire"
)
func getSelectOpts(options *imap.ListOptions) []string {
if options == nil {
return nil
}
var l []string
if options.SelectSubscribed {
l = append(l, "SUBSCRIBED")
}
if options.SelectRemote {
l = append(l, "REMOTE")
}
if options.SelectRecursiveMatch {
l = append(l, "RECURSIVEMATCH")
}
if options.SelectSpecialUse {
l = append(l, "SPECIAL-USE")
}
return l
}
func getReturnOpts(options *imap.ListOptions) []string {
if options == nil {
return nil
}
var l []string
if options.ReturnSubscribed {
l = append(l, "SUBSCRIBED")
}
if options.ReturnChildren {
l = append(l, "CHILDREN")
}
if options.ReturnStatus != nil {
l = append(l, "STATUS")
}
if options.ReturnSpecialUse {
l = append(l, "SPECIAL-USE")
}
return l
}
// List sends a LIST command.
//
// The caller must fully consume the ListCommand. A simple way to do so is to
// defer a call to ListCommand.Close.
//
// A nil options pointer is equivalent to a zero options value.
//
// A non-zero options value requires support for IMAP4rev2 or the LIST-EXTENDED
// extension.
func (c *Client) List(ref, pattern string, options *imap.ListOptions) *ListCommand {
cmd := &ListCommand{
mailboxes: make(chan *imap.ListData, 64),
returnStatus: options != nil && options.ReturnStatus != nil,
}
enc := c.beginCommand("LIST", cmd)
if selectOpts := getSelectOpts(options); len(selectOpts) > 0 {
enc.SP().List(len(selectOpts), func(i int) {
enc.Atom(selectOpts[i])
})
}
enc.SP().Mailbox(ref).SP().Mailbox(pattern)
if returnOpts := getReturnOpts(options); len(returnOpts) > 0 {
enc.SP().Atom("RETURN").SP().List(len(returnOpts), func(i int) {
opt := returnOpts[i]
enc.Atom(opt)
if opt == "STATUS" {
returnStatus := statusItems(options.ReturnStatus)
enc.SP().List(len(returnStatus), func(j int) {
enc.Atom(returnStatus[j])
})
}
})
}
enc.end()
return cmd
}
func (c *Client) handleList() error {
data, err := readList(c.dec)
if err != nil {
return fmt.Errorf("in LIST: %v", err)
}
cmd := c.findPendingCmdFunc(func(cmd command) bool {
switch cmd := cmd.(type) {
case *ListCommand:
return true // TODO: match pattern, check if already handled
case *SelectCommand:
return cmd.mailbox == data.Mailbox && cmd.data.List == nil
default:
return false
}
})
switch cmd := cmd.(type) {
case *ListCommand:
if cmd.returnStatus {
if cmd.pendingData != nil {
cmd.mailboxes <- cmd.pendingData
}
cmd.pendingData = data
} else {
cmd.mailboxes <- data
}
case *SelectCommand:
cmd.data.List = data
}
return nil
}
// ListCommand is a LIST command.
type ListCommand struct {
commandBase
mailboxes chan *imap.ListData
returnStatus bool
pendingData *imap.ListData
}
// Next advances to the next mailbox.
//
// On success, the mailbox LIST data is returned. On error or if there are no
// more mailboxes, nil is returned.
func (cmd *ListCommand) Next() *imap.ListData {
return <-cmd.mailboxes
}
// Close releases the command.
//
// Calling Close unblocks the IMAP client decoder and lets it read the next
// responses. Next will always return nil after Close.
func (cmd *ListCommand) Close() error {
for cmd.Next() != nil {
// ignore
}
return cmd.wait()
}
// Collect accumulates mailboxes into a list.
//
// This is equivalent to calling Next repeatedly and then Close.
func (cmd *ListCommand) Collect() ([]*imap.ListData, error) {
var l []*imap.ListData
for {
data := cmd.Next()
if data == nil {
break
}
l = append(l, data)
}
return l, cmd.Close()
}
func readList(dec *imapwire.Decoder) (*imap.ListData, error) {
var data imap.ListData
var err error
data.Attrs, err = internal.ExpectMailboxAttrList(dec)
if err != nil {
return nil, fmt.Errorf("in mbx-list-flags: %w", err)
}
if !dec.ExpectSP() {
return nil, dec.Err()
}
data.Delim, err = readDelim(dec)
if err != nil {
return nil, err
}
if !dec.ExpectSP() || !dec.ExpectMailbox(&data.Mailbox) {
return nil, dec.Err()
}
if dec.SP() {
err := dec.ExpectList(func() error {
var tag string
if !dec.ExpectAString(&tag) || !dec.ExpectSP() {
return dec.Err()
}
var err error
switch strings.ToUpper(tag) {
case "CHILDINFO":
data.ChildInfo, err = readChildInfoExtendedItem(dec)
if err != nil {
return fmt.Errorf("in childinfo-extended-item: %v", err)
}
case "OLDNAME":
data.OldName, err = readOldNameExtendedItem(dec)
if err != nil {
return fmt.Errorf("in oldname-extended-item: %v", err)
}
default:
if !dec.DiscardValue() {
return fmt.Errorf("in tagged-ext-val: %v", err)
}
}
return nil
})
if err != nil {
return nil, fmt.Errorf("in mbox-list-extended: %v", err)
}
}
return &data, nil
}
func readChildInfoExtendedItem(dec *imapwire.Decoder) (*imap.ListDataChildInfo, error) {
var childInfo imap.ListDataChildInfo
err := dec.ExpectList(func() error {
var opt string
if !dec.ExpectAString(&opt) {
return dec.Err()
}
if strings.ToUpper(opt) == "SUBSCRIBED" {
childInfo.Subscribed = true
}
return nil
})
return &childInfo, err
}
func readOldNameExtendedItem(dec *imapwire.Decoder) (string, error) {
var name string
if !dec.ExpectSpecial('(') || !dec.ExpectMailbox(&name) || !dec.ExpectSpecial(')') {
return "", dec.Err()
}
return name, nil
}
func readDelim(dec *imapwire.Decoder) (rune, error) {
var delimStr string
if dec.Quoted(&delimStr) {
delim, size := utf8.DecodeRuneInString(delimStr)
if delim == utf8.RuneError || size != len(delimStr) {
return 0, fmt.Errorf("mailbox delimiter must be a single rune")
}
return delim, nil
} else if !dec.ExpectNIL() {
return 0, dec.Err()
} else {
return 0, nil
}
}

View File

@@ -1,42 +0,0 @@
package imapclient_test
import (
"reflect"
"testing"
"github.com/emersion/go-imap/v2"
)
func TestList(t *testing.T) {
client, server := newClientServerPair(t, imap.ConnStateAuthenticated)
defer client.Close()
defer server.Close()
options := imap.ListOptions{
ReturnStatus: &imap.StatusOptions{
NumMessages: true,
},
}
mailboxes, err := client.List("", "%", &options).Collect()
if err != nil {
t.Fatalf("List() = %v", err)
}
if len(mailboxes) != 1 {
t.Fatalf("List() returned %v mailboxes, want 1", len(mailboxes))
}
mbox := mailboxes[0]
wantNumMessages := uint32(1)
want := &imap.ListData{
Delim: '/',
Mailbox: "INBOX",
Status: &imap.StatusData{
Mailbox: "INBOX",
NumMessages: &wantNumMessages,
},
}
if !reflect.DeepEqual(mbox, want) {
t.Errorf("got %#v but want %#v", mbox, want)
}
}

View File

@@ -1,205 +0,0 @@
package imapclient
import (
"fmt"
"github.com/emersion/go-imap/v2/internal/imapwire"
)
type GetMetadataDepth int
const (
GetMetadataDepthZero GetMetadataDepth = 0
GetMetadataDepthOne GetMetadataDepth = 1
GetMetadataDepthInfinity GetMetadataDepth = -1
)
func (depth GetMetadataDepth) String() string {
switch depth {
case GetMetadataDepthZero:
return "0"
case GetMetadataDepthOne:
return "1"
case GetMetadataDepthInfinity:
return "infinity"
default:
panic(fmt.Errorf("imapclient: unknown GETMETADATA depth %d", depth))
}
}
// GetMetadataOptions contains options for the GETMETADATA command.
type GetMetadataOptions struct {
MaxSize *uint32
Depth GetMetadataDepth
}
func (options *GetMetadataOptions) names() []string {
if options == nil {
return nil
}
var l []string
if options.MaxSize != nil {
l = append(l, "MAXSIZE")
}
if options.Depth != GetMetadataDepthZero {
l = append(l, "DEPTH")
}
return l
}
// GetMetadata sends a GETMETADATA command.
//
// This command requires support for the METADATA or METADATA-SERVER extension.
func (c *Client) GetMetadata(mailbox string, entries []string, options *GetMetadataOptions) *GetMetadataCommand {
cmd := &GetMetadataCommand{mailbox: mailbox}
enc := c.beginCommand("GETMETADATA", cmd)
enc.SP().Mailbox(mailbox)
if opts := options.names(); len(opts) > 0 {
enc.SP().List(len(opts), func(i int) {
opt := opts[i]
enc.Atom(opt).SP()
switch opt {
case "MAXSIZE":
enc.Number(*options.MaxSize)
case "DEPTH":
enc.Atom(options.Depth.String())
default:
panic(fmt.Errorf("imapclient: unknown GETMETADATA option %q", opt))
}
})
}
enc.SP().List(len(entries), func(i int) {
enc.String(entries[i])
})
enc.end()
return cmd
}
// SetMetadata sends a SETMETADATA command.
//
// To remove an entry, set it to nil.
//
// This command requires support for the METADATA or METADATA-SERVER extension.
func (c *Client) SetMetadata(mailbox string, entries map[string]*[]byte) *Command {
cmd := &Command{}
enc := c.beginCommand("SETMETADATA", cmd)
enc.SP().Mailbox(mailbox).SP().Special('(')
i := 0
for k, v := range entries {
if i > 0 {
enc.SP()
}
enc.String(k).SP()
if v == nil {
enc.NIL()
} else {
enc.String(string(*v)) // TODO: use literals if required
}
i++
}
enc.Special(')')
enc.end()
return cmd
}
func (c *Client) handleMetadata() error {
data, err := readMetadataResp(c.dec)
if err != nil {
return fmt.Errorf("in metadata-resp: %v", err)
}
cmd := c.findPendingCmdFunc(func(anyCmd command) bool {
cmd, ok := anyCmd.(*GetMetadataCommand)
return ok && cmd.mailbox == data.Mailbox
})
if cmd != nil && len(data.EntryValues) > 0 {
cmd := cmd.(*GetMetadataCommand)
cmd.data.Mailbox = data.Mailbox
if cmd.data.Entries == nil {
cmd.data.Entries = make(map[string]*[]byte)
}
// The server might send multiple METADATA responses for a single
// METADATA command
for k, v := range data.EntryValues {
cmd.data.Entries[k] = v
}
} else if handler := c.options.unilateralDataHandler().Metadata; handler != nil && len(data.EntryList) > 0 {
handler(data.Mailbox, data.EntryList)
}
return nil
}
// GetMetadataCommand is a GETMETADATA command.
type GetMetadataCommand struct {
commandBase
mailbox string
data GetMetadataData
}
func (cmd *GetMetadataCommand) Wait() (*GetMetadataData, error) {
return &cmd.data, cmd.wait()
}
// GetMetadataData is the data returned by the GETMETADATA command.
type GetMetadataData struct {
Mailbox string
Entries map[string]*[]byte
}
type metadataResp struct {
Mailbox string
EntryList []string
EntryValues map[string]*[]byte
}
func readMetadataResp(dec *imapwire.Decoder) (*metadataResp, error) {
var data metadataResp
if !dec.ExpectMailbox(&data.Mailbox) || !dec.ExpectSP() {
return nil, dec.Err()
}
isList, err := dec.List(func() error {
var name string
if !dec.ExpectAString(&name) || !dec.ExpectSP() {
return dec.Err()
}
// TODO: decode as []byte
var (
value *[]byte
s string
)
if dec.String(&s) || dec.Literal(&s) {
b := []byte(s)
value = &b
} else if !dec.ExpectNIL() {
return dec.Err()
}
if data.EntryValues == nil {
data.EntryValues = make(map[string]*[]byte)
}
data.EntryValues[name] = value
return nil
})
if err != nil {
return nil, err
} else if !isList {
var name string
if !dec.ExpectAString(&name) {
return nil, dec.Err()
}
data.EntryList = append(data.EntryList, name)
for dec.SP() {
if !dec.ExpectAString(&name) {
return nil, dec.Err()
}
data.EntryList = append(data.EntryList, name)
}
}
return &data, nil
}

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