Compare commits

5 Commits

Author SHA1 Message Date
Bel LaPointe
4b1b194000 move up 2023-04-10 11:00:22 -06:00
Bel LaPointe
f850c567ed k 2023-04-10 10:56:04 -06:00
Bel LaPointe
9d91f3a69e novend 2023-04-10 10:53:48 -06:00
Bel LaPointe
766e4de5fa partial 2023-04-10 10:52:47 -06:00
Bel LaPointe
d4768ad367 fix rec out format, set local time, fix n 2022-04-20 18:16:03 -06:00
122 changed files with 92 additions and 21848 deletions

View File

@@ -3,7 +3,7 @@ package contact
import (
"crypto/tls"
"fmt"
"local/system/sysconf"
"gogs.inhome.blapointe.com/local/system/sysconf"
"log"
"net"
"net/smtp"

View File

@@ -3,7 +3,7 @@ package contact
import (
"fmt"
"io/ioutil"
"local/encryptor"
"gogs.inhome.blapointe.com/local/encryptor"
"os"
"testing"
)

18
cmd/go.mod Normal file
View File

@@ -0,0 +1,18 @@
module gogs.inhome.blapointe.com/local-sandbox/contact
go 1.20
replace gogs.inhome.blapointe.com/local-sandbox/contact/contact => ./contact
require (
gogs.inhome.blapointe.com/local-sandbox/contact/contact v0.0.0-00010101000000-000000000000
gogs.inhome.blapointe.com/local/args v0.0.0-20230410154220-44370f257b34
)
require (
github.com/bytbox/go-pop3 v0.0.0-20120201222208-3046caf0763e // indirect
github.com/emersion/go-imap v1.2.0 // indirect
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
golang.org/x/text v0.3.7 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

18
cmd/go.sum Normal file
View File

@@ -0,0 +1,18 @@
github.com/bytbox/go-pop3 v0.0.0-20120201222208-3046caf0763e h1:mQTN05gz0rDZSABqKMzAPMb5ATWcvvdMljRzEh0LjBo=
github.com/bytbox/go-pop3 v0.0.0-20120201222208-3046caf0763e/go.mod h1:alXX+s7a4cKaIprgjeEboqi4Tm7XR/HXEwUTxUV/ywU=
github.com/emersion/go-imap v1.2.0 h1:lyUQ3+EVM21/qbWE/4Ya5UG9r5+usDxlg4yfp3TgHFA=
github.com/emersion/go-imap v1.2.0/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
gogs.inhome.blapointe.com/local/args v0.0.0-20230410154220-44370f257b34 h1:0tuX5dfOksiOQD1vbJjVNVTVxTTIng7UrUdSLF5T+Ao=
gogs.inhome.blapointe.com/local/args v0.0.0-20230410154220-44370f257b34/go.mod h1:YG9n3Clg7683ohkVnJK2hdX8bBS9EojIsd1qPZumX0Y=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

48
cmd/recv.go Executable file
View File

@@ -0,0 +1,48 @@
package main
import (
"fmt"
"io"
"io/ioutil"
"strings"
"time"
"gogs.inhome.blapointe.com/local-sandbox/contact/contact"
"gogs.inhome.blapointe.com/local/args"
)
func main() {
emailer := contact.NewEmailer()
as := args.NewArgSet()
as.Append(args.STRING, "imap", "imap server:port", "imap.gmail.com:993")
as.Append(args.INT, "n", "limit (<1 for inf)", 10)
if err := as.Parse(); err != nil {
panic(err)
}
emailer.IMAP = as.Get("imap").GetString()
emailer.Limit = as.Get("n").GetInt()
msgs, err := emailer.Read()
if err != nil {
panic(err)
}
for msg := range msgs {
b, _ := ioutil.ReadAll(io.LimitReader(msg.Body, 1024))
s := strings.ReplaceAll(string(b), "\r\n", " ")
s = strings.ReplaceAll(string(s), "\n", " ")
s = strings.ReplaceAll(string(s), "\r", " ")
if !strings.Contains(s, " ") {
s = "..."
}
d, _ := msg.Header.Date()
d = d.In(time.Local)
fmt.Printf(
"@%+v @%+v: %+v: %s\n",
d.Format("06-01-02T15:04Z07"),
msg.Header.Get("From"),
msg.Header.Get("Subject"),
s,
)
}
}

View File

@@ -1,8 +1,8 @@
package main
import (
"local/args"
"local/sandbox/contact/contact"
"gogs.inhome.blapointe.com/local-sandbox/contact/contact"
"gogs.inhome.blapointe.com/local/args"
)
func main() {

View File

@@ -56,7 +56,7 @@ func (e *Emailer) ReadIMAP() (chan *mail.Message, error) {
from := uint32(1)
to := mbox.Messages
if e.Limit > 0 && to > uint32(e.Limit) {
to = uint32(e.Limit)
from = to - uint32(e.Limit-1)
}
seqset := new(imap.SeqSet)
seqset.AddRange(from, to)
@@ -100,7 +100,7 @@ func (e *Emailer) ReadPOP3() (chan *mail.Message, error) {
return nil, err
}
for j, id := range ids {
if e.Limit > 0 && j >= e.Limit {
if e.Limit > 0 && len(ids)-1-j >= e.Limit {
break
}
raw, err := c.Retr(id)

View File

@@ -12,7 +12,7 @@ func TestEmailer(t *testing.T) {
SMTP: "smtp.gmail.com:587",
POP3: "pop.gmail.com:995",
IMAP: "imap.gmail.com:993",
Password: "ML3WQRFSqe9rQ8qNkm",
Password: "lhnjijrvqaesiufp",
}
emails, err := e.Read()

View File

@@ -1,4 +1,4 @@
module local/sandbox/contact/contact
module gogs.inhome.blapointe.com/local-sandbox/contact
go 1.17

View File

28
recv.go
View File

@@ -1,28 +0,0 @@
package main
import (
"local/args"
"local/sandbox/contact/contact"
"log"
)
func main() {
emailer := contact.NewEmailer()
as := args.NewArgSet()
as.Append(args.STRING, "imap", "imap server:port", "imap.gmail.com:993")
as.Append(args.INT, "n", "limit (<1 for inf)", 10)
if err := as.Parse(); err != nil {
panic(err)
}
emailer.IMAP = as.Get("imap").GetString()
emailer.Limit = as.Get("n").GetInt()
msgs, err := emailer.Read()
if err != nil {
panic(err)
}
for msg := range msgs {
log.Println(msg)
}
}

View File

@@ -1 +0,0 @@
*.swp

View File

@@ -1,19 +0,0 @@
Copyright (c) 2012 Scott Lawrence <bytbox@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -1,223 +0,0 @@
// Package pop3 provides an implementation of the Post Office Protocol, Version
// 3 as defined in RFC 1939. Commands specified as optional are not
// implemented; however, this implementation may be trivially extended to
// support them.
package pop3
import (
"bufio"
"crypto/tls"
"errors"
"fmt"
"net"
"strconv"
"strings"
)
// The POP3 client.
type Client struct {
conn net.Conn
bin *bufio.Reader
}
// Dial creates an unsecured connection to the POP3 server at the given address
// and returns the corresponding Client.
func Dial(addr string) (*Client, error) {
conn, err := net.Dial("tcp", addr)
if err != nil {
return nil, err
}
return NewClient(conn)
}
// DialTLS creates a TLS-secured connection to the POP3 server at the given
// address and returns the corresponding Client.
func DialTLS(addr string) (*Client, error) {
conn, err := tls.Dial("tcp", addr, nil)
if err != nil {
return nil, err
}
return NewClient(conn)
}
// NewClient returns a new Client object using an existing connection.
func NewClient(conn net.Conn) (*Client, error) {
client := &Client{
bin: bufio.NewReader(conn),
conn: conn,
}
// send dud command, to read a line
_, err := client.Cmd("")
if err != nil {
return nil, err
}
return client, nil
}
// Convenience function to synchronously run an arbitrary command and wait for
// output. The terminating CRLF must be included in the format string.
//
// Output sent after the first line must be retrieved via readLines.
func (c *Client) Cmd(format string, args ...interface{}) (string, error) {
fmt.Fprintf(c.conn, format, args...)
line, _, err := c.bin.ReadLine()
if err != nil { return "", err }
l := string(line)
if l[0:3] != "+OK" {
err = errors.New(l[5:])
}
if len(l) >= 4 {
return l[4:], err
}
return "", err
}
func (c *Client) ReadLines() (lines []string, err error) {
lines = make([]string, 0)
l, _, err := c.bin.ReadLine()
line := string(l)
for err == nil && line != "." {
if len(line) > 0 && line[0] == '.' {
line = line[1:]
}
lines = append(lines, line)
l, _, err = c.bin.ReadLine()
line = string(l)
}
return
}
// User sends the given username to the server. Generally, there is no reason
// not to use the Auth convenience method.
func (c *Client) User(username string) (err error) {
_, err = c.Cmd("USER %s\r\n", username)
return
}
// Pass sends the given password to the server. The password is sent
// unencrypted unless the connection is already secured by TLS (via DialTLS or
// some other mechanism). Generally, there is no reason not to use the Auth
// convenience method.
func (c *Client) Pass(password string) (err error) {
_, err = c.Cmd("PASS %s\r\n", password)
return
}
// Auth sends the given username and password to the server, calling the User
// and Pass methods as appropriate.
func (c *Client) Auth(username, password string) (err error) {
err = c.User(username)
if err != nil {
return
}
err = c.Pass(password)
return
}
// Stat retrieves a drop listing for the current maildrop, consisting of the
// number of messages and the total size (in octets) of the maildrop.
// Information provided besides the number of messages and the size of the
// maildrop is ignored. In the event of an error, all returned numeric values
// will be 0.
func (c *Client) Stat() (count, size int, err error) {
l, err := c.Cmd("STAT\r\n")
if err != nil {
return 0, 0, err
}
parts := strings.Fields(l)
count, err = strconv.Atoi(parts[0])
if err != nil {
return 0, 0, errors.New("Invalid server response")
}
size, err = strconv.Atoi(parts[1])
if err != nil {
return 0, 0, errors.New("Invalid server response")
}
return
}
// List returns the size of the given message, if it exists. If the message
// does not exist, or another error is encountered, the returned size will be
// 0.
func (c *Client) List(msg int) (size int, err error) {
l, err := c.Cmd("LIST %d\r\n", msg)
if err != nil {
return 0, err
}
size, err = strconv.Atoi(strings.Fields(l)[1])
if err != nil {
return 0, errors.New("Invalid server response")
}
return size, nil
}
// ListAll returns a list of all messages and their sizes.
func (c *Client) ListAll() (msgs []int, sizes []int, err error) {
_, err = c.Cmd("LIST\r\n")
if err != nil {
return
}
lines, err := c.ReadLines()
if err != nil {
return
}
msgs = make([]int, len(lines), len(lines))
sizes = make([]int, len(lines), len(lines))
for i, l := range lines {
var m, s int
fs := strings.Fields(l)
m, err = strconv.Atoi(fs[0])
if err != nil {
return
}
s, err = strconv.Atoi(fs[1])
if err != nil {
return
}
msgs[i] = m
sizes[i] = s
}
return
}
// Retr downloads and returns the given message. The lines are separated by LF,
// whatever the server sent.
func (c *Client) Retr(msg int) (text string, err error) {
_, err = c.Cmd("RETR %d\r\n", msg)
if err != nil {
return "", err
}
lines, err := c.ReadLines()
text = strings.Join(lines, "\n")
return
}
// Dele marks the given message as deleted.
func (c *Client) Dele(msg int) (err error) {
_, err = c.Cmd("DELE %d\r\n", msg)
return
}
// Noop does nothing, but will prolong the end of the connection if the server
// has a timeout set.
func (c *Client) Noop() (err error) {
_, err = c.Cmd("NOOP\r\n")
return
}
// Rset unmarks any messages marked for deletion previously in this session.
func (c *Client) Rset() (err error) {
_, err = c.Cmd("RSET\r\n")
return
}
// Quit sends the QUIT message to the POP3 server and closes the connection.
func (c *Client) Quit() error {
_, err := c.Cmd("QUIT\r\n")
if err != nil {
return err
}
c.conn.Close()
return nil
}

View File

@@ -1,17 +0,0 @@
image: alpine/edge
packages:
- go
sources:
- https://github.com/emersion/go-imap
artifacts:
- coverage.html
tasks:
- build: |
cd go-imap
go build -race -v ./...
- test: |
cd go-imap
go test -coverprofile=coverage.txt -covermode=atomic ./...
- coverage: |
cd go-imap
go tool cover -html=coverage.txt -o ~/coverage.html

View File

@@ -1,28 +0,0 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
/client.go
/server.go
coverage.txt

View File

@@ -1,23 +0,0 @@
The MIT License (MIT)
Copyright (c) 2013 The Go-IMAP Authors
Copyright (c) 2016 emersion
Copyright (c) 2016 Proton Technologies AG
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,177 +0,0 @@
# go-imap
[![godocs.io](https://godocs.io/github.com/emersion/go-imap?status.svg)](https://godocs.io/github.com/emersion/go-imap)
[![builds.sr.ht status](https://builds.sr.ht/~emersion/go-imap/commits.svg)](https://builds.sr.ht/~emersion/go-imap/commits?)
An [IMAP4rev1](https://tools.ietf.org/html/rfc3501) library written in Go. It
can be used to build a client and/or a server.
## Usage
### Client [![godocs.io](https://godocs.io/github.com/emersion/go-imap/client?status.svg)](https://godocs.io/github.com/emersion/go-imap/client)
```go
package main
import (
"log"
"github.com/emersion/go-imap/client"
"github.com/emersion/go-imap"
)
func main() {
log.Println("Connecting to server...")
// Connect to server
c, err := client.DialTLS("mail.example.org:993", nil)
if err != nil {
log.Fatal(err)
}
log.Println("Connected")
// Don't forget to logout
defer c.Logout()
// Login
if err := c.Login("username", "password"); err != nil {
log.Fatal(err)
}
log.Println("Logged in")
// List mailboxes
mailboxes := make(chan *imap.MailboxInfo, 10)
done := make(chan error, 1)
go func () {
done <- c.List("", "*", mailboxes)
}()
log.Println("Mailboxes:")
for m := range mailboxes {
log.Println("* " + m.Name)
}
if err := <-done; err != nil {
log.Fatal(err)
}
// Select INBOX
mbox, err := c.Select("INBOX", false)
if err != nil {
log.Fatal(err)
}
log.Println("Flags for INBOX:", mbox.Flags)
// Get the last 4 messages
from := uint32(1)
to := mbox.Messages
if mbox.Messages > 3 {
// We're using unsigned integers here, only subtract if the result is > 0
from = mbox.Messages - 3
}
seqset := new(imap.SeqSet)
seqset.AddRange(from, to)
messages := make(chan *imap.Message, 10)
done = make(chan error, 1)
go func() {
done <- c.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope}, messages)
}()
log.Println("Last 4 messages:")
for msg := range messages {
log.Println("* " + msg.Envelope.Subject)
}
if err := <-done; err != nil {
log.Fatal(err)
}
log.Println("Done!")
}
```
### Server [![godocs.io](https://godocs.io/github.com/emersion/go-imap/server?status.svg)](https://godocs.io/github.com/emersion/go-imap/server)
```go
package main
import (
"log"
"github.com/emersion/go-imap/server"
"github.com/emersion/go-imap/backend/memory"
)
func main() {
// Create a memory backend
be := memory.New()
// Create a new server
s := server.New(be)
s.Addr = ":1143"
// Since we will use this server for testing only, we can allow plain text
// authentication over unencrypted connections
s.AllowInsecureAuth = true
log.Println("Starting IMAP server at localhost:1143")
if err := s.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
```
You can now use `telnet localhost 1143` to manually connect to the server.
## Extensions
Support for several IMAP extensions is included in go-imap itself. This
includes:
* [APPENDLIMIT](https://tools.ietf.org/html/rfc7889)
* [CHILDREN](https://tools.ietf.org/html/rfc3348)
* [ENABLE](https://tools.ietf.org/html/rfc5161)
* [IDLE](https://tools.ietf.org/html/rfc2177)
* [IMPORTANT](https://tools.ietf.org/html/rfc8457)
* [LITERAL+](https://tools.ietf.org/html/rfc7888)
* [MOVE](https://tools.ietf.org/html/rfc6851)
* [SASL-IR](https://tools.ietf.org/html/rfc4959)
* [SPECIAL-USE](https://tools.ietf.org/html/rfc6154)
* [UNSELECT](https://tools.ietf.org/html/rfc3691)
Support for other extensions is provided via separate packages. See below.
## Extending go-imap
### Extensions
Commands defined in IMAP extensions are available in other packages. See [the
wiki](https://github.com/emersion/go-imap/wiki/Using-extensions#using-client-extensions)
to learn how to use them.
* [COMPRESS](https://github.com/emersion/go-imap-compress)
* [ID](https://github.com/ProtonMail/go-imap-id)
* [METADATA](https://github.com/emersion/go-imap-metadata)
* [NAMESPACE](https://github.com/foxcpp/go-imap-namespace)
* [QUOTA](https://github.com/emersion/go-imap-quota)
* [SORT and THREAD](https://github.com/emersion/go-imap-sortthread)
* [UIDPLUS](https://github.com/emersion/go-imap-uidplus)
### Server backends
* [Memory](https://github.com/emersion/go-imap/tree/master/backend/memory) (for testing)
* [Multi](https://github.com/emersion/go-imap-multi)
* [PGP](https://github.com/emersion/go-imap-pgp)
* [Proxy](https://github.com/emersion/go-imap-proxy)
### Related projects
* [go-message](https://github.com/emersion/go-message) - parsing and formatting MIME and mail messages
* [go-msgauth](https://github.com/emersion/go-msgauth) - handle DKIM, DMARC and Authentication-Results
* [go-pgpmail](https://github.com/emersion/go-pgpmail) - decrypting and encrypting mails with OpenPGP
* [go-sasl](https://github.com/emersion/go-sasl) - sending and receiving SASL authentications
* [go-smtp](https://github.com/emersion/go-smtp) - building SMTP clients and servers
## License
MIT

View File

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

View File

@@ -1,88 +0,0 @@
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
}

View File

@@ -1,380 +0,0 @@
package client
import (
"errors"
"time"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/commands"
"github.com/emersion/go-imap/responses"
)
// ErrNotLoggedIn is returned if a function that requires the client to be
// logged in is called then the client isn't.
var ErrNotLoggedIn = errors.New("Not logged in")
func (c *Client) ensureAuthenticated() error {
state := c.State()
if state != imap.AuthenticatedState && state != imap.SelectedState {
return ErrNotLoggedIn
}
return nil
}
// Select selects a mailbox so that messages in the mailbox can be accessed. Any
// currently selected mailbox is deselected before attempting the new selection.
// Even if the readOnly parameter is set to false, the server can decide to open
// the mailbox in read-only mode.
func (c *Client) Select(name string, readOnly bool) (*imap.MailboxStatus, error) {
if err := c.ensureAuthenticated(); err != nil {
return nil, err
}
cmd := &commands.Select{
Mailbox: name,
ReadOnly: readOnly,
}
mbox := &imap.MailboxStatus{Name: name, Items: make(map[imap.StatusItem]interface{})}
res := &responses.Select{
Mailbox: mbox,
}
c.locker.Lock()
c.mailbox = mbox
c.locker.Unlock()
status, err := c.execute(cmd, res)
if err != nil {
c.locker.Lock()
c.mailbox = nil
c.locker.Unlock()
return nil, err
}
if err := status.Err(); err != nil {
c.locker.Lock()
c.mailbox = nil
c.locker.Unlock()
return nil, err
}
c.locker.Lock()
mbox.ReadOnly = (status.Code == imap.CodeReadOnly)
c.state = imap.SelectedState
c.locker.Unlock()
return mbox, nil
}
// Create creates a mailbox with the given name.
func (c *Client) Create(name string) error {
if err := c.ensureAuthenticated(); err != nil {
return err
}
cmd := &commands.Create{
Mailbox: name,
}
status, err := c.execute(cmd, nil)
if err != nil {
return err
}
return status.Err()
}
// Delete permanently removes the mailbox with the given name.
func (c *Client) Delete(name string) error {
if err := c.ensureAuthenticated(); err != nil {
return err
}
cmd := &commands.Delete{
Mailbox: name,
}
status, err := c.execute(cmd, nil)
if err != nil {
return err
}
return status.Err()
}
// Rename changes the name of a mailbox.
func (c *Client) Rename(existingName, newName string) error {
if err := c.ensureAuthenticated(); err != nil {
return err
}
cmd := &commands.Rename{
Existing: existingName,
New: newName,
}
status, err := c.execute(cmd, nil)
if err != nil {
return err
}
return status.Err()
}
// Subscribe adds the specified mailbox name to the server's set of "active" or
// "subscribed" mailboxes.
func (c *Client) Subscribe(name string) error {
if err := c.ensureAuthenticated(); err != nil {
return err
}
cmd := &commands.Subscribe{
Mailbox: name,
}
status, err := c.execute(cmd, nil)
if err != nil {
return err
}
return status.Err()
}
// Unsubscribe removes the specified mailbox name from the server's set of
// "active" or "subscribed" mailboxes.
func (c *Client) Unsubscribe(name string) error {
if err := c.ensureAuthenticated(); err != nil {
return err
}
cmd := &commands.Unsubscribe{
Mailbox: name,
}
status, err := c.execute(cmd, nil)
if err != nil {
return err
}
return status.Err()
}
// List returns a subset of names from the complete set of all names available
// to the client.
//
// An empty name argument is a special request to return the hierarchy delimiter
// and the root name of the name given in the reference. The character "*" is a
// wildcard, and matches zero or more characters at this position. The
// character "%" is similar to "*", but it does not match a hierarchy delimiter.
func (c *Client) List(ref, name string, ch chan *imap.MailboxInfo) error {
defer close(ch)
if err := c.ensureAuthenticated(); err != nil {
return err
}
cmd := &commands.List{
Reference: ref,
Mailbox: name,
}
res := &responses.List{Mailboxes: ch}
status, err := c.execute(cmd, res)
if err != nil {
return err
}
return status.Err()
}
// Lsub returns a subset of names from the set of names that the user has
// declared as being "active" or "subscribed".
func (c *Client) Lsub(ref, name string, ch chan *imap.MailboxInfo) error {
defer close(ch)
if err := c.ensureAuthenticated(); err != nil {
return err
}
cmd := &commands.List{
Reference: ref,
Mailbox: name,
Subscribed: true,
}
res := &responses.List{
Mailboxes: ch,
Subscribed: true,
}
status, err := c.execute(cmd, res)
if err != nil {
return err
}
return status.Err()
}
// Status requests the status of the indicated mailbox. It does not change the
// currently selected mailbox, nor does it affect the state of any messages in
// the queried mailbox.
//
// See RFC 3501 section 6.3.10 for a list of items that can be requested.
func (c *Client) Status(name string, items []imap.StatusItem) (*imap.MailboxStatus, error) {
if err := c.ensureAuthenticated(); err != nil {
return nil, err
}
cmd := &commands.Status{
Mailbox: name,
Items: items,
}
res := &responses.Status{
Mailbox: new(imap.MailboxStatus),
}
status, err := c.execute(cmd, res)
if err != nil {
return nil, err
}
return res.Mailbox, status.Err()
}
// Append appends the literal argument as a new message to the end of the
// specified destination mailbox. This argument SHOULD be in the format of an
// RFC 2822 message. flags and date are optional arguments and can be set to
// nil and the empty struct.
func (c *Client) Append(mbox string, flags []string, date time.Time, msg imap.Literal) error {
if err := c.ensureAuthenticated(); err != nil {
return err
}
cmd := &commands.Append{
Mailbox: mbox,
Flags: flags,
Date: date,
Message: msg,
}
status, err := c.execute(cmd, nil)
if err != nil {
return err
}
return status.Err()
}
// Enable requests the server to enable the named extensions. The extensions
// which were successfully enabled are returned.
//
// See RFC 5161 section 3.1.
func (c *Client) Enable(caps []string) ([]string, error) {
if ok, err := c.Support("ENABLE"); !ok || err != nil {
return nil, ErrExtensionUnsupported
}
// ENABLE is invalid if a mailbox has been selected.
if c.State() != imap.AuthenticatedState {
return nil, ErrNotLoggedIn
}
cmd := &commands.Enable{Caps: caps}
res := &responses.Enabled{}
if status, err := c.Execute(cmd, res); err != nil {
return nil, err
} else {
return res.Caps, status.Err()
}
}
func (c *Client) idle(stop <-chan struct{}) error {
cmd := &commands.Idle{}
res := &responses.Idle{
Stop: stop,
RepliesCh: make(chan []byte, 10),
}
if status, err := c.Execute(cmd, res); err != nil {
return err
} else {
return status.Err()
}
}
// IdleOptions holds options for Client.Idle.
type IdleOptions struct {
// LogoutTimeout is used to avoid being logged out by the server when
// idling. Each LogoutTimeout, the IDLE command is restarted. If set to
// zero, a default is used. If negative, this behavior is disabled.
LogoutTimeout time.Duration
// Poll interval when the server doesn't support IDLE. If zero, a default
// is used. If negative, polling is always disabled.
PollInterval time.Duration
}
// Idle indicates to the server that the client is ready to receive unsolicited
// mailbox update messages. When the client wants to send commands again, it
// must first close stop.
//
// If the server doesn't support IDLE, go-imap falls back to polling.
func (c *Client) Idle(stop <-chan struct{}, opts *IdleOptions) error {
if ok, err := c.Support("IDLE"); err != nil {
return err
} else if !ok {
return c.idleFallback(stop, opts)
}
logoutTimeout := 25 * time.Minute
if opts != nil {
if opts.LogoutTimeout > 0 {
logoutTimeout = opts.LogoutTimeout
} else if opts.LogoutTimeout < 0 {
return c.idle(stop)
}
}
t := time.NewTicker(logoutTimeout)
defer t.Stop()
for {
stopOrRestart := make(chan struct{})
done := make(chan error, 1)
go func() {
done <- c.idle(stopOrRestart)
}()
select {
case <-t.C:
close(stopOrRestart)
if err := <-done; err != nil {
return err
}
case <-stop:
close(stopOrRestart)
return <-done
case err := <-done:
close(stopOrRestart)
if err != nil {
return err
}
}
}
}
func (c *Client) idleFallback(stop <-chan struct{}, opts *IdleOptions) error {
pollInterval := time.Minute
if opts != nil {
if opts.PollInterval > 0 {
pollInterval = opts.PollInterval
} else if opts.PollInterval < 0 {
return ErrExtensionUnsupported
}
}
t := time.NewTicker(pollInterval)
defer t.Stop()
for {
select {
case <-t.C:
if err := c.Noop(); err != nil {
return err
}
case <-stop:
return nil
case <-c.LoggedOut():
return errors.New("disconnected while idling")
}
}
}

View File

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

View File

@@ -1,367 +0,0 @@
package client
import (
"errors"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/commands"
"github.com/emersion/go-imap/responses"
)
var (
// ErrNoMailboxSelected is returned if a command that requires a mailbox to be
// selected is called when there isn't.
ErrNoMailboxSelected = errors.New("No mailbox selected")
// ErrExtensionUnsupported is returned if a command uses a extension that
// is not supported by the server.
ErrExtensionUnsupported = errors.New("The required extension is not supported by the server")
)
// Check requests a checkpoint of the currently selected mailbox. A checkpoint
// refers to any implementation-dependent housekeeping associated with the
// mailbox that is not normally executed as part of each command.
func (c *Client) Check() error {
if c.State() != imap.SelectedState {
return ErrNoMailboxSelected
}
cmd := new(commands.Check)
status, err := c.execute(cmd, nil)
if err != nil {
return err
}
return status.Err()
}
// Close permanently removes all messages that have the \Deleted flag set from
// the currently selected mailbox, and returns to the authenticated state from
// the selected state.
func (c *Client) Close() error {
if c.State() != imap.SelectedState {
return ErrNoMailboxSelected
}
cmd := new(commands.Close)
status, err := c.execute(cmd, nil)
if err != nil {
return err
} else if err := status.Err(); err != nil {
return err
}
c.locker.Lock()
c.state = imap.AuthenticatedState
c.mailbox = nil
c.locker.Unlock()
return nil
}
// Terminate closes the tcp connection
func (c *Client) Terminate() error {
return c.conn.Close()
}
// Expunge permanently removes all messages that have the \Deleted flag set from
// the currently selected mailbox. If ch is not nil, sends sequence IDs of each
// deleted message to this channel.
func (c *Client) Expunge(ch chan uint32) error {
if ch != nil {
defer close(ch)
}
if c.State() != imap.SelectedState {
return ErrNoMailboxSelected
}
cmd := new(commands.Expunge)
var h responses.Handler
if ch != nil {
h = &responses.Expunge{SeqNums: ch}
}
status, err := c.execute(cmd, h)
if err != nil {
return err
}
return status.Err()
}
func (c *Client) executeSearch(uid bool, criteria *imap.SearchCriteria, charset string) (ids []uint32, status *imap.StatusResp, err error) {
if c.State() != imap.SelectedState {
err = ErrNoMailboxSelected
return
}
var cmd imap.Commander = &commands.Search{
Charset: charset,
Criteria: criteria,
}
if uid {
cmd = &commands.Uid{Cmd: cmd}
}
res := new(responses.Search)
status, err = c.execute(cmd, res)
if err != nil {
return
}
err, ids = status.Err(), res.Ids
return
}
func (c *Client) search(uid bool, criteria *imap.SearchCriteria) (ids []uint32, err error) {
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. When no criteria has been set, all messages in the mailbox
// will be searched using ALL criteria.
func (c *Client) Search(criteria *imap.SearchCriteria) (seqNums []uint32, err error) {
return c.search(false, criteria)
}
// UidSearch is identical to Search, but UIDs are returned instead of message
// sequence numbers.
func (c *Client) UidSearch(criteria *imap.SearchCriteria) (uids []uint32, err error) {
return c.search(true, criteria)
}
func (c *Client) fetch(uid bool, seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error {
defer close(ch)
if c.State() != imap.SelectedState {
return ErrNoMailboxSelected
}
var cmd imap.Commander = &commands.Fetch{
SeqSet: seqset,
Items: items,
}
if uid {
cmd = &commands.Uid{Cmd: cmd}
}
res := &responses.Fetch{Messages: ch, SeqSet: seqset, Uid: uid}
status, err := c.execute(cmd, res)
if err != nil {
return err
}
return status.Err()
}
// Fetch retrieves data associated with a message in the mailbox. See RFC 3501
// section 6.4.5 for a list of items that can be requested.
func (c *Client) Fetch(seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error {
return c.fetch(false, seqset, items, ch)
}
// UidFetch is identical to Fetch, but seqset is interpreted as containing
// unique identifiers instead of message sequence numbers.
func (c *Client) UidFetch(seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error {
return c.fetch(true, seqset, items, ch)
}
func (c *Client) store(uid bool, seqset *imap.SeqSet, item imap.StoreItem, value interface{}, ch chan *imap.Message) error {
if ch != nil {
defer close(ch)
}
if c.State() != imap.SelectedState {
return ErrNoMailboxSelected
}
// TODO: this could break extensions (this only works when item is FLAGS)
if fields, ok := value.([]interface{}); ok {
for i, field := range fields {
if s, ok := field.(string); ok {
fields[i] = imap.RawString(s)
}
}
}
// If ch is nil, the updated values are data which will be lost, so don't
// retrieve it.
if ch == nil {
op, _, err := imap.ParseFlagsOp(item)
if err == nil {
item = imap.FormatFlagsOp(op, true)
}
}
var cmd imap.Commander = &commands.Store{
SeqSet: seqset,
Item: item,
Value: value,
}
if uid {
cmd = &commands.Uid{Cmd: cmd}
}
var h responses.Handler
if ch != nil {
h = &responses.Fetch{Messages: ch, SeqSet: seqset, Uid: uid}
}
status, err := c.execute(cmd, h)
if err != nil {
return err
}
return status.Err()
}
// Store alters data associated with a message in the mailbox. If ch is not nil,
// the updated value of the data will be sent to this channel. See RFC 3501
// section 6.4.6 for a list of items that can be updated.
func (c *Client) Store(seqset *imap.SeqSet, item imap.StoreItem, value interface{}, ch chan *imap.Message) error {
return c.store(false, seqset, item, value, ch)
}
// UidStore is identical to Store, but seqset is interpreted as containing
// unique identifiers instead of message sequence numbers.
func (c *Client) UidStore(seqset *imap.SeqSet, item imap.StoreItem, value interface{}, ch chan *imap.Message) error {
return c.store(true, seqset, item, value, ch)
}
func (c *Client) copy(uid bool, seqset *imap.SeqSet, dest string) error {
if c.State() != imap.SelectedState {
return ErrNoMailboxSelected
}
var cmd imap.Commander = &commands.Copy{
SeqSet: seqset,
Mailbox: dest,
}
if uid {
cmd = &commands.Uid{Cmd: cmd}
}
status, err := c.execute(cmd, nil)
if err != nil {
return err
}
return status.Err()
}
// Copy copies the specified message(s) to the end of the specified destination
// mailbox.
func (c *Client) Copy(seqset *imap.SeqSet, dest string) error {
return c.copy(false, seqset, dest)
}
// UidCopy is identical to Copy, but seqset is interpreted as containing unique
// identifiers instead of message sequence numbers.
func (c *Client) UidCopy(seqset *imap.SeqSet, dest string) error {
return c.copy(true, seqset, dest)
}
func (c *Client) move(uid bool, seqset *imap.SeqSet, dest string) error {
if c.State() != imap.SelectedState {
return ErrNoMailboxSelected
}
if ok, err := c.Support("MOVE"); err != nil {
return err
} else if !ok {
return c.moveFallback(uid, seqset, dest)
}
var cmd imap.Commander = &commands.Move{
SeqSet: seqset,
Mailbox: dest,
}
if uid {
cmd = &commands.Uid{Cmd: cmd}
}
if status, err := c.Execute(cmd, nil); err != nil {
return err
} else {
return status.Err()
}
}
// moveFallback uses COPY, STORE and EXPUNGE for servers which don't support
// MOVE.
func (c *Client) moveFallback(uid bool, seqset *imap.SeqSet, dest string) error {
item := imap.FormatFlagsOp(imap.AddFlags, true)
flags := []interface{}{imap.DeletedFlag}
if uid {
if err := c.UidCopy(seqset, dest); err != nil {
return err
}
if err := c.UidStore(seqset, item, flags, nil); err != nil {
return err
}
} else {
if err := c.Copy(seqset, dest); err != nil {
return err
}
if err := c.Store(seqset, item, flags, nil); err != nil {
return err
}
}
return c.Expunge(nil)
}
// Move moves the specified message(s) to the end of the specified destination
// mailbox.
//
// If the server doesn't support the MOVE extension defined in RFC 6851,
// go-imap will fallback to copy, store and expunge.
func (c *Client) Move(seqset *imap.SeqSet, dest string) error {
return c.move(false, seqset, dest)
}
// UidMove is identical to Move, but seqset is interpreted as containing unique
// identifiers instead of message sequence numbers.
func (c *Client) UidMove(seqset *imap.SeqSet, dest string) error {
return c.move(true, seqset, dest)
}
// Unselect frees server's resources associated with the selected mailbox and
// returns the server to the authenticated state. This command performs the same
// actions as Close, except that no messages are permanently removed from the
// currently selected mailbox.
//
// If client does not support the UNSELECT extension, ErrExtensionUnsupported
// is returned.
func (c *Client) Unselect() error {
if ok, err := c.Support("UNSELECT"); !ok || err != nil {
return ErrExtensionUnsupported
}
if c.State() != imap.SelectedState {
return ErrNoMailboxSelected
}
cmd := &commands.Unselect{}
if status, err := c.Execute(cmd, nil); err != nil {
return err
} else if err := status.Err(); err != nil {
return err
}
c.SetState(imap.AuthenticatedState, nil)
return nil
}

View File

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

View File

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

View File

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

View File

@@ -1,18 +0,0 @@
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
}

View File

@@ -1,18 +0,0 @@
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
}

View File

@@ -1,18 +0,0 @@
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
}

View File

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

View File

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

View File

@@ -1,38 +0,0 @@
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
}

View File

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

View File

@@ -1,23 +0,0 @@
package commands
import (
"github.com/emersion/go-imap"
)
// An ENABLE command, defined in RFC 5161 section 3.1.
type Enable struct {
Caps []string
}
func (cmd *Enable) Command() *imap.Command {
return &imap.Command{
Name: "ENABLE",
Arguments: imap.FormatStringList(cmd.Caps),
}
}
func (cmd *Enable) Parse(fields []interface{}) error {
var err error
cmd.Caps, err = imap.ParseStringList(fields)
return err
}

View File

@@ -1,16 +0,0 @@
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
}

View File

@@ -1,55 +0,0 @@
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 {
items[i] = imap.RawString(item)
}
return &imap.Command{
Name: "FETCH",
Arguments: []interface{}{cmd.SeqSet, items},
}
}
func (cmd *Fetch) Parse(fields []interface{}) error {
if len(fields) < 2 {
return errors.New("No enough arguments")
}
var err error
if seqset, ok := fields[0].(string); !ok {
return errors.New("Sequence set must be an atom")
} else if cmd.SeqSet, err = imap.ParseSeqSet(seqset); err != nil {
return err
}
switch items := fields[1].(type) {
case string: // A macro or a single item
cmd.Items = imap.FetchItem(strings.ToUpper(items)).Expand()
case []interface{}: // A list of items
cmd.Items = make([]imap.FetchItem, 0, len(items))
for _, v := range items {
itemStr, _ := v.(string)
item := imap.FetchItem(strings.ToUpper(itemStr))
cmd.Items = append(cmd.Items, item.Expand()...)
}
default:
return errors.New("Items must be either a string or a list")
}
return nil
}

View File

@@ -1,17 +0,0 @@
package commands
import (
"github.com/emersion/go-imap"
)
// An IDLE command.
// Se RFC 2177 section 3.
type Idle struct{}
func (cmd *Idle) Command() *imap.Command {
return &imap.Command{Name: "IDLE"}
}
func (cmd *Idle) Parse(fields []interface{}) error {
return nil
}

View File

@@ -1,60 +0,0 @@
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
}

View File

@@ -1,36 +0,0 @@
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
}

View File

@@ -1,18 +0,0 @@
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
}

View File

@@ -1,48 +0,0 @@
package commands
import (
"errors"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/utf7"
)
// A MOVE command.
// See RFC 6851 section 3.1.
type Move struct {
SeqSet *imap.SeqSet
Mailbox string
}
func (cmd *Move) Command() *imap.Command {
mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox)
return &imap.Command{
Name: "MOVE",
Arguments: []interface{}{cmd.SeqSet, mailbox},
}
}
func (cmd *Move) Parse(fields []interface{}) (err error) {
if len(fields) < 2 {
return errors.New("No enough arguments")
}
seqset, ok := fields[0].(string)
if !ok {
return errors.New("Invalid sequence set")
}
if cmd.SeqSet, err = imap.ParseSeqSet(seqset); err != nil {
return err
}
mailbox, ok := fields[1].(string)
if !ok {
return errors.New("Mailbox name must be a string")
}
if cmd.Mailbox, err = utf7.Encoding.NewDecoder().String(mailbox); err != nil {
return err
}
return
}

View File

@@ -1,18 +0,0 @@
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
}

View File

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

View File

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

View File

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

View File

@@ -1,18 +0,0 @@
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
}

View File

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

View File

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

View File

@@ -1,63 +0,0 @@
package commands
import (
"errors"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/utf7"
)
// Subscribe is a SUBSCRIBE command, as defined in RFC 3501 section 6.3.6.
type Subscribe struct {
Mailbox string
}
func (cmd *Subscribe) Command() *imap.Command {
mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox)
return &imap.Command{
Name: "SUBSCRIBE",
Arguments: []interface{}{imap.FormatMailboxName(mailbox)},
}
}
func (cmd *Subscribe) Parse(fields []interface{}) error {
if len(fields) < 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{}{imap.FormatMailboxName(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
}

View File

@@ -1,44 +0,0 @@
package commands
import (
"errors"
"strings"
"github.com/emersion/go-imap"
)
// Uid is a UID command, as defined in RFC 3501 section 6.4.8. It wraps another
// command (e.g. wrapping a Fetch command will result in a UID FETCH).
type Uid struct {
Cmd imap.Commander
}
func (cmd *Uid) Command() *imap.Command {
inner := cmd.Cmd.Command()
args := []interface{}{imap.RawString(inner.Name)}
args = append(args, inner.Arguments...)
return &imap.Command{
Name: "UID",
Arguments: args,
}
}
func (cmd *Uid) Parse(fields []interface{}) error {
if len(fields) < 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
}

View File

@@ -1,17 +0,0 @@
package commands
import (
"github.com/emersion/go-imap"
)
// An UNSELECT command.
// See RFC 3691 section 2.
type Unselect struct{}
func (cmd *Unselect) Command() *imap.Command {
return &imap.Command{Name: "UNSELECT"}
}
func (cmd *Unselect) Parse(fields []interface{}) error {
return nil
}

View File

@@ -1,284 +0,0 @@
package imap
import (
"bufio"
"crypto/tls"
"io"
"net"
"sync"
)
// A connection state.
// See RFC 3501 section 3.
type ConnState int
const (
// In the connecting state, the server has not yet sent a greeting and no
// command can be issued.
ConnectingState = 0
// In the not authenticated state, the client MUST supply
// authentication credentials before most commands will be
// permitted. This state is entered when a connection starts
// unless the connection has been pre-authenticated.
NotAuthenticatedState ConnState = 1 << 0
// In the authenticated state, the client is authenticated and MUST
// select a mailbox to access before commands that affect messages
// will be permitted. This state is entered when a
// pre-authenticated connection starts, when acceptable
// authentication credentials have been provided, after an error in
// selecting a mailbox, or after a successful CLOSE command.
AuthenticatedState = 1 << 1
// In a selected state, a mailbox has been selected to access.
// This state is entered when a mailbox has been successfully
// selected.
SelectedState = AuthenticatedState + 1<<2
// In the logout state, the connection is being terminated. This
// state can be entered as a result of a client request (via the
// LOGOUT command) or by unilateral action on the part of either
// the client or server.
LogoutState = 1 << 3
// ConnectedState is either NotAuthenticatedState, AuthenticatedState or
// SelectedState.
ConnectedState = NotAuthenticatedState | AuthenticatedState | SelectedState
)
// A function that upgrades a connection.
//
// This should only be used by libraries implementing an IMAP extension (e.g.
// COMPRESS).
type ConnUpgrader func(conn net.Conn) (net.Conn, error)
type Waiter struct {
start sync.WaitGroup
end sync.WaitGroup
finished bool
}
func NewWaiter() *Waiter {
w := &Waiter{finished: false}
w.start.Add(1)
w.end.Add(1)
return w
}
func (w *Waiter) Wait() {
if !w.finished {
// Signal that we are ready for upgrade to continue.
w.start.Done()
// Wait for upgrade to finish.
w.end.Wait()
w.finished = true
}
}
func (w *Waiter) WaitReady() {
if !w.finished {
// Wait for reader/writer goroutine to be ready for upgrade.
w.start.Wait()
}
}
func (w *Waiter) Close() {
if !w.finished {
// Upgrade is finished, close chanel to release reader/writer
w.end.Done()
}
}
type LockedWriter struct {
lock sync.Mutex
writer io.Writer
}
// NewLockedWriter - goroutine safe writer.
func NewLockedWriter(w io.Writer) io.Writer {
return &LockedWriter{writer: w}
}
func (w *LockedWriter) Write(b []byte) (int, error) {
w.lock.Lock()
defer w.lock.Unlock()
return w.writer.Write(b)
}
type debugWriter struct {
io.Writer
local io.Writer
remote io.Writer
}
// NewDebugWriter creates a new io.Writer that will write local network activity
// to local and remote network activity to remote.
func NewDebugWriter(local, remote io.Writer) io.Writer {
return &debugWriter{Writer: local, local: local, remote: remote}
}
type multiFlusher struct {
flushers []flusher
}
func (mf *multiFlusher) Flush() error {
for _, f := range mf.flushers {
if err := f.Flush(); err != nil {
return err
}
}
return nil
}
func newMultiFlusher(flushers ...flusher) flusher {
return &multiFlusher{flushers}
}
// Underlying connection state information.
type ConnInfo struct {
RemoteAddr net.Addr
LocalAddr net.Addr
// nil if connection is not using TLS.
TLS *tls.ConnectionState
}
// An IMAP connection.
type Conn struct {
net.Conn
*Reader
*Writer
br *bufio.Reader
bw *bufio.Writer
waiter *Waiter
// Print all commands and responses to this io.Writer.
debug io.Writer
}
// NewConn creates a new IMAP connection.
func NewConn(conn net.Conn, r *Reader, w *Writer) *Conn {
c := &Conn{Conn: conn, Reader: r, Writer: w}
c.init()
return c
}
func (c *Conn) createWaiter() *Waiter {
// create new waiter each time.
w := NewWaiter()
c.waiter = w
return w
}
func (c *Conn) init() {
r := io.Reader(c.Conn)
w := io.Writer(c.Conn)
if c.debug != nil {
localDebug, remoteDebug := c.debug, c.debug
if debug, ok := c.debug.(*debugWriter); ok {
localDebug, remoteDebug = debug.local, debug.remote
}
// If local and remote are the same, then we need a LockedWriter.
if localDebug == remoteDebug {
localDebug = NewLockedWriter(localDebug)
remoteDebug = localDebug
}
if localDebug != nil {
w = io.MultiWriter(c.Conn, localDebug)
}
if remoteDebug != nil {
r = io.TeeReader(c.Conn, remoteDebug)
}
}
if c.br == nil {
c.br = bufio.NewReader(r)
c.Reader.reader = c.br
} else {
c.br.Reset(r)
}
if c.bw == nil {
c.bw = bufio.NewWriter(w)
c.Writer.Writer = c.bw
} else {
c.bw.Reset(w)
}
if f, ok := c.Conn.(flusher); ok {
c.Writer.Writer = struct {
io.Writer
flusher
}{
c.bw,
newMultiFlusher(c.bw, f),
}
}
}
func (c *Conn) Info() *ConnInfo {
info := &ConnInfo{
RemoteAddr: c.RemoteAddr(),
LocalAddr: c.LocalAddr(),
}
tlsConn, ok := c.Conn.(*tls.Conn)
if ok {
state := tlsConn.ConnectionState()
info.TLS = &state
}
return info
}
// Write implements io.Writer.
func (c *Conn) Write(b []byte) (n int, err error) {
return c.Writer.Write(b)
}
// Flush writes any buffered data to the underlying connection.
func (c *Conn) Flush() error {
return c.Writer.Flush()
}
// Upgrade a connection, e.g. wrap an unencrypted connection with an encrypted
// tunnel.
func (c *Conn) Upgrade(upgrader ConnUpgrader) error {
// Block reads and writes during the upgrading process
w := c.createWaiter()
defer w.Close()
upgraded, err := upgrader(c.Conn)
if err != nil {
return err
}
c.Conn = upgraded
c.init()
return nil
}
// Called by reader/writer goroutines to wait for Upgrade to finish
func (c *Conn) Wait() {
c.waiter.Wait()
}
// Called by Upgrader to wait for reader/writer goroutines to be ready for
// upgrade.
func (c *Conn) WaitReady() {
c.waiter.WaitReady()
}
// SetDebug defines an io.Writer to which all network activity will be logged.
// If nil is provided, network activity will not be logged.
func (c *Conn) SetDebug(w io.Writer) {
c.debug = w
c.init()
}

View File

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

View File

@@ -1,108 +0,0 @@
// Package imap implements IMAP4rev1 (RFC 3501).
package imap
import (
"errors"
"io"
"strings"
)
// A StatusItem is a mailbox status data item that can be retrieved with a
// STATUS command. See RFC 3501 section 6.3.10.
type StatusItem string
const (
StatusMessages StatusItem = "MESSAGES"
StatusRecent StatusItem = "RECENT"
StatusUidNext StatusItem = "UIDNEXT"
StatusUidValidity StatusItem = "UIDVALIDITY"
StatusUnseen StatusItem = "UNSEEN"
StatusAppendLimit StatusItem = "APPENDLIMIT"
)
// A FetchItem is a message data item that can be fetched.
type FetchItem string
// List of items that can be fetched.
const (
// Macros
FetchAll FetchItem = "ALL"
FetchFast FetchItem = "FAST"
FetchFull FetchItem = "FULL"
// Items
FetchBody FetchItem = "BODY"
FetchBodyStructure FetchItem = "BODYSTRUCTURE"
FetchEnvelope FetchItem = "ENVELOPE"
FetchFlags FetchItem = "FLAGS"
FetchInternalDate FetchItem = "INTERNALDATE"
FetchRFC822 FetchItem = "RFC822"
FetchRFC822Header FetchItem = "RFC822.HEADER"
FetchRFC822Size FetchItem = "RFC822.SIZE"
FetchRFC822Text FetchItem = "RFC822.TEXT"
FetchUid FetchItem = "UID"
)
// Expand expands the item if it's a macro.
func (item FetchItem) Expand() []FetchItem {
switch item {
case FetchAll:
return []FetchItem{FetchFlags, FetchInternalDate, FetchRFC822Size, FetchEnvelope}
case FetchFast:
return []FetchItem{FetchFlags, FetchInternalDate, FetchRFC822Size}
case FetchFull:
return []FetchItem{FetchFlags, FetchInternalDate, FetchRFC822Size, FetchEnvelope, FetchBody}
default:
return []FetchItem{item}
}
}
// FlagsOp is an operation that will be applied on message flags.
type FlagsOp string
const (
// SetFlags replaces existing flags by new ones.
SetFlags FlagsOp = "FLAGS"
// AddFlags adds new flags.
AddFlags = "+FLAGS"
// RemoveFlags removes existing flags.
RemoveFlags = "-FLAGS"
)
// silentOp can be appended to a FlagsOp to prevent the operation from
// triggering unilateral message updates.
const silentOp = ".SILENT"
// A StoreItem is a message data item that can be updated.
type StoreItem string
// FormatFlagsOp returns the StoreItem that executes the flags operation op.
func FormatFlagsOp(op FlagsOp, silent bool) StoreItem {
s := string(op)
if silent {
s += silentOp
}
return StoreItem(s)
}
// ParseFlagsOp parses a flags operation from StoreItem.
func ParseFlagsOp(item StoreItem) (op FlagsOp, silent bool, err error) {
itemStr := string(item)
silent = strings.HasSuffix(itemStr, silentOp)
if silent {
itemStr = strings.TrimSuffix(itemStr, silentOp)
}
op = FlagsOp(itemStr)
if op != SetFlags && op != AddFlags && op != RemoveFlags {
err = errors.New("Unsupported STORE operation")
}
return
}
// CharsetReader, if non-nil, defines a function to generate charset-conversion
// readers, converting from the provided charset into UTF-8. Charsets are always
// lower-case. utf-8 and us-ascii charsets are handled by default. One of the
// the CharsetReader's result values must be non-nil.
var CharsetReader func(charset string, r io.Reader) (io.Reader, error)

View File

@@ -1,13 +0,0 @@
package imap
import (
"io"
)
// A literal, as defined in RFC 3501 section 4.3.
type Literal interface {
io.Reader
// Len returns the number of bytes of the literal.
Len() int
}

View File

@@ -1,8 +0,0 @@
package imap
// Logger is the behaviour used by server/client to
// report errors for accepting connections and unexpected behavior from handlers.
type Logger interface {
Printf(format string, v ...interface{})
Println(v ...interface{})
}

View File

@@ -1,314 +0,0 @@
package imap
import (
"errors"
"fmt"
"strings"
"sync"
"github.com/emersion/go-imap/utf7"
)
// The primary mailbox, as defined in RFC 3501 section 5.1.
const InboxName = "INBOX"
// CanonicalMailboxName returns the canonical form of a mailbox name. Mailbox names can be
// case-sensitive or case-insensitive depending on the backend implementation.
// The special INBOX mailbox is case-insensitive.
func CanonicalMailboxName(name string) string {
if strings.ToUpper(name) == InboxName {
return InboxName
}
return name
}
// Mailbox attributes definied in RFC 3501 section 7.2.2.
const (
// It is not possible for any child levels of hierarchy to exist under this\
// name; no child levels exist now and none can be created in the future.
NoInferiorsAttr = "\\Noinferiors"
// It is not possible to use this name as a selectable mailbox.
NoSelectAttr = "\\Noselect"
// The mailbox has been marked "interesting" by the server; the mailbox
// probably contains messages that have been added since the last time the
// mailbox was selected.
MarkedAttr = "\\Marked"
// The mailbox does not contain any additional messages since the last time
// the mailbox was selected.
UnmarkedAttr = "\\Unmarked"
)
// Mailbox attributes defined in RFC 6154 section 2 (SPECIAL-USE extension).
const (
// This mailbox presents all messages in the user's message store.
AllAttr = "\\All"
// This mailbox is used to archive messages.
ArchiveAttr = "\\Archive"
// This mailbox is used to hold draft messages -- typically, messages that are
// being composed but have not yet been sent.
DraftsAttr = "\\Drafts"
// This mailbox presents all messages marked in some way as "important".
FlaggedAttr = "\\Flagged"
// This mailbox is where messages deemed to be junk mail are held.
JunkAttr = "\\Junk"
// This mailbox is used to hold copies of messages that have been sent.
SentAttr = "\\Sent"
// This mailbox is used to hold messages that have been deleted or marked for
// deletion.
TrashAttr = "\\Trash"
)
// Mailbox attributes defined in RFC 3348 (CHILDREN extension)
const (
// The presence of this attribute indicates that the mailbox has child
// mailboxes.
HasChildrenAttr = "\\HasChildren"
// The presence of this attribute indicates that the mailbox has no child
// mailboxes.
HasNoChildrenAttr = "\\HasNoChildren"
)
// This mailbox attribute is a signal that the mailbox contains messages that
// are likely important to the user. This attribute is defined in RFC 8457
// section 3.
const ImportantAttr = "\\Important"
// Basic mailbox info.
type MailboxInfo struct {
// The mailbox attributes.
Attributes []string
// The server's path separator.
Delimiter string
// The mailbox name.
Name string
}
// Parse mailbox info from fields.
func (info *MailboxInfo) Parse(fields []interface{}) error {
if len(fields) < 3 {
return errors.New("Mailbox info needs at least 3 fields")
}
var err error
if info.Attributes, err = ParseStringList(fields[0]); err != nil {
return err
}
var ok bool
if info.Delimiter, ok = fields[1].(string); !ok {
// The delimiter may be specified as NIL, which gets converted to a nil interface.
if fields[1] != nil {
return errors.New("Mailbox delimiter must be a string")
}
info.Delimiter = ""
}
if name, err := ParseString(fields[2]); err != nil {
return err
} else if name, err := utf7.Encoding.NewDecoder().String(name); err != nil {
return err
} else {
info.Name = CanonicalMailboxName(name)
}
return nil
}
// Format mailbox info to fields.
func (info *MailboxInfo) Format() []interface{} {
name, _ := utf7.Encoding.NewEncoder().String(info.Name)
attrs := make([]interface{}, len(info.Attributes))
for i, attr := range info.Attributes {
attrs[i] = RawString(attr)
}
// If the delimiter is NIL, we need to treat it specially by inserting
// a nil field (so that it's later converted to an unquoted NIL atom).
var del interface{}
if info.Delimiter != "" {
del = info.Delimiter
}
// Thunderbird doesn't understand delimiters if not quoted
return []interface{}{attrs, del, FormatMailboxName(name)}
}
// TODO: optimize this
func (info *MailboxInfo) match(name, pattern string) bool {
i := strings.IndexAny(pattern, "*%")
if i == -1 {
// No more wildcards
return name == pattern
}
// Get parts before and after wildcard
chunk, wildcard, rest := pattern[0:i], pattern[i], pattern[i+1:]
// Check that name begins with chunk
if len(chunk) > 0 && !strings.HasPrefix(name, chunk) {
return false
}
name = strings.TrimPrefix(name, chunk)
// Expand wildcard
var j int
for j = 0; j < len(name); j++ {
if wildcard == '%' && string(name[j]) == info.Delimiter {
break // Stop on delimiter if wildcard is %
}
// Try to match the rest from here
if info.match(name[j:], rest) {
return true
}
}
return info.match(name[j:], rest)
}
// Match checks if a reference and a pattern matches this mailbox name, as
// defined in RFC 3501 section 6.3.8.
func (info *MailboxInfo) Match(reference, pattern string) bool {
name := info.Name
if info.Delimiter != "" && strings.HasPrefix(pattern, info.Delimiter) {
reference = ""
pattern = strings.TrimPrefix(pattern, info.Delimiter)
}
if reference != "" {
if info.Delimiter != "" && !strings.HasSuffix(reference, info.Delimiter) {
reference += info.Delimiter
}
if !strings.HasPrefix(name, reference) {
return false
}
name = strings.TrimPrefix(name, reference)
}
return info.match(name, pattern)
}
// A mailbox status.
type MailboxStatus struct {
// The mailbox name.
Name string
// True if the mailbox is open in read-only mode.
ReadOnly bool
// The mailbox items that are currently filled in. This map's values
// should not be used directly, they must only be used by libraries
// implementing extensions of the IMAP protocol.
Items map[StatusItem]interface{}
// The Items map may be accessed in different goroutines. Protect
// concurrent writes.
ItemsLocker sync.Mutex
// The mailbox flags.
Flags []string
// The mailbox permanent flags.
PermanentFlags []string
// The sequence number of the first unseen message in the mailbox.
UnseenSeqNum uint32
// The number of messages in this mailbox.
Messages uint32
// The number of messages not seen since the last time the mailbox was opened.
Recent uint32
// The number of unread messages.
Unseen uint32
// The next UID.
UidNext uint32
// Together with a UID, it is a unique identifier for a message.
// Must be greater than or equal to 1.
UidValidity uint32
// Per-mailbox limit of message size. Set only if server supports the
// APPENDLIMIT extension.
AppendLimit uint32
}
// Create a new mailbox status that will contain the specified items.
func NewMailboxStatus(name string, items []StatusItem) *MailboxStatus {
status := &MailboxStatus{
Name: name,
Items: make(map[StatusItem]interface{}),
}
for _, k := range items {
status.Items[k] = nil
}
return status
}
func (status *MailboxStatus) Parse(fields []interface{}) error {
status.Items = make(map[StatusItem]interface{})
var k StatusItem
for i, f := range fields {
if i%2 == 0 {
if kstr, ok := f.(string); !ok {
return fmt.Errorf("cannot parse mailbox status: key is not a string, but a %T", f)
} else {
k = StatusItem(strings.ToUpper(kstr))
}
} else {
status.Items[k] = nil
var err error
switch k {
case StatusMessages:
status.Messages, err = ParseNumber(f)
case StatusRecent:
status.Recent, err = ParseNumber(f)
case StatusUnseen:
status.Unseen, err = ParseNumber(f)
case StatusUidNext:
status.UidNext, err = ParseNumber(f)
case StatusUidValidity:
status.UidValidity, err = ParseNumber(f)
case StatusAppendLimit:
status.AppendLimit, err = ParseNumber(f)
default:
status.Items[k] = f
}
if err != nil {
return err
}
}
}
return nil
}
func (status *MailboxStatus) Format() []interface{} {
var fields []interface{}
for k, v := range status.Items {
switch k {
case StatusMessages:
v = status.Messages
case StatusRecent:
v = status.Recent
case StatusUnseen:
v = status.Unseen
case StatusUidNext:
v = status.UidNext
case StatusUidValidity:
v = status.UidValidity
case StatusAppendLimit:
v = status.AppendLimit
}
fields = append(fields, RawString(k), v)
}
return fields
}
func FormatMailboxName(name string) interface{} {
// Some e-mails servers don't handle quoted INBOX names correctly so we special-case it.
if strings.EqualFold(name, "INBOX") {
return RawString(name)
}
return name
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,467 +0,0 @@
package imap
import (
"bytes"
"errors"
"io"
"strconv"
"strings"
)
const (
sp = ' '
cr = '\r'
lf = '\n'
dquote = '"'
literalStart = '{'
literalEnd = '}'
listStart = '('
listEnd = ')'
respCodeStart = '['
respCodeEnd = ']'
)
const (
crlf = "\r\n"
nilAtom = "NIL"
)
// TODO: add CTL to atomSpecials
var (
quotedSpecials = string([]rune{dquote, '\\'})
respSpecials = string([]rune{respCodeEnd})
atomSpecials = string([]rune{listStart, listEnd, literalStart, sp, '%', '*'}) + quotedSpecials + respSpecials
)
type parseError struct {
error
}
func newParseError(text string) error {
return &parseError{errors.New(text)}
}
// IsParseError returns true if the provided error is a parse error produced by
// Reader.
func IsParseError(err error) bool {
_, ok := err.(*parseError)
return ok
}
// A string reader.
type StringReader interface {
// ReadString reads until the first occurrence of delim in the input,
// returning a string containing the data up to and including the delimiter.
// See https://golang.org/pkg/bufio/#Reader.ReadString
ReadString(delim byte) (line string, err error)
}
type reader interface {
io.Reader
io.RuneScanner
StringReader
}
// ParseNumber parses a number.
func ParseNumber(f interface{}) (uint32, error) {
// Useful for tests
if n, ok := f.(uint32); ok {
return n, nil
}
var s string
switch f := f.(type) {
case RawString:
s = string(f)
case string:
s = f
default:
return 0, newParseError("expected a number, got a non-atom")
}
nbr, err := strconv.ParseUint(string(s), 10, 32)
if err != nil {
return 0, &parseError{err}
}
return uint32(nbr), nil
}
// ParseString parses a string, which is either a literal, a quoted string or an
// atom.
func ParseString(f interface{}) (string, error) {
if s, ok := f.(string); ok {
return s, nil
}
// Useful for tests
if a, ok := f.(RawString); ok {
return string(a), nil
}
if l, ok := f.(Literal); ok {
b := make([]byte, l.Len())
if _, err := io.ReadFull(l, b); err != nil {
return "", err
}
return string(b), nil
}
return "", newParseError("expected a string")
}
// Convert a field list to a string list.
func ParseStringList(f interface{}) ([]string, error) {
fields, ok := f.([]interface{})
if !ok {
return nil, newParseError("expected a string list, got a non-list")
}
list := make([]string, len(fields))
for i, f := range fields {
var err error
if list[i], err = ParseString(f); err != nil {
return nil, newParseError("cannot parse string in string list: " + err.Error())
}
}
return list, nil
}
func trimSuffix(str string, suffix rune) string {
return str[:len(str)-1]
}
// An IMAP reader.
type Reader struct {
MaxLiteralSize uint32 // The maximum literal size.
reader
continues chan<- bool
brackets int
inRespCode bool
}
func (r *Reader) ReadSp() error {
char, _, err := r.ReadRune()
if err != nil {
return err
}
if char != sp {
return newParseError("expected a space")
}
return nil
}
func (r *Reader) ReadCrlf() (err error) {
var char rune
if char, _, err = r.ReadRune(); err != nil {
return
}
if char == lf {
return
}
if char != cr {
err = newParseError("line doesn't end with a CR")
return
}
if char, _, err = r.ReadRune(); err != nil {
return
}
if char != lf {
err = newParseError("line doesn't end with a LF")
}
return
}
func (r *Reader) ReadAtom() (interface{}, error) {
r.brackets = 0
var atom string
for {
char, _, err := r.ReadRune()
if err != nil {
return nil, err
}
// TODO: list-wildcards and \
if r.brackets == 0 && (char == listStart || char == literalStart || char == dquote) {
return nil, newParseError("atom contains forbidden char: " + string(char))
}
if char == cr || char == lf {
break
}
if r.brackets == 0 && (char == sp || char == listEnd) {
break
}
if char == respCodeEnd {
if r.brackets == 0 {
if r.inRespCode {
break
} else {
return nil, newParseError("atom contains bad brackets nesting")
}
}
r.brackets--
}
if char == respCodeStart {
r.brackets++
}
atom += string(char)
}
r.UnreadRune()
if atom == nilAtom {
return nil, nil
}
return atom, nil
}
func (r *Reader) ReadLiteral() (Literal, error) {
char, _, err := r.ReadRune()
if err != nil {
return nil, err
} else if char != literalStart {
return nil, newParseError("literal string doesn't start with an open brace")
}
lstr, err := r.ReadString(byte(literalEnd))
if err != nil {
return nil, err
}
lstr = trimSuffix(lstr, literalEnd)
nonSync := strings.HasSuffix(lstr, "+")
if nonSync {
lstr = trimSuffix(lstr, '+')
}
n, err := strconv.ParseUint(lstr, 10, 32)
if err != nil {
return nil, newParseError("cannot parse literal length: " + err.Error())
}
if r.MaxLiteralSize > 0 && uint32(n) > r.MaxLiteralSize {
return nil, newParseError("literal exceeding maximum size")
}
if err := r.ReadCrlf(); err != nil {
return nil, err
}
// Send continuation request if necessary
if r.continues != nil && !nonSync {
r.continues <- true
}
// Read literal
b := make([]byte, n)
if _, err := io.ReadFull(r, b); err != nil {
return nil, err
}
return bytes.NewBuffer(b), nil
}
func (r *Reader) ReadQuotedString() (string, error) {
if char, _, err := r.ReadRune(); err != nil {
return "", err
} else if char != dquote {
return "", newParseError("quoted string doesn't start with a double quote")
}
var buf bytes.Buffer
var escaped bool
for {
char, _, err := r.ReadRune()
if err != nil {
return "", err
}
if char == '\\' && !escaped {
escaped = true
} else {
if char == cr || char == lf {
r.UnreadRune()
return "", newParseError("CR or LF not allowed in quoted string")
}
if char == dquote && !escaped {
break
}
if !strings.ContainsRune(quotedSpecials, char) && escaped {
return "", newParseError("quoted string cannot contain backslash followed by a non-quoted-specials char")
}
buf.WriteRune(char)
escaped = false
}
}
return buf.String(), nil
}
func (r *Reader) ReadFields() (fields []interface{}, err error) {
var char rune
for {
if char, _, err = r.ReadRune(); err != nil {
return
}
if err = r.UnreadRune(); err != nil {
return
}
var field interface{}
ok := true
switch char {
case literalStart:
field, err = r.ReadLiteral()
case dquote:
field, err = r.ReadQuotedString()
case listStart:
field, err = r.ReadList()
case listEnd:
ok = false
case cr:
return
default:
field, err = r.ReadAtom()
}
if err != nil {
return
}
if ok {
fields = append(fields, field)
}
if char, _, err = r.ReadRune(); err != nil {
return
}
if char == cr || char == lf || char == listEnd || char == respCodeEnd {
if char == cr || char == lf {
r.UnreadRune()
}
return
}
if char == listStart {
r.UnreadRune()
continue
}
if char != sp {
err = newParseError("fields are not separated by a space")
return
}
}
}
func (r *Reader) ReadList() (fields []interface{}, err error) {
char, _, err := r.ReadRune()
if err != nil {
return
}
if char != listStart {
err = newParseError("list doesn't start with an open parenthesis")
return
}
fields, err = r.ReadFields()
if err != nil {
return
}
r.UnreadRune()
if char, _, err = r.ReadRune(); err != nil {
return
}
if char != listEnd {
err = newParseError("list doesn't end with a close parenthesis")
}
return
}
func (r *Reader) ReadLine() (fields []interface{}, err error) {
fields, err = r.ReadFields()
if err != nil {
return
}
r.UnreadRune()
err = r.ReadCrlf()
return
}
func (r *Reader) ReadRespCode() (code StatusRespCode, fields []interface{}, err error) {
char, _, err := r.ReadRune()
if err != nil {
return
}
if char != respCodeStart {
err = newParseError("response code doesn't start with an open bracket")
return
}
r.inRespCode = true
fields, err = r.ReadFields()
r.inRespCode = false
if err != nil {
return
}
if len(fields) == 0 {
err = newParseError("response code doesn't contain any field")
return
}
codeStr, ok := fields[0].(string)
if !ok {
err = newParseError("response code doesn't start with a string atom")
return
}
if codeStr == "" {
err = newParseError("response code is empty")
return
}
code = StatusRespCode(strings.ToUpper(codeStr))
fields = fields[1:]
r.UnreadRune()
char, _, err = r.ReadRune()
if err != nil {
return
}
if char != respCodeEnd {
err = newParseError("response code doesn't end with a close bracket")
}
return
}
func (r *Reader) ReadInfo() (info string, err error) {
info, err = r.ReadString(byte(lf))
if err != nil {
return
}
info = strings.TrimSuffix(info, string(lf))
info = strings.TrimSuffix(info, string(cr))
info = strings.TrimLeft(info, " ")
return
}
func NewReader(r reader) *Reader {
return &Reader{reader: r}
}
func NewServerReader(r reader, continues chan<- bool) *Reader {
return &Reader{reader: r, continues: continues}
}
type Parser interface {
Parse(fields []interface{}) error
}

View File

@@ -1,181 +0,0 @@
package imap
import (
"strings"
)
// Resp is an IMAP response. It is either a *DataResp, a
// *ContinuationReq or a *StatusResp.
type Resp interface {
resp()
}
// ReadResp reads a single response from a Reader.
func ReadResp(r *Reader) (Resp, error) {
atom, err := r.ReadAtom()
if err != nil {
return nil, err
}
tag, ok := atom.(string)
if !ok {
return nil, newParseError("response tag is not an atom")
}
if tag == "+" {
if err := r.ReadSp(); err != nil {
r.UnreadRune()
}
resp := &ContinuationReq{}
resp.Info, err = r.ReadInfo()
if err != nil {
return nil, err
}
return resp, nil
}
if err := r.ReadSp(); err != nil {
return nil, err
}
// Can be either data or status
// Try to parse a status
var fields []interface{}
if atom, err := r.ReadAtom(); err == nil {
fields = append(fields, atom)
if err := r.ReadSp(); err == nil {
if name, ok := atom.(string); ok {
status := StatusRespType(name)
switch status {
case StatusRespOk, StatusRespNo, StatusRespBad, StatusRespPreauth, StatusRespBye:
resp := &StatusResp{
Tag: tag,
Type: status,
}
char, _, err := r.ReadRune()
if err != nil {
return nil, err
}
r.UnreadRune()
if char == '[' {
// Contains code & arguments
resp.Code, resp.Arguments, err = r.ReadRespCode()
if err != nil {
return nil, err
}
}
resp.Info, err = r.ReadInfo()
if err != nil {
return nil, err
}
return resp, nil
}
}
} else {
r.UnreadRune()
}
} else {
r.UnreadRune()
}
// Not a status so it's data
resp := &DataResp{Tag: tag}
var remaining []interface{}
remaining, err = r.ReadLine()
if err != nil {
return nil, err
}
resp.Fields = append(fields, remaining...)
return resp, nil
}
// DataResp is an IMAP response containing data.
type DataResp struct {
// The response tag. Can be either "" for untagged responses, "+" for continuation
// requests or a previous command's tag.
Tag string
// The parsed response fields.
Fields []interface{}
}
// NewUntaggedResp creates a new untagged response.
func NewUntaggedResp(fields []interface{}) *DataResp {
return &DataResp{
Tag: "*",
Fields: fields,
}
}
func (r *DataResp) resp() {}
func (r *DataResp) WriteTo(w *Writer) error {
tag := RawString(r.Tag)
if tag == "" {
tag = RawString("*")
}
fields := []interface{}{RawString(tag)}
fields = append(fields, r.Fields...)
return w.writeLine(fields...)
}
// ContinuationReq is a continuation request response.
type ContinuationReq struct {
// The info message sent with the continuation request.
Info string
}
func (r *ContinuationReq) resp() {}
func (r *ContinuationReq) WriteTo(w *Writer) error {
if err := w.writeString("+"); err != nil {
return err
}
if r.Info != "" {
if err := w.writeString(string(sp) + r.Info); err != nil {
return err
}
}
return w.writeCrlf()
}
// ParseNamedResp attempts to parse a named data response.
func ParseNamedResp(resp Resp) (name string, fields []interface{}, ok bool) {
data, ok := resp.(*DataResp)
if !ok || len(data.Fields) == 0 {
return
}
// Some responses (namely EXISTS and RECENT) are formatted like so:
// [num] [name] [...]
// Which is fucking stupid. But we handle that here by checking if the
// response name is a number and then rearranging it.
if len(data.Fields) > 1 {
name, ok := data.Fields[1].(string)
if ok {
if _, err := ParseNumber(data.Fields[0]); err == nil {
fields := []interface{}{data.Fields[0]}
fields = append(fields, data.Fields[2:]...)
return strings.ToUpper(name), fields, true
}
}
}
// IMAP commands are formatted like this:
// [name] [...]
name, ok = data.Fields[0].(string)
if !ok {
return
}
return strings.ToUpper(name), data.Fields[1:], true
}

View File

@@ -1,61 +0,0 @@
package responses
import (
"encoding/base64"
"github.com/emersion/go-imap"
"github.com/emersion/go-sasl"
)
// An AUTHENTICATE response.
type Authenticate struct {
Mechanism sasl.Client
InitialResponse []byte
RepliesCh chan []byte
}
// Implements
func (r *Authenticate) Replies() <-chan []byte {
return r.RepliesCh
}
func (r *Authenticate) writeLine(l string) error {
r.RepliesCh <- []byte(l + "\r\n")
return nil
}
func (r *Authenticate) cancel() error {
return r.writeLine("*")
}
func (r *Authenticate) Handle(resp imap.Resp) error {
cont, ok := resp.(*imap.ContinuationReq)
if !ok {
return ErrUnhandled
}
// Empty challenge, send initial response as stated in RFC 2222 section 5.1
if cont.Info == "" && r.InitialResponse != nil {
encoded := base64.StdEncoding.EncodeToString(r.InitialResponse)
if err := r.writeLine(encoded); err != nil {
return err
}
r.InitialResponse = nil
return nil
}
challenge, err := base64.StdEncoding.DecodeString(cont.Info)
if err != nil {
r.cancel()
return err
}
reply, err := r.Mechanism.Next(challenge)
if err != nil {
r.cancel()
return err
}
encoded := base64.StdEncoding.EncodeToString(reply)
return r.writeLine(encoded)
}

View File

@@ -1,20 +0,0 @@
package responses
import (
"github.com/emersion/go-imap"
)
// A CAPABILITY response.
// See RFC 3501 section 7.2.1
type Capability struct {
Caps []string
}
func (r *Capability) WriteTo(w *imap.Writer) error {
fields := []interface{}{imap.RawString("CAPABILITY")}
for _, cap := range r.Caps {
fields = append(fields, imap.RawString(cap))
}
return imap.NewUntaggedResp(fields).WriteTo(w)
}

View File

@@ -1,33 +0,0 @@
package responses
import (
"github.com/emersion/go-imap"
)
// An ENABLED response, defined in RFC 5161 section 3.2.
type Enabled struct {
Caps []string
}
func (r *Enabled) Handle(resp imap.Resp) error {
name, fields, ok := imap.ParseNamedResp(resp)
if !ok || name != "ENABLED" {
return ErrUnhandled
}
if caps, err := imap.ParseStringList(fields); err != nil {
return err
} else {
r.Caps = append(r.Caps, caps...)
}
return nil
}
func (r *Enabled) WriteTo(w *imap.Writer) error {
fields := []interface{}{imap.RawString("ENABLED")}
for _, cap := range r.Caps {
fields = append(fields, imap.RawString(cap))
}
return imap.NewUntaggedResp(fields).WriteTo(w)
}

View File

@@ -1,43 +0,0 @@
package responses
import (
"github.com/emersion/go-imap"
)
const expungeName = "EXPUNGE"
// An EXPUNGE response.
// See RFC 3501 section 7.4.1
type Expunge struct {
SeqNums chan uint32
}
func (r *Expunge) Handle(resp imap.Resp) error {
name, fields, ok := imap.ParseNamedResp(resp)
if !ok || name != expungeName {
return ErrUnhandled
}
if len(fields) == 0 {
return errNotEnoughFields
}
seqNum, err := imap.ParseNumber(fields[0])
if err != nil {
return err
}
r.SeqNums <- seqNum
return nil
}
func (r *Expunge) WriteTo(w *imap.Writer) error {
for seqNum := range r.SeqNums {
resp := imap.NewUntaggedResp([]interface{}{seqNum, imap.RawString(expungeName)})
if err := resp.WriteTo(w); err != nil {
return err
}
}
return nil
}

View File

@@ -1,70 +0,0 @@
package responses
import (
"github.com/emersion/go-imap"
)
const fetchName = "FETCH"
// A FETCH response.
// See RFC 3501 section 7.4.2
type Fetch struct {
Messages chan *imap.Message
SeqSet *imap.SeqSet
Uid bool
}
func (r *Fetch) Handle(resp imap.Resp) error {
name, fields, ok := imap.ParseNamedResp(resp)
if !ok || name != fetchName {
return ErrUnhandled
} else if len(fields) < 1 {
return errNotEnoughFields
}
seqNum, err := imap.ParseNumber(fields[0])
if err != nil {
return err
}
msgFields, _ := fields[1].([]interface{})
msg := &imap.Message{SeqNum: seqNum}
if err := msg.Parse(msgFields); err != nil {
return err
}
if r.Uid && msg.Uid == 0 {
// we requested UIDs and got a message without one --> unilateral update --> ignore
return ErrUnhandled
}
var num uint32
if r.Uid {
num = msg.Uid
} else {
num = seqNum
}
// Check whether we obtained a result we requested with our SeqSet
// If the result is not contained in our SeqSet we have to handle an additional special case:
// In case we requested UIDs with a dynamic sequence (i.e. * or n:*) and the maximum UID of the mailbox
// is less then our n, the server will supply us with the max UID (cf. RFC 3501 §6.4.8 and §9 `seq-range`).
// Thus, such a result is correct and has to be returned by us.
if !r.SeqSet.Contains(num) && (!r.Uid || !r.SeqSet.Dynamic()) {
return ErrUnhandled
}
r.Messages <- msg
return nil
}
func (r *Fetch) WriteTo(w *imap.Writer) error {
var err error
for msg := range r.Messages {
resp := imap.NewUntaggedResp([]interface{}{msg.SeqNum, imap.RawString(fetchName), msg.Format()})
if err == nil {
err = resp.WriteTo(w)
}
}
return err
}

View File

@@ -1,38 +0,0 @@
package responses
import (
"github.com/emersion/go-imap"
)
// An IDLE response.
type Idle struct {
RepliesCh chan []byte
Stop <-chan struct{}
gotContinuationReq bool
}
func (r *Idle) Replies() <-chan []byte {
return r.RepliesCh
}
func (r *Idle) stop() {
r.RepliesCh <- []byte("DONE\r\n")
}
func (r *Idle) Handle(resp imap.Resp) error {
// Wait for a continuation request
if _, ok := resp.(*imap.ContinuationReq); ok && !r.gotContinuationReq {
r.gotContinuationReq = true
// We got a continuation request, wait for r.Stop to be closed
go func() {
<-r.Stop
r.stop()
}()
return nil
}
return ErrUnhandled
}

View File

@@ -1,57 +0,0 @@
package responses
import (
"github.com/emersion/go-imap"
)
const (
listName = "LIST"
lsubName = "LSUB"
)
// A LIST response.
// If Subscribed is set to true, LSUB will be used instead.
// See RFC 3501 section 7.2.2
type List struct {
Mailboxes chan *imap.MailboxInfo
Subscribed bool
}
func (r *List) Name() string {
if r.Subscribed {
return lsubName
} else {
return listName
}
}
func (r *List) Handle(resp imap.Resp) error {
name, fields, ok := imap.ParseNamedResp(resp)
if !ok || name != r.Name() {
return ErrUnhandled
}
mbox := &imap.MailboxInfo{}
if err := mbox.Parse(fields); err != nil {
return err
}
r.Mailboxes <- mbox
return nil
}
func (r *List) WriteTo(w *imap.Writer) error {
respName := r.Name()
for mbox := range r.Mailboxes {
fields := []interface{}{imap.RawString(respName)}
fields = append(fields, mbox.Format()...)
resp := imap.NewUntaggedResp(fields)
if err := resp.WriteTo(w); err != nil {
return err
}
}
return nil
}

View File

@@ -1,35 +0,0 @@
// IMAP responses defined in RFC 3501.
package responses
import (
"errors"
"github.com/emersion/go-imap"
)
// ErrUnhandled is used when a response hasn't been handled.
var ErrUnhandled = errors.New("imap: unhandled response")
var errNotEnoughFields = errors.New("imap: not enough fields in response")
// Handler handles responses.
type Handler interface {
// Handle processes a response. If the response cannot be processed,
// ErrUnhandledResp must be returned.
Handle(resp imap.Resp) error
}
// HandlerFunc is a function that handles responses.
type HandlerFunc func(resp imap.Resp) error
// Handle implements Handler.
func (f HandlerFunc) Handle(resp imap.Resp) error {
return f(resp)
}
// Replier is a Handler that needs to send raw data (for instance
// AUTHENTICATE).
type Replier interface {
Handler
Replies() <-chan []byte
}

View File

@@ -1,41 +0,0 @@
package responses
import (
"github.com/emersion/go-imap"
)
const searchName = "SEARCH"
// A SEARCH response.
// See RFC 3501 section 7.2.5
type Search struct {
Ids []uint32
}
func (r *Search) Handle(resp imap.Resp) error {
name, fields, ok := imap.ParseNamedResp(resp)
if !ok || name != searchName {
return ErrUnhandled
}
r.Ids = make([]uint32, len(fields))
for i, f := range fields {
if id, err := imap.ParseNumber(f); err != nil {
return err
} else {
r.Ids[i] = id
}
}
return nil
}
func (r *Search) WriteTo(w *imap.Writer) (err error) {
fields := []interface{}{imap.RawString(searchName)}
for _, id := range r.Ids {
fields = append(fields, id)
}
resp := imap.NewUntaggedResp(fields)
return resp.WriteTo(w)
}

View File

@@ -1,142 +0,0 @@
package responses
import (
"fmt"
"github.com/emersion/go-imap"
)
// A SELECT response.
type Select struct {
Mailbox *imap.MailboxStatus
}
func (r *Select) Handle(resp imap.Resp) error {
if r.Mailbox == nil {
r.Mailbox = &imap.MailboxStatus{Items: make(map[imap.StatusItem]interface{})}
}
mbox := r.Mailbox
switch resp := resp.(type) {
case *imap.DataResp:
name, fields, ok := imap.ParseNamedResp(resp)
if !ok || name != "FLAGS" {
return ErrUnhandled
} else if len(fields) < 1 {
return errNotEnoughFields
}
flags, _ := fields[0].([]interface{})
mbox.Flags, _ = imap.ParseStringList(flags)
case *imap.StatusResp:
if len(resp.Arguments) < 1 {
return ErrUnhandled
}
var item imap.StatusItem
switch resp.Code {
case "UNSEEN":
mbox.UnseenSeqNum, _ = imap.ParseNumber(resp.Arguments[0])
case "PERMANENTFLAGS":
flags, _ := resp.Arguments[0].([]interface{})
mbox.PermanentFlags, _ = imap.ParseStringList(flags)
case "UIDNEXT":
mbox.UidNext, _ = imap.ParseNumber(resp.Arguments[0])
item = imap.StatusUidNext
case "UIDVALIDITY":
mbox.UidValidity, _ = imap.ParseNumber(resp.Arguments[0])
item = imap.StatusUidValidity
default:
return ErrUnhandled
}
if item != "" {
mbox.ItemsLocker.Lock()
mbox.Items[item] = nil
mbox.ItemsLocker.Unlock()
}
default:
return ErrUnhandled
}
return nil
}
func (r *Select) WriteTo(w *imap.Writer) error {
mbox := r.Mailbox
if mbox.Flags != nil {
flags := make([]interface{}, len(mbox.Flags))
for i, f := range mbox.Flags {
flags[i] = imap.RawString(f)
}
res := imap.NewUntaggedResp([]interface{}{imap.RawString("FLAGS"), flags})
if err := res.WriteTo(w); err != nil {
return err
}
}
if mbox.PermanentFlags != nil {
flags := make([]interface{}, len(mbox.PermanentFlags))
for i, f := range mbox.PermanentFlags {
flags[i] = imap.RawString(f)
}
statusRes := &imap.StatusResp{
Type: imap.StatusRespOk,
Code: imap.CodePermanentFlags,
Arguments: []interface{}{flags},
Info: "Flags permitted.",
}
if err := statusRes.WriteTo(w); err != nil {
return err
}
}
if mbox.UnseenSeqNum > 0 {
statusRes := &imap.StatusResp{
Type: imap.StatusRespOk,
Code: imap.CodeUnseen,
Arguments: []interface{}{mbox.UnseenSeqNum},
Info: fmt.Sprintf("Message %d is first unseen", mbox.UnseenSeqNum),
}
if err := statusRes.WriteTo(w); err != nil {
return err
}
}
for k := range r.Mailbox.Items {
switch k {
case imap.StatusMessages:
res := imap.NewUntaggedResp([]interface{}{mbox.Messages, imap.RawString("EXISTS")})
if err := res.WriteTo(w); err != nil {
return err
}
case imap.StatusRecent:
res := imap.NewUntaggedResp([]interface{}{mbox.Recent, imap.RawString("RECENT")})
if err := res.WriteTo(w); err != nil {
return err
}
case imap.StatusUidNext:
statusRes := &imap.StatusResp{
Type: imap.StatusRespOk,
Code: imap.CodeUidNext,
Arguments: []interface{}{mbox.UidNext},
Info: "Predicted next UID",
}
if err := statusRes.WriteTo(w); err != nil {
return err
}
case imap.StatusUidValidity:
statusRes := &imap.StatusResp{
Type: imap.StatusRespOk,
Code: imap.CodeUidValidity,
Arguments: []interface{}{mbox.UidValidity},
Info: "UIDs valid",
}
if err := statusRes.WriteTo(w); err != nil {
return err
}
}
}
return nil
}

View File

@@ -1,53 +0,0 @@
package responses
import (
"errors"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/utf7"
)
const statusName = "STATUS"
// A STATUS response.
// See RFC 3501 section 7.2.4
type Status struct {
Mailbox *imap.MailboxStatus
}
func (r *Status) Handle(resp imap.Resp) error {
if r.Mailbox == nil {
r.Mailbox = &imap.MailboxStatus{}
}
mbox := r.Mailbox
name, fields, ok := imap.ParseNamedResp(resp)
if !ok || name != statusName {
return ErrUnhandled
} else if len(fields) < 2 {
return errNotEnoughFields
}
if name, err := imap.ParseString(fields[0]); err != nil {
return err
} else if name, err := utf7.Encoding.NewDecoder().String(name); err != nil {
return err
} else {
mbox.Name = imap.CanonicalMailboxName(name)
}
var items []interface{}
if items, ok = fields[1].([]interface{}); !ok {
return errors.New("STATUS response expects a list as second argument")
}
mbox.Items = nil
return mbox.Parse(items)
}
func (r *Status) WriteTo(w *imap.Writer) error {
mbox := r.Mailbox
name, _ := utf7.Encoding.NewEncoder().String(mbox.Name)
fields := []interface{}{imap.RawString(statusName), imap.FormatMailboxName(name), mbox.Format()}
return imap.NewUntaggedResp(fields).WriteTo(w)
}

View File

@@ -1,371 +0,0 @@
package imap
import (
"errors"
"fmt"
"io"
"net/textproto"
"strings"
"time"
)
func maybeString(mystery interface{}) string {
if s, ok := mystery.(string); ok {
return s
}
return ""
}
func convertField(f interface{}, charsetReader func(io.Reader) io.Reader) string {
// An IMAP string contains only 7-bit data, no need to decode it
if s, ok := f.(string); ok {
return s
}
// If no charset is provided, getting directly the string is faster
if charsetReader == nil {
if stringer, ok := f.(fmt.Stringer); ok {
return stringer.String()
}
}
// Not a string, it must be a literal
l, ok := f.(Literal)
if !ok {
return ""
}
var r io.Reader = l
if charsetReader != nil {
if dec := charsetReader(r); dec != nil {
r = dec
}
}
b := make([]byte, l.Len())
if _, err := io.ReadFull(r, b); err != nil {
return ""
}
return string(b)
}
func popSearchField(fields []interface{}) (interface{}, []interface{}, error) {
if len(fields) == 0 {
return nil, nil, errors.New("imap: no enough fields for search key")
}
return fields[0], fields[1:], nil
}
// SearchCriteria is a search criteria. A message matches the criteria if and
// only if it matches each one of its fields.
type SearchCriteria struct {
SeqNum *SeqSet // Sequence number is in sequence set
Uid *SeqSet // UID is in sequence set
// Time and timezone are ignored
Since time.Time // Internal date is since this date
Before time.Time // Internal date is before this date
SentSince time.Time // Date header field is since this date
SentBefore time.Time // Date header field is before this date
Header textproto.MIMEHeader // Each header field value is present
Body []string // Each string is in the body
Text []string // Each string is in the text (header + body)
WithFlags []string // Each flag is present
WithoutFlags []string // Each flag is not present
Larger uint32 // Size is larger than this number
Smaller uint32 // Size is smaller than this number
Not []*SearchCriteria // Each criteria doesn't match
Or [][2]*SearchCriteria // Each criteria pair has at least one match of two
}
// NewSearchCriteria creates a new search criteria.
func NewSearchCriteria() *SearchCriteria {
return &SearchCriteria{Header: make(textproto.MIMEHeader)}
}
func (c *SearchCriteria) parseField(fields []interface{}, charsetReader func(io.Reader) io.Reader) ([]interface{}, error) {
if len(fields) == 0 {
return nil, nil
}
f := fields[0]
fields = fields[1:]
if subfields, ok := f.([]interface{}); ok {
return fields, c.ParseWithCharset(subfields, charsetReader)
}
key, ok := f.(string)
if !ok {
return nil, fmt.Errorf("imap: invalid search criteria field type: %T", f)
}
key = strings.ToUpper(key)
var err error
switch key {
case "ALL":
// Nothing to do
case "ANSWERED", "DELETED", "DRAFT", "FLAGGED", "RECENT", "SEEN":
c.WithFlags = append(c.WithFlags, CanonicalFlag("\\"+key))
case "BCC", "CC", "FROM", "SUBJECT", "TO":
if f, fields, err = popSearchField(fields); err != nil {
return nil, err
}
if c.Header == nil {
c.Header = make(textproto.MIMEHeader)
}
c.Header.Add(key, convertField(f, charsetReader))
case "BEFORE":
if f, fields, err = popSearchField(fields); err != nil {
return nil, err
} else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil {
return nil, err
} else if c.Before.IsZero() || t.Before(c.Before) {
c.Before = t
}
case "BODY":
if f, fields, err = popSearchField(fields); err != nil {
return nil, err
} else {
c.Body = append(c.Body, convertField(f, charsetReader))
}
case "HEADER":
var f1, f2 interface{}
if f1, fields, err = popSearchField(fields); err != nil {
return nil, err
} else if f2, fields, err = popSearchField(fields); err != nil {
return nil, err
} else {
if c.Header == nil {
c.Header = make(textproto.MIMEHeader)
}
c.Header.Add(maybeString(f1), convertField(f2, charsetReader))
}
case "KEYWORD":
if f, fields, err = popSearchField(fields); err != nil {
return nil, err
} else {
c.WithFlags = append(c.WithFlags, CanonicalFlag(maybeString(f)))
}
case "LARGER":
if f, fields, err = popSearchField(fields); err != nil {
return nil, err
} else if n, err := ParseNumber(f); err != nil {
return nil, err
} else if c.Larger == 0 || n > c.Larger {
c.Larger = n
}
case "NEW":
c.WithFlags = append(c.WithFlags, RecentFlag)
c.WithoutFlags = append(c.WithoutFlags, SeenFlag)
case "NOT":
not := new(SearchCriteria)
if fields, err = not.parseField(fields, charsetReader); err != nil {
return nil, err
}
c.Not = append(c.Not, not)
case "OLD":
c.WithoutFlags = append(c.WithoutFlags, RecentFlag)
case "ON":
if f, fields, err = popSearchField(fields); err != nil {
return nil, err
} else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil {
return nil, err
} else {
c.Since = t
c.Before = t.Add(24 * time.Hour)
}
case "OR":
c1, c2 := new(SearchCriteria), new(SearchCriteria)
if fields, err = c1.parseField(fields, charsetReader); err != nil {
return nil, err
} else if fields, err = c2.parseField(fields, charsetReader); err != nil {
return nil, err
}
c.Or = append(c.Or, [2]*SearchCriteria{c1, c2})
case "SENTBEFORE":
if f, fields, err = popSearchField(fields); err != nil {
return nil, err
} else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil {
return nil, err
} else if c.SentBefore.IsZero() || t.Before(c.SentBefore) {
c.SentBefore = t
}
case "SENTON":
if f, fields, err = popSearchField(fields); err != nil {
return nil, err
} else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil {
return nil, err
} else {
c.SentSince = t
c.SentBefore = t.Add(24 * time.Hour)
}
case "SENTSINCE":
if f, fields, err = popSearchField(fields); err != nil {
return nil, err
} else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil {
return nil, err
} else if c.SentSince.IsZero() || t.After(c.SentSince) {
c.SentSince = t
}
case "SINCE":
if f, fields, err = popSearchField(fields); err != nil {
return nil, err
} else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil {
return nil, err
} else if c.Since.IsZero() || t.After(c.Since) {
c.Since = t
}
case "SMALLER":
if f, fields, err = popSearchField(fields); err != nil {
return nil, err
} else if n, err := ParseNumber(f); err != nil {
return nil, err
} else if c.Smaller == 0 || n < c.Smaller {
c.Smaller = n
}
case "TEXT":
if f, fields, err = popSearchField(fields); err != nil {
return nil, err
} else {
c.Text = append(c.Text, convertField(f, charsetReader))
}
case "UID":
if f, fields, err = popSearchField(fields); err != nil {
return nil, err
} else if c.Uid, err = ParseSeqSet(maybeString(f)); err != nil {
return nil, err
}
case "UNANSWERED", "UNDELETED", "UNDRAFT", "UNFLAGGED", "UNSEEN":
unflag := strings.TrimPrefix(key, "UN")
c.WithoutFlags = append(c.WithoutFlags, CanonicalFlag("\\"+unflag))
case "UNKEYWORD":
if f, fields, err = popSearchField(fields); err != nil {
return nil, err
} else {
c.WithoutFlags = append(c.WithoutFlags, CanonicalFlag(maybeString(f)))
}
default: // Try to parse a sequence set
if c.SeqNum, err = ParseSeqSet(key); err != nil {
return nil, err
}
}
return fields, nil
}
// ParseWithCharset parses a search criteria from the provided fields.
// charsetReader is an optional function that converts from the fields charset
// to UTF-8.
func (c *SearchCriteria) ParseWithCharset(fields []interface{}, charsetReader func(io.Reader) io.Reader) error {
for len(fields) > 0 {
var err error
if fields, err = c.parseField(fields, charsetReader); err != nil {
return err
}
}
return nil
}
// Format formats search criteria to fields. UTF-8 is used.
func (c *SearchCriteria) Format() []interface{} {
var fields []interface{}
if c.SeqNum != nil {
fields = append(fields, c.SeqNum)
}
if c.Uid != nil {
fields = append(fields, RawString("UID"), c.Uid)
}
if !c.Since.IsZero() && !c.Before.IsZero() && c.Before.Sub(c.Since) == 24*time.Hour {
fields = append(fields, RawString("ON"), searchDate(c.Since))
} else {
if !c.Since.IsZero() {
fields = append(fields, RawString("SINCE"), searchDate(c.Since))
}
if !c.Before.IsZero() {
fields = append(fields, RawString("BEFORE"), searchDate(c.Before))
}
}
if !c.SentSince.IsZero() && !c.SentBefore.IsZero() && c.SentBefore.Sub(c.SentSince) == 24*time.Hour {
fields = append(fields, RawString("SENTON"), searchDate(c.SentSince))
} else {
if !c.SentSince.IsZero() {
fields = append(fields, RawString("SENTSINCE"), searchDate(c.SentSince))
}
if !c.SentBefore.IsZero() {
fields = append(fields, RawString("SENTBEFORE"), searchDate(c.SentBefore))
}
}
for key, values := range c.Header {
var prefields []interface{}
switch key {
case "Bcc", "Cc", "From", "Subject", "To":
prefields = []interface{}{RawString(strings.ToUpper(key))}
default:
prefields = []interface{}{RawString("HEADER"), key}
}
for _, value := range values {
fields = append(fields, prefields...)
fields = append(fields, value)
}
}
for _, value := range c.Body {
fields = append(fields, RawString("BODY"), value)
}
for _, value := range c.Text {
fields = append(fields, RawString("TEXT"), value)
}
for _, flag := range c.WithFlags {
var subfields []interface{}
switch flag {
case AnsweredFlag, DeletedFlag, DraftFlag, FlaggedFlag, RecentFlag, SeenFlag:
subfields = []interface{}{RawString(strings.ToUpper(strings.TrimPrefix(flag, "\\")))}
default:
subfields = []interface{}{RawString("KEYWORD"), RawString(flag)}
}
fields = append(fields, subfields...)
}
for _, flag := range c.WithoutFlags {
var subfields []interface{}
switch flag {
case AnsweredFlag, DeletedFlag, DraftFlag, FlaggedFlag, SeenFlag:
subfields = []interface{}{RawString("UN" + strings.ToUpper(strings.TrimPrefix(flag, "\\")))}
case RecentFlag:
subfields = []interface{}{RawString("OLD")}
default:
subfields = []interface{}{RawString("UNKEYWORD"), RawString(flag)}
}
fields = append(fields, subfields...)
}
if c.Larger > 0 {
fields = append(fields, RawString("LARGER"), c.Larger)
}
if c.Smaller > 0 {
fields = append(fields, RawString("SMALLER"), c.Smaller)
}
for _, not := range c.Not {
fields = append(fields, RawString("NOT"), not.Format())
}
for _, or := range c.Or {
fields = append(fields, RawString("OR"), or[0].Format(), or[1].Format())
}
// Not a single criteria given, add ALL criteria as fallback
if len(fields) == 0 {
fields = append(fields, RawString("ALL"))
}
return fields
}

View File

@@ -1,289 +0,0 @@
package imap
import (
"fmt"
"strconv"
"strings"
)
// ErrBadSeqSet is used to report problems with the format of a sequence set
// value.
type ErrBadSeqSet string
func (err ErrBadSeqSet) Error() string {
return fmt.Sprintf("imap: bad sequence set value %q", string(err))
}
// Seq represents a single seq-number or seq-range value (RFC 3501 ABNF). Values
// may be static (e.g. "1", "2:4") or dynamic (e.g. "*", "1:*"). A seq-number is
// represented by setting Start = Stop. Zero is used to represent "*", which is
// safe because seq-number uses nz-number rule. The order of values is always
// Start <= Stop, except when representing "n:*", where Start = n and Stop = 0.
type Seq struct {
Start, Stop uint32
}
// parseSeqNumber parses a single seq-number value (non-zero uint32 or "*").
func parseSeqNumber(v string) (uint32, error) {
if n, err := strconv.ParseUint(v, 10, 32); err == nil && v[0] != '0' {
return uint32(n), nil
} else if v == "*" {
return 0, nil
}
return 0, ErrBadSeqSet(v)
}
// parseSeq creates a new seq instance by parsing strings in the format "n" or
// "n:m", where n and/or m may be "*". An error is returned for invalid values.
func parseSeq(v string) (s Seq, err error) {
if sep := strings.IndexRune(v, ':'); sep < 0 {
s.Start, err = parseSeqNumber(v)
s.Stop = s.Start
return
} else if s.Start, err = parseSeqNumber(v[:sep]); err == nil {
if s.Stop, err = parseSeqNumber(v[sep+1:]); err == nil {
if (s.Stop < s.Start && s.Stop != 0) || s.Start == 0 {
s.Start, s.Stop = s.Stop, s.Start
}
return
}
}
return s, ErrBadSeqSet(v)
}
// Contains returns true if the seq-number q is contained in sequence value s.
// The dynamic value "*" contains only other "*" values, the dynamic range "n:*"
// contains "*" and all numbers >= n.
func (s Seq) Contains(q uint32) bool {
if q == 0 {
return s.Stop == 0 // "*" is contained only in "*" and "n:*"
}
return s.Start != 0 && s.Start <= q && (q <= s.Stop || s.Stop == 0)
}
// Less returns true if s precedes and does not contain seq-number q.
func (s Seq) Less(q uint32) bool {
return (s.Stop < q || q == 0) && s.Stop != 0
}
// Merge combines sequence values s and t into a single union if the two
// intersect or one is a superset of the other. The order of s and t does not
// matter. If the values cannot be merged, s is returned unmodified and ok is
// set to false.
func (s Seq) Merge(t Seq) (union Seq, ok bool) {
if union = s; s == t {
ok = true
return
}
if s.Start != 0 && t.Start != 0 {
// s and t are any combination of "n", "n:m", or "n:*"
if s.Start > t.Start {
s, t = t, s
}
// s starts at or before t, check where it ends
if (s.Stop >= t.Stop && t.Stop != 0) || s.Stop == 0 {
return s, true // s is a superset of t
}
// s is "n" or "n:m", if m == ^uint32(0) then t is "n:*"
if s.Stop+1 >= t.Start || s.Stop == ^uint32(0) {
return Seq{s.Start, t.Stop}, true // s intersects or touches t
}
return
}
// exactly one of s and t is "*"
if s.Start == 0 {
if t.Stop == 0 {
return t, true // s is "*", t is "n:*"
}
} else if s.Stop == 0 {
return s, true // s is "n:*", t is "*"
}
return
}
// String returns sequence value s as a seq-number or seq-range string.
func (s Seq) String() string {
if s.Start == s.Stop {
if s.Start == 0 {
return "*"
}
return strconv.FormatUint(uint64(s.Start), 10)
}
b := strconv.AppendUint(make([]byte, 0, 24), uint64(s.Start), 10)
if s.Stop == 0 {
return string(append(b, ':', '*'))
}
return string(strconv.AppendUint(append(b, ':'), uint64(s.Stop), 10))
}
// SeqSet is used to represent a set of message sequence numbers or UIDs (see
// sequence-set ABNF rule). The zero value is an empty set.
type SeqSet struct {
Set []Seq
}
// ParseSeqSet returns a new SeqSet instance after parsing the set string.
func ParseSeqSet(set string) (s *SeqSet, err error) {
s = new(SeqSet)
return s, s.Add(set)
}
// Add inserts new sequence values into the set. The string format is described
// by RFC 3501 sequence-set ABNF rule. If an error is encountered, all values
// inserted successfully prior to the error remain in the set.
func (s *SeqSet) Add(set string) error {
for _, sv := range strings.Split(set, ",") {
v, err := parseSeq(sv)
if err != nil {
return err
}
s.insert(v)
}
return nil
}
// AddNum inserts new sequence numbers into the set. The value 0 represents "*".
func (s *SeqSet) AddNum(q ...uint32) {
for _, v := range q {
s.insert(Seq{v, v})
}
}
// AddRange inserts a new sequence range into the set.
func (s *SeqSet) AddRange(Start, Stop uint32) {
if (Stop < Start && Stop != 0) || Start == 0 {
s.insert(Seq{Stop, Start})
} else {
s.insert(Seq{Start, Stop})
}
}
// AddSet inserts all values from t into s.
func (s *SeqSet) AddSet(t *SeqSet) {
for _, v := range t.Set {
s.insert(v)
}
}
// Clear removes all values from the set.
func (s *SeqSet) Clear() {
s.Set = s.Set[:0]
}
// Empty returns true if the sequence set does not contain any values.
func (s SeqSet) Empty() bool {
return len(s.Set) == 0
}
// Dynamic returns true if the set contains "*" or "n:*" values.
func (s SeqSet) Dynamic() bool {
return len(s.Set) > 0 && s.Set[len(s.Set)-1].Stop == 0
}
// Contains returns true if the non-zero sequence number or UID q is contained
// in the set. The dynamic range "n:*" contains all q >= n. It is the caller's
// responsibility to handle the special case where q is the maximum UID in the
// mailbox and q < n (i.e. the set cannot match UIDs against "*:n" or "*" since
// it doesn't know what the maximum value is).
func (s SeqSet) Contains(q uint32) bool {
if _, ok := s.search(q); ok {
return q != 0
}
return false
}
// String returns a sorted representation of all contained sequence values.
func (s SeqSet) String() string {
if len(s.Set) == 0 {
return ""
}
b := make([]byte, 0, 64)
for _, v := range s.Set {
b = append(b, ',')
if v.Start == 0 {
b = append(b, '*')
continue
}
b = strconv.AppendUint(b, uint64(v.Start), 10)
if v.Start != v.Stop {
if v.Stop == 0 {
b = append(b, ':', '*')
continue
}
b = strconv.AppendUint(append(b, ':'), uint64(v.Stop), 10)
}
}
return string(b[1:])
}
// insert adds sequence value v to the set.
func (s *SeqSet) insert(v Seq) {
i, _ := s.search(v.Start)
merged := false
if i > 0 {
// try merging with the preceding entry (e.g. "1,4".insert(2), i == 1)
s.Set[i-1], merged = s.Set[i-1].Merge(v)
}
if i == len(s.Set) {
// v was either merged with the last entry or needs to be appended
if !merged {
s.insertAt(i, v)
}
return
} else if merged {
i--
} else if s.Set[i], merged = s.Set[i].Merge(v); !merged {
s.insertAt(i, v) // insert in the middle (e.g. "1,5".insert(3), i == 1)
return
}
// v was merged with s.Set[i], continue trying to merge until the end
for j := i + 1; j < len(s.Set); j++ {
if s.Set[i], merged = s.Set[i].Merge(s.Set[j]); !merged {
if j > i+1 {
// cut out all entries between i and j that were merged
s.Set = append(s.Set[:i+1], s.Set[j:]...)
}
return
}
}
// everything after s.Set[i] was merged
s.Set = s.Set[:i+1]
}
// insertAt inserts a new sequence value v at index i, resizing s.Set as needed.
func (s *SeqSet) insertAt(i int, v Seq) {
if n := len(s.Set); i == n {
// insert at the end
s.Set = append(s.Set, v)
return
} else if n < cap(s.Set) {
// enough space, shift everything at and after i to the right
s.Set = s.Set[:n+1]
copy(s.Set[i+1:], s.Set[i:])
} else {
// allocate new slice and copy everything, n is at least 1
set := make([]Seq, n+1, n*2)
copy(set, s.Set[:i])
copy(set[i+1:], s.Set[i:])
s.Set = set
}
s.Set[i] = v
}
// search attempts to find the index of the sequence set value that contains q.
// If no values contain q, the returned index is the position where q should be
// inserted and ok is set to false.
func (s SeqSet) search(q uint32) (i int, ok bool) {
min, max := 0, len(s.Set)-1
for min < max {
if mid := (min + max) >> 1; s.Set[mid].Less(q) {
min = mid + 1
} else {
max = mid
}
}
if max < 0 || s.Set[min].Less(q) {
return len(s.Set), false // q is the new largest value
}
return min, s.Set[min].Contains(q)
}

View File

@@ -1,136 +0,0 @@
package imap
import (
"errors"
)
// A status response type.
type StatusRespType string
// Status response types defined in RFC 3501 section 7.1.
const (
// The OK response indicates an information message from the server. When
// tagged, it indicates successful completion of the associated command.
// The untagged form indicates an information-only message.
StatusRespOk StatusRespType = "OK"
// The NO response indicates an operational error message from the
// server. When tagged, it indicates unsuccessful completion of the
// associated command. The untagged form indicates a warning; the
// command can still complete successfully.
StatusRespNo StatusRespType = "NO"
// The BAD response indicates an error message from the server. When
// tagged, it reports a protocol-level error in the client's command;
// the tag indicates the command that caused the error. The untagged
// form indicates a protocol-level error for which the associated
// command can not be determined; it can also indicate an internal
// server failure.
StatusRespBad StatusRespType = "BAD"
// The PREAUTH response is always untagged, and is one of three
// possible greetings at connection startup. It indicates that the
// connection has already been authenticated by external means; thus
// no LOGIN command is needed.
StatusRespPreauth StatusRespType = "PREAUTH"
// The BYE response is always untagged, and indicates that the server
// is about to close the connection.
StatusRespBye StatusRespType = "BYE"
)
type StatusRespCode string
// Status response codes defined in RFC 3501 section 7.1.
const (
CodeAlert StatusRespCode = "ALERT"
CodeBadCharset StatusRespCode = "BADCHARSET"
CodeCapability StatusRespCode = "CAPABILITY"
CodeParse StatusRespCode = "PARSE"
CodePermanentFlags StatusRespCode = "PERMANENTFLAGS"
CodeReadOnly StatusRespCode = "READ-ONLY"
CodeReadWrite StatusRespCode = "READ-WRITE"
CodeTryCreate StatusRespCode = "TRYCREATE"
CodeUidNext StatusRespCode = "UIDNEXT"
CodeUidValidity StatusRespCode = "UIDVALIDITY"
CodeUnseen StatusRespCode = "UNSEEN"
)
// A status response.
// See RFC 3501 section 7.1
type StatusResp struct {
// The response tag. If empty, it defaults to *.
Tag string
// The status type.
Type StatusRespType
// The status code.
// See https://www.iana.org/assignments/imap-response-codes/imap-response-codes.xhtml
Code StatusRespCode
// Arguments provided with the status code.
Arguments []interface{}
// The status info.
Info string
}
func (r *StatusResp) resp() {}
// If this status is NO or BAD, returns an error with the status info.
// Otherwise, returns nil.
func (r *StatusResp) Err() error {
if r == nil {
// No status response, connection closed before we get one
return errors.New("imap: connection closed during command execution")
}
if r.Type == StatusRespNo || r.Type == StatusRespBad {
return errors.New(r.Info)
}
return nil
}
func (r *StatusResp) WriteTo(w *Writer) error {
tag := RawString(r.Tag)
if tag == "" {
tag = "*"
}
if err := w.writeFields([]interface{}{RawString(tag), RawString(r.Type)}); err != nil {
return err
}
if err := w.writeString(string(sp)); err != nil {
return err
}
if r.Code != "" {
if err := w.writeRespCode(r.Code, r.Arguments); err != nil {
return err
}
if err := w.writeString(string(sp)); err != nil {
return err
}
}
if err := w.writeString(r.Info); err != nil {
return err
}
return w.writeCrlf()
}
// ErrStatusResp can be returned by a server.Handler to replace the default status
// response. The response tag must be empty.
//
// To suppress default response, Resp should be set to nil.
type ErrStatusResp struct {
// Response to send instead of default.
Resp *StatusResp
}
func (err *ErrStatusResp) Error() string {
if err.Resp == nil {
return "imap: suppressed response"
}
return err.Resp.Info
}

View File

@@ -1,149 +0,0 @@
package utf7
import (
"errors"
"unicode/utf16"
"unicode/utf8"
"golang.org/x/text/transform"
)
// ErrInvalidUTF7 means that a transformer encountered invalid UTF-7.
var ErrInvalidUTF7 = errors.New("utf7: invalid UTF-7")
type decoder struct {
ascii bool
}
func (d *decoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
for i := 0; i < len(src); i++ {
ch := src[i]
if ch < min || ch > max { // Illegal code point in ASCII mode
err = ErrInvalidUTF7
return
}
if ch != '&' {
if nDst+1 > len(dst) {
err = transform.ErrShortDst
return
}
nSrc++
dst[nDst] = ch
nDst++
d.ascii = true
continue
}
// Find the end of the Base64 or "&-" segment
start := i + 1
for i++; i < len(src) && src[i] != '-'; i++ {
if src[i] == '\r' || src[i] == '\n' { // base64 package ignores CR and LF
err = ErrInvalidUTF7
return
}
}
if i == len(src) { // Implicit shift ("&...")
if atEOF {
err = ErrInvalidUTF7
} else {
err = transform.ErrShortSrc
}
return
}
var b []byte
if i == start { // Escape sequence "&-"
b = []byte{'&'}
d.ascii = true
} else { // Control or non-ASCII code points in base64
if !d.ascii { // Null shift ("&...-&...-")
err = ErrInvalidUTF7
return
}
b = decode(src[start:i])
d.ascii = false
}
if len(b) == 0 { // Bad encoding
err = ErrInvalidUTF7
return
}
if nDst+len(b) > len(dst) {
d.ascii = true
err = transform.ErrShortDst
return
}
nSrc = i + 1
for _, ch := range b {
dst[nDst] = ch
nDst++
}
}
if atEOF {
d.ascii = true
}
return
}
func (d *decoder) Reset() {
d.ascii = true
}
// Extracts UTF-16-BE bytes from base64 data and converts them to UTF-8.
// A nil slice is returned if the encoding is invalid.
func decode(b64 []byte) []byte {
var b []byte
// Allocate a single block of memory large enough to store the Base64 data
// (if padding is required), UTF-16-BE bytes, and decoded UTF-8 bytes.
// Since a 2-byte UTF-16 sequence may expand into a 3-byte UTF-8 sequence,
// double the space allocation for UTF-8.
if n := len(b64); b64[n-1] == '=' {
return nil
} else if n&3 == 0 {
b = make([]byte, b64Enc.DecodedLen(n)*3)
} else {
n += 4 - n&3
b = make([]byte, n+b64Enc.DecodedLen(n)*3)
copy(b[copy(b, b64):n], []byte("=="))
b64, b = b[:n], b[n:]
}
// Decode Base64 into the first 1/3rd of b
n, err := b64Enc.Decode(b, b64)
if err != nil || n&1 == 1 {
return nil
}
// Decode UTF-16-BE into the remaining 2/3rds of b
b, s := b[:n], b[n:]
j := 0
for i := 0; i < n; i += 2 {
r := rune(b[i])<<8 | rune(b[i+1])
if utf16.IsSurrogate(r) {
if i += 2; i == n {
return nil
}
r2 := rune(b[i])<<8 | rune(b[i+1])
if r = utf16.DecodeRune(r, r2); r == repl {
return nil
}
} else if min <= r && r <= max {
return nil
}
j += utf8.EncodeRune(s[j:], r)
}
return s[:j]
}

View File

@@ -1,91 +0,0 @@
package utf7
import (
"unicode/utf16"
"unicode/utf8"
"golang.org/x/text/transform"
)
type encoder struct{}
func (e *encoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
for i := 0; i < len(src); {
ch := src[i]
var b []byte
if min <= ch && ch <= max {
b = []byte{ch}
if ch == '&' {
b = append(b, '-')
}
i++
} else {
start := i
// Find the next printable ASCII code point
i++
for i < len(src) && (src[i] < min || src[i] > max) {
i++
}
if !atEOF && i == len(src) {
err = transform.ErrShortSrc
return
}
b = encode(src[start:i])
}
if nDst+len(b) > len(dst) {
err = transform.ErrShortDst
return
}
nSrc = i
for _, ch := range b {
dst[nDst] = ch
nDst++
}
}
return
}
func (e *encoder) Reset() {}
// Converts string s from UTF-8 to UTF-16-BE, encodes the result as base64,
// removes the padding, and adds UTF-7 shifts.
func encode(s []byte) []byte {
// len(s) is sufficient for UTF-8 to UTF-16 conversion if there are no
// control code points (see table below).
b := make([]byte, 0, len(s)+4)
for len(s) > 0 {
r, size := utf8.DecodeRune(s)
if r > utf8.MaxRune {
r, size = utf8.RuneError, 1 // Bug fix (issue 3785)
}
s = s[size:]
if r1, r2 := utf16.EncodeRune(r); r1 != repl {
b = append(b, byte(r1>>8), byte(r1))
r = r2
}
b = append(b, byte(r>>8), byte(r))
}
// Encode as base64
n := b64Enc.EncodedLen(len(b)) + 2
b64 := make([]byte, n)
b64Enc.Encode(b64[1:], b)
// Strip padding
n -= 2 - (len(b)+2)%3
b64 = b64[:n]
// Add UTF-7 shifts
b64[0] = '&'
b64[n-1] = '-'
return b64
}

View File

@@ -1,34 +0,0 @@
// Package utf7 implements modified UTF-7 encoding defined in RFC 3501 section 5.1.3
package utf7
import (
"encoding/base64"
"golang.org/x/text/encoding"
)
const (
min = 0x20 // Minimum self-representing UTF-7 value
max = 0x7E // Maximum self-representing UTF-7 value
repl = '\uFFFD' // Unicode replacement code point
)
var b64Enc = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,")
type enc struct{}
func (e enc) NewDecoder() *encoding.Decoder {
return &encoding.Decoder{
Transformer: &decoder{true},
}
}
func (e enc) NewEncoder() *encoding.Encoder {
return &encoding.Encoder{
Transformer: &encoder{},
}
}
// Encoding is the modified UTF-7 encoding.
var Encoding encoding.Encoding = enc{}

View File

@@ -1,255 +0,0 @@
package imap
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"strconv"
"time"
"unicode"
)
type flusher interface {
Flush() error
}
type (
// A raw string.
RawString string
)
type WriterTo interface {
WriteTo(w *Writer) error
}
func formatNumber(num uint32) string {
return strconv.FormatUint(uint64(num), 10)
}
// Convert a string list to a field list.
func FormatStringList(list []string) (fields []interface{}) {
fields = make([]interface{}, len(list))
for i, v := range list {
fields[i] = v
}
return
}
// Check if a string is 8-bit clean.
func isAscii(s string) bool {
for _, c := range s {
if c > unicode.MaxASCII || unicode.IsControl(c) {
return false
}
}
return true
}
// An IMAP writer.
type Writer struct {
io.Writer
AllowAsyncLiterals bool
continues <-chan bool
}
// Helper function to write a string to w.
func (w *Writer) writeString(s string) error {
_, err := io.WriteString(w.Writer, s)
return err
}
func (w *Writer) writeCrlf() error {
if err := w.writeString(crlf); err != nil {
return err
}
return w.Flush()
}
func (w *Writer) writeNumber(num uint32) error {
return w.writeString(formatNumber(num))
}
func (w *Writer) writeQuoted(s string) error {
return w.writeString(strconv.Quote(s))
}
func (w *Writer) writeQuotedOrLiteral(s string) error {
if !isAscii(s) {
// IMAP doesn't allow 8-bit data outside literals
return w.writeLiteral(bytes.NewBufferString(s))
}
return w.writeQuoted(s)
}
func (w *Writer) writeDateTime(t time.Time, layout string) error {
if t.IsZero() {
return w.writeString(nilAtom)
}
return w.writeQuoted(t.Format(layout))
}
func (w *Writer) writeFields(fields []interface{}) error {
for i, field := range fields {
if i > 0 { // Write separator
if err := w.writeString(string(sp)); err != nil {
return err
}
}
if err := w.writeField(field); err != nil {
return err
}
}
return nil
}
func (w *Writer) writeList(fields []interface{}) error {
if err := w.writeString(string(listStart)); err != nil {
return err
}
if err := w.writeFields(fields); err != nil {
return err
}
return w.writeString(string(listEnd))
}
// LiteralLengthErr is returned when the Len() of the Literal object does not
// match the actual length of the byte stream.
type LiteralLengthErr struct {
Actual int
Expected int
}
func (e LiteralLengthErr) Error() string {
return fmt.Sprintf("imap: size of Literal is not equal to Len() (%d != %d)", e.Expected, e.Actual)
}
func (w *Writer) writeLiteral(l Literal) error {
if l == nil {
return w.writeString(nilAtom)
}
unsyncLiteral := w.AllowAsyncLiterals && l.Len() <= 4096
header := string(literalStart) + strconv.Itoa(l.Len())
if unsyncLiteral {
header += string('+')
}
header += string(literalEnd) + crlf
if err := w.writeString(header); err != nil {
return err
}
// If a channel is available, wait for a continuation request before sending data
if !unsyncLiteral && w.continues != nil {
// Make sure to flush the writer, otherwise we may never receive a continuation request
if err := w.Flush(); err != nil {
return err
}
if !<-w.continues {
return fmt.Errorf("imap: cannot send literal: no continuation request received")
}
}
// In case of bufio.Buffer, it will be 0 after io.Copy.
literalLen := int64(l.Len())
n, err := io.CopyN(w, l, literalLen)
if err != nil {
if err == io.EOF && n != literalLen {
return LiteralLengthErr{int(n), l.Len()}
}
return err
}
extra, _ := io.Copy(ioutil.Discard, l)
if extra != 0 {
return LiteralLengthErr{int(n + extra), l.Len()}
}
return nil
}
func (w *Writer) writeField(field interface{}) error {
if field == nil {
return w.writeString(nilAtom)
}
switch field := field.(type) {
case RawString:
return w.writeString(string(field))
case string:
return w.writeQuotedOrLiteral(field)
case int:
return w.writeNumber(uint32(field))
case uint32:
return w.writeNumber(field)
case Literal:
return w.writeLiteral(field)
case []interface{}:
return w.writeList(field)
case envelopeDateTime:
return w.writeDateTime(time.Time(field), envelopeDateTimeLayout)
case searchDate:
return w.writeDateTime(time.Time(field), searchDateLayout)
case Date:
return w.writeDateTime(time.Time(field), DateLayout)
case DateTime:
return w.writeDateTime(time.Time(field), DateTimeLayout)
case time.Time:
return w.writeDateTime(field, DateTimeLayout)
case *SeqSet:
return w.writeString(field.String())
case *BodySectionName:
// Can contain spaces - that's why we don't just pass it as a string
return w.writeString(string(field.FetchItem()))
}
return fmt.Errorf("imap: cannot format field: %v", field)
}
func (w *Writer) writeRespCode(code StatusRespCode, args []interface{}) error {
if err := w.writeString(string(respCodeStart)); err != nil {
return err
}
fields := []interface{}{RawString(code)}
fields = append(fields, args...)
if err := w.writeFields(fields); err != nil {
return err
}
return w.writeString(string(respCodeEnd))
}
func (w *Writer) writeLine(fields ...interface{}) error {
if err := w.writeFields(fields); err != nil {
return err
}
return w.writeCrlf()
}
func (w *Writer) Flush() error {
if f, ok := w.Writer.(flusher); ok {
return f.Flush()
}
return nil
}
func NewWriter(w io.Writer) *Writer {
return &Writer{Writer: w}
}
func NewClientWriter(w io.Writer, continues <-chan bool) *Writer {
return &Writer{Writer: w, continues: continues}
}

View File

@@ -1,19 +0,0 @@
image: alpine/latest
packages:
- go
# Required by codecov
- bash
- findutils
sources:
- https://github.com/emersion/go-sasl
tasks:
- build: |
cd go-sasl
go build -v ./...
- test: |
cd go-sasl
go test -coverprofile=coverage.txt -covermode=atomic ./...
- upload-coverage: |
cd go-sasl
export CODECOV_TOKEN=3f257f71-a128-4834-8f68-2b534e9f4cb1
curl -s https://codecov.io/bash | bash

View File

@@ -1,24 +0,0 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof

View File

@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2016 emersion
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,17 +0,0 @@
# go-sasl
[![GoDoc](https://godoc.org/github.com/emersion/go-sasl?status.svg)](https://godoc.org/github.com/emersion/go-sasl)
[![Build Status](https://travis-ci.org/emersion/go-sasl.svg?branch=master)](https://travis-ci.org/emersion/go-sasl)
A [SASL](https://tools.ietf.org/html/rfc4422) library written in Go.
Implemented mechanisms:
* [ANONYMOUS](https://tools.ietf.org/html/rfc4505)
* [EXTERNAL](https://tools.ietf.org/html/rfc4422#appendix-A)
* [LOGIN](https://tools.ietf.org/html/draft-murchison-sasl-login-00) (obsolete, use PLAIN instead)
* [PLAIN](https://tools.ietf.org/html/rfc4616)
* [OAUTHBEARER](https://tools.ietf.org/html/rfc7628)
## License
MIT

View File

@@ -1,56 +0,0 @@
package sasl
// The ANONYMOUS mechanism name.
const Anonymous = "ANONYMOUS"
type anonymousClient struct {
Trace string
}
func (c *anonymousClient) Start() (mech string, ir []byte, err error) {
mech = Anonymous
ir = []byte(c.Trace)
return
}
func (c *anonymousClient) Next(challenge []byte) (response []byte, err error) {
return nil, ErrUnexpectedServerChallenge
}
// A client implementation of the ANONYMOUS authentication mechanism, as
// described in RFC 4505.
func NewAnonymousClient(trace string) Client {
return &anonymousClient{trace}
}
// Get trace information from clients logging in anonymously.
type AnonymousAuthenticator func(trace string) error
type anonymousServer struct {
done bool
authenticate AnonymousAuthenticator
}
func (s *anonymousServer) Next(response []byte) (challenge []byte, done bool, err error) {
if s.done {
err = ErrUnexpectedClientResponse
return
}
// No initial response, send an empty challenge
if response == nil {
return []byte{}, false, nil
}
s.done = true
err = s.authenticate(string(response))
done = true
return
}
// A server implementation of the ANONYMOUS authentication mechanism, as
// described in RFC 4505.
func NewAnonymousServer(authenticator AnonymousAuthenticator) Server {
return &anonymousServer{authenticate: authenticator}
}

View File

@@ -1,26 +0,0 @@
package sasl
// The EXTERNAL mechanism name.
const External = "EXTERNAL"
type externalClient struct {
Identity string
}
func (a *externalClient) Start() (mech string, ir []byte, err error) {
mech = External
ir = []byte(a.Identity)
return
}
func (a *externalClient) Next(challenge []byte) (response []byte, err error) {
return nil, ErrUnexpectedServerChallenge
}
// An implementation of the EXTERNAL authentication mechanism, as described in
// RFC 4422. Authorization identity may be left blank to indicate that the
// client is requesting to act as the identity associated with the
// authentication credentials.
func NewExternalClient(identity string) Client {
return &externalClient{identity}
}

View File

@@ -1,89 +0,0 @@
package sasl
import (
"bytes"
)
// The LOGIN mechanism name.
const Login = "LOGIN"
var expectedChallenge = []byte("Password:")
type loginClient struct {
Username string
Password string
}
func (a *loginClient) Start() (mech string, ir []byte, err error) {
mech = "LOGIN"
ir = []byte(a.Username)
return
}
func (a *loginClient) Next(challenge []byte) (response []byte, err error) {
if bytes.Compare(challenge, expectedChallenge) != 0 {
return nil, ErrUnexpectedServerChallenge
} else {
return []byte(a.Password), nil
}
}
// A client implementation of the LOGIN authentication mechanism for SMTP,
// as described in http://www.iana.org/go/draft-murchison-sasl-login
//
// It is considered obsolete, and should not be used when other mechanisms are
// available. For plaintext password authentication use PLAIN mechanism.
func NewLoginClient(username, password string) Client {
return &loginClient{username, password}
}
// Authenticates users with an username and a password.
type LoginAuthenticator func(username, password string) error
type loginState int
const (
loginNotStarted loginState = iota
loginWaitingUsername
loginWaitingPassword
)
type loginServer struct {
state loginState
username, password string
authenticate LoginAuthenticator
}
// A server implementation of the LOGIN authentication mechanism, as described
// in https://tools.ietf.org/html/draft-murchison-sasl-login-00.
//
// LOGIN is obsolete and should only be enabled for legacy clients that cannot
// be updated to use PLAIN.
func NewLoginServer(authenticator LoginAuthenticator) Server {
return &loginServer{authenticate: authenticator}
}
func (a *loginServer) Next(response []byte) (challenge []byte, done bool, err error) {
switch a.state {
case loginNotStarted:
// Check for initial response field, as per RFC4422 section 3
if response == nil {
challenge = []byte("Username:")
break
}
a.state++
fallthrough
case loginWaitingUsername:
a.username = string(response)
challenge = []byte("Password:")
case loginWaitingPassword:
a.password = string(response)
err = a.authenticate(a.username, a.password)
done = true
default:
err = ErrUnexpectedClientResponse
}
a.state++
return
}

View File

@@ -1,191 +0,0 @@
package sasl
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
)
// The OAUTHBEARER mechanism name.
const OAuthBearer = "OAUTHBEARER"
type OAuthBearerError struct {
Status string `json:"status"`
Schemes string `json:"schemes"`
Scope string `json:"scope"`
}
type OAuthBearerOptions struct {
Username string
Token string
Host string
Port int
}
// Implements error
func (err *OAuthBearerError) Error() string {
return fmt.Sprintf("OAUTHBEARER authentication error (%v)", err.Status)
}
type oauthBearerClient struct {
OAuthBearerOptions
}
func (a *oauthBearerClient) Start() (mech string, ir []byte, err error) {
mech = OAuthBearer
var str = "n,a=" + a.Username + ","
if a.Host != "" {
str += "\x01host=" + a.Host
}
if a.Port != 0 {
str += "\x01port=" + strconv.Itoa(a.Port)
}
str += "\x01auth=Bearer " + a.Token + "\x01\x01"
ir = []byte(str)
return
}
func (a *oauthBearerClient) Next(challenge []byte) ([]byte, error) {
authBearerErr := &OAuthBearerError{}
if err := json.Unmarshal(challenge, authBearerErr); err != nil {
return nil, err
} else {
return nil, authBearerErr
}
}
// An implementation of the OAUTHBEARER authentication mechanism, as
// described in RFC 7628.
func NewOAuthBearerClient(opt *OAuthBearerOptions) Client {
return &oauthBearerClient{*opt}
}
type OAuthBearerAuthenticator func(opts OAuthBearerOptions) *OAuthBearerError
type oauthBearerServer struct {
done bool
failErr error
authenticate OAuthBearerAuthenticator
}
func (a *oauthBearerServer) fail(descr string) ([]byte, bool, error) {
blob, err := json.Marshal(OAuthBearerError{
Status: "invalid_request",
Schemes: "bearer",
})
if err != nil {
panic(err) // wtf
}
a.failErr = errors.New(descr)
return blob, false, nil
}
func (a *oauthBearerServer) Next(response []byte) (challenge []byte, done bool, err error) {
// Per RFC, we cannot just send an error, we need to return JSON-structured
// value as a challenge and then after getting dummy response from the
// client stop the exchange.
if a.failErr != nil {
// Server libraries (go-smtp, go-imap) will not call Next on
// protocol-specific SASL cancel response ('*'). However, GS2 (and
// indirectly OAUTHBEARER) defines a protocol-independent way to do so
// using 0x01.
if len(response) != 1 && response[0] != 0x01 {
return nil, true, errors.New("unexpected response")
}
return nil, true, a.failErr
}
if a.done {
err = ErrUnexpectedClientResponse
return
}
// Generate empty challenge.
if response == nil {
return []byte{}, false, nil
}
a.done = true
// Cut n,a=username,\x01host=...\x01auth=...
// into
// n
// a=username
// \x01host=...\x01auth=...\x01\x01
parts := bytes.SplitN(response, []byte{','}, 3)
if len(parts) != 3 {
return a.fail("Invalid response")
}
if !bytes.Equal(parts[0], []byte{'n'}) {
return a.fail("Invalid response, missing 'n'")
}
opts := OAuthBearerOptions{}
if !bytes.HasPrefix(parts[1], []byte("a=")) {
return a.fail("Invalid response, missing 'a'")
}
opts.Username = string(bytes.TrimPrefix(parts[1], []byte("a=")))
// Cut \x01host=...\x01auth=...\x01\x01
// into
// *empty*
// host=...
// auth=...
// *empty*
//
// Note that this code does not do a lot of checks to make sure the input
// follows the exact format specified by RFC.
params := bytes.Split(parts[2], []byte{0x01})
for _, p := range params {
// Skip empty fields (one at start and end).
if len(p) == 0 {
continue
}
pParts := bytes.SplitN(p, []byte{'='}, 2)
if len(pParts) != 2 {
return a.fail("Invalid response, missing '='")
}
switch string(pParts[0]) {
case "host":
opts.Host = string(pParts[1])
case "port":
port, err := strconv.ParseUint(string(pParts[1]), 10, 16)
if err != nil {
return a.fail("Invalid response, malformed 'port' value")
}
opts.Port = int(port)
case "auth":
const prefix = "bearer "
strValue := string(pParts[1])
// Token type is case-insensitive.
if !strings.HasPrefix(strings.ToLower(strValue), prefix) {
return a.fail("Unsupported token type")
}
opts.Token = strValue[len(prefix):]
default:
return a.fail("Invalid response, unknown parameter: " + string(pParts[0]))
}
}
authzErr := a.authenticate(opts)
if authzErr != nil {
blob, err := json.Marshal(authzErr)
if err != nil {
panic(err) // wtf
}
a.failErr = authzErr
return blob, false, nil
}
return nil, true, nil
}
func NewOAuthBearerServer(auth OAuthBearerAuthenticator) Server {
return &oauthBearerServer{authenticate: auth}
}

View File

@@ -1,77 +0,0 @@
package sasl
import (
"bytes"
"errors"
)
// The PLAIN mechanism name.
const Plain = "PLAIN"
type plainClient struct {
Identity string
Username string
Password string
}
func (a *plainClient) Start() (mech string, ir []byte, err error) {
mech = "PLAIN"
ir = []byte(a.Identity + "\x00" + a.Username + "\x00" + a.Password)
return
}
func (a *plainClient) Next(challenge []byte) (response []byte, err error) {
return nil, ErrUnexpectedServerChallenge
}
// A client implementation of the PLAIN authentication mechanism, as described
// in RFC 4616. Authorization identity may be left blank to indicate that it is
// the same as the username.
func NewPlainClient(identity, username, password string) Client {
return &plainClient{identity, username, password}
}
// Authenticates users with an identity, a username and a password. If the
// identity is left blank, it indicates that it is the same as the username.
// If identity is not empty and the server doesn't support it, an error must be
// returned.
type PlainAuthenticator func(identity, username, password string) error
type plainServer struct {
done bool
authenticate PlainAuthenticator
}
func (a *plainServer) Next(response []byte) (challenge []byte, done bool, err error) {
if a.done {
err = ErrUnexpectedClientResponse
return
}
// No initial response, send an empty challenge
if response == nil {
return []byte{}, false, nil
}
a.done = true
parts := bytes.Split(response, []byte("\x00"))
if len(parts) != 3 {
err = errors.New("Invalid response")
return
}
identity := string(parts[0])
username := string(parts[1])
password := string(parts[2])
err = a.authenticate(identity, username, password)
done = true
return
}
// A server implementation of the PLAIN authentication mechanism, as described
// in RFC 4616.
func NewPlainServer(authenticator PlainAuthenticator) Server {
return &plainServer{authenticate: authenticator}
}

View File

@@ -1,45 +0,0 @@
// Library for Simple Authentication and Security Layer (SASL) defined in RFC 4422.
package sasl
// Note:
// Most of this code was copied, with some modifications, from net/smtp. It
// would be better if Go provided a standard package (e.g. crypto/sasl) that
// could be shared by SMTP, IMAP, and other packages.
import (
"errors"
)
// Common SASL errors.
var (
ErrUnexpectedClientResponse = errors.New("sasl: unexpected client response")
ErrUnexpectedServerChallenge = errors.New("sasl: unexpected server challenge")
)
// Client interface to perform challenge-response authentication.
type Client interface {
// Begins SASL authentication with the server. It returns the
// authentication mechanism name and "initial response" data (if required by
// the selected mechanism). A non-nil error causes the client to abort the
// authentication attempt.
//
// A nil ir value is different from a zero-length value. The nil value
// indicates that the selected mechanism does not use an initial response,
// while a zero-length value indicates an empty initial response, which must
// be sent to the server.
Start() (mech string, ir []byte, err error)
// Continues challenge-response authentication. A non-nil error causes
// the client to abort the authentication attempt.
Next(challenge []byte) (response []byte, err error)
}
// Server interface to perform challenge-response authentication.
type Server interface {
// Begins or continues challenge-response authentication. If the client
// supplies an initial response, response is non-nil.
//
// If the authentication is finished, done is set to true. If the
// authentication has failed, an error is returned.
Next(response []byte) (challenge []byte, done bool, err error)
}

3
vendor/golang.org/x/text/AUTHORS generated vendored
View File

@@ -1,3 +0,0 @@
# This source code refers to The Go Authors for copyright purposes.
# The master list of authors is in the main Go distribution,
# visible at http://tip.golang.org/AUTHORS.

View File

@@ -1,3 +0,0 @@
# This source code was written by the Go contributors.
# The master list of contributors is in the main Go distribution,
# visible at http://tip.golang.org/CONTRIBUTORS.

27
vendor/golang.org/x/text/LICENSE generated vendored
View File

@@ -1,27 +0,0 @@
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

22
vendor/golang.org/x/text/PATENTS generated vendored
View File

@@ -1,22 +0,0 @@
Additional IP Rights Grant (Patents)
"This implementation" means the copyrightable works distributed by
Google as part of the Go project.
Google hereby grants to You a perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable (except as stated in this section)
patent license to make, have made, use, offer to sell, sell, import,
transfer and otherwise run, modify and propagate the contents of this
implementation of Go, where such license applies only to those patent
claims, both currently owned or controlled by Google and acquired in
the future, licensable by Google that are necessarily infringed by this
implementation of Go. This grant does not include claims that would be
infringed only as a consequence of further modification of this
implementation. If you or your agent or exclusive licensee institute or
order or agree to the institution of patent litigation against any
entity (including a cross-claim or counterclaim in a lawsuit) alleging
that this implementation of Go or any code incorporated within this
implementation of Go constitutes direct or contributory patent
infringement, or inducement of patent infringement, then any patent
rights granted to you under this License for this implementation of Go
shall terminate as of the date such litigation is filed.

View File

@@ -1,335 +0,0 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package encoding defines an interface for character encodings, such as Shift
// JIS and Windows 1252, that can convert to and from UTF-8.
//
// Encoding implementations are provided in other packages, such as
// golang.org/x/text/encoding/charmap and
// golang.org/x/text/encoding/japanese.
package encoding // import "golang.org/x/text/encoding"
import (
"errors"
"io"
"strconv"
"unicode/utf8"
"golang.org/x/text/encoding/internal/identifier"
"golang.org/x/text/transform"
)
// TODO:
// - There seems to be some inconsistency in when decoders return errors
// and when not. Also documentation seems to suggest they shouldn't return
// errors at all (except for UTF-16).
// - Encoders seem to rely on or at least benefit from the input being in NFC
// normal form. Perhaps add an example how users could prepare their output.
// Encoding is a character set encoding that can be transformed to and from
// UTF-8.
type Encoding interface {
// NewDecoder returns a Decoder.
NewDecoder() *Decoder
// NewEncoder returns an Encoder.
NewEncoder() *Encoder
}
// A Decoder converts bytes to UTF-8. It implements transform.Transformer.
//
// Transforming source bytes that are not of that encoding will not result in an
// error per se. Each byte that cannot be transcoded will be represented in the
// output by the UTF-8 encoding of '\uFFFD', the replacement rune.
type Decoder struct {
transform.Transformer
// This forces external creators of Decoders to use names in struct
// initializers, allowing for future extendibility without having to break
// code.
_ struct{}
}
// Bytes converts the given encoded bytes to UTF-8. It returns the converted
// bytes or nil, err if any error occurred.
func (d *Decoder) Bytes(b []byte) ([]byte, error) {
b, _, err := transform.Bytes(d, b)
if err != nil {
return nil, err
}
return b, nil
}
// String converts the given encoded string to UTF-8. It returns the converted
// string or "", err if any error occurred.
func (d *Decoder) String(s string) (string, error) {
s, _, err := transform.String(d, s)
if err != nil {
return "", err
}
return s, nil
}
// Reader wraps another Reader to decode its bytes.
//
// The Decoder may not be used for any other operation as long as the returned
// Reader is in use.
func (d *Decoder) Reader(r io.Reader) io.Reader {
return transform.NewReader(r, d)
}
// An Encoder converts bytes from UTF-8. It implements transform.Transformer.
//
// Each rune that cannot be transcoded will result in an error. In this case,
// the transform will consume all source byte up to, not including the offending
// rune. Transforming source bytes that are not valid UTF-8 will be replaced by
// `\uFFFD`. To return early with an error instead, use transform.Chain to
// preprocess the data with a UTF8Validator.
type Encoder struct {
transform.Transformer
// This forces external creators of Encoders to use names in struct
// initializers, allowing for future extendibility without having to break
// code.
_ struct{}
}
// Bytes converts bytes from UTF-8. It returns the converted bytes or nil, err if
// any error occurred.
func (e *Encoder) Bytes(b []byte) ([]byte, error) {
b, _, err := transform.Bytes(e, b)
if err != nil {
return nil, err
}
return b, nil
}
// String converts a string from UTF-8. It returns the converted string or
// "", err if any error occurred.
func (e *Encoder) String(s string) (string, error) {
s, _, err := transform.String(e, s)
if err != nil {
return "", err
}
return s, nil
}
// Writer wraps another Writer to encode its UTF-8 output.
//
// The Encoder may not be used for any other operation as long as the returned
// Writer is in use.
func (e *Encoder) Writer(w io.Writer) io.Writer {
return transform.NewWriter(w, e)
}
// ASCIISub is the ASCII substitute character, as recommended by
// https://unicode.org/reports/tr36/#Text_Comparison
const ASCIISub = '\x1a'
// Nop is the nop encoding. Its transformed bytes are the same as the source
// bytes; it does not replace invalid UTF-8 sequences.
var Nop Encoding = nop{}
type nop struct{}
func (nop) NewDecoder() *Decoder {
return &Decoder{Transformer: transform.Nop}
}
func (nop) NewEncoder() *Encoder {
return &Encoder{Transformer: transform.Nop}
}
// Replacement is the replacement encoding. Decoding from the replacement
// encoding yields a single '\uFFFD' replacement rune. Encoding from UTF-8 to
// the replacement encoding yields the same as the source bytes except that
// invalid UTF-8 is converted to '\uFFFD'.
//
// It is defined at http://encoding.spec.whatwg.org/#replacement
var Replacement Encoding = replacement{}
type replacement struct{}
func (replacement) NewDecoder() *Decoder {
return &Decoder{Transformer: replacementDecoder{}}
}
func (replacement) NewEncoder() *Encoder {
return &Encoder{Transformer: replacementEncoder{}}
}
func (replacement) ID() (mib identifier.MIB, other string) {
return identifier.Replacement, ""
}
type replacementDecoder struct{ transform.NopResetter }
func (replacementDecoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
if len(dst) < 3 {
return 0, 0, transform.ErrShortDst
}
if atEOF {
const fffd = "\ufffd"
dst[0] = fffd[0]
dst[1] = fffd[1]
dst[2] = fffd[2]
nDst = 3
}
return nDst, len(src), nil
}
type replacementEncoder struct{ transform.NopResetter }
func (replacementEncoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
r, size := rune(0), 0
for ; nSrc < len(src); nSrc += size {
r = rune(src[nSrc])
// Decode a 1-byte rune.
if r < utf8.RuneSelf {
size = 1
} else {
// Decode a multi-byte rune.
r, size = utf8.DecodeRune(src[nSrc:])
if size == 1 {
// All valid runes of size 1 (those below utf8.RuneSelf) were
// handled above. We have invalid UTF-8 or we haven't seen the
// full character yet.
if !atEOF && !utf8.FullRune(src[nSrc:]) {
err = transform.ErrShortSrc
break
}
r = '\ufffd'
}
}
if nDst+utf8.RuneLen(r) > len(dst) {
err = transform.ErrShortDst
break
}
nDst += utf8.EncodeRune(dst[nDst:], r)
}
return nDst, nSrc, err
}
// HTMLEscapeUnsupported wraps encoders to replace source runes outside the
// repertoire of the destination encoding with HTML escape sequences.
//
// This wrapper exists to comply to URL and HTML forms requiring a
// non-terminating legacy encoder. The produced sequences may lead to data
// loss as they are indistinguishable from legitimate input. To avoid this
// issue, use UTF-8 encodings whenever possible.
func HTMLEscapeUnsupported(e *Encoder) *Encoder {
return &Encoder{Transformer: &errorHandler{e, errorToHTML}}
}
// ReplaceUnsupported wraps encoders to replace source runes outside the
// repertoire of the destination encoding with an encoding-specific
// replacement.
//
// This wrapper is only provided for backwards compatibility and legacy
// handling. Its use is strongly discouraged. Use UTF-8 whenever possible.
func ReplaceUnsupported(e *Encoder) *Encoder {
return &Encoder{Transformer: &errorHandler{e, errorToReplacement}}
}
type errorHandler struct {
*Encoder
handler func(dst []byte, r rune, err repertoireError) (n int, ok bool)
}
// TODO: consider making this error public in some form.
type repertoireError interface {
Replacement() byte
}
func (h errorHandler) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
nDst, nSrc, err = h.Transformer.Transform(dst, src, atEOF)
for err != nil {
rerr, ok := err.(repertoireError)
if !ok {
return nDst, nSrc, err
}
r, sz := utf8.DecodeRune(src[nSrc:])
n, ok := h.handler(dst[nDst:], r, rerr)
if !ok {
return nDst, nSrc, transform.ErrShortDst
}
err = nil
nDst += n
if nSrc += sz; nSrc < len(src) {
var dn, sn int
dn, sn, err = h.Transformer.Transform(dst[nDst:], src[nSrc:], atEOF)
nDst += dn
nSrc += sn
}
}
return nDst, nSrc, err
}
func errorToHTML(dst []byte, r rune, err repertoireError) (n int, ok bool) {
buf := [8]byte{}
b := strconv.AppendUint(buf[:0], uint64(r), 10)
if n = len(b) + len("&#;"); n >= len(dst) {
return 0, false
}
dst[0] = '&'
dst[1] = '#'
dst[copy(dst[2:], b)+2] = ';'
return n, true
}
func errorToReplacement(dst []byte, r rune, err repertoireError) (n int, ok bool) {
if len(dst) == 0 {
return 0, false
}
dst[0] = err.Replacement()
return 1, true
}
// ErrInvalidUTF8 means that a transformer encountered invalid UTF-8.
var ErrInvalidUTF8 = errors.New("encoding: invalid UTF-8")
// UTF8Validator is a transformer that returns ErrInvalidUTF8 on the first
// input byte that is not valid UTF-8.
var UTF8Validator transform.Transformer = utf8Validator{}
type utf8Validator struct{ transform.NopResetter }
func (utf8Validator) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
n := len(src)
if n > len(dst) {
n = len(dst)
}
for i := 0; i < n; {
if c := src[i]; c < utf8.RuneSelf {
dst[i] = c
i++
continue
}
_, size := utf8.DecodeRune(src[i:])
if size == 1 {
// All valid runes of size 1 (those below utf8.RuneSelf) were
// handled above. We have invalid UTF-8 or we haven't seen the
// full character yet.
err = ErrInvalidUTF8
if !atEOF && !utf8.FullRune(src[i:]) {
err = transform.ErrShortSrc
}
return i, i, err
}
if i+size > len(dst) {
return i, i, transform.ErrShortDst
}
for ; size > 0; size-- {
dst[i] = src[i]
i++
}
}
if len(src) > len(dst) {
err = transform.ErrShortDst
}
return n, n, err
}

View File

@@ -1,81 +0,0 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:generate go run gen.go
// Package identifier defines the contract between implementations of Encoding
// and Index by defining identifiers that uniquely identify standardized coded
// character sets (CCS) and character encoding schemes (CES), which we will
// together refer to as encodings, for which Encoding implementations provide
// converters to and from UTF-8. This package is typically only of concern to
// implementers of Indexes and Encodings.
//
// One part of the identifier is the MIB code, which is defined by IANA and
// uniquely identifies a CCS or CES. Each code is associated with data that
// references authorities, official documentation as well as aliases and MIME
// names.
//
// Not all CESs are covered by the IANA registry. The "other" string that is
// returned by ID can be used to identify other character sets or versions of
// existing ones.
//
// It is recommended that each package that provides a set of Encodings provide
// the All and Common variables to reference all supported encodings and
// commonly used subset. This allows Index implementations to include all
// available encodings without explicitly referencing or knowing about them.
package identifier
// Note: this package is internal, but could be made public if there is a need
// for writing third-party Indexes and Encodings.
// References:
// - http://source.icu-project.org/repos/icu/icu/trunk/source/data/mappings/convrtrs.txt
// - http://www.iana.org/assignments/character-sets/character-sets.xhtml
// - http://www.iana.org/assignments/ianacharset-mib/ianacharset-mib
// - http://www.ietf.org/rfc/rfc2978.txt
// - https://www.unicode.org/reports/tr22/
// - http://www.w3.org/TR/encoding/
// - https://encoding.spec.whatwg.org/
// - https://encoding.spec.whatwg.org/encodings.json
// - https://tools.ietf.org/html/rfc6657#section-5
// Interface can be implemented by Encodings to define the CCS or CES for which
// it implements conversions.
type Interface interface {
// ID returns an encoding identifier. Exactly one of the mib and other
// values should be non-zero.
//
// In the usual case it is only necessary to indicate the MIB code. The
// other string can be used to specify encodings for which there is no MIB,
// such as "x-mac-dingbat".
//
// The other string may only contain the characters a-z, A-Z, 0-9, - and _.
ID() (mib MIB, other string)
// NOTE: the restrictions on the encoding are to allow extending the syntax
// with additional information such as versions, vendors and other variants.
}
// A MIB identifies an encoding. It is derived from the IANA MIB codes and adds
// some identifiers for some encodings that are not covered by the IANA
// standard.
//
// See http://www.iana.org/assignments/ianacharset-mib.
type MIB uint16
// These additional MIB types are not defined in IANA. They are added because
// they are common and defined within the text repo.
const (
// Unofficial marks the start of encodings not registered by IANA.
Unofficial MIB = 10000 + iota
// Replacement is the WhatWG replacement encoding.
Replacement
// XUserDefined is the code for x-user-defined.
XUserDefined
// MacintoshCyrillic is the code for x-mac-cyrillic.
MacintoshCyrillic
)

File diff suppressed because it is too large Load Diff

View File

@@ -1,709 +0,0 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package transform provides reader and writer wrappers that transform the
// bytes passing through as well as various transformations. Example
// transformations provided by other packages include normalization and
// conversion between character sets.
package transform // import "golang.org/x/text/transform"
import (
"bytes"
"errors"
"io"
"unicode/utf8"
)
var (
// ErrShortDst means that the destination buffer was too short to
// receive all of the transformed bytes.
ErrShortDst = errors.New("transform: short destination buffer")
// ErrShortSrc means that the source buffer has insufficient data to
// complete the transformation.
ErrShortSrc = errors.New("transform: short source buffer")
// ErrEndOfSpan means that the input and output (the transformed input)
// are not identical.
ErrEndOfSpan = errors.New("transform: input and output are not identical")
// errInconsistentByteCount means that Transform returned success (nil
// error) but also returned nSrc inconsistent with the src argument.
errInconsistentByteCount = errors.New("transform: inconsistent byte count returned")
// errShortInternal means that an internal buffer is not large enough
// to make progress and the Transform operation must be aborted.
errShortInternal = errors.New("transform: short internal buffer")
)
// Transformer transforms bytes.
type Transformer interface {
// Transform writes to dst the transformed bytes read from src, and
// returns the number of dst bytes written and src bytes read. The
// atEOF argument tells whether src represents the last bytes of the
// input.
//
// Callers should always process the nDst bytes produced and account
// for the nSrc bytes consumed before considering the error err.
//
// A nil error means that all of the transformed bytes (whether freshly
// transformed from src or left over from previous Transform calls)
// were written to dst. A nil error can be returned regardless of
// whether atEOF is true. If err is nil then nSrc must equal len(src);
// the converse is not necessarily true.
//
// ErrShortDst means that dst was too short to receive all of the
// transformed bytes. ErrShortSrc means that src had insufficient data
// to complete the transformation. If both conditions apply, then
// either error may be returned. Other than the error conditions listed
// here, implementations are free to report other errors that arise.
Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error)
// Reset resets the state and allows a Transformer to be reused.
Reset()
}
// SpanningTransformer extends the Transformer interface with a Span method
// that determines how much of the input already conforms to the Transformer.
type SpanningTransformer interface {
Transformer
// Span returns a position in src such that transforming src[:n] results in
// identical output src[:n] for these bytes. It does not necessarily return
// the largest such n. The atEOF argument tells whether src represents the
// last bytes of the input.
//
// Callers should always account for the n bytes consumed before
// considering the error err.
//
// A nil error means that all input bytes are known to be identical to the
// output produced by the Transformer. A nil error can be returned
// regardless of whether atEOF is true. If err is nil, then n must
// equal len(src); the converse is not necessarily true.
//
// ErrEndOfSpan means that the Transformer output may differ from the
// input after n bytes. Note that n may be len(src), meaning that the output
// would contain additional bytes after otherwise identical output.
// ErrShortSrc means that src had insufficient data to determine whether the
// remaining bytes would change. Other than the error conditions listed
// here, implementations are free to report other errors that arise.
//
// Calling Span can modify the Transformer state as a side effect. In
// effect, it does the transformation just as calling Transform would, only
// without copying to a destination buffer and only up to a point it can
// determine the input and output bytes are the same. This is obviously more
// limited than calling Transform, but can be more efficient in terms of
// copying and allocating buffers. Calls to Span and Transform may be
// interleaved.
Span(src []byte, atEOF bool) (n int, err error)
}
// NopResetter can be embedded by implementations of Transformer to add a nop
// Reset method.
type NopResetter struct{}
// Reset implements the Reset method of the Transformer interface.
func (NopResetter) Reset() {}
// Reader wraps another io.Reader by transforming the bytes read.
type Reader struct {
r io.Reader
t Transformer
err error
// dst[dst0:dst1] contains bytes that have been transformed by t but
// not yet copied out via Read.
dst []byte
dst0, dst1 int
// src[src0:src1] contains bytes that have been read from r but not
// yet transformed through t.
src []byte
src0, src1 int
// transformComplete is whether the transformation is complete,
// regardless of whether or not it was successful.
transformComplete bool
}
const defaultBufSize = 4096
// NewReader returns a new Reader that wraps r by transforming the bytes read
// via t. It calls Reset on t.
func NewReader(r io.Reader, t Transformer) *Reader {
t.Reset()
return &Reader{
r: r,
t: t,
dst: make([]byte, defaultBufSize),
src: make([]byte, defaultBufSize),
}
}
// Read implements the io.Reader interface.
func (r *Reader) Read(p []byte) (int, error) {
n, err := 0, error(nil)
for {
// Copy out any transformed bytes and return the final error if we are done.
if r.dst0 != r.dst1 {
n = copy(p, r.dst[r.dst0:r.dst1])
r.dst0 += n
if r.dst0 == r.dst1 && r.transformComplete {
return n, r.err
}
return n, nil
} else if r.transformComplete {
return 0, r.err
}
// Try to transform some source bytes, or to flush the transformer if we
// are out of source bytes. We do this even if r.r.Read returned an error.
// As the io.Reader documentation says, "process the n > 0 bytes returned
// before considering the error".
if r.src0 != r.src1 || r.err != nil {
r.dst0 = 0
r.dst1, n, err = r.t.Transform(r.dst, r.src[r.src0:r.src1], r.err == io.EOF)
r.src0 += n
switch {
case err == nil:
if r.src0 != r.src1 {
r.err = errInconsistentByteCount
}
// The Transform call was successful; we are complete if we
// cannot read more bytes into src.
r.transformComplete = r.err != nil
continue
case err == ErrShortDst && (r.dst1 != 0 || n != 0):
// Make room in dst by copying out, and try again.
continue
case err == ErrShortSrc && r.src1-r.src0 != len(r.src) && r.err == nil:
// Read more bytes into src via the code below, and try again.
default:
r.transformComplete = true
// The reader error (r.err) takes precedence over the
// transformer error (err) unless r.err is nil or io.EOF.
if r.err == nil || r.err == io.EOF {
r.err = err
}
continue
}
}
// Move any untransformed source bytes to the start of the buffer
// and read more bytes.
if r.src0 != 0 {
r.src0, r.src1 = 0, copy(r.src, r.src[r.src0:r.src1])
}
n, r.err = r.r.Read(r.src[r.src1:])
r.src1 += n
}
}
// TODO: implement ReadByte (and ReadRune??).
// Writer wraps another io.Writer by transforming the bytes read.
// The user needs to call Close to flush unwritten bytes that may
// be buffered.
type Writer struct {
w io.Writer
t Transformer
dst []byte
// src[:n] contains bytes that have not yet passed through t.
src []byte
n int
}
// NewWriter returns a new Writer that wraps w by transforming the bytes written
// via t. It calls Reset on t.
func NewWriter(w io.Writer, t Transformer) *Writer {
t.Reset()
return &Writer{
w: w,
t: t,
dst: make([]byte, defaultBufSize),
src: make([]byte, defaultBufSize),
}
}
// Write implements the io.Writer interface. If there are not enough
// bytes available to complete a Transform, the bytes will be buffered
// for the next write. Call Close to convert the remaining bytes.
func (w *Writer) Write(data []byte) (n int, err error) {
src := data
if w.n > 0 {
// Append bytes from data to the last remainder.
// TODO: limit the amount copied on first try.
n = copy(w.src[w.n:], data)
w.n += n
src = w.src[:w.n]
}
for {
nDst, nSrc, err := w.t.Transform(w.dst, src, false)
if _, werr := w.w.Write(w.dst[:nDst]); werr != nil {
return n, werr
}
src = src[nSrc:]
if w.n == 0 {
n += nSrc
} else if len(src) <= n {
// Enough bytes from w.src have been consumed. We make src point
// to data instead to reduce the copying.
w.n = 0
n -= len(src)
src = data[n:]
if n < len(data) && (err == nil || err == ErrShortSrc) {
continue
}
}
switch err {
case ErrShortDst:
// This error is okay as long as we are making progress.
if nDst > 0 || nSrc > 0 {
continue
}
case ErrShortSrc:
if len(src) < len(w.src) {
m := copy(w.src, src)
// If w.n > 0, bytes from data were already copied to w.src and n
// was already set to the number of bytes consumed.
if w.n == 0 {
n += m
}
w.n = m
err = nil
} else if nDst > 0 || nSrc > 0 {
// Not enough buffer to store the remainder. Keep processing as
// long as there is progress. Without this case, transforms that
// require a lookahead larger than the buffer may result in an
// error. This is not something one may expect to be common in
// practice, but it may occur when buffers are set to small
// sizes during testing.
continue
}
case nil:
if w.n > 0 {
err = errInconsistentByteCount
}
}
return n, err
}
}
// Close implements the io.Closer interface.
func (w *Writer) Close() error {
src := w.src[:w.n]
for {
nDst, nSrc, err := w.t.Transform(w.dst, src, true)
if _, werr := w.w.Write(w.dst[:nDst]); werr != nil {
return werr
}
if err != ErrShortDst {
return err
}
src = src[nSrc:]
}
}
type nop struct{ NopResetter }
func (nop) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
n := copy(dst, src)
if n < len(src) {
err = ErrShortDst
}
return n, n, err
}
func (nop) Span(src []byte, atEOF bool) (n int, err error) {
return len(src), nil
}
type discard struct{ NopResetter }
func (discard) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
return 0, len(src), nil
}
var (
// Discard is a Transformer for which all Transform calls succeed
// by consuming all bytes and writing nothing.
Discard Transformer = discard{}
// Nop is a SpanningTransformer that copies src to dst.
Nop SpanningTransformer = nop{}
)
// chain is a sequence of links. A chain with N Transformers has N+1 links and
// N+1 buffers. Of those N+1 buffers, the first and last are the src and dst
// buffers given to chain.Transform and the middle N-1 buffers are intermediate
// buffers owned by the chain. The i'th link transforms bytes from the i'th
// buffer chain.link[i].b at read offset chain.link[i].p to the i+1'th buffer
// chain.link[i+1].b at write offset chain.link[i+1].n, for i in [0, N).
type chain struct {
link []link
err error
// errStart is the index at which the error occurred plus 1. Processing
// errStart at this level at the next call to Transform. As long as
// errStart > 0, chain will not consume any more source bytes.
errStart int
}
func (c *chain) fatalError(errIndex int, err error) {
if i := errIndex + 1; i > c.errStart {
c.errStart = i
c.err = err
}
}
type link struct {
t Transformer
// b[p:n] holds the bytes to be transformed by t.
b []byte
p int
n int
}
func (l *link) src() []byte {
return l.b[l.p:l.n]
}
func (l *link) dst() []byte {
return l.b[l.n:]
}
// Chain returns a Transformer that applies t in sequence.
func Chain(t ...Transformer) Transformer {
if len(t) == 0 {
return nop{}
}
c := &chain{link: make([]link, len(t)+1)}
for i, tt := range t {
c.link[i].t = tt
}
// Allocate intermediate buffers.
b := make([][defaultBufSize]byte, len(t)-1)
for i := range b {
c.link[i+1].b = b[i][:]
}
return c
}
// Reset resets the state of Chain. It calls Reset on all the Transformers.
func (c *chain) Reset() {
for i, l := range c.link {
if l.t != nil {
l.t.Reset()
}
c.link[i].p, c.link[i].n = 0, 0
}
}
// TODO: make chain use Span (is going to be fun to implement!)
// Transform applies the transformers of c in sequence.
func (c *chain) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
// Set up src and dst in the chain.
srcL := &c.link[0]
dstL := &c.link[len(c.link)-1]
srcL.b, srcL.p, srcL.n = src, 0, len(src)
dstL.b, dstL.n = dst, 0
var lastFull, needProgress bool // for detecting progress
// i is the index of the next Transformer to apply, for i in [low, high].
// low is the lowest index for which c.link[low] may still produce bytes.
// high is the highest index for which c.link[high] has a Transformer.
// The error returned by Transform determines whether to increase or
// decrease i. We try to completely fill a buffer before converting it.
for low, i, high := c.errStart, c.errStart, len(c.link)-2; low <= i && i <= high; {
in, out := &c.link[i], &c.link[i+1]
nDst, nSrc, err0 := in.t.Transform(out.dst(), in.src(), atEOF && low == i)
out.n += nDst
in.p += nSrc
if i > 0 && in.p == in.n {
in.p, in.n = 0, 0
}
needProgress, lastFull = lastFull, false
switch err0 {
case ErrShortDst:
// Process the destination buffer next. Return if we are already
// at the high index.
if i == high {
return dstL.n, srcL.p, ErrShortDst
}
if out.n != 0 {
i++
// If the Transformer at the next index is not able to process any
// source bytes there is nothing that can be done to make progress
// and the bytes will remain unprocessed. lastFull is used to
// detect this and break out of the loop with a fatal error.
lastFull = true
continue
}
// The destination buffer was too small, but is completely empty.
// Return a fatal error as this transformation can never complete.
c.fatalError(i, errShortInternal)
case ErrShortSrc:
if i == 0 {
// Save ErrShortSrc in err. All other errors take precedence.
err = ErrShortSrc
break
}
// Source bytes were depleted before filling up the destination buffer.
// Verify we made some progress, move the remaining bytes to the errStart
// and try to get more source bytes.
if needProgress && nSrc == 0 || in.n-in.p == len(in.b) {
// There were not enough source bytes to proceed while the source
// buffer cannot hold any more bytes. Return a fatal error as this
// transformation can never complete.
c.fatalError(i, errShortInternal)
break
}
// in.b is an internal buffer and we can make progress.
in.p, in.n = 0, copy(in.b, in.src())
fallthrough
case nil:
// if i == low, we have depleted the bytes at index i or any lower levels.
// In that case we increase low and i. In all other cases we decrease i to
// fetch more bytes before proceeding to the next index.
if i > low {
i--
continue
}
default:
c.fatalError(i, err0)
}
// Exhausted level low or fatal error: increase low and continue
// to process the bytes accepted so far.
i++
low = i
}
// If c.errStart > 0, this means we found a fatal error. We will clear
// all upstream buffers. At this point, no more progress can be made
// downstream, as Transform would have bailed while handling ErrShortDst.
if c.errStart > 0 {
for i := 1; i < c.errStart; i++ {
c.link[i].p, c.link[i].n = 0, 0
}
err, c.errStart, c.err = c.err, 0, nil
}
return dstL.n, srcL.p, err
}
// Deprecated: Use runes.Remove instead.
func RemoveFunc(f func(r rune) bool) Transformer {
return removeF(f)
}
type removeF func(r rune) bool
func (removeF) Reset() {}
// Transform implements the Transformer interface.
func (t removeF) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
for r, sz := rune(0), 0; len(src) > 0; src = src[sz:] {
if r = rune(src[0]); r < utf8.RuneSelf {
sz = 1
} else {
r, sz = utf8.DecodeRune(src)
if sz == 1 {
// Invalid rune.
if !atEOF && !utf8.FullRune(src) {
err = ErrShortSrc
break
}
// We replace illegal bytes with RuneError. Not doing so might
// otherwise turn a sequence of invalid UTF-8 into valid UTF-8.
// The resulting byte sequence may subsequently contain runes
// for which t(r) is true that were passed unnoticed.
if !t(r) {
if nDst+3 > len(dst) {
err = ErrShortDst
break
}
nDst += copy(dst[nDst:], "\uFFFD")
}
nSrc++
continue
}
}
if !t(r) {
if nDst+sz > len(dst) {
err = ErrShortDst
break
}
nDst += copy(dst[nDst:], src[:sz])
}
nSrc += sz
}
return
}
// grow returns a new []byte that is longer than b, and copies the first n bytes
// of b to the start of the new slice.
func grow(b []byte, n int) []byte {
m := len(b)
if m <= 32 {
m = 64
} else if m <= 256 {
m *= 2
} else {
m += m >> 1
}
buf := make([]byte, m)
copy(buf, b[:n])
return buf
}
const initialBufSize = 128
// String returns a string with the result of converting s[:n] using t, where
// n <= len(s). If err == nil, n will be len(s). It calls Reset on t.
func String(t Transformer, s string) (result string, n int, err error) {
t.Reset()
if s == "" {
// Fast path for the common case for empty input. Results in about a
// 86% reduction of running time for BenchmarkStringLowerEmpty.
if _, _, err := t.Transform(nil, nil, true); err == nil {
return "", 0, nil
}
}
// Allocate only once. Note that both dst and src escape when passed to
// Transform.
buf := [2 * initialBufSize]byte{}
dst := buf[:initialBufSize:initialBufSize]
src := buf[initialBufSize : 2*initialBufSize]
// The input string s is transformed in multiple chunks (starting with a
// chunk size of initialBufSize). nDst and nSrc are per-chunk (or
// per-Transform-call) indexes, pDst and pSrc are overall indexes.
nDst, nSrc := 0, 0
pDst, pSrc := 0, 0
// pPrefix is the length of a common prefix: the first pPrefix bytes of the
// result will equal the first pPrefix bytes of s. It is not guaranteed to
// be the largest such value, but if pPrefix, len(result) and len(s) are
// all equal after the final transform (i.e. calling Transform with atEOF
// being true returned nil error) then we don't need to allocate a new
// result string.
pPrefix := 0
for {
// Invariant: pDst == pPrefix && pSrc == pPrefix.
n := copy(src, s[pSrc:])
nDst, nSrc, err = t.Transform(dst, src[:n], pSrc+n == len(s))
pDst += nDst
pSrc += nSrc
// TODO: let transformers implement an optional Spanner interface, akin
// to norm's QuickSpan. This would even allow us to avoid any allocation.
if !bytes.Equal(dst[:nDst], src[:nSrc]) {
break
}
pPrefix = pSrc
if err == ErrShortDst {
// A buffer can only be short if a transformer modifies its input.
break
} else if err == ErrShortSrc {
if nSrc == 0 {
// No progress was made.
break
}
// Equal so far and !atEOF, so continue checking.
} else if err != nil || pPrefix == len(s) {
return string(s[:pPrefix]), pPrefix, err
}
}
// Post-condition: pDst == pPrefix + nDst && pSrc == pPrefix + nSrc.
// We have transformed the first pSrc bytes of the input s to become pDst
// transformed bytes. Those transformed bytes are discontiguous: the first
// pPrefix of them equal s[:pPrefix] and the last nDst of them equal
// dst[:nDst]. We copy them around, into a new dst buffer if necessary, so
// that they become one contiguous slice: dst[:pDst].
if pPrefix != 0 {
newDst := dst
if pDst > len(newDst) {
newDst = make([]byte, len(s)+nDst-nSrc)
}
copy(newDst[pPrefix:pDst], dst[:nDst])
copy(newDst[:pPrefix], s[:pPrefix])
dst = newDst
}
// Prevent duplicate Transform calls with atEOF being true at the end of
// the input. Also return if we have an unrecoverable error.
if (err == nil && pSrc == len(s)) ||
(err != nil && err != ErrShortDst && err != ErrShortSrc) {
return string(dst[:pDst]), pSrc, err
}
// Transform the remaining input, growing dst and src buffers as necessary.
for {
n := copy(src, s[pSrc:])
atEOF := pSrc+n == len(s)
nDst, nSrc, err := t.Transform(dst[pDst:], src[:n], atEOF)
pDst += nDst
pSrc += nSrc
// If we got ErrShortDst or ErrShortSrc, do not grow as long as we can
// make progress. This may avoid excessive allocations.
if err == ErrShortDst {
if nDst == 0 {
dst = grow(dst, pDst)
}
} else if err == ErrShortSrc {
if atEOF {
return string(dst[:pDst]), pSrc, err
}
if nSrc == 0 {
src = grow(src, 0)
}
} else if err != nil || pSrc == len(s) {
return string(dst[:pDst]), pSrc, err
}
}
}
// Bytes returns a new byte slice with the result of converting b[:n] using t,
// where n <= len(b). If err == nil, n will be len(b). It calls Reset on t.
func Bytes(t Transformer, b []byte) (result []byte, n int, err error) {
return doAppend(t, 0, make([]byte, len(b)), b)
}
// Append appends the result of converting src[:n] using t to dst, where
// n <= len(src), If err == nil, n will be len(src). It calls Reset on t.
func Append(t Transformer, dst, src []byte) (result []byte, n int, err error) {
if len(dst) == cap(dst) {
n := len(src) + len(dst) // It is okay for this to be 0.
b := make([]byte, n)
dst = b[:copy(b, dst)]
}
return doAppend(t, len(dst), dst[:cap(dst)], src)
}
func doAppend(t Transformer, pDst int, dst, src []byte) (result []byte, n int, err error) {
t.Reset()
pSrc := 0
for {
nDst, nSrc, err := t.Transform(dst[pDst:], src[pSrc:], true)
pDst += nDst
pSrc += nSrc
if err != ErrShortDst {
return dst[:pDst], pSrc, err
}
// Grow the destination buffer, but do not grow as long as we can make
// progress. This may avoid excessive allocations.
if nDst == 0 {
dst = grow(dst, pDst)
}
}
}

17
vendor/gopkg.in/yaml.v2/.travis.yml generated vendored
View File

@@ -1,17 +0,0 @@
language: go
go:
- "1.4.x"
- "1.5.x"
- "1.6.x"
- "1.7.x"
- "1.8.x"
- "1.9.x"
- "1.10.x"
- "1.11.x"
- "1.12.x"
- "1.13.x"
- "1.14.x"
- "tip"
go_import_path: gopkg.in/yaml.v2

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