Initial commit

This commit is contained in:
2018-03-05 12:19:04 +00:00
commit 811b90224f
114 changed files with 16465 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/crockery
/crockery.db

21
DESIGN.md Normal file
View File

@@ -0,0 +1,21 @@
# Crockery design
We need:
* SMTP server
* Receive emails -> middlewares -> storage
* https://github.com/emersion/go-smtp ?
* IMAP server
* Receive logins
* HTTP server
* Serve autodiscovery bumpf
* ActiveSync protocol
* Storage
* Accounts
* Passwords!
* Emails
* TLS keys + certificates
* Search emails - some sort of inverted index necessary
* https://github.com/blevesearch/bleve ?
* Embedded database best. Ideally we have a single file to work with

46
Gopkg.lock generated Normal file
View File

@@ -0,0 +1,46 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/emersion/go-imap"
packages = [
".",
"backend",
"commands",
"responses",
"server",
"utf7"
]
revision = "840b16b212bff35b595e708937c6d60861cfab49"
version = "v0.9"
[[projects]]
branch = "master"
name = "github.com/emersion/go-sasl"
packages = ["."]
revision = "7e096a0a6197b89989e8cc31016daa67c8c62051"
[[projects]]
branch = "master"
name = "github.com/emersion/go-smtp"
packages = ["."]
revision = "a63104657743890cb7c2fd54f15a2725291f6a9f"
[[projects]]
name = "golang.org/x/text"
packages = [
"encoding",
"encoding/internal/identifier",
"internal/gen",
"transform",
"unicode/cldr"
]
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
version = "v0.3.0"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "a8a093c114e9fd6d1de5e43c061f3cf1d08fa5279da70af9c0cd030030fe7737"
solver-name = "gps-cdcl"
solver-version = 1

34
Gopkg.toml Normal file
View File

@@ -0,0 +1,34 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[[constraint]]
branch = "master"
name = "github.com/emersion/go-smtp"
[prune]
go-tests = true
unused-packages = true

120
README.md Normal file
View File

@@ -0,0 +1,120 @@
# Crockery - simple email hosting
## What
* SMTP + IMAP email, plus bundled HTTP webmail client
* Devoted to the task of serving email for a single domain
* Can get its own valid TLS certificates via LE, or use supplied ones
* Built-in anti-spam & anti-virus measures
* Built-in DKIM support
* Built-in DMARC support
* Can tell you what DNS records you need, and check them for validity
* Multiple email accounts for your domain
* A "master" wildcard account (if desired)
* Simple import from GMail, etc
* Simple export to GMail, etc
* Nice fast search
* Interact with crockery with simple, natural-ish emails to do things
* SMTP TLS verification - enabled by default
* Try to email foo@shady.net - verification fails, email marked pending, fail in 7 days
* Crockery emails user, "Yo. We can't send this email securely. Here are your options:"
* User hits reply-to (goes to unique email address on same domain) saying what to do:
* Cancel
* Retry
* Send just this email
* Whitelist shady.net (pins certificate)
* What other interactions might we like?
## What not
* JMAP. Not today.
* Maildir/mbox support. Imports and exports via IMAP, backups by "copy this file"
* Bring your own X (where X is MTA, IMAP server, database, etc, etc)
* Multiple-domain support (maybe later)
* CalDAV/Carddav (yet)
* Externally sourced accounts (yet)
* Sending email without having an account
* Managesieve (for now)
## Why
Email architecture is really complicated, and setting up a mail server of your
own is painful. Various projects exist to try to make it easier, eg:
* iRedMail
* docker-mailserver
* symbiosis
* ...
(Personally, I had some ansible recipes: )
Even among people who run their own websites, it's rare to run your own email.
It's just too painful. Much of this pain is caused by ultra-configurable
components that need to be orchestrated. Each has its own (remarkably obtuse)
configuration language, and each can do far more than the common case for a
small, single-domain site.
So, let's make a single binary where all you have to do is:
* Register a domain
* Upload some DNS records
* Run the binary
and it gives you a sensible email setup without any additional configuration!
If it has to share the domain with a HTTP server (quite common), allow the
HTTP-specific parts of mail to be served via reverse proxying. This includes:
* activesync push notifications
* autodiscovery
Probably other stuff. Email is big, and just keeps getting bigger.
## How
### Initialize a new database
```
crockery init -domain example.com
```
You'll be prompted to enter a password for the `postmaster@example.com` master
account, or you can provide on the command line: `-adminPassword x`, or in an
environment variable: `CROCKERY_PASSWORD=x`.
You can provide a custom database name with `-db <filename>`
### Run the server
```
./crockery
```
Again, you can use `-db <filename>` if the default of `./crockery.db` doesn't
suit.
### Configuration
Mostly non-existent, aside from that listed above.
We'll need to have a few *offline* commands baked into the `crockery` binary,
apart from `init` as detailed above. Some ideas:
```
$ crockery change x y z (domain, user password, etc)
$ crockery reindex (throw away existing indexes, regenerate)
$ crockery whitelist tls <domain> (allow the domain to be sent to with insecure/no TLS)
$ crockery whitelist receipt <domain> (bypass antispam for this domain)
$ crockery blacklist receipt <domain> (no emails to be permitted from this domain
```
It's inconvenient for some of these functions to require the server to be
offline, so we'll need to provide another configuration interface too.
I like sending emails to it.
Since we're going to need a HTTP server anyway, we could expose admin functions
using that, if logged in as postmaster. It could also expose an API that the
`crockery X` commands could connect to, rather than hitting the store directly.
We can use the `.well-known` integration to avoid the need to configure there,
too.

46
cmd/crockery/main.go Normal file
View File

@@ -0,0 +1,46 @@
package main
import (
"context"
"log"
"os"
"os/signal"
"ur.gs/crockery/internal/services"
"ur.gs/crockery/internal/store"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
datastore, err := store.New(ctx, "crockery.db")
if err != nil {
log.Fatal("Couldn't open crockery.db:", err)
}
srv, err := services.New(ctx, datastore)
if err != nil {
log.Fatal("Couldn't start services:", err)
}
sig := make(chan os.Signal, 1)
done := make(chan bool)
signal.Notify(sig, os.Interrupt)
go func() {
s := <-sig
log.Print("Got signal: ", s)
cancel()
}()
go func() {
srv.Run()
close(done)
}()
<-done
log.Println("All services finished, exiting")
os.Exit(0)
}

62
internal/imap/server.go Normal file
View File

@@ -0,0 +1,62 @@
package imap
import (
"context"
"io"
"log"
imapbackend "github.com/emersion/go-imap/backend"
imapserver "github.com/emersion/go-imap/server"
"ur.gs/crockery/internal/store"
)
type Server interface {
Run()
io.Closer
}
func NewServer(cancel context.CancelFunc, datastore store.Interface, starttls bool) Server {
out := &concrete{
cancel: cancel,
store: datastore,
}
out.server = imapserver.New(out)
if starttls {
out.server.Addr = ":143"
} else {
out.server.Addr = ":993"
}
return out
}
type concrete struct {
cancel context.CancelFunc
store store.Interface
server *imapserver.Server
}
func (c *concrete) Run() {
if err := c.server.ListenAndServe(); err != nil {
log.Printf("Error serving IMAP %s: %v", c.server.Addr, err)
} else {
log.Printf("Stopped listening on IMAP %s", c.server.Addr)
}
c.cancel()
}
// backend implementation for go-smtp
func (c *concrete) Login(string, string) (imapbackend.User, error) {
return nil, nil
}
func (c *concrete) Close() error {
c.cancel() // FIXME: this doesn't touch the server
return nil
}

View File

@@ -0,0 +1,69 @@
package services
import (
"context"
"io"
"log"
"ur.gs/crockery/internal/imap"
"ur.gs/crockery/internal/smtp"
"ur.gs/crockery/internal/store"
)
type Interface interface {
Run() // Run the services
}
func New(ctx context.Context, datastore store.Interface) (Interface, error) {
ctx, cancelFunc := context.WithCancel(ctx)
return &concrete{
ctx: ctx,
cancel: cancelFunc,
store: datastore,
}, nil
}
type genServer interface {
Run()
io.Closer
}
type concrete struct {
ctx context.Context
cancel context.CancelFunc
store store.Interface
// This is only mutated in the Run() method
servers []genServer
}
// For now, bind unconditionally to these ports, on IPv4 + IPv6:
// 25 - incoming SMTP (STARTTLS)
// 587 - outgoing SMTP (STARTTLS)
// 143 - IMAP (STARTTLS)
// 993 - IMAP (TLS)
//
// Each listener gets incoming connections serviced against the passed-in store.
func (c *concrete) Run() {
c.servers = []genServer{
smtp.NewServer(c.cancel, c.store, false),
smtp.NewServer(c.cancel, c.store, true),
imap.NewServer(c.cancel, c.store, false),
imap.NewServer(c.cancel, c.store, true),
}
log.Print("Running all services")
for _, srv := range c.servers {
go srv.Run()
}
<-c.ctx.Done()
log.Print("Closing all services")
for _, srv := range c.servers {
srv.Close()
}
}

62
internal/smtp/server.go Normal file
View File

@@ -0,0 +1,62 @@
package smtp
import (
"context"
"io"
"log"
"github.com/emersion/go-smtp"
"ur.gs/crockery/internal/store"
)
type Server interface {
Run()
io.Closer
}
func NewServer(cancel context.CancelFunc, datastore store.Interface, submission bool) Server {
out := &concrete{
cancel: cancel,
store: datastore,
}
out.server = smtp.NewServer(out)
out.server.Domain = datastore.Domain()
if submission {
out.server.Addr = ":587"
} else {
out.server.Addr = ":25"
}
return out
}
type concrete struct {
cancel context.CancelFunc
store store.Interface
server *smtp.Server
}
func (c *concrete) Run() {
if err := c.server.ListenAndServe(); err != nil {
log.Printf("Error serving SMTP %s: %v", c.server.Addr, err)
} else {
log.Printf("Stopped listening on SMTP %s", c.server.Addr)
}
c.cancel()
}
// backend implementation for go-smtp
func (c *concrete) Login(string, string) (smtp.User, error) {
return nil, nil
}
func (c *concrete) Close() error {
c.cancel() // FIXME: this doesn't touch the server
return nil
}

21
internal/store/store.go Normal file
View File

@@ -0,0 +1,21 @@
package store
import (
"context"
)
type Interface interface {
Domain() string
}
func New(ctx context.Context, filename string) (Interface, error) {
return &concrete{domain: "example.com"}, nil
}
type concrete struct {
domain string
}
func (c *concrete) Domain() string {
return c.domain
}

28
vendor/github.com/emersion/go-imap/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,28 @@
# 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

5
vendor/github.com/emersion/go-imap/.travis.yml generated vendored Normal file
View File

