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