Initial commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/crockery
|
||||
/crockery.db
|
21
DESIGN.md
Normal file
21
DESIGN.md
Normal 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
46
Gopkg.lock
generated
Normal 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
34
Gopkg.toml
Normal 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
120
README.md
Normal 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
46
cmd/crockery/main.go
Normal 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
62
internal/imap/server.go
Normal 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
|
||||
}
|
69
internal/services/services.go
Normal file
69
internal/services/services.go
Normal 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
62
internal/smtp/server.go
Normal 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
21
internal/store/store.go
Normal 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
28
vendor/github.com/emersion/go-imap/.gitignore
generated
vendored
Normal 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
5
vendor/github.com/emersion/go-imap/.travis.yml
generated
vendored
Normal 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
23
vendor/github.com/emersion/go-imap/LICENSE
generated
vendored
Normal 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
183
vendor/github.com/emersion/go-imap/README.md
generated
vendored
Normal file
@@ -0,0 +1,183 @@
|
||||
# go-imap
|
||||
|
||||
[](https://godoc.org/github.com/emersion/go-imap)
|
||||
[](https://travis-ci.org/emersion/go-imap)
|
||||
[](https://codecov.io/gh/emersion/go-imap)
|
||||
[](https://goreportcard.com/report/github.com/emersion/go-imap)
|
||||
[](https://github.com/emersion/stability-badges#unstable)
|
||||
[](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 [](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 [](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
16
vendor/github.com/emersion/go-imap/backend/backend.go
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
// Package backend defines an IMAP server backend interface.
|
||||
package backend
|
||||
|
||||
import "errors"
|
||||
|
||||
// ErrInvalidCredentials is returned by Backend.Login when a username or a
|
||||
// password is incorrect.
|
||||
var ErrInvalidCredentials = errors.New("Invalid credentials")
|
||||
|
||||
// Backend is an IMAP server backend. A backend operation always deals with
|
||||
// users.
|
||||
type Backend interface {
|
||||
// Login authenticates a user. If the username or the password is incorrect,
|
||||
// it returns ErrInvalidCredentials.
|
||||
Login(username, password string) (User, error)
|
||||
}
|
78
vendor/github.com/emersion/go-imap/backend/mailbox.go
generated
vendored
Normal file
78
vendor/github.com/emersion/go-imap/backend/mailbox.go
generated
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
)
|
||||
|
||||
// Mailbox represents a mailbox belonging to a user in the mail storage system.
|
||||
// A mailbox operation always deals with messages.
|
||||
type Mailbox interface {
|
||||
// Name returns this mailbox name.
|
||||
Name() string
|
||||
|
||||
// Info returns this mailbox info.
|
||||
Info() (*imap.MailboxInfo, error)
|
||||
|
||||
// Status returns this mailbox status. The fields Name, Flags 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
109
vendor/github.com/emersion/go-imap/backend/updates.go
generated
vendored
Normal 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
92
vendor/github.com/emersion/go-imap/backend/user.go
generated
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
package backend
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// ErrNoSuchMailbox is returned by User.GetMailbox, User.DeleteMailbox and
|
||||
// User.RenameMailbox when retrieving, deleting or renaming a mailbox that
|
||||
// doesn't exist.
|
||||
ErrNoSuchMailbox = errors.New("No such mailbox")
|
||||
// ErrMailboxAlreadyExists is returned by User.CreateMailbox and
|
||||
// User.RenameMailbox when creating or renaming mailbox that already exists.
|
||||
ErrMailboxAlreadyExists = errors.New("Mailbox already exists")
|
||||
)
|
||||
|
||||
// User represents a user in the mail storage system. A user operation always
|
||||
// deals with mailboxes.
|
||||
type User interface {
|
||||
// Username returns this user's username.
|
||||
Username() string
|
||||
|
||||
// ListMailboxes returns a list of mailboxes belonging to this user. If
|
||||
// subscribed is set to true, only returns subscribed mailboxes.
|
||||
ListMailboxes(subscribed bool) ([]Mailbox, error)
|
||||
|
||||
// GetMailbox returns a mailbox. If it doesn't exist, it returns
|
||||
// ErrNoSuchMailbox.
|
||||
GetMailbox(name string) (Mailbox, error)
|
||||
|
||||
// CreateMailbox creates a new mailbox.
|
||||
//
|
||||
// If the mailbox already exists, an error must be returned. If the mailbox
|
||||
// name is suffixed with the server's hierarchy separator character, this is a
|
||||
// declaration that the client intends to create mailbox names under this name
|
||||
// in the hierarchy.
|
||||
//
|
||||
// If the server's hierarchy separator character appears elsewhere in the
|
||||
// name, the server SHOULD create any superior hierarchical names that are
|
||||
// needed for the CREATE command to be successfully completed. In other
|
||||
// words, an attempt to create "foo/bar/zap" on a server in which "/" is the
|
||||
// hierarchy separator character SHOULD create foo/ and foo/bar/ if they do
|
||||
// not already exist.
|
||||
//
|
||||
// If a new mailbox is created with the same name as a mailbox which was
|
||||
// deleted, its unique identifiers MUST be greater than any unique identifiers
|
||||
// used in the previous incarnation of the mailbox UNLESS the new incarnation
|
||||
// has a different unique identifier validity value.
|
||||
CreateMailbox(name string) error
|
||||
|
||||
// DeleteMailbox permanently remove the mailbox with the given name. It is an
|
||||
// error to // attempt to delete INBOX or a mailbox name that does not exist.
|
||||
//
|
||||
// The DELETE command MUST NOT remove inferior hierarchical names. For
|
||||
// example, if a mailbox "foo" has an inferior "foo.bar" (assuming "." is the
|
||||
// hierarchy delimiter character), removing "foo" MUST NOT remove "foo.bar".
|
||||
//
|
||||
// The value of the highest-used unique identifier of the deleted mailbox MUST
|
||||
// be preserved so that a new mailbox created with the same name will not
|
||||
// reuse the identifiers of the former incarnation, UNLESS the new incarnation
|
||||
// has a different unique identifier validity value.
|
||||
DeleteMailbox(name string) error
|
||||
|
||||
// RenameMailbox changes the name of a mailbox. It is an error to attempt to
|
||||
// rename from a mailbox name that does not exist or to a mailbox name that
|
||||
// already exists.
|
||||
//
|
||||
// If the name has inferior hierarchical names, then the inferior hierarchical
|
||||
// names MUST also be renamed. For example, a rename of "foo" to "zap" will
|
||||
// rename "foo/bar" (assuming "/" is the hierarchy delimiter character) to
|
||||
// "zap/bar".
|
||||
//
|
||||
// If the server's hierarchy separator character appears in the name, the
|
||||
// server SHOULD create any superior hierarchical names that are needed for
|
||||
// the RENAME command to complete successfully. In other words, an attempt to
|
||||
// rename "foo/bar/zap" to baz/rag/zowie on a server in which "/" is the
|
||||
// hierarchy separator character SHOULD create baz/ and baz/rag/ if they do
|
||||
// not already exist.
|
||||
//
|
||||
// The value of the highest-used unique identifier of the old mailbox name
|
||||
// MUST be preserved so that a new mailbox created with the same name will not
|
||||
// reuse the identifiers of the former incarnation, UNLESS the new incarnation
|
||||
// has a different unique identifier validity value.
|
||||
//
|
||||
// Renaming INBOX is permitted, and has special behavior. It moves all
|
||||
// messages in INBOX to a new mailbox with the given name, leaving INBOX
|
||||
// empty. If the server implementation supports inferior hierarchical names
|
||||
// of INBOX, these are unaffected by a rename of INBOX.
|
||||
RenameMailbox(existingName, newName string) error
|
||||
|
||||
// Logout is called when this User will no longer be used, likely because the
|
||||
// client closed the connection.
|
||||
Logout() error
|
||||
}
|
89
vendor/github.com/emersion/go-imap/command.go
generated
vendored
Normal file
89
vendor/github.com/emersion/go-imap/command.go
generated
vendored
Normal 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
95
vendor/github.com/emersion/go-imap/commands/append.go
generated
vendored
Normal 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
|
||||
}
|
83
vendor/github.com/emersion/go-imap/commands/authenticate.go
generated
vendored
Normal file
83
vendor/github.com/emersion/go-imap/commands/authenticate.go
generated
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-sasl"
|
||||
)
|
||||
|
||||
// AuthenticateConn is a connection that supports IMAP authentication.
|
||||
type AuthenticateConn interface {
|
||||
io.Reader
|
||||
|
||||
// WriteResp writes an IMAP response to this connection.
|
||||
WriteResp(res imap.WriterTo) error
|
||||
}
|
||||
|
||||
// Authenticate is an AUTHENTICATE command, as defined in RFC 3501 section
|
||||
// 6.2.2.
|
||||
type Authenticate struct {
|
||||
Mechanism string
|
||||
}
|
||||
|
||||
func (cmd *Authenticate) Command() *imap.Command {
|
||||
return &imap.Command{
|
||||
Name: 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
vendor/github.com/emersion/go-imap/commands/capability.go
generated
vendored
Normal file
18
vendor/github.com/emersion/go-imap/commands/capability.go
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-imap"
|
||||
)
|
||||
|
||||
// Capability is a CAPABILITY command, as defined in RFC 3501 section 6.1.1.
|
||||
type Capability struct{}
|
||||
|
||||
func (c *Capability) Command() *imap.Command {
|
||||
return &imap.Command{
|
||||
Name: 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
18
vendor/github.com/emersion/go-imap/commands/check.go
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-imap"
|
||||
)
|
||||
|
||||
// Check is a CHECK command, as defined in RFC 3501 section 6.4.1.
|
||||
type Check struct{}
|
||||
|
||||
func (cmd *Check) Command() *imap.Command {
|
||||
return &imap.Command{
|
||||
Name: 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
18
vendor/github.com/emersion/go-imap/commands/close.go
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-imap"
|
||||
)
|
||||
|
||||
// Close is a CLOSE command, as defined in RFC 3501 section 6.4.2.
|
||||
type Close struct{}
|
||||
|
||||
func (cmd *Close) Command() *imap.Command {
|
||||
return &imap.Command{
|
||||
Name: imap.Close,
|
||||
}
|
||||
}
|
||||
|
||||
func (cmd *Close) Parse(fields []interface{}) error {
|
||||
return nil
|
||||
}
|
2
vendor/github.com/emersion/go-imap/commands/commands.go
generated
vendored
Normal file
2
vendor/github.com/emersion/go-imap/commands/commands.go
generated
vendored
Normal 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
47
vendor/github.com/emersion/go-imap/commands/copy.go
generated
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/utf7"
|
||||
)
|
||||
|
||||
// Copy is a COPY command, as defined in RFC 3501 section 6.4.7.
|
||||
type Copy struct {
|
||||
SeqSet *imap.SeqSet
|
||||
Mailbox string
|
||||
}
|
||||
|
||||
func (cmd *Copy) Command() *imap.Command {
|
||||
mailbox, _ := utf7.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
38
vendor/github.com/emersion/go-imap/commands/create.go
generated
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/utf7"
|
||||
)
|
||||
|
||||
// Create is a CREATE command, as defined in RFC 3501 section 6.3.3.
|
||||
type Create struct {
|
||||
Mailbox string
|
||||
}
|
||||
|
||||
func (cmd *Create) Command() *imap.Command {
|
||||
mailbox, _ := utf7.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
38
vendor/github.com/emersion/go-imap/commands/delete.go
generated
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/utf7"
|
||||
)
|
||||
|
||||
// Delete is a DELETE command, as defined in RFC 3501 section 6.3.3.
|
||||
type Delete struct {
|
||||
Mailbox string
|
||||
}
|
||||
|
||||
func (cmd *Delete) Command() *imap.Command {
|
||||
mailbox, _ := utf7.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
16
vendor/github.com/emersion/go-imap/commands/expunge.go
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-imap"
|
||||
)
|
||||
|
||||
// Expunge is an EXPUNGE command, as defined in RFC 3501 section 6.4.3.
|
||||
type Expunge struct{}
|
||||
|
||||
func (cmd *Expunge) Command() *imap.Command {
|
||||
return &imap.Command{Name: 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
70
vendor/github.com/emersion/go-imap/commands/fetch.go
generated
vendored
Normal 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
57
vendor/github.com/emersion/go-imap/commands/list.go
generated
vendored
Normal 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
36
vendor/github.com/emersion/go-imap/commands/login.go
generated
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
)
|
||||
|
||||
// Login is a LOGIN command, as defined in RFC 3501 section 6.2.2.
|
||||
type Login struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
func (cmd *Login) Command() *imap.Command {
|
||||
return &imap.Command{
|
||||
Name: 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
18
vendor/github.com/emersion/go-imap/commands/logout.go
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-imap"
|
||||
)
|
||||
|
||||
// Logout is a LOGOUT command, as defined in RFC 3501 section 6.1.3.
|
||||
type Logout struct{}
|
||||
|
||||
func (c *Logout) Command() *imap.Command {
|
||||
return &imap.Command{
|
||||
Name: 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
18
vendor/github.com/emersion/go-imap/commands/noop.go
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-imap"
|
||||
)
|
||||
|
||||
// Noop is a NOOP command, as defined in RFC 3501 section 6.1.2.
|
||||
type Noop struct{}
|
||||
|
||||
func (c *Noop) Command() *imap.Command {
|
||||
return &imap.Command{
|
||||
Name: 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
48
vendor/github.com/emersion/go-imap/commands/rename.go
generated
vendored
Normal 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
57
vendor/github.com/emersion/go-imap/commands/search.go
generated
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
)
|
||||
|
||||
// Search is a SEARCH command, as defined in RFC 3501 section 6.4.4.
|
||||
type Search struct {
|
||||
Charset string
|
||||
Criteria *imap.SearchCriteria
|
||||
}
|
||||
|
||||
func (cmd *Search) Command() *imap.Command {
|
||||
var args []interface{}
|
||||
if cmd.Charset != "" {
|
||||
args = append(args, "CHARSET", cmd.Charset)
|
||||
}
|
||||
args = append(args, cmd.Criteria.Format()...)
|
||||
|
||||
return &imap.Command{
|
||||
Name: 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
45
vendor/github.com/emersion/go-imap/commands/select.go
generated
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/utf7"
|
||||
)
|
||||
|
||||
// Select is a SELECT command, as defined in RFC 3501 section 6.3.1. If ReadOnly
|
||||
// is set to true, the EXAMINE command will be used instead.
|
||||
type Select struct {
|
||||
Mailbox string
|
||||
ReadOnly bool
|
||||
}
|
||||
|
||||
func (cmd *Select) Command() *imap.Command {
|
||||
name := 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
|
||||
}
|
18
vendor/github.com/emersion/go-imap/commands/starttls.go
generated
vendored
Normal file
18
vendor/github.com/emersion/go-imap/commands/starttls.go
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-imap"
|
||||
)
|
||||
|
||||
// StartTLS is a STARTTLS command, as defined in RFC 3501 section 6.2.1.
|
||||
type StartTLS struct{}
|
||||
|
||||
func (cmd *StartTLS) Command() *imap.Command {
|
||||
return &imap.Command{
|
||||
Name: 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
55
vendor/github.com/emersion/go-imap/commands/status.go
generated
vendored
Normal 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
45
vendor/github.com/emersion/go-imap/commands/store.go
generated
vendored
Normal 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
|
||||
}
|
71
vendor/github.com/emersion/go-imap/commands/subscribe.go
generated
vendored
Normal file
71
vendor/github.com/emersion/go-imap/commands/subscribe.go
generated
vendored
Normal 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
44
vendor/github.com/emersion/go-imap/commands/uid.go
generated
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
)
|
||||
|
||||
// Uid is a UID command, as defined in RFC 3501 section 6.4.8. It wraps another
|
||||
// command (e.g. wrapping a Fetch command will result in a UID FETCH).
|
||||
type Uid struct {
|
||||
Cmd imap.Commander
|
||||
}
|
||||
|
||||
func (cmd *Uid) Command() *imap.Command {
|
||||
inner := cmd.Cmd.Command()
|
||||
|
||||
args := []interface{}{inner.Name}
|
||||
args = append(args, inner.Arguments...)
|
||||
|
||||
return &imap.Command{
|
||||
Name: 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
197
vendor/github.com/emersion/go-imap/conn.go
generated
vendored
Normal 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
72
vendor/github.com/emersion/go-imap/date.go
generated
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
package imap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Date and time layouts.
|
||||
// Dovecot adds a leading zero to dates:
|
||||
// https://github.com/dovecot/core/blob/4fbd5c5e113078e72f29465ccc96d44955ceadc2/src/lib-imap/imap-date.c#L166
|
||||
// Cyrus adds a leading space to dates:
|
||||
// https://github.com/cyrusimap/cyrus-imapd/blob/1cb805a3bffbdf829df0964f3b802cdc917e76db/lib/times.c#L543
|
||||
// GMail doesn't support leading spaces in dates used in SEARCH commands.
|
||||
const (
|
||||
// Defined in RFC 3501 as date-text on page 83.
|
||||
DateLayout = "_2-Jan-2006"
|
||||
// Defined in RFC 3501 as date-time on page 83.
|
||||
DateTimeLayout = "_2-Jan-2006 15:04:05 -0700"
|
||||
// Defined in RFC 5322 section 3.3, mentioned as env-date in RFC 3501 page 84.
|
||||
envelopeDateTimeLayout = "Mon, 02 Jan 2006 15:04:05 -0700"
|
||||
// Use as an example in RFC 3501 page 54.
|
||||
searchDateLayout = "2-Jan-2006"
|
||||
)
|
||||
|
||||
// time.Time with a specific layout.
|
||||
type (
|
||||
Date time.Time
|
||||
DateTime time.Time
|
||||
envelopeDateTime time.Time
|
||||
searchDate time.Time
|
||||
)
|
||||
|
||||
// Permutations of the layouts defined in RFC 5322, section 3.3.
|
||||
var envelopeDateTimeLayouts = [...]string{
|
||||
envelopeDateTimeLayout, // popular, try it first
|
||||
"_2 Jan 2006 15:04:05 -0700",
|
||||
"_2 Jan 2006 15:04:05 MST",
|
||||
"_2 Jan 2006 15:04:05 -0700 (MST)",
|
||||
"_2 Jan 2006 15:04 -0700",
|
||||
"_2 Jan 2006 15:04 MST",
|
||||
"_2 Jan 2006 15:04 -0700 (MST)",
|
||||
"_2 Jan 06 15:04:05 -0700",
|
||||
"_2 Jan 06 15:04:05 MST",
|
||||
"_2 Jan 06 15:04:05 -0700 (MST)",
|
||||
"_2 Jan 06 15:04 -0700",
|
||||
"_2 Jan 06 15:04 MST",
|
||||
"_2 Jan 06 15:04 -0700 (MST)",
|
||||
"Mon, _2 Jan 2006 15:04:05 -0700",
|
||||
"Mon, _2 Jan 2006 15:04:05 MST",
|
||||
"Mon, _2 Jan 2006 15:04:05 -0700 (MST)",
|
||||
"Mon, _2 Jan 2006 15:04 -0700",
|
||||
"Mon, _2 Jan 2006 15:04 MST",
|
||||
"Mon, _2 Jan 2006 15:04 -0700 (MST)",
|
||||
"Mon, _2 Jan 06 15:04:05 -0700",
|
||||
"Mon, _2 Jan 06 15:04:05 MST",
|
||||
"Mon, _2 Jan 06 15:04:05 -0700 (MST)",
|
||||
"Mon, _2 Jan 06 15:04 -0700",
|
||||
"Mon, _2 Jan 06 15:04 MST",
|
||||
"Mon, _2 Jan 06 15:04 -0700 (MST)",
|
||||
}
|
||||
|
||||
// Try parsing the date based on the layouts defined in RFC 5322, section 3.3.
|
||||
// Inspired by https://github.com/golang/go/blob/master/src/net/mail/message.go
|
||||
func parseMessageDateTime(maybeDate string) (time.Time, error) {
|
||||
for _, layout := range envelopeDateTimeLayouts {
|
||||
parsed, err := time.Parse(layout, maybeDate)
|
||||
if err == nil {
|
||||
return parsed, nil
|
||||
}
|
||||
}
|
||||
return time.Time{}, fmt.Errorf("date %s could not be parsed", maybeDate)
|
||||
}
|
121
vendor/github.com/emersion/go-imap/handle.go
generated
vendored
Normal file
121
vendor/github.com/emersion/go-imap/handle.go
generated
vendored
Normal 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
28
vendor/github.com/emersion/go-imap/imap.go
generated
vendored
Normal 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
13
vendor/github.com/emersion/go-imap/literal.go
generated
vendored
Normal 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
8
vendor/github.com/emersion/go-imap/logger.go
generated
vendored
Normal 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
246
vendor/github.com/emersion/go-imap/mailbox.go
generated
vendored
Normal 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
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
430
vendor/github.com/emersion/go-imap/read.go
generated
vendored
Normal 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
156
vendor/github.com/emersion/go-imap/response.go
generated
vendored
Normal 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
|
||||
}
|
72
vendor/github.com/emersion/go-imap/responses/authenticate.go
generated
vendored
Normal file
72
vendor/github.com/emersion/go-imap/responses/authenticate.go
generated
vendored
Normal 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
|
||||
}
|
39
vendor/github.com/emersion/go-imap/responses/capability.go
generated
vendored
Normal file
39
vendor/github.com/emersion/go-imap/responses/capability.go
generated
vendored
Normal 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)
|
||||
}
|
45
vendor/github.com/emersion/go-imap/responses/expunge.go
generated
vendored
Normal file
45
vendor/github.com/emersion/go-imap/responses/expunge.go
generated
vendored
Normal 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
55
vendor/github.com/emersion/go-imap/responses/fetch.go
generated
vendored
Normal 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
59
vendor/github.com/emersion/go-imap/responses/list.go
generated
vendored
Normal 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
|
||||
}
|
2
vendor/github.com/emersion/go-imap/responses/responses.go
generated
vendored
Normal file
2
vendor/github.com/emersion/go-imap/responses/responses.go
generated
vendored
Normal 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
37
vendor/github.com/emersion/go-imap/responses/search.go
generated
vendored
Normal 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
145
vendor/github.com/emersion/go-imap/responses/select.go
generated
vendored
Normal 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
63
vendor/github.com/emersion/go-imap/responses/status.go
generated
vendored
Normal 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
367
vendor/github.com/emersion/go-imap/search.go
generated
vendored
Normal 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
290
vendor/github.com/emersion/go-imap/seqset.go
generated
vendored
Normal 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
52
vendor/github.com/emersion/go-imap/server/cmd_any.go
generated
vendored
Normal 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
261
vendor/github.com/emersion/go-imap/server/cmd_auth.go
generated
vendored
Normal 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
127
vendor/github.com/emersion/go-imap/server/cmd_noauth.go
generated
vendored
Normal 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)
|
||||
}
|
313
vendor/github.com/emersion/go-imap/server/cmd_selected.go
generated
vendored
Normal file
313
vendor/github.com/emersion/go-imap/server/cmd_selected.go
generated
vendored
Normal 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
383
vendor/github.com/emersion/go-imap/server/conn.go
generated
vendored
Normal 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
426
vendor/github.com/emersion/go-imap/server/server.go
generated
vendored
Normal 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
120
vendor/github.com/emersion/go-imap/status.go
generated
vendored
Normal 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
152
vendor/github.com/emersion/go-imap/utf7/decoder.go
generated
vendored
Normal 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
96
vendor/github.com/emersion/go-imap/utf7/encoder.go
generated
vendored
Normal 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
15
vendor/github.com/emersion/go-imap/utf7/utf7.go
generated
vendored
Normal 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
229
vendor/github.com/emersion/go-imap/write.go
generated
vendored
Normal 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
24
vendor/github.com/emersion/go-sasl/.gitignore
generated
vendored
Normal 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
3
vendor/github.com/emersion/go-sasl/.travis.yml
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
language: go
|
||||
go:
|
||||
- 1.5
|
21
vendor/github.com/emersion/go-sasl/LICENSE
generated
vendored
Normal file
21
vendor/github.com/emersion/go-sasl/LICENSE
generated
vendored
Normal 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
17
vendor/github.com/emersion/go-sasl/README.md
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
# go-sasl
|
||||
|
||||
[](https://godoc.org/github.com/emersion/go-sasl)
|
||||
[](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
56
vendor/github.com/emersion/go-sasl/anonymous.go
generated
vendored
Normal 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
26
vendor/github.com/emersion/go-sasl/external.go
generated
vendored
Normal 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
47
vendor/github.com/emersion/go-sasl/login.go
generated
vendored
Normal 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
77
vendor/github.com/emersion/go-sasl/plain.go
generated
vendored
Normal 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
45
vendor/github.com/emersion/go-sasl/sasl.go
generated
vendored
Normal 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
48
vendor/github.com/emersion/go-sasl/xoauth2.go
generated
vendored
Normal 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
26
vendor/github.com/emersion/go-smtp/.gitignore
generated
vendored
Normal 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
5
vendor/github.com/emersion/go-smtp/.travis.yml
generated
vendored
Normal 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
24
vendor/github.com/emersion/go-smtp/LICENSE
generated
vendored
Normal 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
125
vendor/github.com/emersion/go-smtp/README.md
generated
vendored
Executable file
@@ -0,0 +1,125 @@
|
||||
# go-smtp
|
||||
|
||||
[](https://godoc.org/github.com/emersion/go-smtp)
|
||||
[](https://travis-ci.org/emersion/go-smtp)
|
||||
[](https://codecov.io/gh/emersion/go-smtp)
|
||||
[](https://github.com/emersion/stability-badges#unstable)
|
||||
[](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
19
vendor/github.com/emersion/go-smtp/backend.go
generated
vendored
Normal 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
384
vendor/github.com/emersion/go-smtp/client.go
generated
vendored
Normal 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
432
vendor/github.com/emersion/go-smtp/conn.go
generated
vendored
Normal 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
57
vendor/github.com/emersion/go-smtp/data.go
generated
vendored
Normal 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
66
vendor/github.com/emersion/go-smtp/parse.go
generated
vendored
Normal 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
187
vendor/github.com/emersion/go-smtp/server.go
generated
vendored
Executable 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
7
vendor/github.com/emersion/go-smtp/smtp.go
generated
vendored
Normal 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
3
vendor/golang.org/x/text/AUTHORS
generated
vendored
Normal 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
3
vendor/golang.org/x/text/CONTRIBUTORS
generated
vendored
Normal 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
27
vendor/golang.org/x/text/LICENSE
generated
vendored
Normal 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
22
vendor/golang.org/x/text/PATENTS
generated
vendored
Normal 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
335
vendor/golang.org/x/text/encoding/encoding.go
generated
vendored
Normal 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
Reference in New Issue
Block a user