@@ -0,0 +1,5 @@
language: go
go:
- 1.8
script: bash <(curl -sL https://gist.github.com/emersion/49d4dda535497002639626bd9e16480c/raw/codecov-go.sh)
after_script: bash <(curl -s https://codecov.io/bash)

23
vendor/github.com/emersion/go-imap/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,23 @@
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.

183
vendor/github.com/emersion/go-imap/README.md generated vendored Normal file
View File

@@ -0,0 +1,183 @@
# go-imap
[![GoDoc](https://godoc.org/github.com/emersion/go-imap?status.svg)](https://godoc.org/github.com/emersion/go-imap)
[![Build Status](https://travis-ci.org/emersion/go-imap.svg?branch=master)](https://travis-ci.org/emersion/go-imap)
[![Codecov](https://codecov.io/gh/emersion/go-imap/branch/master/graph/badge.svg)](https://codecov.io/gh/emersion/go-imap)
[![Go Report
Card](https://goreportcard.com/badge/github.com/emersion/go-imap)](https://goreportcard.com/report/github.com/emersion/go-imap)
[![Unstable](https://img.shields.io/badge/stability-unstable-yellow.svg)](https://github.com/emersion/stability-badges#unstable)
[![Gitter chat](https://badges.gitter.im/goimap/Lobby.svg)](https://gitter.im/goimap/Lobby)
An [IMAP4rev1](https://tools.ietf.org/html/rfc3501) library written in Go. It
can be used to build a client and/or a server.
```bash
go get github.com/emersion/go-imap/...
```
## Why?
Other IMAP implementations in Go:
* Require to make [many type assertions or conversions](https://github.com/emersion/neutron/blob/ca635850e2223d6cfe818664ef901fa6e3c1d859/backend/imap/util.go#L110)
* Are not idiomatic or are [ugly](https://github.com/jordwest/imap-server/blob/master/conn/commands.go#L53)
* Are [not pleasant to use](https://github.com/emersion/neutron/blob/ca635850e2223d6cfe818664ef901fa6e3c1d859/backend/imap/messages.go#L228)
* Implement a server _xor_ a client, not both
* Don't implement unilateral updates (i.e. the server can't notify clients for
new messages)
* Do not have a good test coverage
* Don't handle encoding and charset automatically
## Usage
### Client [![GoDoc](https://godoc.org/github.com/emersion/go-imap/client?status.svg)](https://godoc.org/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 substract if the result is > 0
from = mbox.Messages - 3
}
seqset := new(imap.SeqSet)
seqset.AddRange(from, to)
messages := make(chan *imap.Message, 10)
done = make(chan error, 1)
go func() {
done <- c.Fetch(seqset, []string{imap.EnvelopeMsgAttr}, messages)
}()
log.Println("Last 4 messages:")
for msg := range messages {
log.Println("* " + msg.Envelope.Subject)
}
if err := <-done; err != nil {
log.Fatal(err)
}
log.Println("Done!")
}
```
### Server [![GoDoc](https://godoc.org/github.com/emersion/go-imap/server?status.svg)](https://godoc.org/github.com/emersion/go-imap/server)
```go
package main
import (
"log"
"github.com/emersion/go-imap/server"
"github.com/emersion/go-imap/backend/memory"
)
func main() {
// Create a memory backend
be := memory.New()
// Create a new server
s := server.New(be)
s.Addr = ":1143"
// Since we will use this server for testing only, we can allow plain text
// authentication over unencrypted connections
s.AllowInsecureAuth = true
log.Println("Starting IMAP server at localhost:1143")
if err := s.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
```
You can now use `telnet localhost 1143` to manually connect to the server.
## Extending go-imap
### Extensions
Commands defined in IMAP extensions are available in other packages. See [the
wiki](https://github.com/emersion/go-imap/wiki/Using-extensions#using-client-extensions)
to learn how to use them.
* [APPENDLIMIT](https://github.com/emersion/go-imap-appendlimit)
* [COMPRESS](https://github.com/emersion/go-imap-compress)
* [ENABLE](https://github.com/emersion/go-imap-enable)
* [ID](https://github.com/ProtonMail/go-imap-id)
* [IDLE](https://github.com/emersion/go-imap-idle)
* [MOVE](https://github.com/emersion/go-imap-move)
* [QUOTA](https://github.com/emersion/go-imap-quota)
* [SPECIAL-USE](https://github.com/emersion/go-imap-specialuse)
* [UNSELECT](https://github.com/emersion/go-imap-unselect)
* [UIDPLUS](https://github.com/emersion/go-imap-uidplus)
### Server backends
* [Memory](https://github.com/emersion/go-imap/tree/master/backend/memory) (for testing)
* [Multi](https://github.com/emersion/go-imap-multi)
* [PGP](https://github.com/emersion/go-imap-pgp)
* [Proxy](https://github.com/emersion/go-imap-proxy)
### Related projects
* [go-message](https://github.com/emersion/go-message) - parsing and formatting MIME and mail messages
* [go-pgpmail](https://github.com/emersion/go-pgpmail) - decrypting and encrypting mails with OpenPGP
* [go-sasl](https://github.com/emersion/go-sasl) - sending and receiving SASL authentications
* [go-smtp](https://github.com/emersion/go-smtp) - building SMTP clients and servers
* [go-dkim](https://github.com/emersion/go-dkim) - creating and verifying DKIM signatures
## License
MIT

16
vendor/github.com/emersion/go-imap/backend/backend.go generated vendored Normal file
View File

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

78
vendor/github.com/emersion/go-imap/backend/mailbox.go generated vendored Normal file
View File

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

109
vendor/github.com/emersion/go-imap/backend/updates.go generated vendored Normal file
View File

@@ -0,0 +1,109 @@
package backend
import (
"github.com/emersion/go-imap"
)
// Update contains user and mailbox information about an unilateral backend
// update.
type Update struct {
// The user targeted by this update. If empty, all connected users will
// be notified.
Username string
// The mailbox targeted by this update. If empty, the update targets all
// mailboxes.
Mailbox string
// A channel that will be closed once the update has been processed.
done chan struct{}
}
// Done returns a channel that is closed when the update has been broadcast to
// all clients.
func (u *Update) Done() <-chan struct{} {
if u.done == nil {
u.done = make(chan struct{})
}
return u.done
}
// DoneUpdate marks an update as done.
// TODO: remove this function
func DoneUpdate(u *Update) {
if u.done != nil {
close(u.done)
}
}
// StatusUpdate is a status update. See RFC 3501 section 7.1 for a list of
// status responses.
type StatusUpdate struct {
Update
*imap.StatusResp
}
// MailboxUpdate is a mailbox update.
type MailboxUpdate struct {
Update
*imap.MailboxStatus
}
// MessageUpdate is a message update.
type MessageUpdate struct {
Update
*imap.Message
}
// ExpungeUpdate is an expunge update.
type ExpungeUpdate struct {
Update
SeqNum uint32
}
// Updater is a Backend that implements Updater is able to send unilateral
// backend updates. Backends not implementing this interface don't correctly
// send unilateral updates, for instance if a user logs in from two connections
// and deletes a message from one of them, the over is not aware that such a
// mesage has been deleted. More importantly, backends implementing Updater can
// notify the user for external updates such as new message notifications.
type Updater interface {
// Updates returns a set of channels where updates are sent to.
Updates() <-chan interface{}
}
// UpdaterMailbox is a Mailbox that implements UpdaterMailbox is able to poll
// updates for new messages or message status updates during a period of
// inactivity.
type UpdaterMailbox interface {
// Poll requests mailbox updates.
Poll() error
}
// WaitUpdates returns a channel that's closed when all provided updates have
// been dispatched to all clients. It panics if one of the provided value is
// not an update.
func WaitUpdates(updates ...interface{}) <-chan struct{} {
done := make(chan struct{})
var chs []<-chan struct{}
for _, u := range updates {
uu, ok := u.(interface {
Done() <-chan struct{}
})
if !ok {
panic("imap: cannot wait for update: provided value is not a valid update")
}
chs = append(chs, uu.Done())
}
go func() {
// Wait for all updates to be sent
for _, ch := range chs {
<-ch
}
close(done)
}()
return done
}

92
vendor/github.com/emersion/go-imap/backend/user.go generated vendored Normal file
View File

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

89
vendor/github.com/emersion/go-imap/command.go generated vendored Normal file
View File

@@ -0,0 +1,89 @@
package imap
import (
"errors"
"strings"
)
// IMAP4rev1 commands.
const (
Capability string = "CAPABILITY"
Noop = "NOOP"
Logout = "LOGOUT"
StartTLS = "STARTTLS"
Authenticate = "AUTHENTICATE"
Login = "LOGIN"
Select = "SELECT"
Examine = "EXAMINE"
Create = "CREATE"
Delete = "DELETE"
Rename = "RENAME"
Subscribe = "SUBSCRIBE"
Unsubscribe = "UNSUBSCRIBE"
List = "LIST"
Lsub = "LSUB"
Status = "STATUS"
Append = "APPEND"
Check = "CHECK"
Close = "CLOSE"
Expunge = "EXPUNGE"
Search = "SEARCH"
Fetch = "FETCH"
Store = "STORE"
Copy = "COPY"
Uid = "UID"
)
// A value that can be converted to a command.
type Commander interface {
Command() *Command
}
// A command.
type Command struct {
// The command tag. It acts as a unique identifier for this command. If empty,
// the command is untagged.
Tag string
// The command name.
Name string
// The command arguments.
Arguments []interface{}
}
// Implements the Commander interface.
func (cmd *Command) Command() *Command {
return cmd
}
func (cmd *Command) WriteTo(w *Writer) error {
tag := cmd.Tag
if tag == "" {
tag = "*"
}
fields := []interface{}{tag, cmd.Name}
fields = append(fields, cmd.Arguments...)
return w.writeLine(fields...)
}
// Parse a command from fields.
func (cmd *Command) Parse(fields []interface{}) error {
if len(fields) < 2 {
return errors.New("imap: cannot parse command: no enough fields")
}
var ok bool
if cmd.Tag, ok = fields[0].(string); !ok {
return errors.New("imap: cannot parse command: invalid tag")
}
if cmd.Name, ok = fields[1].(string); !ok {
return errors.New("imap: cannot parse command: invalid name")
}
cmd.Name = strings.ToUpper(cmd.Name) // Command names are case-insensitive
cmd.Arguments = fields[2:]
return nil
}

95
vendor/github.com/emersion/go-imap/commands/append.go generated vendored Normal file
View File

@@ -0,0 +1,95 @@
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.Encoder.String(cmd.Mailbox)
args = append(args, mailbox)
if cmd.Flags != nil {
flags := make([]interface{}, len(cmd.Flags))
for i, flag := range cmd.Flags {
flags[i] = flag
}
args = append(args, flags)
}
if !cmd.Date.IsZero() {
args = append(args, cmd.Date)
}
args = append(args, cmd.Message)
return &imap.Command{
Name: imap.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, ok := fields[0].(string); !ok {
return errors.New("Mailbox name must be a string")
} else if mailbox, err = utf7.Decoder.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 {
date, ok := fields[0].(string)
if !ok {
return errors.New("Date must be a string")
}
if cmd.Date, err = time.Parse(imap.DateTimeLayout, date); err != nil {
return err
}
}
}
return
}

View File

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

View File

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

18
vendor/github.com/emersion/go-imap/commands/check.go generated vendored Normal file
View File

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

18
vendor/github.com/emersion/go-imap/commands/close.go generated vendored Normal file
View File

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

View File

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

47
vendor/github.com/emersion/go-imap/commands/copy.go generated vendored Normal file
View File

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

38
vendor/github.com/emersion/go-imap/commands/create.go generated vendored Normal file
View File

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

38
vendor/github.com/emersion/go-imap/commands/delete.go generated vendored Normal file
View File

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

16
vendor/github.com/emersion/go-imap/commands/expunge.go generated vendored Normal file
View File

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

70
vendor/github.com/emersion/go-imap/commands/fetch.go generated vendored Normal file
View File

@@ -0,0 +1,70 @@
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 []string
}
func (cmd *Fetch) Command() *imap.Command {
items := make([]interface{}, len(cmd.Items))
for i, item := range cmd.Items {
if section, err := imap.NewBodySectionName(item); err == nil {
items[i] = section
} else {
items[i] = item
}
}
return &imap.Command{
Name: imap.Fetch,
Arguments: []interface{}{cmd.SeqSet, items},
}
}
func (cmd *Fetch) Parse(fields []interface{}) error {
if len(fields) < 2 {
return errors.New("No enough arguments")
}
seqset, ok := fields[0].(string)
if !ok {
return errors.New("Sequence set must be a string")
}
var err error
if cmd.SeqSet, err = imap.NewSeqSet(seqset); err != nil {
return err
}
switch items := fields[1].(type) {
case string: // A macro or a single item
switch strings.ToUpper(items) {
case "ALL":
cmd.Items = []string{"FLAGS", "INTERNALDATE", "RFC822.SIZE", "ENVELOPE"}
case "FAST":
cmd.Items = []string{"FLAGS", "INTERNALDATE", "RFC822.SIZE"}
case "FULL":
cmd.Items = []string{"FLAGS", "INTERNALDATE", "RFC822.SIZE", "ENVELOPE", "BODY"}
default:
cmd.Items = []string{strings.ToUpper(items)}
}
case []interface{}: // A list of items
cmd.Items = make([]string, len(items))
for i, v := range items {
item, _ := v.(string)
cmd.Items[i] = strings.ToUpper(item)
}
default:
return errors.New("Items must be either a string or a list")
}
return nil
}

57
vendor/github.com/emersion/go-imap/commands/list.go generated vendored Normal file
View File

@@ -0,0 +1,57 @@
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 := imap.List
if cmd.Subscribed {
name = imap.Lsub
}
ref, _ := utf7.Encoder.String(cmd.Reference)
mailbox, _ := utf7.Encoder.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")
}
if mailbox, ok := fields[0].(string); !ok {
return errors.New("Reference name must be a string")
} else if mailbox, err := utf7.Decoder.String(mailbox); err != nil {
return err
} else {
// TODO: canonical mailbox path
cmd.Reference = imap.CanonicalMailboxName(mailbox)
}
if mailbox, ok := fields[1].(string); !ok {
return errors.New("Mailbox name must be a string")
} else if mailbox, err := utf7.Decoder.String(mailbox); err != nil {
return err
} else {
cmd.Mailbox = imap.CanonicalMailboxName(mailbox)
}
return nil
}

36
vendor/github.com/emersion/go-imap/commands/login.go generated vendored Normal file
View File

@@ -0,0 +1,36 @@
package commands
import (
"errors"
"github.com/emersion/go-imap"
)
// Login is a LOGIN command, as defined in RFC 3501 section 6.2.2.
type Login struct {
Username string
Password string
}
func (cmd *Login) Command() *imap.Command {
return &imap.Command{
Name: imap.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 ok bool
if cmd.Username, ok = fields[0].(string); !ok {
return errors.New("Username is not a string")
}
if cmd.Password, ok = fields[1].(string); !ok {
return errors.New("Password is not a string")
}
return nil
}

18
vendor/github.com/emersion/go-imap/commands/logout.go generated vendored Normal file
View File

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

18
vendor/github.com/emersion/go-imap/commands/noop.go generated vendored Normal file
View File

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

48
vendor/github.com/emersion/go-imap/commands/rename.go generated vendored Normal file
View File

@@ -0,0 +1,48 @@
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 {
existingName, _ := utf7.Encoder.String(cmd.Existing)
newName, _ := utf7.Encoder.String(cmd.New)
return &imap.Command{
Name: imap.Rename,
Arguments: []interface{}{existingName, newName},
}
}
func (cmd *Rename) Parse(fields []interface{}) error {
if len(fields) < 2 {
return errors.New("No enough arguments")
}
if existingName, ok := fields[0].(string); !ok {
return errors.New("Mailbox name must be a string")
} else if existingName, err := utf7.Decoder.String(existingName); err != nil {
return err
} else {
cmd.Existing = imap.CanonicalMailboxName(existingName)
}
if newName, ok := fields[1].(string); !ok {
return errors.New("Mailbox name must be a string")
} else if newName, err := utf7.Decoder.String(newName); err != nil {
return err
} else {
cmd.New = imap.CanonicalMailboxName(newName)
}
return nil
}

57
vendor/github.com/emersion/go-imap/commands/search.go generated vendored Normal file
View File

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

45
vendor/github.com/emersion/go-imap/commands/select.go generated vendored Normal file
View File

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

View File

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

55
vendor/github.com/emersion/go-imap/commands/status.go generated vendored Normal file
View File

@@ -0,0 +1,55 @@
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 []string
}
func (cmd *Status) Command() *imap.Command {
mailbox, _ := utf7.Encoder.String(cmd.Mailbox)
items := make([]interface{}, len(cmd.Items))
for i, f := range cmd.Items {
items[i] = f
}
return &imap.Command{
Name: imap.Status,
Arguments: []interface{}{mailbox, items},
}
}
func (cmd *Status) Parse(fields []interface{}) error {
if len(fields) < 2 {
return errors.New("No enough arguments")
}
if mailbox, ok := fields[0].(string); !ok {
return errors.New("Mailbox name must be a string")
} else if mailbox, err := utf7.Decoder.String(mailbox); err != nil {
return err
} else {
cmd.Mailbox = imap.CanonicalMailboxName(mailbox)
}
if items, ok := fields[1].([]interface{}); !ok {
return errors.New("Items must be a list")
} else {
cmd.Items = make([]string, len(items))
for i, v := range items {
item, _ := v.(string)
cmd.Items[i] = strings.ToUpper(item)
}
}
return nil
}

45
vendor/github.com/emersion/go-imap/commands/store.go generated vendored Normal file
View File

@@ -0,0 +1,45 @@
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 string
Value interface{}
}
func (cmd *Store) Command() *imap.Command {
return &imap.Command{
Name: imap.Store,
Arguments: []interface{}{cmd.SeqSet, cmd.Item, cmd.Value},
}
}
func (cmd *Store) Parse(fields []interface{}) (err error) {
if len(fields) < 3 {
return errors.New("No enough arguments")
}
seqset, ok := fields[0].(string)
if !ok {
return errors.New("Invalid sequence set")
}
if cmd.SeqSet, err = imap.NewSeqSet(seqset); err != nil {
return err
}
if cmd.Item, ok = fields[1].(string); !ok {
return errors.New("Item name must be a string")
}
cmd.Item = strings.ToUpper(cmd.Item)
cmd.Value = fields[2]
return
}

View File

@@ -0,0 +1,71 @@
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.Encoder.String(cmd.Mailbox)
return &imap.Command{
Name: imap.Subscribe,
Arguments: []interface{}{mailbox},
}
}
func (cmd *Subscribe) Parse(fields []interface{}) (err error) {
if len(fields) < 0 {
return errors.New("No enogh arguments")
}
mailbox, ok := fields[0].(string)
if !ok {
return errors.New("Mailbox name must be a string")
}
if cmd.Mailbox, err = utf7.Decoder.String(mailbox); err != nil {
return err
}
return
}
// An UNSUBSCRIBE command.
// See RFC 3501 section 6.3.7
type Unsubscribe struct {
Mailbox string
}
func (cmd *Unsubscribe) Command() *imap.Command {
mailbox, _ := utf7.Encoder.String(cmd.Mailbox)
return &imap.Command{
Name: imap.Unsubscribe,
Arguments: []interface{}{mailbox},
}
}
func (cmd *Unsubscribe) Parse(fields []interface{}) (err error) {
if len(fields) < 0 {
return errors.New("No enogh arguments")
}
mailbox, ok := fields[0].(string)
if !ok {
return errors.New("Mailbox name must be a string")
}
if cmd.Mailbox, err = utf7.Decoder.String(mailbox); err != nil {
return err
}
return
}

44
vendor/github.com/emersion/go-imap/commands/uid.go generated vendored Normal file
View File

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

197
vendor/github.com/emersion/go-imap/conn.go generated vendored Normal file
View File

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

72
vendor/github.com/emersion/go-imap/date.go generated vendored Normal file
View File

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

121
vendor/github.com/emersion/go-imap/handle.go generated vendored Normal file
View File

@@ -0,0 +1,121 @@
package imap
import (
"sync"
)
// A response that can be either accepted or rejected by a handler.
type RespHandle struct {
Resp interface{}
Accepts chan bool
}
// Accept this response. This means that the handler will process it.
func (h *RespHandle) Accept() {
h.Accepts <- true
}
// Reject this response. The handler cannot process it.
func (h *RespHandle) Reject() {
h.Accepts <- false
}
// Accept this response if it has the specified name. If not, reject it.
func (h *RespHandle) AcceptNamedResp(name string) (fields []interface{}, accepted bool) {
res, ok := h.Resp.(*Resp)
if !ok || len(res.Fields) == 0 {
h.Reject()
return
}
n, ok := res.Fields[0].(string)
if !ok || n != name {
h.Reject()
return
}
h.Accept()
fields = res.Fields[1:]
accepted = true
return
}
// Delivers responses to handlers.
type RespHandler chan *RespHandle
// Handles responses from a handler.
type RespHandlerFrom interface {
HandleFrom(hdlr RespHandler) error
}
// A RespHandlerFrom that forwards responses to multiple RespHandler.
type MultiRespHandler struct {
handlers []RespHandler
locker sync.Locker
}
func NewMultiRespHandler() *MultiRespHandler {
return &MultiRespHandler{
locker: &sync.Mutex{},
}
}
func (mh *MultiRespHandler) HandleFrom(ch RespHandler) error {
for rh := range ch {
mh.locker.Lock()
accepted := false
for i := len(mh.handlers) - 1; i >= 0; i-- {
hdlr := mh.handlers[i]
rh := &RespHandle{
Resp: rh.Resp,
Accepts: make(chan bool),
}
hdlr <- rh
if accepted = <-rh.Accepts; accepted {
break
}
}
mh.locker.Unlock()
if accepted {
rh.Accept()
} else {
rh.Reject()
}
}
mh.locker.Lock()
for _, hdlr := range mh.handlers {
close(hdlr)
}
mh.handlers = nil
mh.locker.Unlock()
return nil
}
func (mh *MultiRespHandler) Add(hdlr RespHandler) {
if hdlr == nil {
return
}
mh.locker.Lock()
mh.handlers = append(mh.handlers, hdlr)
mh.locker.Unlock()
}
func (mh *MultiRespHandler) Del(hdlr RespHandler) {
mh.locker.Lock()
for i, h := range mh.handlers {
if h == hdlr {
close(hdlr)
mh.handlers = append(mh.handlers[:i], mh.handlers[i+1:]...)
}
}
mh.locker.Unlock()
}

28
vendor/github.com/emersion/go-imap/imap.go generated vendored Normal file
View File

@@ -0,0 +1,28 @@
// Package imap implements IMAP4rev1 (RFC 3501).
package imap
import (
"io"
)
// 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"
// 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)

13
vendor/github.com/emersion/go-imap/literal.go generated vendored Normal file
View File

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

8
vendor/github.com/emersion/go-imap/logger.go generated vendored Normal file
View File

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

246
vendor/github.com/emersion/go-imap/mailbox.go generated vendored Normal file
View File

@@ -0,0 +1,246 @@
package imap
import (
"errors"
"strings"
"sync"
"github.com/emersion/go-imap/utf7"
)
// The primary mailbox, as defined in RFC 3501 section 5.1.
const InboxName = "INBOX"
// 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"
)
// 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")
}
info.Attributes, _ = ParseStringList(fields[0])
info.Delimiter, _ = fields[1].(string)
name, _ := fields[2].(string)
info.Name, _ = utf7.Decoder.String(name)
info.Name = CanonicalMailboxName(info.Name)
return nil
}
// Format mailbox info to fields.
func (info *MailboxInfo) Format() []interface{} {
name, _ := utf7.Encoder.String(info.Name)
// Thunderbird doesn't understand delimiters if not quoted
return []interface{}{FormatStringList(info.Attributes), Quoted(info.Delimiter), 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 strings.HasPrefix(pattern, info.Delimiter) {
reference = ""
pattern = strings.TrimPrefix(pattern, info.Delimiter)
}
if reference != "" {
if !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)
}
// Mailbox status items.
const (
MailboxFlags = "FLAGS"
MailboxPermanentFlags = "PERMANENTFLAGS"
// Defined in RFC 3501 section 6.3.10.
MailboxMessages = "MESSAGES"
MailboxRecent = "RECENT"
MailboxUnseen = "UNSEEN"
MailboxUidNext = "UIDNEXT"
MailboxUidValidity = "UIDVALIDITY"
)
// 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[string]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 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
}
// Create a new mailbox status that will contain the specified items.
func NewMailboxStatus(name string, items []string) *MailboxStatus {
status := &MailboxStatus{
Name: name,
Items: make(map[string]interface{}),
}
for _, k := range items {
status.Items[k] = nil
}
return status
}
func (status *MailboxStatus) Parse(fields []interface{}) error {
status.Items = make(map[string]interface{})
var k string
for i, f := range fields {
if i%2 == 0 {
var ok bool
if k, ok = f.(string); !ok {
return errors.New("Key is not a string")
}
k = strings.ToUpper(k)
} else {
status.Items[k] = nil
var err error
switch k {
case MailboxMessages:
status.Messages, err = ParseNumber(f)
case MailboxRecent:
status.Recent, err = ParseNumber(f)
case MailboxUnseen:
status.Unseen, err = ParseNumber(f)
case MailboxUidNext:
status.UidNext, err = ParseNumber(f)
case MailboxUidValidity:
status.UidValidity, 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 MailboxMessages:
v = status.Messages
case MailboxRecent:
v = status.Recent
case MailboxUnseen:
v = status.Unseen
case MailboxUidNext:
v = status.UidNext
case MailboxUidValidity:
v = status.UidValidity
}
fields = append(fields, k, v)
}
return fields
}

1048
vendor/github.com/emersion/go-imap/message.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

430
vendor/github.com/emersion/go-imap/read.go generated vendored Normal file
View File

@@ -0,0 +1,430 @@
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
}
// Convert a field to a number.
func ParseNumber(f interface{}) (uint32, error) {
// Useful for tests
if n, ok := f.(uint32); ok {
return n, nil
}
s, ok := f.(string)
if !ok {
return 0, newParseError("number is not a string")
}
nbr, err := strconv.ParseUint(s, 10, 32)
if err != nil {
return 0, &parseError{err}
}
return uint32(nbr), nil
}
// Convert a field list to a string list.
func ParseStringList(f interface{}) ([]string, error) {
fields, ok := f.([]interface{})
if !ok {
return nil, newParseError("string list is not a list")
}
list := make([]string, len(fields))
for i, f := range fields {
var ok bool
if list[i], ok = f.(string); !ok {
return nil, newParseError("string list contains a non-string")
}
}
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("not a space")
}
return nil
}
func (r *Reader) ReadCrlf() (err error) {
var char rune
if char, _, err = r.ReadRune(); err != nil {
return
}
if char != cr {
err = newParseError("line doesn't end with a CR")
}
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 {
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 == "NIL" {
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)
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 {
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 == listEnd || char == respCodeEnd {
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 string, 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
}
code, ok := fields[0].(string)
if !ok {
err = newParseError("response code doesn't start with a string atom")
return
}
if code == "" {
err = newParseError("response code is empty")
return
}
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(cr))
if err != nil {
return
}
info = strings.TrimSuffix(info, string(cr))
info = strings.TrimLeft(info, " ")
var char rune
if char, _, err = r.ReadRune(); err != nil {
return
}
if char != lf {
err = newParseError("line doesn't end with a LF")
}
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
}

156
vendor/github.com/emersion/go-imap/response.go generated vendored Normal file
View File

@@ -0,0 +1,156 @@
package imap
import (
"errors"
)
// A value that can be converted to a Resp.
type Responser interface {
Response() *Resp
}
// A response.
// See RFC 3501 section 2.2.2
type Resp 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{}
}
func (r *Resp) WriteTo(w *Writer) error {
tag := r.Tag
if tag == "" {
tag = "*"
}
fields := []interface{}{tag}
fields = append(fields, r.Fields...)
return w.writeLine(fields...)
}
// Create a new untagged response.
func NewUntaggedResp(fields []interface{}) *Resp {
return &Resp{
Tag: "*",
Fields: fields,
}
}
// A continuation request.
type ContinuationResp struct {
// The info message sent with the continuation request.
Info string
}
func (r *ContinuationResp) 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()
}
// Read a single response from a Reader. Returns either a continuation request,
// a status response or a raw response.
func ReadResp(r *Reader) (out interface{}, err error) {
atom, err := r.ReadAtom()
if err != nil {
return
}
tag, ok := atom.(string)
if !ok {
err = errors.New("Response tag is not an atom")
return
}
if tag == "+" {
if err := r.ReadSp(); err != nil {
r.UnreadRune()
}
res := &ContinuationResp{}
res.Info, err = r.ReadInfo()
if err != nil {
return
}
out = res
return
}
if err = r.ReadSp(); err != nil {
return
}
// Can be either data or status
// Try to parse a status
isStatus := false
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)
if status == StatusOk || status == StatusNo || status == StatusBad || status == StatusPreauth || status == StatusBye {
isStatus = true
res := &StatusResp{
Tag: tag,
Type: status,
}
var char rune
if char, _, err = r.ReadRune(); err != nil {
return
}
r.UnreadRune()
if char == '[' {
// Contains code & arguments
res.Code, res.Arguments, err = r.ReadRespCode()
if err != nil {
return
}
}
res.Info, err = r.ReadInfo()
if err != nil {
return
}
out = res
}
}
} else {
r.UnreadRune()
}
} else {
r.UnreadRune()
}
if !isStatus {
// Not a status so it's data
res := &Resp{Tag: tag}
var remaining []interface{}
remaining, err = r.ReadLine()
if err != nil {
return
}
res.Fields = append(fields, remaining...)
out = res
}
return
}

View File

@@ -0,0 +1,72 @@
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
Writer *imap.Writer
}
func (r *Authenticate) HandleFrom(hdlr imap.RespHandler) (err error) {
w := r.Writer
// Cancel auth if an error occurs
defer (func() {
if err != nil {
w.Write([]byte("*\r\n"))
w.Flush()
}
})()
for h := range hdlr {
cont, ok := h.Resp.(*imap.ContinuationResp)
if !ok {
h.Reject()
continue
}
h.Accept()
// 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 = w.Write([]byte(encoded + "\r\n")); err != nil {
return
}
if err = w.Flush(); err != nil {
return
}
r.InitialResponse = nil
continue
}
var challenge []byte
challenge, err = base64.StdEncoding.DecodeString(cont.Info)
if err != nil {
return
}
var res []byte
res, err = r.Mechanism.Next(challenge)
if err != nil {
return
}
encoded := base64.StdEncoding.EncodeToString(res)
if _, err = w.Write([]byte(encoded + "\r\n")); err != nil {
return
}
if err = w.Flush(); err != nil {
return
}
}
return
}

View File

@@ -0,0 +1,39 @@
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) HandleFrom(hdlr imap.RespHandler) (err error) {
for h := range hdlr {
caps, ok := h.AcceptNamedResp(imap.Capability)
if !ok {
continue
}
r.Caps = make([]string, len(caps))
for i, c := range caps {
r.Caps[i], _ = c.(string)
}
return
}
return
}
func (r *Capability) WriteTo(w *imap.Writer) error {
fields := []interface{}{imap.Capability}
for _, cap := range r.Caps {
fields = append(fields, cap)
}
res := &imap.Resp{Fields: fields}
return res.WriteTo(w)
}

View File

@@ -0,0 +1,45 @@
package responses
import (
"github.com/emersion/go-imap"
)
// An EXPUNGE response.
// See RFC 3501 section 7.4.1
type Expunge struct {
SeqNums chan uint32
}
func (r *Expunge) HandleFrom(hdlr imap.RespHandler) error {
defer close(r.SeqNums)
for h := range hdlr {
res, ok := h.Resp.(*imap.Resp)
if !ok || len(res.Fields) < 3 {
h.Reject()
continue
}
if name, ok := res.Fields[1].(string); !ok || name != imap.Expunge {
h.Reject()
continue
}
h.Accept()
seqNum, _ := imap.ParseNumber(res.Fields[0])
r.SeqNums <- seqNum
}
return nil
}
func (r *Expunge) WriteTo(w *imap.Writer) error {
for seqNum := range r.SeqNums {
res := imap.NewUntaggedResp([]interface{}{seqNum, imap.Expunge})
if err := res.WriteTo(w); err != nil {
return err
}
}
return nil
}

55
vendor/github.com/emersion/go-imap/responses/fetch.go generated vendored Normal file
View File

@@ -0,0 +1,55 @@
package responses
import (
"github.com/emersion/go-imap"
)
// A FETCH response.
// See RFC 3501 section 7.4.2
type Fetch struct {
Messages chan *imap.Message
}
func (r *Fetch) HandleFrom(hdlr imap.RespHandler) error {
defer close(r.Messages)
for h := range hdlr {
res, ok := h.Resp.(*imap.Resp)
if !ok || len(res.Fields) < 3 {
h.Reject()
continue
}
if name, ok := res.Fields[1].(string); !ok || name != imap.Fetch {
h.Reject()
continue
}
h.Accept()
seqNum, _ := imap.ParseNumber(res.Fields[0])
fields, _ := res.Fields[2].([]interface{})
msg := &imap.Message{
SeqNum: seqNum,
}
if err := msg.Parse(fields); err != nil {
return err
}
r.Messages <- msg
}
return nil
}
func (r *Fetch) WriteTo(w *imap.Writer) error {
for msg := range r.Messages {
res := imap.NewUntaggedResp([]interface{}{msg.SeqNum, imap.Fetch, msg.Format()})
if err := res.WriteTo(w); err != nil {
return err
}
}
return nil
}

59
vendor/github.com/emersion/go-imap/responses/list.go generated vendored Normal file
View File

@@ -0,0 +1,59 @@
package responses
import (
"github.com/emersion/go-imap"
)
// 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 imap.Lsub
} else {
return imap.List
}
}
func (r *List) HandleFrom(hdlr imap.RespHandler) error {
defer close(r.Mailboxes)
name := r.Name()
for h := range hdlr {
fields, ok := h.AcceptNamedResp(name)
if !ok {
continue
}
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 {
name := r.Name()
for mbox := range r.Mailboxes {
fields := []interface{}{name}
fields = append(fields, mbox.Format()...)
res := imap.NewUntaggedResp(fields)
if err := res.WriteTo(w); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,2 @@
// IMAP responses defined in RFC 3501.
package responses

37
vendor/github.com/emersion/go-imap/responses/search.go generated vendored Normal file
View File

@@ -0,0 +1,37 @@
package responses
import (
"github.com/emersion/go-imap"
)
// A SEARCH response.
// See RFC 3501 section 7.2.5
type Search struct {
Ids []uint32
}
func (r *Search) HandleFrom(hdlr imap.RespHandler) (err error) {
for h := range hdlr {
fields, ok := h.AcceptNamedResp(imap.Search)
if !ok {
continue
}
for _, f := range fields {
id, _ := imap.ParseNumber(f)
r.Ids = append(r.Ids, id)
}
}
return
}
func (r *Search) WriteTo(w *imap.Writer) (err error) {
fields := []interface{}{imap.Search}
for _, id := range r.Ids {
fields = append(fields, id)
}
res := imap.NewUntaggedResp(fields)
return res.WriteTo(w)
}

145
vendor/github.com/emersion/go-imap/responses/select.go generated vendored Normal file
View File

@@ -0,0 +1,145 @@
package responses
import (
"fmt"
"github.com/emersion/go-imap"
)
// A SELECT response.
type Select struct {
Mailbox *imap.MailboxStatus
}
func (r *Select) HandleFrom(hdlr imap.RespHandler) (err error) {
if r.Mailbox == nil {
r.Mailbox = &imap.MailboxStatus{}
}
mbox := r.Mailbox
mbox.Items = make(map[string]interface{})
for h := range hdlr {
switch res := h.Resp.(type) {
case *imap.Resp:
fields, ok := h.AcceptNamedResp(imap.MailboxFlags)
if !ok {
break
}
flags, _ := fields[0].([]interface{})
mbox.Flags, _ = imap.ParseStringList(flags)
mbox.ItemsLocker.Lock()
mbox.Items[imap.MailboxFlags] = nil
mbox.ItemsLocker.Unlock()
case *imap.StatusResp:
if len(res.Arguments) < 1 {
h.Accepts <- false
break
}
accepted := true
switch res.Code {
case imap.MailboxUnseen:
mbox.Unseen, _ = imap.ParseNumber(res.Arguments[0])
mbox.ItemsLocker.Lock()
mbox.Items[imap.MailboxUnseen] = nil
mbox.ItemsLocker.Unlock()
case imap.MailboxPermanentFlags:
flags, _ := res.Arguments[0].([]interface{})
mbox.PermanentFlags, _ = imap.ParseStringList(flags)
mbox.ItemsLocker.Lock()
mbox.Items[imap.MailboxPermanentFlags] = nil
mbox.ItemsLocker.Unlock()
case imap.MailboxUidNext:
mbox.UidNext, _ = imap.ParseNumber(res.Arguments[0])
mbox.ItemsLocker.Lock()
mbox.Items[imap.MailboxUidNext] = nil
mbox.ItemsLocker.Unlock()
case imap.MailboxUidValidity:
mbox.UidValidity, _ = imap.ParseNumber(res.Arguments[0])
mbox.ItemsLocker.Lock()
mbox.Items[imap.MailboxUidValidity] = nil
mbox.ItemsLocker.Unlock()
default:
accepted = false
}
h.Accepts <- accepted
}
}
return
}
func (r *Select) WriteTo(w *imap.Writer) (err error) {
status := r.Mailbox
for k := range r.Mailbox.Items {
switch k {
case imap.MailboxFlags:
flags := make([]interface{}, len(status.Flags))
for i, f := range status.Flags {
flags[i] = f
}
res := imap.NewUntaggedResp([]interface{}{"FLAGS", flags})
if err = res.WriteTo(w); err != nil {
return
}
case imap.MailboxPermanentFlags:
flags := make([]interface{}, len(status.PermanentFlags))
for i, f := range status.PermanentFlags {
flags[i] = f
}
statusRes := &imap.StatusResp{
Type: imap.StatusOk,
Code: imap.CodePermanentFlags,
Arguments: []interface{}{flags},
Info: "Flags permitted.",
}
if err = statusRes.WriteTo(w); err != nil {
return
}
case imap.MailboxMessages:
res := imap.NewUntaggedResp([]interface{}{status.Messages, "EXISTS"})
if err = res.WriteTo(w); err != nil {
return
}
case imap.MailboxRecent:
res := imap.NewUntaggedResp([]interface{}{status.Recent, "RECENT"})
if err = res.WriteTo(w); err != nil {
return
}
case imap.MailboxUnseen:
statusRes := &imap.StatusResp{
Type: imap.StatusOk,
Code: imap.CodeUnseen,
Arguments: []interface{}{status.Unseen},
Info: fmt.Sprintf("Message %d is first unseen", status.Unseen),
}
if err = statusRes.WriteTo(w); err != nil {
return
}
case imap.MailboxUidNext:
statusRes := &imap.StatusResp{
Type: imap.StatusOk,
Code: imap.CodeUidNext,
Arguments: []interface{}{status.UidNext},
Info: "Predicted next UID",
}
if err = statusRes.WriteTo(w); err != nil {
return
}
case imap.MailboxUidValidity:
statusRes := &imap.StatusResp{
Type: imap.StatusOk,
Code: imap.CodeUidValidity,
Arguments: []interface{}{status.UidValidity},
Info: "UIDs valid",
}
if err = statusRes.WriteTo(w); err != nil {
return
}
}
}
return
}

63
vendor/github.com/emersion/go-imap/responses/status.go generated vendored Normal file
View File

@@ -0,0 +1,63 @@
package responses
import (
"errors"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/utf7"
)
// A STATUS response.
// See RFC 3501 section 7.2.4
type Status struct {
Mailbox *imap.MailboxStatus
}
func (r *Status) HandleFrom(hdlr imap.RespHandler) error {
if r.Mailbox == nil {
r.Mailbox = &imap.MailboxStatus{}
}
mbox := r.Mailbox
mbox.Items = nil
for h := range hdlr {
fields, ok := h.AcceptNamedResp(imap.Status)
if !ok {
continue
}
if len(fields) < 2 {
return errors.New("STATUS response expects two fields")
}
name, ok := fields[0].(string)
if !ok {
return errors.New("STATUS response expects a string as first argument")
}
mbox.Name, _ = utf7.Decoder.String(name)
mbox.Name = imap.CanonicalMailboxName(mbox.Name)
var items []interface{}
if items, ok = fields[1].([]interface{}); !ok {
return errors.New("STATUS response expects a list as second argument")
}
if err := mbox.Parse(items); err != nil {
return err
}
}
return nil
}
func (r *Status) WriteTo(w *imap.Writer) error {
mbox := r.Mailbox
fields := []interface{}{imap.Status, mbox.Name, mbox.Format()}
res := imap.NewUntaggedResp(fields)
if err := res.WriteTo(w); err != nil {
return err
}
return nil
}

367
vendor/github.com/emersion/go-imap/search.go generated vendored Normal file
View File

@@ -0,0 +1,367 @@
package imap
import (
"errors"
"fmt"
"io"
"io/ioutil"
"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, err := ioutil.ReadAll(r)
if 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 = NewSeqSet(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 = NewSeqSet(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, "UID", c.Uid)
}
if !c.Since.IsZero() && !c.Before.IsZero() && c.Before.Sub(c.Since) == 24*time.Hour {
fields = append(fields, "ON", searchDate(c.Since))
} else {
if !c.Since.IsZero() {
fields = append(fields, "SINCE", searchDate(c.Since))
}
if !c.Before.IsZero() {
fields = append(fields, "BEFORE", searchDate(c.Before))
}
}
if !c.SentSince.IsZero() && !c.SentBefore.IsZero() && c.SentBefore.Sub(c.SentSince) == 24*time.Hour {
fields = append(fields, "SENTON", searchDate(c.SentSince))
} else {
if !c.SentSince.IsZero() {
fields = append(fields, "SENTSINCE", searchDate(c.SentSince))
}
if !c.SentBefore.IsZero() {
fields = append(fields, "SENTBEFORE", searchDate(c.SentBefore))
}
}
for key, values := range c.Header {
var prefields []interface{}
switch key {
case "Bcc", "Cc", "From", "Subject", "To":
prefields = []interface{}{strings.ToUpper(key)}
default:
prefields = []interface{}{"HEADER", key}
}
for _, value := range values {
fields = append(fields, prefields...)
fields = append(fields, value)
}
}
for _, value := range c.Body {
fields = append(fields, "BODY", value)
}
for _, value := range c.Text {
fields = append(fields, "TEXT", value)
}
for _, flag := range c.WithFlags {
var subfields []interface{}
switch flag {
case AnsweredFlag, DeletedFlag, DraftFlag, FlaggedFlag, RecentFlag, SeenFlag:
subfields = []interface{}{strings.ToUpper(strings.TrimPrefix(flag, "\\"))}
default:
subfields = []interface{}{"KEYWORD", flag}
}
fields = append(fields, subfields...)
}
for _, flag := range c.WithoutFlags {
var subfields []interface{}
switch flag {
case AnsweredFlag, DeletedFlag, DraftFlag, FlaggedFlag, SeenFlag:
subfields = []interface{}{"UN" + strings.ToUpper(strings.TrimPrefix(flag, "\\"))}
case RecentFlag:
subfields = []interface{}{"OLD"}
default:
subfields = []interface{}{"UNKEYWORD", flag}
}
fields = append(fields, subfields...)
}
if c.Larger > 0 {
fields = append(fields, "LARGER", c.Larger)
}
if c.Smaller > 0 {
fields = append(fields, "SMALLER", c.Smaller)
}
for _, not := range c.Not {
fields = append(fields, "NOT", not.Format())
}
for _, or := range c.Or {
fields = append(fields, "OR", or[0].Format(), or[1].Format())
}
return fields
}

290
vendor/github.com/emersion/go-imap/seqset.go generated vendored Normal file
View File

@@ -0,0 +1,290 @@
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
}
// NewSeqSet returns a new SeqSet instance after parsing the set string.
func NewSeqSet(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
return
}
// 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)
}

52
vendor/github.com/emersion/go-imap/server/cmd_any.go generated vendored Normal file
View File

@@ -0,0 +1,52 @@
package server
import (
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/backend"
"github.com/emersion/go-imap/commands"
"github.com/emersion/go-imap/responses"
)
type Capability struct {
commands.Capability
}
func (cmd *Capability) Handle(conn Conn) error {
res := &responses.Capability{Caps: conn.Capabilities()}
return conn.WriteResp(res)
}
type Noop struct {
commands.Noop
}
func (cmd *Noop) Handle(conn Conn) error {
ctx := conn.Context()
if ctx.Mailbox != nil {
// If a mailbox is selected, NOOP can be used to poll for server updates
if mbox, ok := ctx.Mailbox.(backend.UpdaterMailbox); ok {
return mbox.Poll()
}
}
return nil
}
type Logout struct {
commands.Logout
}
func (cmd *Logout) Handle(conn Conn) error {
res := &imap.StatusResp{
Type: imap.StatusBye,
Info: "Closing connection",
}
if err := conn.WriteResp(res); err != nil {
return err
}
// Request to close the connection
conn.Context().State = imap.LogoutState
return nil
}

261
vendor/github.com/emersion/go-imap/server/cmd_auth.go generated vendored Normal file
View File

@@ -0,0 +1,261 @@
package server
import (
"errors"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/backend"
"github.com/emersion/go-imap/commands"
"github.com/emersion/go-imap/responses"
)
// imap errors in Authenticated state.
var (
ErrNotAuthenticated = errors.New("Not authenticated")
)
type Select struct {
commands.Select
}
func (cmd *Select) Handle(conn Conn) error {
ctx := conn.Context()
if ctx.User == nil {
return ErrNotAuthenticated
}
mbox, err := ctx.User.GetMailbox(cmd.Mailbox)
if err != nil {
return err
}
items := []string{
imap.MailboxFlags, imap.MailboxPermanentFlags,
imap.MailboxMessages, imap.MailboxRecent, imap.MailboxUnseen,
imap.MailboxUidNext, imap.MailboxUidValidity,
}
status, err := mbox.Status(items)
if err != nil {
return err
}
ctx.Mailbox = mbox
ctx.MailboxReadOnly = cmd.ReadOnly || status.ReadOnly
res := &responses.Select{Mailbox: status}
if err := conn.WriteResp(res); err != nil {
return err
}
code := imap.CodeReadWrite
if ctx.MailboxReadOnly {
code = imap.CodeReadOnly
}
return ErrStatusResp(&imap.StatusResp{
Type: imap.StatusOk,
Code: code,
})
}
type Create struct {
commands.Create
}
func (cmd *Create) Handle(conn Conn) error {
ctx := conn.Context()
if ctx.User == nil {
return ErrNotAuthenticated
}
return ctx.User.CreateMailbox(cmd.Mailbox)
}
type Delete struct {
commands.Delete
}
func (cmd *Delete) Handle(conn Conn) error {
ctx := conn.Context()
if ctx.User == nil {
return ErrNotAuthenticated
}
return ctx.User.DeleteMailbox(cmd.Mailbox)
}
type Rename struct {
commands.Rename
}
func (cmd *Rename) Handle(conn Conn) error {
ctx := conn.Context()
if ctx.User == nil {
return ErrNotAuthenticated
}
return ctx.User.RenameMailbox(cmd.Existing, cmd.New)
}
type Subscribe struct {
commands.Subscribe
}
func (cmd *Subscribe) Handle(conn Conn) error {
ctx := conn.Context()
if ctx.User == nil {
return ErrNotAuthenticated
}
mbox, err := ctx.User.GetMailbox(cmd.Mailbox)
if err != nil {
return err
}
return mbox.SetSubscribed(true)
}
type Unsubscribe struct {
commands.Unsubscribe
}
func (cmd *Unsubscribe) Handle(conn Conn) error {
ctx := conn.Context()
if ctx.User == nil {
return ErrNotAuthenticated
}
mbox, err := ctx.User.GetMailbox(cmd.Mailbox)
if err != nil {
return err
}
return mbox.SetSubscribed(false)
}
type List struct {
commands.List
}
func (cmd *List) Handle(conn Conn) error {
ctx := conn.Context()
if ctx.User == nil {
return ErrNotAuthenticated
}
ch := make(chan *imap.MailboxInfo)
res := &responses.List{Mailboxes: ch, Subscribed: cmd.Subscribed}
done := make(chan error, 1)
go (func() {
done <- conn.WriteResp(res)
close(done)
})()
mailboxes, err := ctx.User.ListMailboxes(cmd.Subscribed)
if err != nil {
close(ch)
return err
}
for _, mbox := range mailboxes {
info, err := mbox.Info()
if err != nil {
close(ch)
return err
}
// An empty ("" string) mailbox name argument is a special request to return
// the hierarchy delimiter and the root name of the name given in the
// reference.
if cmd.Mailbox == "" {
ch <- &imap.MailboxInfo{
Attributes: []string{imap.NoSelectAttr},
Delimiter: info.Delimiter,
Name: info.Delimiter,
}
break
}
if info.Match(cmd.Reference, cmd.Mailbox) {
ch <- info
}
}
close(ch)
return <-done
}
type Status struct {
commands.Status
}
func (cmd *Status) Handle(conn Conn) error {
ctx := conn.Context()
if ctx.User == nil {
return ErrNotAuthenticated
}
mbox, err := ctx.User.GetMailbox(cmd.Mailbox)
if err != nil {
return err
}
status, err := mbox.Status(cmd.Items)
if err != nil {
return err
}
// Only keep items thqat have been requested
items := make(map[string]interface{})
for _, k := range cmd.Items {
items[k] = status.Items[k]
}
status.Items = items
res := &responses.Status{Mailbox: status}
return conn.WriteResp(res)
}
type Append struct {
commands.Append
}
func (cmd *Append) Handle(conn Conn) error {
ctx := conn.Context()
if ctx.User == nil {
return ErrNotAuthenticated
}
mbox, err := ctx.User.GetMailbox(cmd.Mailbox)
if err == backend.ErrNoSuchMailbox {
return ErrStatusResp(&imap.StatusResp{
Type: imap.StatusNo,
Code: imap.CodeTryCreate,
Info: err.Error(),
})
} else if err != nil {
return err
}
if err := mbox.CreateMessage(cmd.Flags, cmd.Date, cmd.Message); err != nil {
return err
}
// If APPEND targets the currently selected mailbox, send an untagged EXISTS
// Do this only if the backend doesn't send updates itself
if conn.Server().Updates == nil && ctx.Mailbox != nil && ctx.Mailbox.Name() == mbox.Name() {
status, err := mbox.Status([]string{imap.MailboxMessages})
if err != nil {
return err
}
res := &responses.Select{Mailbox: status}
if err := conn.WriteResp(res); err != nil {
return err
}
}
return nil
}

127
vendor/github.com/emersion/go-imap/server/cmd_noauth.go generated vendored Normal file
View File

@@ -0,0 +1,127 @@
package server
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"
)
// IMAP errors in Not Authenticated state.
var (
ErrAlreadyAuthenticated = errors.New("Already authenticated")
ErrAuthDisabled = errors.New("Authentication disabled")
)
type StartTLS struct {
commands.StartTLS
}
func (cmd *StartTLS) Handle(conn Conn) error {
ctx := conn.Context()
if ctx.State != imap.NotAuthenticatedState {
return ErrAlreadyAuthenticated
}
if conn.IsTLS() {
return errors.New("TLS is already enabled")
}
if conn.Server().TLSConfig == nil {
return errors.New("TLS support not enabled")
}
// Send an OK status response to let the client know that the TLS handshake
// can begin
return ErrStatusResp(&imap.StatusResp{
Type: imap.StatusOk,
Info: "Begin TLS negotiation now",
})
}
func (cmd *StartTLS) Upgrade(conn Conn) error {
tlsConfig := conn.Server().TLSConfig
var tlsConn *tls.Conn
err := conn.Upgrade(func(conn net.Conn) (net.Conn, error) {
tlsConn = tls.Server(conn, tlsConfig)
err := tlsConn.Handshake()
return tlsConn, err
})
if err != nil {
return err
}
conn.setTLSConn(tlsConn)
res := &responses.Capability{Caps: conn.Capabilities()}
return conn.WriteResp(res)
}
func afterAuthStatus(conn Conn) error {
return ErrStatusResp(&imap.StatusResp{
Type: imap.StatusOk,
Code: imap.CodeCapability,
Arguments: imap.FormatStringList(conn.Capabilities()),
})
}
func canAuth(conn Conn) bool {
for _, cap := range conn.Capabilities() {
if cap == "AUTH=PLAIN" {
return true
}
}
return false
}
type Login struct {
commands.Login
}
func (cmd *Login) Handle(conn Conn) error {
ctx := conn.Context()
if ctx.State != imap.NotAuthenticatedState {
return ErrAlreadyAuthenticated
}
if !canAuth(conn) {
return ErrAuthDisabled
}
user, err := conn.Server().Backend.Login(cmd.Username, cmd.Password)
if err != nil {
return err
}
ctx.State = imap.AuthenticatedState
ctx.User = user
return afterAuthStatus(conn)
}
type Authenticate struct {
commands.Authenticate
}
func (cmd *Authenticate) Handle(conn Conn) error {
ctx := conn.Context()
if ctx.State != imap.NotAuthenticatedState {
return ErrAlreadyAuthenticated
}
if !canAuth(conn) {
return ErrAuthDisabled
}
mechanisms := map[string]sasl.Server{}
for name, newSasl := range conn.Server().auths {
mechanisms[name] = newSasl(conn)
}
err := cmd.Authenticate.Handle(mechanisms, conn)
if err != nil {
return err
}
return afterAuthStatus(conn)
}

View File

@@ -0,0 +1,313 @@
package server
import (
"errors"
"strings"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/commands"
"github.com/emersion/go-imap/responses"
)
// imap errors in Selected state.
var (
ErrNoMailboxSelected = errors.New("No mailbox selected")
ErrMailboxReadOnly = errors.New("Mailbox opened in read-only mode")
)
// A command handler that supports UIDs.
type UidHandler interface {
Handler
// Handle this command using UIDs for a given connection.
UidHandle(conn Conn) error
}
type Check struct {
commands.Check
}
func (cmd *Check) Handle(conn Conn) error {
ctx := conn.Context()
if ctx.Mailbox == nil {
return ErrNoMailboxSelected
}
if ctx.MailboxReadOnly {
return ErrMailboxReadOnly
}
return ctx.Mailbox.Check()
}
type Close struct {
commands.Close
}
func (cmd *Close) Handle(conn Conn) error {
ctx := conn.Context()
if ctx.Mailbox == nil {
return ErrNoMailboxSelected
}
mailbox := ctx.Mailbox
ctx.Mailbox = nil
ctx.MailboxReadOnly = false
if err := mailbox.Expunge(); err != nil {
return err
}
// No need to send expunge updates here, since the mailbox is already unselected
return nil
}
type Expunge struct {
commands.Expunge
}
func (cmd *Expunge) Handle(conn Conn) error {
ctx := conn.Context()
if ctx.Mailbox == nil {
return ErrNoMailboxSelected
}
if ctx.MailboxReadOnly {
return ErrMailboxReadOnly
}
// Get a list of messages that will be deleted
// That will allow us to send expunge updates if the backend doesn't support it
var seqnums []uint32
if conn.Server().Updates == nil {
criteria := &imap.SearchCriteria{
WithFlags: []string{imap.DeletedFlag},
}
var err error
seqnums, err = ctx.Mailbox.SearchMessages(false, criteria)
if err != nil {
return err
}
}
if err := ctx.Mailbox.Expunge(); err != nil {
return err
}
// If the backend doesn't support expunge updates, let's do it ourselves
if conn.Server().Updates == nil {
done := make(chan error)
defer close(done)
ch := make(chan uint32)
res := &responses.Expunge{SeqNums: ch}
go (func() {
done <- conn.WriteResp(res)
})()
// Iterate sequence numbers from the last one to the first one, as deleting
// messages changes their respective numbers
for i := len(seqnums) - 1; i >= 0; i-- {
ch <- seqnums[i]
}
close(ch)
if err := <-done; err != nil {
return err
}
}
return nil
}
type Search struct {
commands.Search
}
func (cmd *Search) handle(uid bool, conn Conn) error {
ctx := conn.Context()
if ctx.Mailbox == nil {
return ErrNoMailboxSelected
}
ids, err := ctx.Mailbox.SearchMessages(uid, cmd.Criteria)
if err != nil {
return err
}
res := &responses.Search{Ids: ids}
return conn.WriteResp(res)
}
func (cmd *Search) Handle(conn Conn) error {
return cmd.handle(false, conn)
}
func (cmd *Search) UidHandle(conn Conn) error {
return cmd.handle(true, conn)
}
type Fetch struct {
commands.Fetch
}
func (cmd *Fetch) handle(uid bool, conn Conn) error {
ctx := conn.Context()
if ctx.Mailbox == nil {
return ErrNoMailboxSelected
}
ch := make(chan *imap.Message)
res := &responses.Fetch{Messages: ch}
done := make(chan error, 1)
go (func() {
done <- conn.WriteResp(res)
})()
err := ctx.Mailbox.ListMessages(uid, cmd.SeqSet, cmd.Items, ch)
if err != nil {
return err
}
return <-done
}
func (cmd *Fetch) Handle(conn Conn) error {
return cmd.handle(false, conn)
}
func (cmd *Fetch) UidHandle(conn Conn) error {
// Append UID to the list of requested items if it isn't already present
hasUid := false
for _, item := range cmd.Items {
if item == "UID" {
hasUid = true
break
}
}
if !hasUid {
cmd.Items = append(cmd.Items, "UID")
}
return cmd.handle(true, conn)
}
type Store struct {
commands.Store
}
func (cmd *Store) handle(uid bool, conn Conn) error {
ctx := conn.Context()
if ctx.Mailbox == nil {
return ErrNoMailboxSelected
}
if ctx.MailboxReadOnly {
return ErrMailboxReadOnly
}
itemStr := cmd.Item
silent := strings.HasSuffix(itemStr, imap.SilentOp)
if silent {
itemStr = strings.TrimSuffix(itemStr, imap.SilentOp)
}
item := imap.FlagsOp(itemStr)
if item != imap.SetFlags && item != imap.AddFlags && item != imap.RemoveFlags {
return errors.New("Unsupported STORE operation")
}
flagsList, ok := cmd.Value.([]interface{})
if !ok {
return errors.New("Flags must be a list")
}
flags, err := imap.ParseStringList(flagsList)
if err != nil {
return err
}
for i, flag := range flags {
flags[i] = imap.CanonicalFlag(flag)
}
// If the backend supports message updates, this will prevent this connection
// from receiving them
// TODO: find a better way to do this, without conn.silent
*conn.silent() = silent
err = ctx.Mailbox.UpdateMessagesFlags(uid, cmd.SeqSet, item, flags)
*conn.silent() = false
if err != nil {
return err
}
// Not silent: send FETCH updates if the backend doesn't support message
// updates
if conn.Server().Updates == nil && !silent {
inner := &Fetch{}
inner.SeqSet = cmd.SeqSet
inner.Items = []string{"FLAGS"}
if uid {
inner.Items = append(inner.Items, "UID")
}
if err := inner.handle(uid, conn); err != nil {
return err
}
}
return nil
}
func (cmd *Store) Handle(conn Conn) error {
return cmd.handle(false, conn)
}
func (cmd *Store) UidHandle(conn Conn) error {
return cmd.handle(true, conn)
}
type Copy struct {
commands.Copy
}
func (cmd *Copy) handle(uid bool, conn Conn) error {
ctx := conn.Context()
if ctx.Mailbox == nil {
return ErrNoMailboxSelected
}
return ctx.Mailbox.CopyMessages(uid, cmd.SeqSet, cmd.Mailbox)
}
func (cmd *Copy) Handle(conn Conn) error {
return cmd.handle(false, conn)
}
func (cmd *Copy) UidHandle(conn Conn) error {
return cmd.handle(true, conn)
}
type Uid struct {
commands.Uid
}
func (cmd *Uid) Handle(conn Conn) error {
inner := cmd.Cmd.Command()
hdlr, err := conn.commandHandler(inner)
if err != nil {
return err
}
uidHdlr, ok := hdlr.(UidHandler)
if !ok {
return errors.New("Command unsupported with UID")
}
if err := uidHdlr.UidHandle(conn); err != nil {
return err
}
return ErrStatusResp(&imap.StatusResp{
Type: imap.StatusOk,
Info: imap.Uid + " " + inner.Name + " completed",
})
}

383
vendor/github.com/emersion/go-imap/server/conn.go generated vendored Normal file
View File

@@ -0,0 +1,383 @@
package server
import (
"crypto/tls"
"errors"
"io"
"net"
"sync"
"time"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/backend"
)
// Conn is a connection to a client.
type Conn interface {
io.Reader
// Server returns this connection's server.
Server() *Server
// Context returns this connection's context.
Context() *Context
// Capabilities returns a list of capabilities enabled for this connection.
Capabilities() []string
// WriteResp writes a response to this connection.
WriteResp(res imap.WriterTo) error
// IsTLS returns true if TLS is enabled.
IsTLS() bool
// TLSState returns the TLS connection state if TLS is enabled, nil otherwise.
TLSState() *tls.ConnectionState
// Upgrade upgrades a connection, e.g. wrap an unencrypted connection with an
// encrypted tunnel.
Upgrade(upgrader imap.ConnUpgrader) error
// Close closes this connection.
Close() error
setTLSConn(*tls.Conn)
silent() *bool // TODO: remove this
serve() error
commandHandler(cmd *imap.Command) (hdlr Handler, err error)
}
// Context stores a connection's metadata.
type Context struct {
// This connection's current state.
State imap.ConnState
// If the client is logged in, the user.
User backend.User
// If the client has selected a mailbox, the mailbox.
Mailbox backend.Mailbox
// True if the currently selected mailbox has been opened in read-only mode.
MailboxReadOnly bool
// Responses to send to the client.
Responses chan<- imap.WriterTo
}
type conn struct {
*imap.Conn
s *Server
ctx *Context
l sync.Locker
tlsConn *tls.Conn
continues chan bool
responses chan imap.WriterTo
silentVal bool
}
func newConn(s *Server, c net.Conn) *conn {
// Create an imap.Reader and an imap.Writer
continues := make(chan bool)
r := imap.NewServerReader(nil, continues)
w := imap.NewWriter(nil)
responses := make(chan imap.WriterTo)
tlsConn, _ := c.(*tls.Conn)
conn := &conn{
Conn: imap.NewConn(c, r, w),
s: s,
l: &sync.Mutex{},
ctx: &Context{
State: imap.ConnectingState,
Responses: responses,
},
tlsConn: tlsConn,
continues: continues,
responses: responses,
}
if s.Debug != nil {
conn.Conn.SetDebug(s.Debug)
}
if s.MaxLiteralSize > 0 {
conn.Conn.MaxLiteralSize = s.MaxLiteralSize
}
conn.l.Lock()
go conn.send()
return conn
}
func (c *conn) Server() *Server {
return c.s
}
func (c *conn) Context() *Context {
return c.ctx
}
type response struct {
response imap.WriterTo
done chan struct{}
}
func (r *response) WriteTo(w *imap.Writer) error {
err := r.response.WriteTo(w)
close(r.done)
return err
}
func (c *conn) setDeadline() {
if c.s.AutoLogout == 0 {
return
}
dur := c.s.AutoLogout
if dur < MinAutoLogout {
dur = MinAutoLogout
}
t := time.Now().Add(dur)
c.Conn.SetDeadline(t)
}
func (c *conn) WriteResp(r imap.WriterTo) error {
done := make(chan struct{})
c.responses <- &response{r, done}
<-done
c.setDeadline()
return nil
}
func (c *conn) Close() error {
if c.ctx.User != nil {
c.ctx.User.Logout()
}
if err := c.Conn.Close(); err != nil {
return err
}
close(c.continues)
c.ctx.State = imap.LogoutState
return nil
}
func (c *conn) Capabilities() []string {
caps := []string{"IMAP4rev1"}
if c.ctx.State == imap.NotAuthenticatedState {
if !c.IsTLS() && c.s.TLSConfig != nil {
caps = append(caps, "STARTTLS")
}
if !c.canAuth() {
caps = append(caps, "LOGINDISABLED")
} else {
for name := range c.s.auths {
caps = append(caps, "AUTH="+name)
}
}
}
for _, ext := range c.s.extensions {
caps = append(caps, ext.Capabilities(c)...)
}
return caps
}
func (c *conn) send() {
// Send continuation requests
go func() {
for range c.continues {
res := &imap.ContinuationResp{Info: "send literal"}
if err := res.WriteTo(c.Writer); err != nil {
c.Server().ErrorLog.Println("cannot send continuation request: ", err)
} else if err := c.Writer.Flush(); err != nil {
c.Server().ErrorLog.Println("cannot flush connection: ", err)
}
}
}()
// Send responses
for {
// Get a response that needs to be sent
res := <-c.responses
// Request to send the response
c.l.Lock()
// Send the response
if err := res.WriteTo(c.Writer); err != nil {
c.Server().ErrorLog.Println("cannot send response: ", err)
} else if err := c.Writer.Flush(); err != nil {
c.Server().ErrorLog.Println("cannot flush connection: ", err)
}
c.l.Unlock()
}
}
func (c *conn) greet() error {
c.ctx.State = imap.NotAuthenticatedState
caps := c.Capabilities()
args := make([]interface{}, len(caps))
for i, cap := range caps {
args[i] = cap
}
greeting := &imap.StatusResp{
Type: imap.StatusOk,
Code: imap.CodeCapability,
Arguments: args,
Info: "IMAP4rev1 Service Ready",
}
c.l.Unlock()
defer c.l.Lock()
return c.WriteResp(greeting)
}
func (c *conn) setTLSConn(tlsConn *tls.Conn) {
c.tlsConn = tlsConn
}
func (c *conn) IsTLS() bool {
return c.tlsConn != nil
}
func (c *conn) TLSState() *tls.ConnectionState {
if c.tlsConn != nil {
state := c.tlsConn.ConnectionState()
return &state
}
return nil
}
// canAuth checks if the client can use plain text authentication.
func (c *conn) canAuth() bool {
return c.IsTLS() || c.s.AllowInsecureAuth
}
func (c *conn) silent() *bool {
return &c.silentVal
}
func (c *conn) serve() error {
// Send greeting
if err := c.greet(); err != nil {
return err
}
for {
if c.ctx.State == imap.LogoutState {
return nil
}
var res *imap.StatusResp
var up Upgrader
c.Wait()
fields, err := c.ReadLine()
if err == io.EOF || c.ctx.State == imap.LogoutState {
return nil
}
c.setDeadline()
if err != nil {
if imap.IsParseError(err) {
res = &imap.StatusResp{
Type: imap.StatusBad,
Info: err.Error(),
}
} else {
c.s.ErrorLog.Println("cannot read command:", err)
return err
}
} else {
cmd := &imap.Command{}
if err := cmd.Parse(fields); err != nil {
res = &imap.StatusResp{
Tag: cmd.Tag,
Type: imap.StatusBad,
Info: err.Error(),
}
} else {
var err error
res, up, err = c.handleCommand(cmd)
if err != nil {
res = &imap.StatusResp{
Tag: cmd.Tag,
Type: imap.StatusBad,
Info: err.Error(),
}
}
}
}
if res != nil {
c.l.Unlock()
if err := c.WriteResp(res); err != nil {
c.s.ErrorLog.Println("cannot write response:", err)
c.l.Lock()
continue
}
if up != nil && res.Type == imap.StatusOk {
if err := up.Upgrade(c); err != nil {
c.s.ErrorLog.Println("cannot upgrade connection:", err)
return err
}
}
c.l.Lock()
}
}
}
func (c *conn) commandHandler(cmd *imap.Command) (hdlr Handler, err error) {
newHandler := c.s.Command(cmd.Name)
if newHandler == nil {
err = errors.New("Unknown command")
return
}
hdlr = newHandler()
err = hdlr.Parse(cmd.Arguments)
return
}
func (c *conn) handleCommand(cmd *imap.Command) (res *imap.StatusResp, up Upgrader, err error) {
hdlr, err := c.commandHandler(cmd)
if err != nil {
return
}
c.l.Unlock()
defer c.l.Lock()
hdlrErr := hdlr.Handle(c)
if statusErr, ok := hdlrErr.(*errStatusResp); ok {
res = statusErr.resp
} else if hdlrErr != nil {
res = &imap.StatusResp{
Type: imap.StatusNo,
Info: hdlrErr.Error(),
}
} else {
res = &imap.StatusResp{
Type: imap.StatusOk,
}
}
if res != nil {
res.Tag = cmd.Tag
if res.Type == imap.StatusOk && res.Info == "" {
res.Info = cmd.Name + " completed"
}
}
up, _ = hdlr.(Upgrader)
return
}

426
vendor/github.com/emersion/go-imap/server/server.go generated vendored Normal file
View File

@@ -0,0 +1,426 @@
// Package server provides an IMAP server.
package server
import (
"crypto/tls"
"errors"
"io"
"log"
"net"
"os"
"sync"
"time"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/backend"
"github.com/emersion/go-imap/responses"
"github.com/emersion/go-sasl"
)
// The minimum autologout duration defined in RFC 3501 section 5.4.
const MinAutoLogout = 30 * time.Minute
// A command handler.
type Handler interface {
imap.Parser
// Handle this command for a given connection.
//
// By default, after this function has returned a status response is sent. To
// prevent this behavior handlers can use ErrStatusResp or ErrNoStatusResp.
Handle(conn Conn) error
}
// A connection upgrader. If a Handler is also an Upgrader, the connection will
// be upgraded after the Handler succeeds.
//
// This should only be used by libraries implementing an IMAP extension (e.g.
// COMPRESS).
type Upgrader interface {
// Upgrade the connection. This method should call conn.Upgrade().
Upgrade(conn Conn) error
}
// A function that creates handlers.
type HandlerFactory func() Handler
// A function that creates SASL servers.
type SaslServerFactory func(conn Conn) sasl.Server
// An IMAP extension.
type Extension interface {
// Get capabilities provided by this extension for a given connection.
Capabilities(c Conn) []string
// Get the command handler factory for the provided command name.
Command(name string) HandlerFactory
}
// An extension that provides additional features to each connection.
type ConnExtension interface {
Extension
// This function will be called when a client connects to the server. It can
// be used to add new features to the default Conn interface by implementing
// new methods.
NewConn(c Conn) Conn
}
type errStatusResp struct {
resp *imap.StatusResp
}
func (err *errStatusResp) Error() string {
return ""
}
// ErrStatusResp can be returned by a Handler to replace the default status
// response. The response tag must be empty.
//
// To disable the default status response, use ErrNoStatusResp instead.
func ErrStatusResp(res *imap.StatusResp) error {
return &errStatusResp{res}
}
// ErrNoStatusResp can be returned by a Handler to prevent the default status
// response from being sent.
func ErrNoStatusResp() error {
return &errStatusResp{nil}
}
// An IMAP server.
type Server struct {
locker sync.Mutex
listeners map[net.Listener]struct{}
conns map[Conn]struct{}
commands map[string]HandlerFactory
auths map[string]SaslServerFactory
extensions []Extension
// TCP address to listen on.
Addr string
// This server's TLS configuration.
TLSConfig *tls.Config
// This server's backend.
Backend backend.Backend
// Backend updates that will be sent to connected clients.
Updates <-chan interface{}
// Automatically logout clients after a duration. To do not logout users
// automatically, set this to zero. The duration MUST be at least
// MinAutoLogout (as stated in RFC 3501 section 5.4).
AutoLogout time.Duration
// Allow authentication over unencrypted connections.
AllowInsecureAuth bool
// An io.Writer to which all network activity will be mirrored.
Debug io.Writer
// ErrorLog specifies an optional logger for errors accepting
// connections and unexpected behavior from handlers.
// If nil, logging goes to os.Stderr via the log package's
// standard logger.
ErrorLog imap.Logger
// The maximum literal size, in bytes. Literals exceeding this size will be
// rejected. A value of zero disables the limit (this is the default).
MaxLiteralSize uint32
}
// Create a new IMAP server from an existing listener.
func New(bkd backend.Backend) *Server {
s := &Server{
listeners: make(map[net.Listener]struct{}),
conns: make(map[Conn]struct{}),
Backend: bkd,
ErrorLog: log.New(os.Stderr, "imap/server: ", log.LstdFlags),
}
s.auths = map[string]SaslServerFactory{
sasl.Plain: func(conn Conn) sasl.Server {
return sasl.NewPlainServer(func(identity, username, password string) error {
if identity != "" && identity != username {
return errors.New("Identities not supported")
}
user, err := bkd.Login(username, password)
if err != nil {
return err
}
ctx := conn.Context()
ctx.State = imap.AuthenticatedState
ctx.User = user
return nil
})
},
}
s.commands = map[string]HandlerFactory{
imap.Noop: func() Handler { return &Noop{} },
imap.Capability: func() Handler { return &Capability{} },
imap.Logout: func() Handler { return &Logout{} },
imap.StartTLS: func() Handler { return &StartTLS{} },
imap.Login: func() Handler { return &Login{} },
imap.Authenticate: func() Handler { return &Authenticate{} },
imap.Select: func() Handler { return &Select{} },
imap.Examine: func() Handler {
hdlr := &Select{}
hdlr.ReadOnly = true
return hdlr
},
imap.Create: func() Handler { return &Create{} },
imap.Delete: func() Handler { return &Delete{} },
imap.Rename: func() Handler { return &Rename{} },
imap.Subscribe: func() Handler { return &Subscribe{} },
imap.Unsubscribe: func() Handler { return &Unsubscribe{} },
imap.List: func() Handler { return &List{} },
imap.Lsub: func() Handler {
hdlr := &List{}
hdlr.Subscribed = true
return hdlr
},
imap.Status: func() Handler { return &Status{} },
imap.Append: func() Handler { return &Append{} },
imap.Check: func() Handler { return &Check{} },
imap.Close: func() Handler { return &Close{} },
imap.Expunge: func() Handler { return &Expunge{} },
imap.Search: func() Handler { return &Search{} },
imap.Fetch: func() Handler { return &Fetch{} },
imap.Store: func() Handler { return &Store{} },
imap.Copy: func() Handler { return &Copy{} },
imap.Uid: func() Handler { return &Uid{} },
}
return s
}
// Serve accepts incoming connections on the Listener l.
func (s *Server) Serve(l net.Listener) error {
s.locker.Lock()
s.listeners[l] = struct{}{}
s.locker.Unlock()
defer func() {
s.locker.Lock()
defer s.locker.Unlock()
l.Close()
delete(s.listeners, l)
}()
go s.listenUpdates()
for {
c, err := l.Accept()
if err != nil {
return err
}
var conn Conn = newConn(s, c)
for _, ext := range s.extensions {
if ext, ok := ext.(ConnExtension); ok {
conn = ext.NewConn(conn)
}
}
go s.serveConn(conn)
}
}
// ListenAndServe listens on the TCP network address s.Addr and then calls Serve
// to handle requests on incoming connections.
//
// If s.Addr is blank, ":imap" is used.
func (s *Server) ListenAndServe() error {
addr := s.Addr
if addr == "" {
addr = ":imap"
}
l, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return s.Serve(l)
}
// ListenAndServeTLS listens on the TCP network address s.Addr and then calls
// Serve to handle requests on incoming TLS connections.
//
// If s.Addr is blank, ":imaps" is used.
func (s *Server) ListenAndServeTLS() error {
addr := s.Addr
if addr == "" {
addr = ":imaps"
}
l, err := tls.Listen("tcp", addr, s.TLSConfig)
if err != nil {
return err
}
return s.Serve(l)
}
func (s *Server) serveConn(conn Conn) error {
s.locker.Lock()
s.conns[conn] = struct{}{}
s.locker.Unlock()
defer func() {
s.locker.Lock()
defer s.locker.Unlock()
conn.Close()
delete(s.conns, conn)
}()
return conn.serve()
}
// Get a command handler factory for the provided command name.
func (s *Server) Command(name string) HandlerFactory {
// Extensions can override builtin commands
for _, ext := range s.extensions {
if h := ext.Command(name); h != nil {
return h
}
}
return s.commands[name]
}
func (s *Server) listenUpdates() (err error) {
updater, ok := s.Backend.(backend.Updater)
if !ok {
return
}
s.Updates = updater.Updates()
for {
item := <-s.Updates
var (
update *backend.Update
res imap.WriterTo
)
switch item := item.(type) {
case *backend.StatusUpdate:
update = &item.Update
res = item.StatusResp
case *backend.MailboxUpdate:
update = &item.Update
res = &responses.Select{Mailbox: item.MailboxStatus}
case *backend.MessageUpdate:
update = &item.Update
ch := make(chan *imap.Message, 1)
ch <- item.Message
close(ch)
res = &responses.Fetch{Messages: ch}
case *backend.ExpungeUpdate:
update = &item.Update
ch := make(chan uint32, 1)
ch <- item.SeqNum
close(ch)
res = &responses.Expunge{SeqNums: ch}
default:
s.ErrorLog.Printf("unhandled update: %T\n", item)
}
if update == nil || res == nil {
continue
}
sends := make(chan struct{})
wait := 0
s.locker.Lock()
for conn := range s.conns {
ctx := conn.Context()
if update.Username != "" && (ctx.User == nil || ctx.User.Username() != update.Username) {
continue
}
if update.Mailbox != "" && (ctx.Mailbox == nil || ctx.Mailbox.Name() != update.Mailbox) {
continue
}
if *conn.silent() {
// If silent is set, do not send message updates
if _, ok := res.(*responses.Fetch); ok {
continue
}
}
conn := conn // Copy conn to a local variable
go func() {
done := make(chan struct{})
conn.Context().Responses <- &response{
response: res,
done: done,
}
<-done
sends <- struct{}{}
}()
wait++
}
s.locker.Unlock()
if wait > 0 {
go func() {
for done := 0; done < wait; done++ {
<-sends
}
close(sends)
backend.DoneUpdate(update)
}()
} else {
backend.DoneUpdate(update)
}
}
}
// ForEachConn iterates through all opened connections.
func (s *Server) ForEachConn(f func(Conn)) {
s.locker.Lock()
defer s.locker.Unlock()
for conn := range s.conns {
f(conn)
}
}
// Stops listening and closes all current connections.
func (s *Server) Close() error {
s.locker.Lock()
defer s.locker.Unlock()
for l := range s.listeners {
l.Close()
}
for conn := range s.conns {
conn.Close()
}
return nil
}
// Enable some IMAP extensions on this server.
//
// This function should not be called directly, it must only be used by
// libraries implementing extensions of the IMAP protocol.
func (s *Server) Enable(extensions ...Extension) {
s.extensions = append(s.extensions, extensions...)
}
// Enable an authentication mechanism on this server.
//
// This function should not be called directly, it must only be used by
// libraries implementing extensions of the IMAP protocol.
func (s *Server) EnableAuth(name string, f SaslServerFactory) {
s.auths[name] = f
}

120
vendor/github.com/emersion/go-imap/status.go generated vendored Normal file
View File

@@ -0,0 +1,120 @@
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.
StatusOk 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.
StatusNo = "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.
StatusBad = "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.
StatusPreauth = "PREAUTH"
// The BYE response is always untagged, and indicates that the server
// is about to close the connection.
StatusBye = "BYE"
)
// Status response codes defined in RFC 3501 section 7.1.
const (
CodeAlert = "ALERT"
CodeBadCharset = "BADCHARSET"
CodeCapability = "CAPABILITY"
CodeParse = "PARSE"
CodePermanentFlags = "PERMANENTFLAGS"
CodeReadOnly = "READ-ONLY"
CodeReadWrite = "READ-WRITE"
CodeTryCreate = "TRYCREATE"
CodeUidNext = "UIDNEXT"
CodeUidValidity = "UIDVALIDITY"
CodeUnseen = "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 string
// Arguments provided with the status code.
Arguments []interface{}
// The status info.
Info string
}
// 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 == StatusNo || r.Type == StatusBad {
return errors.New(r.Info)
}
return nil
}
func (r *StatusResp) WriteTo(w *Writer) error {
tag := r.Tag
if tag == "" {
tag = "*"
}
if err := w.writeFields([]interface{}{tag, string(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()
}

152
vendor/github.com/emersion/go-imap/utf7/decoder.go generated vendored Normal file
View File

@@ -0,0 +1,152 @@
package utf7
import (
"errors"
"unicode/utf16"
"unicode/utf8"
"golang.org/x/text/encoding"
"golang.org/x/text/transform"
)
var ErrBadUtf7 = errors.New("bad utf-7 encoding")
var Decoder = &encoding.Decoder{
Transformer: &decoder{true},
}
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 = ErrBadUtf7
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 = ErrBadUtf7
return
}
}
if i == len(src) { // Implicit shift ("&...")
if atEOF {
err = ErrBadUtf7
} 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 = ErrBadUtf7
return
}
b = decode(src[start:i])
d.ascii = false
}
if len(b) == 0 { // Bad encoding
err = ErrBadUtf7
return
}
if nDst+len(b) > len(dst) {
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, enc.DecodedLen(n)*3)
} else {
n += 4 - n&3
b = make([]byte, n+enc.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 := enc.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]
}

96
vendor/github.com/emersion/go-imap/utf7/encoder.go generated vendored Normal file
View File

@@ -0,0 +1,96 @@
package utf7
import (
"unicode/utf16"
"unicode/utf8"
"golang.org/x/text/encoding"
"golang.org/x/text/transform"
)
var Encoder = &encoding.Encoder{
Transformer: &encoder{},
}
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 := enc.EncodedLen(len(b)) + 2
b64 := make([]byte, n)
enc.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
}

15
vendor/github.com/emersion/go-imap/utf7/utf7.go generated vendored Normal file
View File

@@ -0,0 +1,15 @@
// Modified UTF-7 encoding defined in RFC 3501 section 5.1.3
package utf7
import (
"encoding/base64"
)
const (
min = 0x20 // Minimum self-representing UTF-7 value
max = 0x7E // Maximum self-representing UTF-7 value
repl = '\uFFFD' // Unicode replacement code point
)
var enc = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,")

229
vendor/github.com/emersion/go-imap/write.go generated vendored Normal file
View File

@@ -0,0 +1,229 @@
package imap
import (
"bytes"
"fmt"
"io"
"strconv"
"strings"
"time"
"unicode"
)
type flusher interface {
Flush() error
}
// A string that will be quoted.
type Quoted 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
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) writeAtom(s string) error {
return w.writeString(s)
}
func (w *Writer) writeAstring(s string) error {
if !isAscii(s) {
// IMAP doesn't allow 8-bit data outside literals
return w.writeLiteral(bytes.NewBufferString(s))
}
specials := string([]rune{dquote, listStart, listEnd, literalStart, sp})
if strings.ToUpper(s) == nilAtom || s == "" || strings.ContainsAny(s, specials) {
return w.writeQuoted(s)
}
return w.writeAtom(s)
}
func (w *Writer) writeDateTime(t time.Time, layout string) error {
if t.IsZero() {
return w.writeAtom(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))
}
func (w *Writer) writeLiteral(l Literal) error {
if l == nil {
return w.writeString(nilAtom)
}
header := string(literalStart) + strconv.Itoa(l.Len()) + 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 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")
}
}
_, err := io.Copy(w, l)
return err
}
func (w *Writer) writeField(field interface{}) error {
if field == nil {
return w.writeAtom(nilAtom)
}
switch field := field.(type) {
case string:
return w.writeAstring(field)
case Quoted:
return w.writeQuoted(string(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(field.String())
}
return fmt.Errorf("imap: cannot format field: %v", field)
}
func (w *Writer) writeRespCode(code string, args []interface{}) error {
if err := w.writeString(string(respCodeStart)); err != nil {
return err
}
fields := []interface{}{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}
}

24
vendor/github.com/emersion/go-sasl/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,24 @@
# 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

3
vendor/github.com/emersion/go-sasl/.travis.yml generated vendored Normal file
View File

@@ -0,0 +1,3 @@
language: go
go:
- 1.5

21
vendor/github.com/emersion/go-sasl/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
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.

17
vendor/github.com/emersion/go-sasl/README.md generated vendored Normal file
View File

@@ -0,0 +1,17 @@
# 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)
* [LOGIN](https://tools.ietf.org/html/draft-murchison-sasl-login-00) (only server, obsolete, use PLAIN instead)
* [PLAIN](https://tools.ietf.org/html/rfc4616)
* [XOAUTH2](https://developers.google.com/gmail/xoauth2_protocol)
## License
MIT

56
vendor/github.com/emersion/go-sasl/anonymous.go generated vendored Normal file
View File

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

26
vendor/github.com/emersion/go-sasl/external.go generated vendored Normal file
View File

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

47
vendor/github.com/emersion/go-sasl/login.go generated vendored Normal file
View File

@@ -0,0 +1,47 @@
package sasl
// The LOGIN mechanism name.
const Login = "LOGIN"
// Authenticates users with an username and a password.
type LoginAuthenticator func(username, password string) error
type loginState int
const (
loginNotStarted loginState = iota
loginWaitingUsername
loginWaitingPassword
loginCompleted
)
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.
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:
challenge = []byte("Username:")
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
}

77
vendor/github.com/emersion/go-sasl/plain.go generated vendored Normal file
View File

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

45
vendor/github.com/emersion/go-sasl/sasl.go generated vendored Normal file
View File

@@ -0,0 +1,45 @@
// 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)
}

48
vendor/github.com/emersion/go-sasl/xoauth2.go generated vendored Normal file
View File

@@ -0,0 +1,48 @@
package sasl
import (
"encoding/json"
"fmt"
)
// The XOAUTH2 mechanism name.
const Xoauth2 = "XOAUTH2"
// An XOAUTH2 error.
type Xoauth2Error struct {
Status string `json:"status"`
Schemes string `json:"schemes"`
Scope string `json:"scope"`
}
// Implements error.
func (err *Xoauth2Error) Error() string {
return fmt.Sprintf("XOAUTH2 authentication error (%v)", err.Status)
}
type xoauth2Client struct {
Username string
Token string
}
func (a *xoauth2Client) Start() (mech string, ir []byte, err error) {
mech = Xoauth2
ir = []byte("user=" + a.Username + "\x01auth=Bearer " + a.Token + "\x01\x01")
return
}
func (a *xoauth2Client) Next(challenge []byte) ([]byte, error) {
// Server sent an error response
xoauth2Err := &Xoauth2Error{}
if err := json.Unmarshal(challenge, xoauth2Err); err != nil {
return nil, err
} else {
return nil, xoauth2Err
}
}
// An implementation of the XOAUTH2 authentication mechanism, as
// described in https://developers.google.com/gmail/xoauth2_protocol.
func NewXoauth2Client(username, token string) Client {
return &xoauth2Client{username, token}
}

26
vendor/github.com/emersion/go-smtp/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,26 @@
# 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
/main.go

5
vendor/github.com/emersion/go-smtp/.travis.yml generated vendored Normal file
View File

@@ -0,0 +1,5 @@
language: go
go:
- 1.7
script: bash <(curl -sL https://gist.github.com/emersion/49d4dda535497002639626bd9e16480c/raw/codecov-go.sh)
after_script: bash <(curl -s https://codecov.io/bash)

24
vendor/github.com/emersion/go-smtp/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,24 @@
The MIT License (MIT)
Copyright (c) 2010 The Go Authors
Copyright (c) 2014 Gleez Technologies
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.

125
vendor/github.com/emersion/go-smtp/README.md generated vendored Executable file
View File

@@ -0,0 +1,125 @@
# go-smtp
[![GoDoc](https://godoc.org/github.com/emersion/go-smtp?status.svg)](https://godoc.org/github.com/emersion/go-smtp)
[![Build Status](https://travis-ci.org/emersion/go-smtp.svg?branch=master)](https://travis-ci.org/emersion/go-smtp)
[![codecov](https://codecov.io/gh/emersion/go-smtp/branch/master/graph/badge.svg)](https://codecov.io/gh/emersion/go-smtp)
[![stability-unstable](https://img.shields.io/badge/stability-unstable-yellow.svg)](https://github.com/emersion/stability-badges#unstable)
[![Go Report Card](https://goreportcard.com/badge/github.com/emersion/go-smtp)](https://goreportcard.com/report/github.com/emersion/go-smtp)
An ESMTP client and server library written in Go.
## Features
* ESMTP client & server implementing [RFC 5321](https://tools.ietf.org/html/rfc5321)
* Support for SMTP [AUTH](https://tools.ietf.org/html/rfc4954) and [PIPELINING](https://tools.ietf.org/html/rfc2920)
* UTF-8 support for subject and message
## Usage
### Client
```go
package main
import (
"log"
"strings"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
)
func main() {
// Set up authentication information.
auth := sasl.NewPlainClient("", "user@example.com", "password")
// Connect to the server, authenticate, set the sender and recipient,
// and send the email all in one step.
to := []string{"recipient@example.net"}
msg := strings.NewReader("To: recipient@example.net\r\n" +
"Subject: discount Gophers!\r\n" +
"\r\n" +
"This is the email body.\r\n")
err := smtp.SendMail("mail.example.com:25", auth, "sender@example.org", to, msg)
if err != nil {
log.Fatal(err)
}
}
```
If you need more control, you can use `Client` instead.
### Server
```go
package main
import (
"errors"
"io/ioutil"
"log"
"github.com/emersion/go-smtp"
)
type Backend struct{}
func (bkd *Backend) Login(username, password string) (smtp.User, error) {
if username != "username" || password != "password" {
return nil, errors.New("Invalid username or password")
}
return &User{}, nil
}
type User struct{}
func (u *User) Send(from string, to []string, r io.Reader) error {
log.Println("Sending message:", from, to)
if b, err := ioutil.ReadAll(r); err != nil {
return err
} else {
log.Println("Data:", string(b))
}
return nil
}
func (u *User) Logout() error {
return nil
}
func main() {
be := &Backend{}
s := smtp.NewServer(be)
s.Addr = ":1025"
s.Domain = "localhost"
s.MaxIdleSeconds = 300
s.MaxMessageBytes = 1024 * 1024
s.MaxRecipients = 50
s.AllowInsecureAuth = true
log.Println("Starting server at", s.Addr)
if err := s.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
```
You can use the server manually with `telnet`:
```
$ telnet localhost 1025
EHLO localhost
AUTH PLAIN
AHVzZXJuYW1lAHBhc3N3b3Jk
MAIL FROM:<root@nsa.gov>
RCPT TO:<root@gchq.gov.uk>
DATA
Hey <3
.
```
## Licence
MIT

19
vendor/github.com/emersion/go-smtp/backend.go generated vendored Normal file
View File

@@ -0,0 +1,19 @@
package smtp
import (
"io"
)
// A SMTP server backend.
type Backend interface {
// Authenticate a user.
Login(username, password string) (User, error)
}
// An authenticated user.
type User interface {
// Send an e-mail.
Send(from string, to []string, r io.Reader) error
// Logout is called when this User will no longer be used.
Logout() error
}

384
vendor/github.com/emersion/go-smtp/client.go generated vendored Normal file
View File

@@ -0,0 +1,384 @@
// Copyright 2010 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 smtp
import (
"crypto/tls"
"encoding/base64"
"errors"
"io"
"net"
"net/textproto"
"strings"
"github.com/emersion/go-sasl"
)
// A Client represents a client connection to an SMTP server.
type Client struct {
// Text is the textproto.Conn used by the Client. It is exported to allow for
// clients to add extensions.
Text *textproto.Conn
// keep a reference to the connection so it can be used to create a TLS
// connection later
conn net.Conn
// whether the Client is using TLS
tls bool
serverName string
// map of supported extensions
ext map[string]string
// supported auth mechanisms
auth []string
localName string // the name to use in HELO/EHLO
didHello bool // whether we've said HELO/EHLO
helloError error // the error from the hello
}
// Dial returns a new Client connected to an SMTP server at addr.
// The addr must include a port, as in "mail.example.com:smtp".
func Dial(addr string) (*Client, error) {
conn, err := net.Dial("tcp", addr)
if err != nil {
return nil, err
}
host, _, _ := net.SplitHostPort(addr)
return NewClient(conn, host)
}
// NewClient returns a new Client using an existing connection and host as a
// server name to be used when authenticating.
func NewClient(conn net.Conn, host string) (*Client, error) {
text := textproto.NewConn(conn)
_, _, err := text.ReadResponse(220)
if err != nil {
text.Close()
return nil, err
}
c := &Client{Text: text, conn: conn, serverName: host, localName: "localhost"}
return c, nil
}
// Close closes the connection.
func (c *Client) Close() error {
return c.Text.Close()
}
// hello runs a hello exchange if needed.
func (c *Client) hello() error {
if !c.didHello {
c.didHello = true
err := c.ehlo()
if err != nil {
c.helloError = c.helo()
}
}
return c.helloError
}
// Hello sends a HELO or EHLO to the server as the given host name.
// Calling this method is only necessary if the client needs control
// over the host name used. The client will introduce itself as "localhost"
// automatically otherwise. If Hello is called, it must be called before
// any of the other methods.
func (c *Client) Hello(localName string) error {
if c.didHello {
return errors.New("smtp: Hello called after other methods")
}
c.localName = localName
return c.hello()
}
// cmd is a convenience function that sends a command and returns the response
func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) {
id, err := c.Text.Cmd(format, args...)
if err != nil {
return 0, "", err
}
c.Text.StartResponse(id)
defer c.Text.EndResponse(id)
code, msg, err := c.Text.ReadResponse(expectCode)
return code, msg, err
}
// helo sends the HELO greeting to the server. It should be used only when the
// server does not support ehlo.
func (c *Client) helo() error {
c.ext = nil
_, _, err := c.cmd(250, "HELO %s", c.localName)
return err
}
// ehlo sends the EHLO (extended hello) greeting to the server. It
// should be the preferred greeting for servers that support it.
func (c *Client) ehlo() error {
_, msg, err := c.cmd(250, "EHLO %s", c.localName)
if err != nil {
return err
}
ext := make(map[string]string)
extList := strings.Split(msg, "\n")
if len(extList) > 1 {
extList = extList[1:]
for _, line := range extList {
args := strings.SplitN(line, " ", 2)
if len(args) > 1 {
ext[args[0]] = args[1]
} else {
ext[args[0]] = ""
}
}
}
if mechs, ok := ext["AUTH"]; ok {
c.auth = strings.Split(mechs, " ")
}
c.ext = ext
return err
}
// StartTLS sends the STARTTLS command and encrypts all further communication.
// Only servers that advertise the STARTTLS extension support this function.
func (c *Client) StartTLS(config *tls.Config) error {
if err := c.hello(); err != nil {
return err
}
_, _, err := c.cmd(220, "STARTTLS")
if err != nil {
return err
}
c.conn = tls.Client(c.conn, config)
c.Text = textproto.NewConn(c.conn)
c.tls = true
return c.ehlo()
}
// TLSConnectionState returns the client's TLS connection state.
// The return values are their zero values if StartTLS did
// not succeed.
func (c *Client) TLSConnectionState() (state tls.ConnectionState, ok bool) {
tc, ok := c.conn.(*tls.Conn)
if !ok {
return
}
return tc.ConnectionState(), true
}
// Verify checks the validity of an email address on the server.
// If Verify returns nil, the address is valid. A non-nil return
// does not necessarily indicate an invalid address. Many servers
// will not verify addresses for security reasons.
func (c *Client) Verify(addr string) error {
if err := c.hello(); err != nil {
return err
}
_, _, err := c.cmd(250, "VRFY %s", addr)
return err
}
// Auth authenticates a client using the provided authentication mechanism.
// A failed authentication closes the connection.
// Only servers that advertise the AUTH extension support this function.
func (c *Client) Auth(a sasl.Client) error {
if err := c.hello(); err != nil {
return err
}
encoding := base64.StdEncoding
mech, resp, err := a.Start()
if err != nil {
c.Quit()
return err
}
resp64 := make([]byte, encoding.EncodedLen(len(resp)))
encoding.Encode(resp64, resp)
code, msg64, err := c.cmd(0, "AUTH %s %s", mech, resp64)
for err == nil {
var msg []byte
switch code {
case 334:
msg, err = encoding.DecodeString(msg64)
case 235:
// the last message isn't base64 because it isn't a challenge
msg = []byte(msg64)
default:
err = &textproto.Error{Code: code, Msg: msg64}
}
if err == nil {
if code == 334 {
resp, err = a.Next(msg)
} else {
resp = nil
}
}
if err != nil {
// abort the AUTH
c.cmd(501, "*")
c.Quit()
break
}
if resp == nil {
break
}
resp64 = make([]byte, encoding.EncodedLen(len(resp)))
encoding.Encode(resp64, resp)
code, msg64, err = c.cmd(0, string(resp64))
}
return err
}
// Mail issues a MAIL command to the server using the provided email address.
// If the server supports the 8BITMIME extension, Mail adds the BODY=8BITMIME
// parameter.
// This initiates a mail transaction and is followed by one or more Rcpt calls.
func (c *Client) Mail(from string) error {
if err := c.hello(); err != nil {
return err
}
cmdStr := "MAIL FROM:<%s>"
if c.ext != nil {
if _, ok := c.ext["8BITMIME"]; ok {
cmdStr += " BODY=8BITMIME"
}
}
_, _, err := c.cmd(250, cmdStr, from)
return err
}
// Rcpt issues a RCPT command to the server using the provided email address.
// A call to Rcpt must be preceded by a call to Mail and may be followed by
// a Data call or another Rcpt call.
func (c *Client) Rcpt(to string) error {
_, _, err := c.cmd(25, "RCPT TO:<%s>", to)
return err
}
type dataCloser struct {
c *Client
io.WriteCloser
}
func (d *dataCloser) Close() error {
d.WriteCloser.Close()
_, _, err := d.c.Text.ReadResponse(250)
return err
}
// Data issues a DATA command to the server and returns a writer that
// can be used to write the mail headers and body. The caller should
// close the writer before calling any more methods on c. A call to
// Data must be preceded by one or more calls to Rcpt.
func (c *Client) Data() (io.WriteCloser, error) {
_, _, err := c.cmd(354, "DATA")
if err != nil {
return nil, err
}
return &dataCloser{c, c.Text.DotWriter()}, nil
}
var testHookStartTLS func(*tls.Config) // nil, except for tests
// SendMail connects to the server at addr, switches to TLS if
// possible, authenticates with the optional mechanism a if possible,
// and then sends an email from address from, to addresses to, with
// message r.
// The addr must include a port, as in "mail.example.com:smtp".
//
// The addresses in the to parameter are the SMTP RCPT addresses.
//
// The r parameter should be an RFC 822-style email with headers
// first, a blank line, and then the message body. The lines of r
// should be CRLF terminated. The r headers should usually include
// fields such as "From", "To", "Subject", and "Cc". Sending "Bcc"
// messages is accomplished by including an email address in the to
// parameter but not including it in the r headers.
//
// The SendMail function and the the net/smtp package are low-level
// mechanisms and provide no support for DKIM signing, MIME
// attachments (see the mime/multipart package), or other mail
// functionality. Higher-level packages exist outside of the standard
// library.
func SendMail(addr string, a sasl.Client, from string, to []string, r io.Reader) error {
c, err := Dial(addr)
if err != nil {
return err
}
defer c.Close()
if err = c.hello(); err != nil {
return err
}
if ok, _ := c.Extension("STARTTLS"); ok {
config := &tls.Config{ServerName: c.serverName}
if testHookStartTLS != nil {
testHookStartTLS(config)
}
if err = c.StartTLS(config); err != nil {
return err
}
}
if a != nil && c.ext != nil {
if _, ok := c.ext["AUTH"]; ok {
if err = c.Auth(a); err != nil {
return err
}
}
}
if err = c.Mail(from); err != nil {
return err
}
for _, addr := range to {
if err = c.Rcpt(addr); err != nil {
return err
}
}
w, err := c.Data()
if err != nil {
return err
}
_, err = io.Copy(w, r)
if err != nil {
return err
}
err = w.Close()
if err != nil {
return err
}
return c.Quit()
}
// Extension reports whether an extension is support by the server.
// The extension name is case-insensitive. If the extension is supported,
// Extension also returns a string that contains any parameters the
// server specifies for the extension.
func (c *Client) Extension(ext string) (bool, string) {
if err := c.hello(); err != nil {
return false, ""
}
if c.ext == nil {
return false, ""
}
ext = strings.ToUpper(ext)
param, ok := c.ext[ext]
return ok, param
}
// Reset sends the RSET command to the server, aborting the current mail
// transaction.
func (c *Client) Reset() error {
if err := c.hello(); err != nil {
return err
}
_, _, err := c.cmd(250, "RSET")
return err
}
// Quit sends the QUIT command and closes the connection to the server.
func (c *Client) Quit() error {
if err := c.hello(); err != nil {
return err
}
_, _, err := c.cmd(221, "QUIT")
if err != nil {
return err
}
return c.Text.Close()
}

432
vendor/github.com/emersion/go-smtp/conn.go generated vendored Normal file
View File

@@ -0,0 +1,432 @@
package smtp
import (
"crypto/tls"
"encoding/base64"
"fmt"
"io"
"io/ioutil"
"net"
"net/textproto"
"regexp"
"strconv"
"strings"
"sync"
"time"
)
// A SMTP message.
type message struct {
// The message contents.
io.Reader
// The sender e-mail address.
From string
// The recipients e-mail addresses.
To []string
}
type Conn struct {
conn net.Conn
text *textproto.Conn
server *Server
helo string
msg *message
nbrErrors int
user User
locker sync.Mutex
}
func newConn(c net.Conn, s *Server) *Conn {
sc := &Conn{
server: s,
conn: c,
}
sc.init()
return sc
}
func (c *Conn) init() {
var rwc io.ReadWriteCloser = c.conn
if c.server.Debug != nil {
rwc = struct {
io.Reader
io.Writer
io.Closer
}{
io.TeeReader(c.conn, c.server.Debug),
io.MultiWriter(c.conn, c.server.Debug),
c.conn,
}
}
c.text = textproto.NewConn(rwc)
}
// Commands are dispatched to the appropriate handler functions.
func (c *Conn) handle(cmd string, arg string) {
if cmd == "" {
c.WriteResponse(500, "Speak up")
return
}
switch cmd {
case "SEND", "SOML", "SAML", "EXPN", "HELP", "TURN":
// These commands are not implemented in any state
c.WriteResponse(502, fmt.Sprintf("%v command not implemented", cmd))
case "HELO", "EHLO":
c.handleGreet((cmd == "EHLO"), arg)
case "MAIL":
c.handleMail(arg)
case "RCPT":
c.handleRcpt(arg)
case "VRFY":
c.WriteResponse(252, "Cannot VRFY user, but will accept message")
case "NOOP":
c.WriteResponse(250, "I have sucessfully done nothing")
case "RSET": // Reset session
c.reset()
c.WriteResponse(250, "Session reset")
case "DATA":
c.handleData(arg)
case "QUIT":
c.WriteResponse(221, "Goodnight and good luck")
c.Close()
case "AUTH":
c.handleAuth(arg)
case "STARTTLS":
c.handleStartTLS()
default:
c.WriteResponse(500, fmt.Sprintf("Syntax error, %v command unrecognized", cmd))
c.nbrErrors++
if c.nbrErrors > 3 {
c.WriteResponse(500, "Too many unrecognized commands")
c.Close()
}
}
}
func (c *Conn) Server() *Server {
return c.server
}
func (c *Conn) User() User {
c.locker.Lock()
defer c.locker.Unlock()
return c.user
}
func (c *Conn) SetUser(user User) {
c.locker.Lock()
defer c.locker.Unlock()
c.user = user
}
func (c *Conn) Close() error {
if user := c.User(); user != nil {
user.Logout()
}
return c.conn.Close()
}
// Check if this connection is encrypted.
func (c *Conn) IsTLS() bool {
_, ok := c.conn.(*tls.Conn)
return ok
}
// GREET state -> waiting for HELO
func (c *Conn) handleGreet(enhanced bool, arg string) {
if !enhanced {
domain, err := parseHelloArgument(arg)
if err != nil {
c.WriteResponse(501, "Domain/address argument required for HELO")
return
}
c.helo = domain
c.WriteResponse(250, fmt.Sprintf("Hello %s", domain))
} else {
domain, err := parseHelloArgument(arg)
if err != nil {
c.WriteResponse(501, "Domain/address argument required for EHLO")
return
}
c.helo = domain
caps := []string{}
caps = append(caps, c.server.caps...)
if c.server.TLSConfig != nil && !c.IsTLS() {
caps = append(caps, "STARTTLS")
}
if c.IsTLS() || c.server.AllowInsecureAuth {
authCap := "AUTH"
for name, _ := range c.server.auths {
authCap += " " + name
}
caps = append(caps, authCap)
}
if c.server.MaxMessageBytes > 0 {
caps = append(caps, fmt.Sprintf("SIZE %v", c.server.MaxMessageBytes))
}
args := []string{"Hello " + domain}
args = append(args, caps...)
c.WriteResponse(250, args...)
}
}
// READY state -> waiting for MAIL
func (c *Conn) handleMail(arg string) {
if c.helo == "" {
c.WriteResponse(502, "Please introduce yourself first.")
return
}
if c.msg == nil {
c.WriteResponse(502, "Please authenticate first.")
return
}
// Match FROM, while accepting '>' as quoted pair and in double quoted strings
// (?i) makes the regex case insensitive, (?:) is non-grouping sub-match
re := regexp.MustCompile("(?i)^FROM:\\s*<((?:\\\\>|[^>])+|\"[^\"]+\"@[^>]+)>( [\\w= ]+)?$")
m := re.FindStringSubmatch(arg)
if m == nil {
c.WriteResponse(501, "Was expecting MAIL arg syntax of FROM:<address>")
return
}
from := m[1]
// This is where the Conn may put BODY=8BITMIME, but we already
// read the DATA as bytes, so it does not effect our processing.
if m[2] != "" {
args, err := parseArgs(m[2])
if err != nil {
c.WriteResponse(501, "Unable to parse MAIL ESMTP parameters")
return
}
if args["SIZE"] != "" {
size, err := strconv.ParseInt(args["SIZE"], 10, 32)
if err != nil {
c.WriteResponse(501, "Unable to parse SIZE as an integer")
return
}
if c.server.MaxMessageBytes > 0 && int(size) > c.server.MaxMessageBytes {
c.WriteResponse(552, "Max message size exceeded")
return
}
}
}
c.msg.From = from
c.WriteResponse(250, fmt.Sprintf("Roger, accepting mail from <%v>", from))
}
// MAIL state -> waiting for RCPTs followed by DATA
func (c *Conn) handleRcpt(arg string) {
if c.msg == nil || c.msg.From == "" {
c.WriteResponse(502, "Missing MAIL FROM command.")
return
}
if (len(arg) < 4) || (strings.ToUpper(arg[0:3]) != "TO:") {
c.WriteResponse(501, "Was expecting RCPT arg syntax of TO:<address>")
return
}
// TODO: This trim is probably too forgiving
recipient := strings.Trim(arg[3:], "<> ")
if c.server.MaxRecipients > 0 && len(c.msg.To) >= c.server.MaxRecipients {
c.WriteResponse(552, fmt.Sprintf("Maximum limit of %v recipients reached", c.server.MaxRecipients))
return
}
c.msg.To = append(c.msg.To, recipient)
c.WriteResponse(250, fmt.Sprintf("I'll make sure <%v> gets this", recipient))
}
func (c *Conn) handleAuth(arg string) {
if c.helo == "" {
c.WriteResponse(502, "Please introduce yourself first.")
return
}
if arg == "" {
c.WriteResponse(502, "Missing parameter")
return
}
parts := strings.Fields(arg)
mechanism := strings.ToUpper(parts[0])
// Parse client initial response if there is one
var ir []byte
if len(parts) > 1 {
var err error
ir, err = base64.StdEncoding.DecodeString(parts[1])
if err != nil {
return
}
}
newSasl, ok := c.server.auths[mechanism]
if !ok {
c.WriteResponse(504, "Unsupported authentication mechanism")
return
}
sasl := newSasl(c)
response := ir
for {
challenge, done, err := sasl.Next(response)
if err != nil {
c.WriteResponse(454, err.Error())
return
}
if done {
break
}
encoded := ""
if len(challenge) > 0 {
encoded = base64.StdEncoding.EncodeToString(challenge)
}
c.WriteResponse(334, encoded)
encoded, err = c.ReadLine()
if err != nil {
return // TODO: error handling
}
response, err = base64.StdEncoding.DecodeString(encoded)
if err != nil {
c.WriteResponse(454, "Invalid base64 data")
return
}
}
if c.User() != nil {
c.WriteResponse(235, "Authentication succeeded")
c.msg = &message{}
}
}
func (c *Conn) handleStartTLS() {
if c.IsTLS() {
c.WriteResponse(502, "Already running in TLS")
return
}
if c.server.TLSConfig == nil {
c.WriteResponse(502, "TLS not supported")
return
}
c.WriteResponse(220, "Ready to start TLS")
// Upgrade to TLS
var tlsConn *tls.Conn
tlsConn = tls.Server(c.conn, c.server.TLSConfig)
if err := tlsConn.Handshake(); err != nil {
c.WriteResponse(550, "Handshake error")
}
c.conn = tlsConn
c.init()
// Reset envelope as a new EHLO/HELO is required after STARTTLS
c.reset()
}
// DATA
func (c *Conn) handleData(arg string) {
if arg != "" {
c.WriteResponse(501, "DATA command should not have any arguments")
return
}
if c.msg == nil || c.msg.From == "" || len(c.msg.To) == 0 {
c.WriteResponse(502, "Missing RCPT TO command.")
return
}
// We have recipients, go to accept data
c.WriteResponse(354, "Go ahead. End your data with <CR><LF>.<CR><LF>")
c.msg.Reader = newDataReader(c)
err := c.User().Send(c.msg.From, c.msg.To, c.msg.Reader)
io.Copy(ioutil.Discard, c.msg.Reader) // Make sure all the data has been consumed
if err != nil {
if smtperr, ok := err.(*smtpError); ok {
c.WriteResponse(smtperr.Code, smtperr.Message)
} else {
c.WriteResponse(554, "Error: transaction failed, blame it on the weather: "+err.Error())
}
} else {
c.WriteResponse(250, "Ok: queued")
}
c.reset()
}
func (c *Conn) Reject() {
c.WriteResponse(421, "Too busy. Try again later.")
c.Close()
}
func (c *Conn) greet() {
c.WriteResponse(220, fmt.Sprintf("%v ESMTP Service Ready", c.server.Domain))
}
// Calculate the next read or write deadline based on MaxIdleSeconds.
func (c *Conn) nextDeadline() time.Time {
if c.server.MaxIdleSeconds == 0 {
return time.Time{} // No deadline
}
return time.Now().Add(time.Duration(c.server.MaxIdleSeconds) * time.Second)
}
func (c *Conn) WriteResponse(code int, text ...string) {
// TODO: error handling
c.conn.SetDeadline(c.nextDeadline())
for i := 0; i < len(text)-1; i++ {
c.text.PrintfLine("%v-%v", code, text[i])
}
c.text.PrintfLine("%v %v", code, text[len(text)-1])
}
// Reads a line of input
func (c *Conn) ReadLine() (string, error) {
if err := c.conn.SetReadDeadline(c.nextDeadline()); err != nil {
return "", err
}
return c.text.ReadLine()
}
func (c *Conn) reset() {
if user := c.User(); user != nil {
user.Logout()
}
c.locker.Lock()
c.user = nil
c.msg = nil
c.locker.Unlock()
}

57
vendor/github.com/emersion/go-smtp/data.go generated vendored Normal file
View File

@@ -0,0 +1,57 @@
package smtp
import (
"io"
)
type smtpError struct {
Code int
Message string
}
func (err *smtpError) Error() string {
return err.Message
}
var ErrDataTooLarge = &smtpError{
Code: 552,
Message: "Maximum message size exceeded",
}
type dataReader struct {
r io.Reader
limited bool
n int64 // Maximum bytes remaining
}
func newDataReader(c *Conn) io.Reader {
dr := &dataReader{
r: c.text.DotReader(),
}
if c.server.MaxMessageBytes > 0 {
dr.limited = true
dr.n = int64(c.server.MaxMessageBytes)
}
return dr
}
func (r *dataReader) Read(b []byte) (n int, err error) {
if r.limited {
if r.n <= 0 {
return 0, ErrDataTooLarge
}
if int64(len(b)) > r.n {
b = b[0:r.n]
}
}
n, err = r.r.Read(b)
if r.limited {
r.n -= int64(n)
}
return
}

66
vendor/github.com/emersion/go-smtp/parse.go generated vendored Normal file
View File

@@ -0,0 +1,66 @@
package smtp
import (
"fmt"
"regexp"
"strings"
)
func parseCmd(line string) (cmd string, arg string, err error) {
line = strings.TrimRight(line, "\r\n")
l := len(line)
switch {
case strings.HasPrefix(line, "STARTTLS"):
return "STARTTLS", "", nil
case l == 0:
return "", "", nil
case l < 4:
return "", "", fmt.Errorf("Command too short: %q", line)
case l == 4:
return strings.ToUpper(line), "", nil
case l == 5:
// Too long to be only command, too short to have args
return "", "", fmt.Errorf("Mangled command: %q", line)
}
// If we made it here, command is long enough to have args
if line[4] != ' ' {
// There wasn't a space after the command?
return "", "", fmt.Errorf("Mangled command: %q", line)
}
// I'm not sure if we should trim the args or not, but we will for now
//return strings.ToUpper(line[0:4]), strings.Trim(line[5:], " "), nil
return strings.ToUpper(line[0:4]), strings.Trim(line[5:], " \n\r"), nil
}
// Takes the arguments proceeding a command and files them
// into a map[string]string after uppercasing each key. Sample arg
// string:
// " BODY=8BITMIME SIZE=1024"
// The leading space is mandatory.
func parseArgs(arg string) (args map[string]string, err error) {
args = map[string]string{}
re := regexp.MustCompile(" (\\w+)=(\\w+)")
pm := re.FindAllStringSubmatch(arg, -1)
if pm == nil {
return nil, fmt.Errorf("Failed to parse arg string: %q", arg)
}
for _, m := range pm {
args[strings.ToUpper(m[1])] = m[2]
}
return args, nil
}
func parseHelloArgument(arg string) (string, error) {
domain := arg
if idx := strings.IndexRune(arg, ' '); idx >= 0 {
domain = arg[:idx]
}
if domain == "" {
return "", fmt.Errorf("Invalid domain")
}
return domain, nil
}

187
vendor/github.com/emersion/go-smtp/server.go generated vendored Executable file
View File

@@ -0,0 +1,187 @@
package smtp
import (
"crypto/tls"
"errors"
"io"
"net"
"sync"
"github.com/emersion/go-sasl"
)
// A function that creates SASL servers.
type SaslServerFactory func(conn *Conn) sasl.Server
// A SMTP server.
type Server struct {
// TCP address to listen on.
Addr string
// The server TLS configuration.
TLSConfig *tls.Config
Domain string
MaxRecipients int
MaxIdleSeconds int
MaxMessageBytes int
AllowInsecureAuth bool
Debug io.Writer
// The server backend.
Backend Backend
listener net.Listener
caps []string
auths map[string]SaslServerFactory
locker sync.Mutex
conns map[*Conn]struct{}
}
// New creates a new SMTP server.
func NewServer(be Backend) *Server {
return &Server{
Backend: be,
caps: []string{"PIPELINING", "8BITMIME"},
auths: map[string]SaslServerFactory{
sasl.Plain: func(conn *Conn) sasl.Server {
return sasl.NewPlainServer(func(identity, username, password string) error {
if identity != "" && identity != username {
return errors.New("Identities not supported")
}
user, err := be.Login(username, password)
if err != nil {
return err
}
conn.SetUser(user)
return nil
})
},
},
conns: make(map[*Conn]struct{}),
}
}
// Serve accepts incoming connections on the Listener l.
func (s *Server) Serve(l net.Listener) error {
s.listener = l
defer s.Close()
for {
c, err := l.Accept()
if err != nil {
return err
}
go s.handleConn(newConn(c, s))
}
}
func (s *Server) handleConn(c *Conn) error {
s.locker.Lock()
s.conns[c] = struct{}{}
s.locker.Unlock()
defer func() {
c.Close()
s.locker.Lock()
delete(s.conns, c)
s.locker.Unlock()
}()
c.greet()
for {
line, err := c.ReadLine()
if err == nil {
cmd, arg, err := parseCmd(line)
if err != nil {
c.nbrErrors++
c.WriteResponse(501, "Bad command")
continue
}
c.handle(cmd, arg)
} else {
if err == io.EOF {
return nil
}
if neterr, ok := err.(net.Error); ok && neterr.Timeout() {
c.WriteResponse(221, "Idle timeout, bye bye")
return nil
}
c.WriteResponse(221, "Connection error, sorry")
return err
}
}
}
// ListenAndServe listens on the TCP network address s.Addr and then calls Serve
// to handle requests on incoming connections.
//
// If s.Addr is blank, ":smtp" is used.
func (s *Server) ListenAndServe() error {
addr := s.Addr
if addr == "" {
addr = ":smtp"
}
l, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return s.Serve(l)
}
// ListenAndServeTLS listens on the TCP network address s.Addr and then calls
// Serve to handle requests on incoming TLS connections.
//
// If s.Addr is blank, ":smtps" is used.
func (s *Server) ListenAndServeTLS() error {
addr := s.Addr
if addr == "" {
addr = ":smtps"
}
l, err := tls.Listen("tcp", addr, s.TLSConfig)
if err != nil {
return err
}
return s.Serve(l)
}
// Close stops the server.
func (s *Server) Close() {
s.listener.Close()
s.locker.Lock()
defer s.locker.Unlock()
for conn := range s.conns {
conn.Close()
}
}
// EnableAuth enables an authentication mechanism on this server.
//
// This function should not be called directly, it must only be used by
// libraries implementing extensions of the SMTP protocol.
func (s *Server) EnableAuth(name string, f SaslServerFactory) {
s.auths[name] = f
}
// ForEachConn iterates through all opened connections.
func (s *Server) ForEachConn(f func(*Conn)) {
s.locker.Lock()
defer s.locker.Unlock()
for conn := range s.conns {
f(conn)
}
}

7
vendor/github.com/emersion/go-smtp/smtp.go generated vendored Normal file
View File

@@ -0,0 +1,7 @@
// Package smtp implements the Simple Mail Transfer Protocol as defined in RFC 5321.
// It also implements the following extensions:
// 8BITMIME RFC 1652
// AUTH RFC 2554
// STARTTLS RFC 3207
// Additional extensions may be handled by other packages.
package smtp

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

@@ -0,0 +1,3 @@
# 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.

3
vendor/golang.org/x/text/CONTRIBUTORS generated vendored Normal file
View File

@@ -0,0 +1,3 @@
# 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 Normal file
View File

@@ -0,0 +1,27 @@
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 Normal file
View File

@@ -0,0 +1,22 @@
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.

335
vendor/golang.org/x/text/encoding/encoding.go generated vendored Normal file
View File

@@ -0,0 +1,335 @@
// 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
// http://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
}

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