From c4159d895ac399ca55326f7b4ff8bfbf8402e654 Mon Sep 17 00:00:00 2001 From: Jordan Date: Sat, 4 Feb 2023 23:54:03 -0700 Subject: initial commit --- .gitignore | 3 + Makefile | 23 + README | 22 + UNLICENSE | 24 + go.mod | 11 + go.sum | 12 + message.eml.example | 8 + pigeon.go | 325 ++++ pigeon.json.example | 14 + vendor/github.com/emersion/go-imap/.build.yml | 17 + vendor/github.com/emersion/go-imap/.gitignore | 28 + vendor/github.com/emersion/go-imap/LICENSE | 23 + vendor/github.com/emersion/go-imap/README.md | 178 +++ .../github.com/emersion/go-imap/client/client.go | 689 +++++++++ .../github.com/emersion/go-imap/client/cmd_any.go | 88 ++ .../github.com/emersion/go-imap/client/cmd_auth.go | 380 +++++ .../emersion/go-imap/client/cmd_noauth.go | 174 +++ .../emersion/go-imap/client/cmd_selected.go | 367 +++++ vendor/github.com/emersion/go-imap/client/tag.go | 24 + vendor/github.com/emersion/go-imap/command.go | 57 + .../github.com/emersion/go-imap/commands/append.go | 93 ++ .../emersion/go-imap/commands/authenticate.go | 124 ++ .../emersion/go-imap/commands/capability.go | 18 + .../github.com/emersion/go-imap/commands/check.go | 18 + .../github.com/emersion/go-imap/commands/close.go | 18 + .../emersion/go-imap/commands/commands.go | 2 + .../github.com/emersion/go-imap/commands/copy.go | 47 + .../github.com/emersion/go-imap/commands/create.go | 38 + .../github.com/emersion/go-imap/commands/delete.go | 38 + .../github.com/emersion/go-imap/commands/enable.go | 23 + .../emersion/go-imap/commands/expunge.go | 16 + .../github.com/emersion/go-imap/commands/fetch.go | 63 + .../github.com/emersion/go-imap/commands/idle.go | 17 + .../github.com/emersion/go-imap/commands/list.go | 60 + .../github.com/emersion/go-imap/commands/login.go | 36 + .../github.com/emersion/go-imap/commands/logout.go | 18 + .../github.com/emersion/go-imap/commands/move.go | 48 + .../github.com/emersion/go-imap/commands/noop.go | 18 + .../github.com/emersion/go-imap/commands/rename.go | 51 + .../github.com/emersion/go-imap/commands/search.go | 57 + .../github.com/emersion/go-imap/commands/select.go | 45 + .../emersion/go-imap/commands/starttls.go | 18 + .../github.com/emersion/go-imap/commands/status.go | 58 + .../github.com/emersion/go-imap/commands/store.go | 50 + .../emersion/go-imap/commands/subscribe.go | 63 + vendor/github.com/emersion/go-imap/commands/uid.go | 44 + .../emersion/go-imap/commands/unselect.go | 17 + vendor/github.com/emersion/go-imap/conn.go | 284 ++++ vendor/github.com/emersion/go-imap/date.go | 71 + vendor/github.com/emersion/go-imap/imap.go | 108 ++ vendor/github.com/emersion/go-imap/literal.go | 13 + vendor/github.com/emersion/go-imap/logger.go | 8 + vendor/github.com/emersion/go-imap/mailbox.go | 314 ++++ vendor/github.com/emersion/go-imap/message.go | 1186 ++++++++++++++ vendor/github.com/emersion/go-imap/read.go | 467 ++++++ vendor/github.com/emersion/go-imap/response.go | 181 +++ .../emersion/go-imap/responses/authenticate.go | 61 + .../emersion/go-imap/responses/capability.go | 20 + .../emersion/go-imap/responses/enabled.go | 33 + .../emersion/go-imap/responses/expunge.go | 43 + .../github.com/emersion/go-imap/responses/fetch.go | 70 + .../github.com/emersion/go-imap/responses/idle.go | 38 + .../github.com/emersion/go-imap/responses/list.go | 57 + .../emersion/go-imap/responses/responses.go | 35 + .../emersion/go-imap/responses/search.go | 41 + .../emersion/go-imap/responses/select.go | 142 ++ .../emersion/go-imap/responses/status.go | 53 + vendor/github.com/emersion/go-imap/search.go | 371 +++++ vendor/github.com/emersion/go-imap/seqset.go | 289 ++++ vendor/github.com/emersion/go-imap/status.go | 136 ++ vendor/github.com/emersion/go-imap/utf7/decoder.go | 149 ++ vendor/github.com/emersion/go-imap/utf7/encoder.go | 91 ++ vendor/github.com/emersion/go-imap/utf7/utf7.go | 34 + vendor/github.com/emersion/go-imap/write.go | 255 +++ vendor/github.com/emersion/go-sasl/.build.yml | 19 + vendor/github.com/emersion/go-sasl/.gitignore | 24 + vendor/github.com/emersion/go-sasl/LICENSE | 21 + vendor/github.com/emersion/go-sasl/README.md | 17 + vendor/github.com/emersion/go-sasl/anonymous.go | 56 + vendor/github.com/emersion/go-sasl/external.go | 26 + vendor/github.com/emersion/go-sasl/login.go | 89 ++ vendor/github.com/emersion/go-sasl/oauthbearer.go | 191 +++ vendor/github.com/emersion/go-sasl/plain.go | 77 + vendor/github.com/emersion/go-sasl/sasl.go | 45 + vendor/github.com/emersion/go-smtp/.build.yml | 17 + vendor/github.com/emersion/go-smtp/.gitignore | 26 + vendor/github.com/emersion/go-smtp/LICENSE | 24 + vendor/github.com/emersion/go-smtp/README.md | 181 +++ vendor/github.com/emersion/go-smtp/backend.go | 108 ++ vendor/github.com/emersion/go-smtp/client.go | 722 +++++++++ vendor/github.com/emersion/go-smtp/conn.go | 986 ++++++++++++ vendor/github.com/emersion/go-smtp/data.go | 147 ++ .../emersion/go-smtp/lengthlimit_reader.go | 47 + vendor/github.com/emersion/go-smtp/parse.go | 72 + vendor/github.com/emersion/go-smtp/server.go | 292 ++++ vendor/github.com/emersion/go-smtp/smtp.go | 30 + vendor/golang.org/x/text/AUTHORS | 3 + vendor/golang.org/x/text/CONTRIBUTORS | 3 + vendor/golang.org/x/text/LICENSE | 27 + vendor/golang.org/x/text/PATENTS | 22 + vendor/golang.org/x/text/encoding/encoding.go | 335 ++++ .../encoding/internal/identifier/identifier.go | 81 + .../x/text/encoding/internal/identifier/mib.go | 1619 ++++++++++++++++++++ vendor/golang.org/x/text/transform/transform.go | 709 +++++++++ vendor/modules.txt | 18 + 105 files changed, 14149 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README create mode 100644 UNLICENSE create mode 100644 go.mod create mode 100644 go.sum create mode 100644 message.eml.example create mode 100644 pigeon.go create mode 100644 pigeon.json.example create mode 100644 vendor/github.com/emersion/go-imap/.build.yml create mode 100644 vendor/github.com/emersion/go-imap/.gitignore create mode 100644 vendor/github.com/emersion/go-imap/LICENSE create mode 100644 vendor/github.com/emersion/go-imap/README.md create mode 100644 vendor/github.com/emersion/go-imap/client/client.go create mode 100644 vendor/github.com/emersion/go-imap/client/cmd_any.go create mode 100644 vendor/github.com/emersion/go-imap/client/cmd_auth.go create mode 100644 vendor/github.com/emersion/go-imap/client/cmd_noauth.go create mode 100644 vendor/github.com/emersion/go-imap/client/cmd_selected.go create mode 100644 vendor/github.com/emersion/go-imap/client/tag.go create mode 100644 vendor/github.com/emersion/go-imap/command.go create mode 100644 vendor/github.com/emersion/go-imap/commands/append.go create mode 100644 vendor/github.com/emersion/go-imap/commands/authenticate.go create mode 100644 vendor/github.com/emersion/go-imap/commands/capability.go create mode 100644 vendor/github.com/emersion/go-imap/commands/check.go create mode 100644 vendor/github.com/emersion/go-imap/commands/close.go create mode 100644 vendor/github.com/emersion/go-imap/commands/commands.go create mode 100644 vendor/github.com/emersion/go-imap/commands/copy.go create mode 100644 vendor/github.com/emersion/go-imap/commands/create.go create mode 100644 vendor/github.com/emersion/go-imap/commands/delete.go create mode 100644 vendor/github.com/emersion/go-imap/commands/enable.go create mode 100644 vendor/github.com/emersion/go-imap/commands/expunge.go create mode 100644 vendor/github.com/emersion/go-imap/commands/fetch.go create mode 100644 vendor/github.com/emersion/go-imap/commands/idle.go create mode 100644 vendor/github.com/emersion/go-imap/commands/list.go create mode 100644 vendor/github.com/emersion/go-imap/commands/login.go create mode 100644 vendor/github.com/emersion/go-imap/commands/logout.go create mode 100644 vendor/github.com/emersion/go-imap/commands/move.go create mode 100644 vendor/github.com/emersion/go-imap/commands/noop.go create mode 100644 vendor/github.com/emersion/go-imap/commands/rename.go create mode 100644 vendor/github.com/emersion/go-imap/commands/search.go create mode 100644 vendor/github.com/emersion/go-imap/commands/select.go create mode 100644 vendor/github.com/emersion/go-imap/commands/starttls.go create mode 100644 vendor/github.com/emersion/go-imap/commands/status.go create mode 100644 vendor/github.com/emersion/go-imap/commands/store.go create mode 100644 vendor/github.com/emersion/go-imap/commands/subscribe.go create mode 100644 vendor/github.com/emersion/go-imap/commands/uid.go create mode 100644 vendor/github.com/emersion/go-imap/commands/unselect.go create mode 100644 vendor/github.com/emersion/go-imap/conn.go create mode 100644 vendor/github.com/emersion/go-imap/date.go create mode 100644 vendor/github.com/emersion/go-imap/imap.go create mode 100644 vendor/github.com/emersion/go-imap/literal.go create mode 100644 vendor/github.com/emersion/go-imap/logger.go create mode 100644 vendor/github.com/emersion/go-imap/mailbox.go create mode 100644 vendor/github.com/emersion/go-imap/message.go create mode 100644 vendor/github.com/emersion/go-imap/read.go create mode 100644 vendor/github.com/emersion/go-imap/response.go create mode 100644 vendor/github.com/emersion/go-imap/responses/authenticate.go create mode 100644 vendor/github.com/emersion/go-imap/responses/capability.go create mode 100644 vendor/github.com/emersion/go-imap/responses/enabled.go create mode 100644 vendor/github.com/emersion/go-imap/responses/expunge.go create mode 100644 vendor/github.com/emersion/go-imap/responses/fetch.go create mode 100644 vendor/github.com/emersion/go-imap/responses/idle.go create mode 100644 vendor/github.com/emersion/go-imap/responses/list.go create mode 100644 vendor/github.com/emersion/go-imap/responses/responses.go create mode 100644 vendor/github.com/emersion/go-imap/responses/search.go create mode 100644 vendor/github.com/emersion/go-imap/responses/select.go create mode 100644 vendor/github.com/emersion/go-imap/responses/status.go create mode 100644 vendor/github.com/emersion/go-imap/search.go create mode 100644 vendor/github.com/emersion/go-imap/seqset.go create mode 100644 vendor/github.com/emersion/go-imap/status.go create mode 100644 vendor/github.com/emersion/go-imap/utf7/decoder.go create mode 100644 vendor/github.com/emersion/go-imap/utf7/encoder.go create mode 100644 vendor/github.com/emersion/go-imap/utf7/utf7.go create mode 100644 vendor/github.com/emersion/go-imap/write.go create mode 100644 vendor/github.com/emersion/go-sasl/.build.yml create mode 100644 vendor/github.com/emersion/go-sasl/.gitignore create mode 100644 vendor/github.com/emersion/go-sasl/LICENSE create mode 100644 vendor/github.com/emersion/go-sasl/README.md create mode 100644 vendor/github.com/emersion/go-sasl/anonymous.go create mode 100644 vendor/github.com/emersion/go-sasl/external.go create mode 100644 vendor/github.com/emersion/go-sasl/login.go create mode 100644 vendor/github.com/emersion/go-sasl/oauthbearer.go create mode 100644 vendor/github.com/emersion/go-sasl/plain.go create mode 100644 vendor/github.com/emersion/go-sasl/sasl.go create mode 100644 vendor/github.com/emersion/go-smtp/.build.yml create mode 100644 vendor/github.com/emersion/go-smtp/.gitignore create mode 100644 vendor/github.com/emersion/go-smtp/LICENSE create mode 100644 vendor/github.com/emersion/go-smtp/README.md create mode 100644 vendor/github.com/emersion/go-smtp/backend.go create mode 100644 vendor/github.com/emersion/go-smtp/client.go create mode 100644 vendor/github.com/emersion/go-smtp/conn.go create mode 100644 vendor/github.com/emersion/go-smtp/data.go create mode 100644 vendor/github.com/emersion/go-smtp/lengthlimit_reader.go create mode 100644 vendor/github.com/emersion/go-smtp/parse.go create mode 100644 vendor/github.com/emersion/go-smtp/server.go create mode 100644 vendor/github.com/emersion/go-smtp/smtp.go create mode 100644 vendor/golang.org/x/text/AUTHORS create mode 100644 vendor/golang.org/x/text/CONTRIBUTORS create mode 100644 vendor/golang.org/x/text/LICENSE create mode 100644 vendor/golang.org/x/text/PATENTS create mode 100644 vendor/golang.org/x/text/encoding/encoding.go create mode 100644 vendor/golang.org/x/text/encoding/internal/identifier/identifier.go create mode 100644 vendor/golang.org/x/text/encoding/internal/identifier/mib.go create mode 100644 vendor/golang.org/x/text/transform/transform.go create mode 100644 vendor/modules.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f2bc918 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +autoreply +message.eml +pigeon.json diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ac4846a --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +.POSIX: +.SUFFIXES: + +GO = go +RM = rm +GOFLAGS = -o pigeon +PREFIX = /usr/local +BINDIR = $(PREFIX)/bin + +goflags = $(GOFLAGS) + +all: pigeon + +pigeon: + $(GO) build $(goflags) -ldflags "-X main.buildPrefix=$(PREFIX)" + +clean: + $(RM) -f pigeon + +install: all + mkdir -p $(DESTDIR)$(BINDIR) + cp -f pigeon $(DESTDIR)$(BINDIR) + diff --git a/README b/README new file mode 100644 index 0000000..9d77e5a --- /dev/null +++ b/README @@ -0,0 +1,22 @@ +pigeon is a simple RFC-compliant email autoresponder; it monitors IMAP for +mailbox updates and replies to unseen messages with content rendered from the +configured template. pigeon supports per-address cooldown timers to prevent mail +loops, and can ignore messages sent to alias addresses. + +pigeon is opinionated and is intended to be configured with accounts used by +automated systems or unattended (and presently empty) inboxes; it's designed to +accommodate a particularly narrow use case. + +pigeon's idling logic is borrowed from aerc[1] and makes use of emersion's +well-designed go-imap[2] and go-smtp[3] libraries. + +Usage of ./pigeon: + -conf string + path to JSON configuration (default "./pigeon.json") + -eml string + path to EML message (default "./message.eml") + +[1] https://sr.ht/~rjarry/aerc/ +[2] https://github.com/emersion/go-imap +[3] https://github.com/emersion/go-smtp + diff --git a/UNLICENSE b/UNLICENSE new file mode 100644 index 0000000..68a49da --- /dev/null +++ b/UNLICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +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 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. + +For more information, please refer to diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1a35ccf --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module pigeon + +go 1.19 + +require ( + github.com/emersion/go-imap v1.2.1 + github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 + github.com/emersion/go-smtp v0.16.0 +) + +require golang.org/x/text v0.3.7 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7ccbe2d --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA= +github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY= +github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4= +github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ= +github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= +github.com/emersion/go-smtp v0.16.0 h1:eB9CY9527WdEZSs5sWisTmilDX7gG+Q/2IdRcmubpa8= +github.com/emersion/go-smtp v0.16.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= +github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/message.eml.example b/message.eml.example new file mode 100644 index 0000000..041a56e --- /dev/null +++ b/message.eml.example @@ -0,0 +1,8 @@ +From: "{{.FromName}}" <{{.FromEmail}}> +To: <{{.ToEmail}}> +Subject: {{.Subject}} +Date: {{.Date}} +MIME-Version: 1.0 +Content-Type: text/plain; charset="utf-8" + +{{.Message}} diff --git a/pigeon.go b/pigeon.go new file mode 100644 index 0000000..c169129 --- /dev/null +++ b/pigeon.go @@ -0,0 +1,325 @@ +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "log" + "os" + "sync" + "text/template" + "time" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/client" + "github.com/emersion/go-sasl" + "github.com/emersion/go-smtp" +) + +type Config struct { + FromEmail string `json:"from-email"` + FromName string `json:"from-name"` + Subject string `json:"subject"` + IMAPServer string `json:"imap-server"` + IMAPEmail string `json:"imap-email"` + IMAPPass string `json:"imap-pass"` + SMTPServer string `json:"smtp-server"` + SMTPEmail string `json:"smtp-email"` + SMTPPass string `json:"smtp-pass"` + Message string `json:"message"` + Cooldown string `json:"cooldown"` + IgnoreAlias bool `json:"ignore-alias"` +} + +type Email struct { + ToEmail string + FromName string + FromEmail string + Subject string + Date string + Message string +} + +type worker struct { + config Config + sender sasl.Client + client *client.Client + idler *idler + mbox *imap.MailboxStatus + seen seen + emlPath string +} + +type seen struct { + sync.Mutex + emails map[string]time.Time + cooldown time.Duration +} + +type idler struct { + sync.Mutex + client *client.Client + stop chan struct{} + done chan error + waiting bool + idleing bool +} + +var ( + errIdleTimeout = fmt.Errorf("idle timeout") + errIdleModeHangs = fmt.Errorf("idle mode hangs; waiting to reconnect") +) + +func newIdler(c *client.Client) *idler { + return &idler{client: c, done: make(chan error)} +} + +func (i *idler) setWaiting(wait bool) { + i.Lock() + i.waiting = wait + i.Unlock() +} + +func (i *idler) isWaiting() bool { + i.Lock() + defer i.Unlock() + return i.waiting +} + +func (i *idler) isReady() bool { + i.Lock() + defer i.Unlock() + return (!i.waiting && i.client != nil && + i.client.State() == imap.SelectedState) +} + +func (i *idler) setIdleing(v bool) { + i.Lock() + defer i.Unlock() + i.idleing = v +} + +func (i *idler) Start() { + switch { + case i.isReady(): + i.stop = make(chan struct{}) + go func() { + select { + case <-i.stop: + log.Println("Idle debounced...") + i.done <- nil + case <-time.After(10 * time.Millisecond): + i.setIdleing(true) + log.Println("Entered idle mode...") + now := time.Now() + err := i.client.Idle(i.stop, + &client.IdleOptions{ + LogoutTimeout: 0, + PollInterval: 0, + }) + i.setIdleing(false) + i.done <- err + log.Printf("Elapsed idle time: %v", time.Since(now)) + } + }() + case i.isWaiting(): + log.Println("Not started: wait for idle to exit...") + default: + log.Println("Not started: client not ready...") + } +} + +func (i *idler) Stop() error { + var reterr error + switch { + case i.isReady(): + close(i.stop) + select { + case err := <-i.done: + if err == nil { + log.Println("Idle done...") + } else { + log.Printf("Idle done with err: %v", err) + } + reterr = nil + case <-time.After(10 * time.Second): + log.Println("Idle err (timeout); waiting in background...") + i.waitOnIdle() + + reterr = errIdleTimeout + } + case i.isWaiting(): + log.Println("Not stopped: still idleing/hanging...") + reterr = errIdleModeHangs + default: + log.Println("Not stopped: client not ready...") + reterr = nil + } + return reterr +} + +func (i *idler) waitOnIdle() { + i.setWaiting(true) + log.Println("Waiting for idle in background...") + go func() { + err := <-i.done + if err == nil { + log.Println("Idle waited...") + } else { + log.Printf("Idle waited; with err: %v", err) + } + i.setWaiting(false) + i.stop = make(chan struct{}) + fmt.Println("restart") + i.Start() + }() +} + +func (w *worker) handleUpdate(update *client.MailboxUpdate) { + log.Println("New update, stopping idle...") + defer func() { + w.idler.Start() + }() + if err := w.idler.Stop(); err != nil { + log.Fatal(err) + } + + log.Println("Searching for messages...") + criteria := imap.NewSearchCriteria() + criteria.WithoutFlags = []string{imap.SeenFlag} + ids, err := w.client.Search(criteria) + if err != nil { + log.Fatal(err) + } + if len(ids) == 0 { + log.Println("No IDs Found") + return + } + log.Println("IDs found:", ids) + seqset := new(imap.SeqSet) + seqset.AddNum(ids...) + messages := make(chan *imap.Message, 10) + done := make(chan error, 1) + go func() { + done <- w.client.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope}, messages) + }() + + tmpl, err := template.ParseFiles(w.emlPath) + if err != nil { + log.Fatal(err) + } + for msg := range messages { + log.Println("* ", msg.Envelope.From[0].Address(), msg.Envelope.Subject) + if w.config.IgnoreAlias { + foundAddress := false + for _, toEmail := range msg.Envelope.To { + if toEmail.Address() == w.config.FromEmail { + foundAddress = true + } + } + if !foundAddress { + log.Println("Our address absent from 'to' set, skipping...") + continue + } + } + eml := Email{ + ToEmail: msg.Envelope.From[0].Address(), + FromEmail: w.config.FromEmail, + FromName: w.config.FromName, + Subject: w.config.Subject, + Date: time.Now().Format(time.RFC1123Z), + Message: w.config.Message, + } + if prevSend, exists := w.seen.emails[eml.ToEmail]; exists { + log.Println("Address exists, checking time...") + diff := time.Now().Sub(prevSend) + if diff < w.seen.cooldown { + log.Println("Address seen too recently, skipping...") + continue + } + } + log.Println("Sending email...") + to := []string{msg.Envelope.From[0].Address()} + buf := new(bytes.Buffer) + err := tmpl.Execute(buf, &eml) + if err != nil { + log.Fatal(err) + } + err = smtp.SendMail(w.config.SMTPServer, w.sender, w.config.SMTPEmail, to, buf) + if err != nil { + log.Fatal(err) + } + log.Println("Adding address to seen map...") + w.seen.Lock() + w.seen.emails[eml.ToEmail] = time.Now() + w.seen.Unlock() + } + + log.Println("Marking messages as seen...") + item := imap.FormatFlagsOp(imap.AddFlags, true) + flags := []interface{}{imap.SeenFlag} + err = w.client.Store(seqset, item, flags, nil) + if err != nil { + log.Fatal(err) + } + if err := <-done; err != nil { + log.Fatal(err) + } +} + +func main() { + var configPath string + var w worker + var err error + + flag.StringVar(&configPath, "conf", "./pigeon.json", "path to JSON configuration") + flag.StringVar(&w.emlPath, "eml", "./message.eml", "path to EML message") + flag.Parse() + conf, err := os.ReadFile(configPath) + if err != nil { + log.Fatal(err) + } + err = json.Unmarshal(conf, &w.config) + if err != nil { + log.Fatal(err) + } + if cd, err := time.ParseDuration(w.config.Cooldown); err != nil { + log.Fatal(err) + } else { + w.seen.cooldown = cd + } + w.seen.emails = make(map[string]time.Time) + + log.Println("Connecting to server...") + w.client, err = client.DialTLS(w.config.IMAPServer, nil) + if err != nil { + log.Fatal(err) + } + // w.client.SetDebug(os.Stdout) + defer w.client.Logout() + + if err := w.client.Login(w.config.IMAPEmail, w.config.IMAPPass); err != nil { + log.Fatal(err) + } + log.Println("Logged in") + + w.idler = newIdler(w.client) + w.mbox, err = w.client.Select("INBOX", false) + if err != nil { + log.Fatal(err) + } + + w.sender = sasl.NewPlainClient("", w.config.SMTPEmail, w.config.SMTPPass) + updates := make(chan client.Update, 1) + w.client.Updates = updates + w.idler.Start() + for { + update := <-updates + switch update := update.(type) { + case *client.MailboxUpdate: + log.Println("Mailbox update received, processing...") + w.handleUpdate(update) + } + } +} diff --git a/pigeon.json.example b/pigeon.json.example new file mode 100644 index 0000000..dcd91e6 --- /dev/null +++ b/pigeon.json.example @@ -0,0 +1,14 @@ +{ + "from-email": "john@example.com", + "from-name": "John Doe", + "subject": "On vacation! :)", + "imap-server": "example.com:993", + "imap-email": "john@example.com", + "imap-pass": "password", + "smtp-server": "example.com:587", + "smtp-email": "john@example.com", + "smtp-pass": "password", + "cooldown": "24h", + "ignore-alias": true, + "message": "Hello,\r\n\r\nSorry I missed you!\r\n\r\nJohn", +} diff --git a/vendor/github.com/emersion/go-imap/.build.yml b/vendor/github.com/emersion/go-imap/.build.yml new file mode 100644 index 0000000..2617917 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/.build.yml @@ -0,0 +1,17 @@ +image: alpine/edge +packages: + - go +sources: + - https://github.com/emersion/go-imap +artifacts: + - coverage.html +tasks: + - build: | + cd go-imap + go build -race -v ./... + - test: | + cd go-imap + go test -coverprofile=coverage.txt -covermode=atomic ./... + - coverage: | + cd go-imap + go tool cover -html=coverage.txt -o ~/coverage.html diff --git a/vendor/github.com/emersion/go-imap/.gitignore b/vendor/github.com/emersion/go-imap/.gitignore new file mode 100644 index 0000000..59506a2 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/.gitignore @@ -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 diff --git a/vendor/github.com/emersion/go-imap/LICENSE b/vendor/github.com/emersion/go-imap/LICENSE new file mode 100644 index 0000000..f55742d --- /dev/null +++ b/vendor/github.com/emersion/go-imap/LICENSE @@ -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. diff --git a/vendor/github.com/emersion/go-imap/README.md b/vendor/github.com/emersion/go-imap/README.md new file mode 100644 index 0000000..199ab3b --- /dev/null +++ b/vendor/github.com/emersion/go-imap/README.md @@ -0,0 +1,178 @@ +# go-imap + +[![godocs.io](https://godocs.io/github.com/emersion/go-imap?status.svg)](https://godocs.io/github.com/emersion/go-imap) +[![builds.sr.ht status](https://builds.sr.ht/~emersion/go-imap/commits/master.svg)](https://builds.sr.ht/~emersion/go-imap/commits/master?) + +An [IMAP4rev1](https://tools.ietf.org/html/rfc3501) library written in Go. It +can be used to build a client and/or a server. + +## Usage + +### Client [![godocs.io](https://godocs.io/github.com/emersion/go-imap/client?status.svg)](https://godocs.io/github.com/emersion/go-imap/client) + +```go +package main + +import ( + "log" + + "github.com/emersion/go-imap/client" + "github.com/emersion/go-imap" +) + +func main() { + log.Println("Connecting to server...") + + // Connect to server + c, err := client.DialTLS("mail.example.org:993", nil) + if err != nil { + log.Fatal(err) + } + log.Println("Connected") + + // Don't forget to logout + defer c.Logout() + + // Login + if err := c.Login("username", "password"); err != nil { + log.Fatal(err) + } + log.Println("Logged in") + + // List mailboxes + mailboxes := make(chan *imap.MailboxInfo, 10) + done := make(chan error, 1) + go func () { + done <- c.List("", "*", mailboxes) + }() + + log.Println("Mailboxes:") + for m := range mailboxes { + log.Println("* " + m.Name) + } + + if err := <-done; err != nil { + log.Fatal(err) + } + + // Select INBOX + mbox, err := c.Select("INBOX", false) + if err != nil { + log.Fatal(err) + } + log.Println("Flags for INBOX:", mbox.Flags) + + // Get the last 4 messages + from := uint32(1) + to := mbox.Messages + if mbox.Messages > 3 { + // We're using unsigned integers here, only subtract if the result is > 0 + from = mbox.Messages - 3 + } + seqset := new(imap.SeqSet) + seqset.AddRange(from, to) + + messages := make(chan *imap.Message, 10) + done = make(chan error, 1) + go func() { + done <- c.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope}, messages) + }() + + log.Println("Last 4 messages:") + for msg := range messages { + log.Println("* " + msg.Envelope.Subject) + } + + if err := <-done; err != nil { + log.Fatal(err) + } + + log.Println("Done!") +} +``` + +### Server [![godocs.io](https://godocs.io/github.com/emersion/go-imap/server?status.svg)](https://godocs.io/github.com/emersion/go-imap/server) + +```go +package main + +import ( + "log" + + "github.com/emersion/go-imap/server" + "github.com/emersion/go-imap/backend/memory" +) + +func main() { + // Create a memory backend + be := memory.New() + + // Create a new server + s := server.New(be) + s.Addr = ":1143" + // Since we will use this server for testing only, we can allow plain text + // authentication over unencrypted connections + s.AllowInsecureAuth = true + + log.Println("Starting IMAP server at localhost:1143") + if err := s.ListenAndServe(); err != nil { + log.Fatal(err) + } +} +``` + +You can now use `telnet localhost 1143` to manually connect to the server. + +## Extensions + +Support for several IMAP extensions is included in go-imap itself. This +includes: + +* [APPENDLIMIT](https://tools.ietf.org/html/rfc7889) +* [CHILDREN](https://tools.ietf.org/html/rfc3348) +* [ENABLE](https://tools.ietf.org/html/rfc5161) +* [IDLE](https://tools.ietf.org/html/rfc2177) +* [IMPORTANT](https://tools.ietf.org/html/rfc8457) +* [LITERAL+](https://tools.ietf.org/html/rfc7888) +* [MOVE](https://tools.ietf.org/html/rfc6851) +* [SASL-IR](https://tools.ietf.org/html/rfc4959) +* [SPECIAL-USE](https://tools.ietf.org/html/rfc6154) +* [UNSELECT](https://tools.ietf.org/html/rfc3691) + +Support for other extensions is provided via separate packages. See below. + +## Extending go-imap + +### Extensions + +Commands defined in IMAP extensions are available in other packages. See [the +wiki](https://github.com/emersion/go-imap/wiki/Using-extensions#using-client-extensions) +to learn how to use them. + +* [COMPRESS](https://github.com/emersion/go-imap-compress) +* [ID](https://github.com/ProtonMail/go-imap-id) +* [METADATA](https://github.com/emersion/go-imap-metadata) +* [NAMESPACE](https://github.com/foxcpp/go-imap-namespace) +* [QUOTA](https://github.com/emersion/go-imap-quota) +* [SORT and THREAD](https://github.com/emersion/go-imap-sortthread) +* [UIDPLUS](https://github.com/emersion/go-imap-uidplus) + +### Server backends + +* [Memory](https://github.com/emersion/go-imap/tree/master/backend/memory) (for testing) +* [Multi](https://github.com/emersion/go-imap-multi) +* [PGP](https://github.com/emersion/go-imap-pgp) +* [Proxy](https://github.com/emersion/go-imap-proxy) +* [Notmuch](https://github.com/stbenjam/go-imap-notmuch) - Experimental gateway for [Notmuch](https://notmuchmail.org/) + +### Related projects + +* [go-message](https://github.com/emersion/go-message) - parsing and formatting MIME and mail messages +* [go-msgauth](https://github.com/emersion/go-msgauth) - handle DKIM, DMARC and Authentication-Results +* [go-pgpmail](https://github.com/emersion/go-pgpmail) - decrypting and encrypting mails with OpenPGP +* [go-sasl](https://github.com/emersion/go-sasl) - sending and receiving SASL authentications +* [go-smtp](https://github.com/emersion/go-smtp) - building SMTP clients and servers + +## License + +MIT diff --git a/vendor/github.com/emersion/go-imap/client/client.go b/vendor/github.com/emersion/go-imap/client/client.go new file mode 100644 index 0000000..8b6fc84 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/client/client.go @@ -0,0 +1,689 @@ +// Package client provides an IMAP client. +// +// It is not safe to use the same Client from multiple goroutines. In general, +// the IMAP protocol doesn't make it possible to send multiple independent +// IMAP commands on the same connection. +package client + +import ( + "crypto/tls" + "fmt" + "io" + "log" + "net" + "os" + "sync" + "time" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/commands" + "github.com/emersion/go-imap/responses" +) + +// errClosed is used when a connection is closed while waiting for a command +// response. +var errClosed = fmt.Errorf("imap: connection closed") + +// errUnregisterHandler is returned by a response handler to unregister itself. +var errUnregisterHandler = fmt.Errorf("imap: unregister handler") + +// Update is an unilateral server update. +type Update interface { + update() +} + +// StatusUpdate is delivered when a status update is received. +type StatusUpdate struct { + Status *imap.StatusResp +} + +func (u *StatusUpdate) update() {} + +// MailboxUpdate is delivered when a mailbox status changes. +type MailboxUpdate struct { + Mailbox *imap.MailboxStatus +} + +func (u *MailboxUpdate) update() {} + +// ExpungeUpdate is delivered when a message is deleted. +type ExpungeUpdate struct { + SeqNum uint32 +} + +func (u *ExpungeUpdate) update() {} + +// MessageUpdate is delivered when a message attribute changes. +type MessageUpdate struct { + Message *imap.Message +} + +func (u *MessageUpdate) update() {} + +// Client is an IMAP client. +type Client struct { + conn *imap.Conn + isTLS bool + serverName string + + loggedOut chan struct{} + continues chan<- bool + upgrading bool + + handlers []responses.Handler + handlersLocker sync.Mutex + + // The current connection state. + state imap.ConnState + // The selected mailbox, if there is one. + mailbox *imap.MailboxStatus + // The cached server capabilities. + caps map[string]bool + // state, mailbox and caps may be accessed in different goroutines. Protect + // access. + locker sync.Mutex + + // A channel to which unilateral updates from the server will be sent. An + // update can be one of: *StatusUpdate, *MailboxUpdate, *MessageUpdate, + // *ExpungeUpdate. Note that blocking this channel blocks the whole client, + // so it's recommended to use a separate goroutine and a buffered channel to + // prevent deadlocks. + Updates chan<- Update + + // ErrorLog specifies an optional logger for errors accepting connections and + // unexpected behavior from handlers. By default, logging goes to os.Stderr + // via the log package's standard logger. The logger must be safe to use + // simultaneously from multiple goroutines. + ErrorLog imap.Logger + + // Timeout specifies a maximum amount of time to wait on a command. + // + // A Timeout of zero means no timeout. This is the default. + Timeout time.Duration +} + +func (c *Client) registerHandler(h responses.Handler) { + if h == nil { + return + } + + c.handlersLocker.Lock() + c.handlers = append(c.handlers, h) + c.handlersLocker.Unlock() +} + +func (c *Client) handle(resp imap.Resp) error { + c.handlersLocker.Lock() + for i := len(c.handlers) - 1; i >= 0; i-- { + if err := c.handlers[i].Handle(resp); err != responses.ErrUnhandled { + if err == errUnregisterHandler { + c.handlers = append(c.handlers[:i], c.handlers[i+1:]...) + err = nil + } + c.handlersLocker.Unlock() + return err + } + } + c.handlersLocker.Unlock() + return responses.ErrUnhandled +} + +func (c *Client) reader() { + defer close(c.loggedOut) + // Loop while connected. + for { + connected, err := c.readOnce() + if err != nil { + c.ErrorLog.Println("error reading response:", err) + } + if !connected { + return + } + } +} + +func (c *Client) readOnce() (bool, error) { + if c.State() == imap.LogoutState { + return false, nil + } + + resp, err := imap.ReadResp(c.conn.Reader) + if err == io.EOF || c.State() == imap.LogoutState { + return false, nil + } else if err != nil { + if imap.IsParseError(err) { + return true, err + } else { + return false, err + } + } + + if err := c.handle(resp); err != nil && err != responses.ErrUnhandled { + c.ErrorLog.Println("cannot handle response ", resp, err) + } + return true, nil +} + +func (c *Client) writeReply(reply []byte) error { + if _, err := c.conn.Writer.Write(reply); err != nil { + return err + } + // Flush reply + return c.conn.Writer.Flush() +} + +type handleResult struct { + status *imap.StatusResp + err error +} + +func (c *Client) execute(cmdr imap.Commander, h responses.Handler) (*imap.StatusResp, error) { + cmd := cmdr.Command() + cmd.Tag = generateTag() + + var replies <-chan []byte + if replier, ok := h.(responses.Replier); ok { + replies = replier.Replies() + } + + if c.Timeout > 0 { + err := c.conn.SetDeadline(time.Now().Add(c.Timeout)) + if err != nil { + return nil, err + } + } else { + // It's possible the client had a timeout set from a previous command, but no + // longer does. Ensure we respect that. The zero time means no deadline. + if err := c.conn.SetDeadline(time.Time{}); err != nil { + return nil, err + } + } + + // Check if we are upgrading. + upgrading := c.upgrading + + // Add handler before sending command, to be sure to get the response in time + // (in tests, the response is sent right after our command is received, so + // sometimes the response was received before the setup of this handler) + doneHandle := make(chan handleResult, 1) + unregister := make(chan struct{}) + c.registerHandler(responses.HandlerFunc(func(resp imap.Resp) error { + select { + case <-unregister: + // If an error occured while sending the command, abort + return errUnregisterHandler + default: + } + + if s, ok := resp.(*imap.StatusResp); ok && s.Tag == cmd.Tag { + // This is the command's status response, we're done + doneHandle <- handleResult{s, nil} + // Special handling of connection upgrading. + if upgrading { + c.upgrading = false + // Wait for upgrade to finish. + c.conn.Wait() + } + // Cancel any pending literal write + select { + case c.continues <- false: + default: + } + return errUnregisterHandler + } + + if h != nil { + // Pass the response to the response handler + if err := h.Handle(resp); err != nil && err != responses.ErrUnhandled { + // If the response handler returns an error, abort + doneHandle <- handleResult{nil, err} + return errUnregisterHandler + } else { + return err + } + } + return responses.ErrUnhandled + })) + + // Send the command to the server + if err := cmd.WriteTo(c.conn.Writer); err != nil { + // Error while sending the command + close(unregister) + + if err, ok := err.(imap.LiteralLengthErr); ok { + // Expected > Actual + // The server is waiting for us to write + // more bytes, we don't have them. Run. + // Expected < Actual + // We are about to send a potentially truncated message, we don't + // want this (ths terminating CRLF is not sent at this point). + c.conn.Close() + return nil, err + } + + return nil, err + } + // Flush writer if we are upgrading + if upgrading { + if err := c.conn.Writer.Flush(); err != nil { + // Error while sending the command + close(unregister) + return nil, err + } + } + + for { + select { + case reply := <-replies: + // Response handler needs to send a reply (Used for AUTHENTICATE) + if err := c.writeReply(reply); err != nil { + close(unregister) + return nil, err + } + case <-c.loggedOut: + // If the connection is closed (such as from an I/O error), ensure we + // realize this and don't block waiting on a response that will never + // come. loggedOut is a channel that closes when the reader goroutine + // ends. + close(unregister) + return nil, errClosed + case result := <-doneHandle: + return result.status, result.err + } + } +} + +// State returns the current connection state. +func (c *Client) State() imap.ConnState { + c.locker.Lock() + state := c.state + c.locker.Unlock() + return state +} + +// Mailbox returns the selected mailbox. It returns nil if there isn't one. +func (c *Client) Mailbox() *imap.MailboxStatus { + // c.Mailbox fields are not supposed to change, so we can return the pointer. + c.locker.Lock() + mbox := c.mailbox + c.locker.Unlock() + return mbox +} + +// SetState sets this connection's internal state. +// +// This function should not be called directly, it must only be used by +// libraries implementing extensions of the IMAP protocol. +func (c *Client) SetState(state imap.ConnState, mailbox *imap.MailboxStatus) { + c.locker.Lock() + c.state = state + c.mailbox = mailbox + c.locker.Unlock() +} + +// Execute executes a generic command. cmdr is a value that can be converted to +// a raw command and h is a response handler. The function returns when the +// command has completed or failed, in this case err is nil. A non-nil err value +// indicates a network error. +// +// This function should not be called directly, it must only be used by +// libraries implementing extensions of the IMAP protocol. +func (c *Client) Execute(cmdr imap.Commander, h responses.Handler) (*imap.StatusResp, error) { + return c.execute(cmdr, h) +} + +func (c *Client) handleContinuationReqs() { + c.registerHandler(responses.HandlerFunc(func(resp imap.Resp) error { + if _, ok := resp.(*imap.ContinuationReq); ok { + go func() { + c.continues <- true + }() + return nil + } + return responses.ErrUnhandled + })) +} + +func (c *Client) gotStatusCaps(args []interface{}) { + c.locker.Lock() + + c.caps = make(map[string]bool) + for _, cap := range args { + if cap, ok := cap.(string); ok { + c.caps[cap] = true + } + } + + c.locker.Unlock() +} + +// The server can send unilateral data. This function handles it. +func (c *Client) handleUnilateral() { + c.registerHandler(responses.HandlerFunc(func(resp imap.Resp) error { + switch resp := resp.(type) { + case *imap.StatusResp: + if resp.Tag != "*" { + return responses.ErrUnhandled + } + + switch resp.Type { + case imap.StatusRespOk, imap.StatusRespNo, imap.StatusRespBad: + if c.Updates != nil { + c.Updates <- &StatusUpdate{resp} + } + case imap.StatusRespBye: + c.locker.Lock() + c.state = imap.LogoutState + c.mailbox = nil + c.locker.Unlock() + + c.conn.Close() + + if c.Updates != nil { + c.Updates <- &StatusUpdate{resp} + } + default: + return responses.ErrUnhandled + } + case *imap.DataResp: + name, fields, ok := imap.ParseNamedResp(resp) + if !ok { + return responses.ErrUnhandled + } + + switch name { + case "CAPABILITY": + c.gotStatusCaps(fields) + case "EXISTS": + if c.Mailbox() == nil { + break + } + + if messages, err := imap.ParseNumber(fields[0]); err == nil { + c.locker.Lock() + c.mailbox.Messages = messages + c.locker.Unlock() + + c.mailbox.ItemsLocker.Lock() + c.mailbox.Items[imap.StatusMessages] = nil + c.mailbox.ItemsLocker.Unlock() + } + + if c.Updates != nil { + c.Updates <- &MailboxUpdate{c.Mailbox()} + } + case "RECENT": + if c.Mailbox() == nil { + break + } + + if recent, err := imap.ParseNumber(fields[0]); err == nil { + c.locker.Lock() + c.mailbox.Recent = recent + c.locker.Unlock() + + c.mailbox.ItemsLocker.Lock() + c.mailbox.Items[imap.StatusRecent] = nil + c.mailbox.ItemsLocker.Unlock() + } + + if c.Updates != nil { + c.Updates <- &MailboxUpdate{c.Mailbox()} + } + case "EXPUNGE": + seqNum, _ := imap.ParseNumber(fields[0]) + + if c.Updates != nil { + c.Updates <- &ExpungeUpdate{seqNum} + } + case "FETCH": + seqNum, _ := imap.ParseNumber(fields[0]) + fields, _ := fields[1].([]interface{}) + + msg := &imap.Message{SeqNum: seqNum} + if err := msg.Parse(fields); err != nil { + break + } + + if c.Updates != nil { + c.Updates <- &MessageUpdate{msg} + } + default: + return responses.ErrUnhandled + } + default: + return responses.ErrUnhandled + } + return nil + })) +} + +func (c *Client) handleGreetAndStartReading() error { + var greetErr error + gotGreet := false + + c.registerHandler(responses.HandlerFunc(func(resp imap.Resp) error { + status, ok := resp.(*imap.StatusResp) + if !ok { + greetErr = fmt.Errorf("invalid greeting received from server: not a status response") + return errUnregisterHandler + } + + c.locker.Lock() + switch status.Type { + case imap.StatusRespPreauth: + c.state = imap.AuthenticatedState + case imap.StatusRespBye: + c.state = imap.LogoutState + case imap.StatusRespOk: + c.state = imap.NotAuthenticatedState + default: + c.state = imap.LogoutState + c.locker.Unlock() + greetErr = fmt.Errorf("invalid greeting received from server: %v", status.Type) + return errUnregisterHandler + } + c.locker.Unlock() + + if status.Code == imap.CodeCapability { + c.gotStatusCaps(status.Arguments) + } + + gotGreet = true + return errUnregisterHandler + })) + + // call `readOnce` until we get the greeting or an error + for !gotGreet { + connected, err := c.readOnce() + // Check for read errors + if err != nil { + // return read errors + return err + } + // Check for invalid greet + if greetErr != nil { + // return read errors + return greetErr + } + // Check if connection was closed. + if !connected { + // connection closed. + return io.EOF + } + } + + // We got the greeting, now start the reader goroutine. + go c.reader() + + return nil +} + +// Upgrade a connection, e.g. wrap an unencrypted connection with an encrypted +// tunnel. +// +// This function should not be called directly, it must only be used by +// libraries implementing extensions of the IMAP protocol. +func (c *Client) Upgrade(upgrader imap.ConnUpgrader) error { + return c.conn.Upgrade(upgrader) +} + +// Writer returns the imap.Writer for this client's connection. +// +// This function should not be called directly, it must only be used by +// libraries implementing extensions of the IMAP protocol. +func (c *Client) Writer() *imap.Writer { + return c.conn.Writer +} + +// IsTLS checks if this client's connection has TLS enabled. +func (c *Client) IsTLS() bool { + return c.isTLS +} + +// LoggedOut returns a channel which is closed when the connection to the server +// is closed. +func (c *Client) LoggedOut() <-chan struct{} { + return c.loggedOut +} + +// SetDebug defines an io.Writer to which all network activity will be logged. +// If nil is provided, network activity will not be logged. +func (c *Client) SetDebug(w io.Writer) { + // Need to send a command to unblock the reader goroutine. + cmd := new(commands.Noop) + err := c.Upgrade(func(conn net.Conn) (net.Conn, error) { + // Flag connection as in upgrading + c.upgrading = true + if status, err := c.execute(cmd, nil); err != nil { + return nil, err + } else if err := status.Err(); err != nil { + return nil, err + } + + // Wait for reader to block. + c.conn.WaitReady() + + c.conn.SetDebug(w) + return conn, nil + }) + if err != nil { + log.Println("SetDebug:", err) + } + +} + +// New creates a new client from an existing connection. +func New(conn net.Conn) (*Client, error) { + continues := make(chan bool) + w := imap.NewClientWriter(nil, continues) + r := imap.NewReader(nil) + + c := &Client{ + conn: imap.NewConn(conn, r, w), + loggedOut: make(chan struct{}), + continues: continues, + state: imap.ConnectingState, + ErrorLog: log.New(os.Stderr, "imap/client: ", log.LstdFlags), + } + + c.handleContinuationReqs() + c.handleUnilateral() + if err := c.handleGreetAndStartReading(); err != nil { + return c, err + } + + plusOk, _ := c.Support("LITERAL+") + minusOk, _ := c.Support("LITERAL-") + // We don't use non-sync literal if it is bigger than 4096 bytes, so + // LITERAL- is fine too. + c.conn.AllowAsyncLiterals = plusOk || minusOk + + return c, nil +} + +// Dial connects to an IMAP server using an unencrypted connection. +func Dial(addr string) (*Client, error) { + return DialWithDialer(new(net.Dialer), addr) +} + +type Dialer interface { + // Dial connects to the given address. + Dial(network, addr string) (net.Conn, error) +} + +// DialWithDialer connects to an IMAP server using an unencrypted connection +// using dialer.Dial. +// +// Among other uses, this allows to apply a dial timeout. +func DialWithDialer(dialer Dialer, addr string) (*Client, error) { + conn, err := dialer.Dial("tcp", addr) + if err != nil { + return nil, err + } + + // We don't return to the caller until we try to receive a greeting. As such, + // there is no way to set the client's Timeout for that action. As a + // workaround, if the dialer has a timeout set, use that for the connection's + // deadline. + if netDialer, ok := dialer.(*net.Dialer); ok && netDialer.Timeout > 0 { + err := conn.SetDeadline(time.Now().Add(netDialer.Timeout)) + if err != nil { + return nil, err + } + } + + c, err := New(conn) + if err != nil { + return nil, err + } + + c.serverName, _, _ = net.SplitHostPort(addr) + return c, nil +} + +// DialTLS connects to an IMAP server using an encrypted connection. +func DialTLS(addr string, tlsConfig *tls.Config) (*Client, error) { + return DialWithDialerTLS(new(net.Dialer), addr, tlsConfig) +} + +// DialWithDialerTLS connects to an IMAP server using an encrypted connection +// using dialer.Dial. +// +// Among other uses, this allows to apply a dial timeout. +func DialWithDialerTLS(dialer Dialer, addr string, tlsConfig *tls.Config) (*Client, error) { + conn, err := dialer.Dial("tcp", addr) + if err != nil { + return nil, err + } + + serverName, _, _ := net.SplitHostPort(addr) + if tlsConfig == nil { + tlsConfig = &tls.Config{} + } + if tlsConfig.ServerName == "" { + tlsConfig = tlsConfig.Clone() + tlsConfig.ServerName = serverName + } + tlsConn := tls.Client(conn, tlsConfig) + + // We don't return to the caller until we try to receive a greeting. As such, + // there is no way to set the client's Timeout for that action. As a + // workaround, if the dialer has a timeout set, use that for the connection's + // deadline. + if netDialer, ok := dialer.(*net.Dialer); ok && netDialer.Timeout > 0 { + err := tlsConn.SetDeadline(time.Now().Add(netDialer.Timeout)) + if err != nil { + return nil, err + } + } + + c, err := New(tlsConn) + if err != nil { + return nil, err + } + + c.isTLS = true + c.serverName = serverName + return c, nil +} diff --git a/vendor/github.com/emersion/go-imap/client/cmd_any.go b/vendor/github.com/emersion/go-imap/client/cmd_any.go new file mode 100644 index 0000000..cb0d38a --- /dev/null +++ b/vendor/github.com/emersion/go-imap/client/cmd_any.go @@ -0,0 +1,88 @@ +package client + +import ( + "errors" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/commands" +) + +// ErrAlreadyLoggedOut is returned if Logout is called when the client is +// already logged out. +var ErrAlreadyLoggedOut = errors.New("Already logged out") + +// Capability requests a listing of capabilities that the server supports. +// Capabilities are often returned by the server with the greeting or with the +// STARTTLS and LOGIN responses, so usually explicitly requesting capabilities +// isn't needed. +// +// Most of the time, Support should be used instead. +func (c *Client) Capability() (map[string]bool, error) { + cmd := &commands.Capability{} + + if status, err := c.execute(cmd, nil); err != nil { + return nil, err + } else if err := status.Err(); err != nil { + return nil, err + } + + c.locker.Lock() + caps := c.caps + c.locker.Unlock() + return caps, nil +} + +// Support checks if cap is a capability supported by the server. If the server +// hasn't sent its capabilities yet, Support requests them. +func (c *Client) Support(cap string) (bool, error) { + c.locker.Lock() + ok := c.caps != nil + c.locker.Unlock() + + // If capabilities are not cached, request them + if !ok { + if _, err := c.Capability(); err != nil { + return false, err + } + } + + c.locker.Lock() + supported := c.caps[cap] + c.locker.Unlock() + + return supported, nil +} + +// Noop always succeeds and does nothing. +// +// It can be used as a periodic poll for new messages or message status updates +// during a period of inactivity. It can also be used to reset any inactivity +// autologout timer on the server. +func (c *Client) Noop() error { + cmd := new(commands.Noop) + + status, err := c.execute(cmd, nil) + if err != nil { + return err + } + return status.Err() +} + +// Logout gracefully closes the connection. +func (c *Client) Logout() error { + if c.State() == imap.LogoutState { + return ErrAlreadyLoggedOut + } + + cmd := new(commands.Logout) + + if status, err := c.execute(cmd, nil); err == errClosed { + // Server closed connection, that's what we want anyway + return nil + } else if err != nil { + return err + } else if status != nil { + return status.Err() + } + return nil +} diff --git a/vendor/github.com/emersion/go-imap/client/cmd_auth.go b/vendor/github.com/emersion/go-imap/client/cmd_auth.go new file mode 100644 index 0000000..a280017 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/client/cmd_auth.go @@ -0,0 +1,380 @@ +package client + +import ( + "errors" + "time" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/commands" + "github.com/emersion/go-imap/responses" +) + +// ErrNotLoggedIn is returned if a function that requires the client to be +// logged in is called then the client isn't. +var ErrNotLoggedIn = errors.New("Not logged in") + +func (c *Client) ensureAuthenticated() error { + state := c.State() + if state != imap.AuthenticatedState && state != imap.SelectedState { + return ErrNotLoggedIn + } + return nil +} + +// Select selects a mailbox so that messages in the mailbox can be accessed. Any +// currently selected mailbox is deselected before attempting the new selection. +// Even if the readOnly parameter is set to false, the server can decide to open +// the mailbox in read-only mode. +func (c *Client) Select(name string, readOnly bool) (*imap.MailboxStatus, error) { + if err := c.ensureAuthenticated(); err != nil { + return nil, err + } + + cmd := &commands.Select{ + Mailbox: name, + ReadOnly: readOnly, + } + + mbox := &imap.MailboxStatus{Name: name, Items: make(map[imap.StatusItem]interface{})} + res := &responses.Select{ + Mailbox: mbox, + } + c.locker.Lock() + c.mailbox = mbox + c.locker.Unlock() + + status, err := c.execute(cmd, res) + if err != nil { + c.locker.Lock() + c.mailbox = nil + c.locker.Unlock() + return nil, err + } + if err := status.Err(); err != nil { + c.locker.Lock() + c.mailbox = nil + c.locker.Unlock() + return nil, err + } + + c.locker.Lock() + mbox.ReadOnly = (status.Code == imap.CodeReadOnly) + c.state = imap.SelectedState + c.locker.Unlock() + return mbox, nil +} + +// Create creates a mailbox with the given name. +func (c *Client) Create(name string) error { + if err := c.ensureAuthenticated(); err != nil { + return err + } + + cmd := &commands.Create{ + Mailbox: name, + } + + status, err := c.execute(cmd, nil) + if err != nil { + return err + } + return status.Err() +} + +// Delete permanently removes the mailbox with the given name. +func (c *Client) Delete(name string) error { + if err := c.ensureAuthenticated(); err != nil { + return err + } + + cmd := &commands.Delete{ + Mailbox: name, + } + + status, err := c.execute(cmd, nil) + if err != nil { + return err + } + return status.Err() +} + +// Rename changes the name of a mailbox. +func (c *Client) Rename(existingName, newName string) error { + if err := c.ensureAuthenticated(); err != nil { + return err + } + + cmd := &commands.Rename{ + Existing: existingName, + New: newName, + } + + status, err := c.execute(cmd, nil) + if err != nil { + return err + } + return status.Err() +} + +// Subscribe adds the specified mailbox name to the server's set of "active" or +// "subscribed" mailboxes. +func (c *Client) Subscribe(name string) error { + if err := c.ensureAuthenticated(); err != nil { + return err + } + + cmd := &commands.Subscribe{ + Mailbox: name, + } + + status, err := c.execute(cmd, nil) + if err != nil { + return err + } + return status.Err() +} + +// Unsubscribe removes the specified mailbox name from the server's set of +// "active" or "subscribed" mailboxes. +func (c *Client) Unsubscribe(name string) error { + if err := c.ensureAuthenticated(); err != nil { + return err + } + + cmd := &commands.Unsubscribe{ + Mailbox: name, + } + + status, err := c.execute(cmd, nil) + if err != nil { + return err + } + return status.Err() +} + +// List returns a subset of names from the complete set of all names available +// to the client. +// +// An empty name argument is a special request to return the hierarchy delimiter +// and the root name of the name given in the reference. The character "*" is a +// wildcard, and matches zero or more characters at this position. The +// character "%" is similar to "*", but it does not match a hierarchy delimiter. +func (c *Client) List(ref, name string, ch chan *imap.MailboxInfo) error { + defer close(ch) + + if err := c.ensureAuthenticated(); err != nil { + return err + } + + cmd := &commands.List{ + Reference: ref, + Mailbox: name, + } + res := &responses.List{Mailboxes: ch} + + status, err := c.execute(cmd, res) + if err != nil { + return err + } + return status.Err() +} + +// Lsub returns a subset of names from the set of names that the user has +// declared as being "active" or "subscribed". +func (c *Client) Lsub(ref, name string, ch chan *imap.MailboxInfo) error { + defer close(ch) + + if err := c.ensureAuthenticated(); err != nil { + return err + } + + cmd := &commands.List{ + Reference: ref, + Mailbox: name, + Subscribed: true, + } + res := &responses.List{ + Mailboxes: ch, + Subscribed: true, + } + + status, err := c.execute(cmd, res) + if err != nil { + return err + } + return status.Err() +} + +// Status requests the status of the indicated mailbox. It does not change the +// currently selected mailbox, nor does it affect the state of any messages in +// the queried mailbox. +// +// See RFC 3501 section 6.3.10 for a list of items that can be requested. +func (c *Client) Status(name string, items []imap.StatusItem) (*imap.MailboxStatus, error) { + if err := c.ensureAuthenticated(); err != nil { + return nil, err + } + + cmd := &commands.Status{ + Mailbox: name, + Items: items, + } + res := &responses.Status{ + Mailbox: new(imap.MailboxStatus), + } + + status, err := c.execute(cmd, res) + if err != nil { + return nil, err + } + return res.Mailbox, status.Err() +} + +// Append appends the literal argument as a new message to the end of the +// specified destination mailbox. This argument SHOULD be in the format of an +// RFC 2822 message. flags and date are optional arguments and can be set to +// nil and the empty struct. +func (c *Client) Append(mbox string, flags []string, date time.Time, msg imap.Literal) error { + if err := c.ensureAuthenticated(); err != nil { + return err + } + + cmd := &commands.Append{ + Mailbox: mbox, + Flags: flags, + Date: date, + Message: msg, + } + + status, err := c.execute(cmd, nil) + if err != nil { + return err + } + return status.Err() +} + +// Enable requests the server to enable the named extensions. The extensions +// which were successfully enabled are returned. +// +// See RFC 5161 section 3.1. +func (c *Client) Enable(caps []string) ([]string, error) { + if ok, err := c.Support("ENABLE"); !ok || err != nil { + return nil, ErrExtensionUnsupported + } + + // ENABLE is invalid if a mailbox has been selected. + if c.State() != imap.AuthenticatedState { + return nil, ErrNotLoggedIn + } + + cmd := &commands.Enable{Caps: caps} + res := &responses.Enabled{} + + if status, err := c.Execute(cmd, res); err != nil { + return nil, err + } else { + return res.Caps, status.Err() + } +} + +func (c *Client) idle(stop <-chan struct{}) error { + cmd := &commands.Idle{} + + res := &responses.Idle{ + Stop: stop, + RepliesCh: make(chan []byte, 10), + } + + if status, err := c.Execute(cmd, res); err != nil { + return err + } else { + return status.Err() + } +} + +// IdleOptions holds options for Client.Idle. +type IdleOptions struct { + // LogoutTimeout is used to avoid being logged out by the server when + // idling. Each LogoutTimeout, the IDLE command is restarted. If set to + // zero, a default is used. If negative, this behavior is disabled. + LogoutTimeout time.Duration + // Poll interval when the server doesn't support IDLE. If zero, a default + // is used. If negative, polling is always disabled. + PollInterval time.Duration +} + +// Idle indicates to the server that the client is ready to receive unsolicited +// mailbox update messages. When the client wants to send commands again, it +// must first close stop. +// +// If the server doesn't support IDLE, go-imap falls back to polling. +func (c *Client) Idle(stop <-chan struct{}, opts *IdleOptions) error { + if ok, err := c.Support("IDLE"); err != nil { + return err + } else if !ok { + return c.idleFallback(stop, opts) + } + + logoutTimeout := 25 * time.Minute + if opts != nil { + if opts.LogoutTimeout > 0 { + logoutTimeout = opts.LogoutTimeout + } else if opts.LogoutTimeout < 0 { + return c.idle(stop) + } + } + + t := time.NewTicker(logoutTimeout) + defer t.Stop() + + for { + stopOrRestart := make(chan struct{}) + done := make(chan error, 1) + go func() { + done <- c.idle(stopOrRestart) + }() + + select { + case <-t.C: + close(stopOrRestart) + if err := <-done; err != nil { + return err + } + case <-stop: + close(stopOrRestart) + return <-done + case err := <-done: + close(stopOrRestart) + if err != nil { + return err + } + } + } +} + +func (c *Client) idleFallback(stop <-chan struct{}, opts *IdleOptions) error { + pollInterval := time.Minute + if opts != nil { + if opts.PollInterval > 0 { + pollInterval = opts.PollInterval + } else if opts.PollInterval < 0 { + return ErrExtensionUnsupported + } + } + + t := time.NewTicker(pollInterval) + defer t.Stop() + + for { + select { + case <-t.C: + if err := c.Noop(); err != nil { + return err + } + case <-stop: + return nil + case <-c.LoggedOut(): + return errors.New("disconnected while idling") + } + } +} diff --git a/vendor/github.com/emersion/go-imap/client/cmd_noauth.go b/vendor/github.com/emersion/go-imap/client/cmd_noauth.go new file mode 100644 index 0000000..f9b34d3 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/client/cmd_noauth.go @@ -0,0 +1,174 @@ +package client + +import ( + "crypto/tls" + "errors" + "net" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/commands" + "github.com/emersion/go-imap/responses" + "github.com/emersion/go-sasl" +) + +var ( + // ErrAlreadyLoggedIn is returned if Login or Authenticate is called when the + // client is already logged in. + ErrAlreadyLoggedIn = errors.New("Already logged in") + // ErrTLSAlreadyEnabled is returned if StartTLS is called when TLS is already + // enabled. + ErrTLSAlreadyEnabled = errors.New("TLS is already enabled") + // ErrLoginDisabled is returned if Login or Authenticate is called when the + // server has disabled authentication. Most of the time, calling enabling TLS + // solves the problem. + ErrLoginDisabled = errors.New("Login is disabled in current state") +) + +// SupportStartTLS checks if the server supports STARTTLS. +func (c *Client) SupportStartTLS() (bool, error) { + return c.Support("STARTTLS") +} + +// StartTLS starts TLS negotiation. +func (c *Client) StartTLS(tlsConfig *tls.Config) error { + if c.isTLS { + return ErrTLSAlreadyEnabled + } + + if tlsConfig == nil { + tlsConfig = new(tls.Config) + } + if tlsConfig.ServerName == "" { + tlsConfig = tlsConfig.Clone() + tlsConfig.ServerName = c.serverName + } + + cmd := new(commands.StartTLS) + + err := c.Upgrade(func(conn net.Conn) (net.Conn, error) { + // Flag connection as in upgrading + c.upgrading = true + if status, err := c.execute(cmd, nil); err != nil { + return nil, err + } else if err := status.Err(); err != nil { + return nil, err + } + + // Wait for reader to block. + c.conn.WaitReady() + tlsConn := tls.Client(conn, tlsConfig) + if err := tlsConn.Handshake(); err != nil { + return nil, err + } + + // Capabilities change when TLS is enabled + c.locker.Lock() + c.caps = nil + c.locker.Unlock() + + return tlsConn, nil + }) + if err != nil { + return err + } + + c.isTLS = true + return nil +} + +// SupportAuth checks if the server supports a given authentication mechanism. +func (c *Client) SupportAuth(mech string) (bool, error) { + return c.Support("AUTH=" + mech) +} + +// Authenticate indicates a SASL authentication mechanism to the server. If the +// server supports the requested authentication mechanism, it performs an +// authentication protocol exchange to authenticate and identify the client. +func (c *Client) Authenticate(auth sasl.Client) error { + if c.State() != imap.NotAuthenticatedState { + return ErrAlreadyLoggedIn + } + + mech, ir, err := auth.Start() + if err != nil { + return err + } + + cmd := &commands.Authenticate{ + Mechanism: mech, + } + + irOk, err := c.Support("SASL-IR") + if err != nil { + return err + } + if irOk { + cmd.InitialResponse = ir + } + + res := &responses.Authenticate{ + Mechanism: auth, + InitialResponse: ir, + RepliesCh: make(chan []byte, 10), + } + if irOk { + res.InitialResponse = nil + } + + status, err := c.execute(cmd, res) + if err != nil { + return err + } + if err = status.Err(); err != nil { + return err + } + + c.locker.Lock() + c.state = imap.AuthenticatedState + c.caps = nil // Capabilities change when user is logged in + c.locker.Unlock() + + if status.Code == "CAPABILITY" { + c.gotStatusCaps(status.Arguments) + } + + return nil +} + +// Login identifies the client to the server and carries the plaintext password +// authenticating this user. +func (c *Client) Login(username, password string) error { + if state := c.State(); state == imap.AuthenticatedState || state == imap.SelectedState { + return ErrAlreadyLoggedIn + } + + c.locker.Lock() + loginDisabled := c.caps != nil && c.caps["LOGINDISABLED"] + c.locker.Unlock() + if loginDisabled { + return ErrLoginDisabled + } + + cmd := &commands.Login{ + Username: username, + Password: password, + } + + status, err := c.execute(cmd, nil) + if err != nil { + return err + } + if err = status.Err(); err != nil { + return err + } + + c.locker.Lock() + c.state = imap.AuthenticatedState + c.caps = nil // Capabilities change when user is logged in + c.locker.Unlock() + + if status.Code == "CAPABILITY" { + c.gotStatusCaps(status.Arguments) + } + return nil +} diff --git a/vendor/github.com/emersion/go-imap/client/cmd_selected.go b/vendor/github.com/emersion/go-imap/client/cmd_selected.go new file mode 100644 index 0000000..0fb71ad --- /dev/null +++ b/vendor/github.com/emersion/go-imap/client/cmd_selected.go @@ -0,0 +1,367 @@ +package client + +import ( + "errors" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/commands" + "github.com/emersion/go-imap/responses" +) + +var ( + // ErrNoMailboxSelected is returned if a command that requires a mailbox to be + // selected is called when there isn't. + ErrNoMailboxSelected = errors.New("No mailbox selected") + + // ErrExtensionUnsupported is returned if a command uses a extension that + // is not supported by the server. + ErrExtensionUnsupported = errors.New("The required extension is not supported by the server") +) + +// Check requests a checkpoint of the currently selected mailbox. A checkpoint +// refers to any implementation-dependent housekeeping associated with the +// mailbox that is not normally executed as part of each command. +func (c *Client) Check() error { + if c.State() != imap.SelectedState { + return ErrNoMailboxSelected + } + + cmd := new(commands.Check) + + status, err := c.execute(cmd, nil) + if err != nil { + return err + } + + return status.Err() +} + +// Close permanently removes all messages that have the \Deleted flag set from +// the currently selected mailbox, and returns to the authenticated state from +// the selected state. +func (c *Client) Close() error { + if c.State() != imap.SelectedState { + return ErrNoMailboxSelected + } + + cmd := new(commands.Close) + + status, err := c.execute(cmd, nil) + if err != nil { + return err + } else if err := status.Err(); err != nil { + return err + } + + c.locker.Lock() + c.state = imap.AuthenticatedState + c.mailbox = nil + c.locker.Unlock() + return nil +} + +// Terminate closes the tcp connection +func (c *Client) Terminate() error { + return c.conn.Close() +} + +// Expunge permanently removes all messages that have the \Deleted flag set from +// the currently selected mailbox. If ch is not nil, sends sequence IDs of each +// deleted message to this channel. +func (c *Client) Expunge(ch chan uint32) error { + if ch != nil { + defer close(ch) + } + + if c.State() != imap.SelectedState { + return ErrNoMailboxSelected + } + + cmd := new(commands.Expunge) + + var h responses.Handler + if ch != nil { + h = &responses.Expunge{SeqNums: ch} + } + + status, err := c.execute(cmd, h) + if err != nil { + return err + } + return status.Err() +} + +func (c *Client) executeSearch(uid bool, criteria *imap.SearchCriteria, charset string) (ids []uint32, status *imap.StatusResp, err error) { + if c.State() != imap.SelectedState { + err = ErrNoMailboxSelected + return + } + + var cmd imap.Commander = &commands.Search{ + Charset: charset, + Criteria: criteria, + } + if uid { + cmd = &commands.Uid{Cmd: cmd} + } + + res := new(responses.Search) + + status, err = c.execute(cmd, res) + if err != nil { + return + } + + err, ids = status.Err(), res.Ids + return +} + +func (c *Client) search(uid bool, criteria *imap.SearchCriteria) (ids []uint32, err error) { + ids, status, err := c.executeSearch(uid, criteria, "UTF-8") + if status != nil && status.Code == imap.CodeBadCharset { + // Some servers don't support UTF-8 + ids, _, err = c.executeSearch(uid, criteria, "US-ASCII") + } + return +} + +// Search searches the mailbox for messages that match the given searching +// criteria. Searching criteria consist of one or more search keys. The response +// contains a list of message sequence IDs corresponding to those messages that +// match the searching criteria. When multiple keys are specified, the result is +// the intersection (AND function) of all the messages that match those keys. +// Criteria must be UTF-8 encoded. See RFC 3501 section 6.4.4 for a list of +// searching criteria. When no criteria has been set, all messages in the mailbox +// will be searched using ALL criteria. +func (c *Client) Search(criteria *imap.SearchCriteria) (seqNums []uint32, err error) { + return c.search(false, criteria) +} + +// UidSearch is identical to Search, but UIDs are returned instead of message +// sequence numbers. +func (c *Client) UidSearch(criteria *imap.SearchCriteria) (uids []uint32, err error) { + return c.search(true, criteria) +} + +func (c *Client) fetch(uid bool, seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error { + defer close(ch) + + if c.State() != imap.SelectedState { + return ErrNoMailboxSelected + } + + var cmd imap.Commander = &commands.Fetch{ + SeqSet: seqset, + Items: items, + } + if uid { + cmd = &commands.Uid{Cmd: cmd} + } + + res := &responses.Fetch{Messages: ch, SeqSet: seqset, Uid: uid} + + status, err := c.execute(cmd, res) + if err != nil { + return err + } + return status.Err() +} + +// Fetch retrieves data associated with a message in the mailbox. See RFC 3501 +// section 6.4.5 for a list of items that can be requested. +func (c *Client) Fetch(seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error { + return c.fetch(false, seqset, items, ch) +} + +// UidFetch is identical to Fetch, but seqset is interpreted as containing +// unique identifiers instead of message sequence numbers. +func (c *Client) UidFetch(seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error { + return c.fetch(true, seqset, items, ch) +} + +func (c *Client) store(uid bool, seqset *imap.SeqSet, item imap.StoreItem, value interface{}, ch chan *imap.Message) error { + if ch != nil { + defer close(ch) + } + + if c.State() != imap.SelectedState { + return ErrNoMailboxSelected + } + + // TODO: this could break extensions (this only works when item is FLAGS) + if fields, ok := value.([]interface{}); ok { + for i, field := range fields { + if s, ok := field.(string); ok { + fields[i] = imap.RawString(s) + } + } + } + + // If ch is nil, the updated values are data which will be lost, so don't + // retrieve it. + if ch == nil { + op, _, err := imap.ParseFlagsOp(item) + if err == nil { + item = imap.FormatFlagsOp(op, true) + } + } + + var cmd imap.Commander = &commands.Store{ + SeqSet: seqset, + Item: item, + Value: value, + } + if uid { + cmd = &commands.Uid{Cmd: cmd} + } + + var h responses.Handler + if ch != nil { + h = &responses.Fetch{Messages: ch, SeqSet: seqset, Uid: uid} + } + + status, err := c.execute(cmd, h) + if err != nil { + return err + } + return status.Err() +} + +// Store alters data associated with a message in the mailbox. If ch is not nil, +// the updated value of the data will be sent to this channel. See RFC 3501 +// section 6.4.6 for a list of items that can be updated. +func (c *Client) Store(seqset *imap.SeqSet, item imap.StoreItem, value interface{}, ch chan *imap.Message) error { + return c.store(false, seqset, item, value, ch) +} + +// UidStore is identical to Store, but seqset is interpreted as containing +// unique identifiers instead of message sequence numbers. +func (c *Client) UidStore(seqset *imap.SeqSet, item imap.StoreItem, value interface{}, ch chan *imap.Message) error { + return c.store(true, seqset, item, value, ch) +} + +func (c *Client) copy(uid bool, seqset *imap.SeqSet, dest string) error { + if c.State() != imap.SelectedState { + return ErrNoMailboxSelected + } + + var cmd imap.Commander = &commands.Copy{ + SeqSet: seqset, + Mailbox: dest, + } + if uid { + cmd = &commands.Uid{Cmd: cmd} + } + + status, err := c.execute(cmd, nil) + if err != nil { + return err + } + return status.Err() +} + +// Copy copies the specified message(s) to the end of the specified destination +// mailbox. +func (c *Client) Copy(seqset *imap.SeqSet, dest string) error { + return c.copy(false, seqset, dest) +} + +// UidCopy is identical to Copy, but seqset is interpreted as containing unique +// identifiers instead of message sequence numbers. +func (c *Client) UidCopy(seqset *imap.SeqSet, dest string) error { + return c.copy(true, seqset, dest) +} + +func (c *Client) move(uid bool, seqset *imap.SeqSet, dest string) error { + if c.State() != imap.SelectedState { + return ErrNoMailboxSelected + } + + if ok, err := c.Support("MOVE"); err != nil { + return err + } else if !ok { + return c.moveFallback(uid, seqset, dest) + } + + var cmd imap.Commander = &commands.Move{ + SeqSet: seqset, + Mailbox: dest, + } + if uid { + cmd = &commands.Uid{Cmd: cmd} + } + + if status, err := c.Execute(cmd, nil); err != nil { + return err + } else { + return status.Err() + } +} + +// moveFallback uses COPY, STORE and EXPUNGE for servers which don't support +// MOVE. +func (c *Client) moveFallback(uid bool, seqset *imap.SeqSet, dest string) error { + item := imap.FormatFlagsOp(imap.AddFlags, true) + flags := []interface{}{imap.DeletedFlag} + if uid { + if err := c.UidCopy(seqset, dest); err != nil { + return err + } + + if err := c.UidStore(seqset, item, flags, nil); err != nil { + return err + } + } else { + if err := c.Copy(seqset, dest); err != nil { + return err + } + + if err := c.Store(seqset, item, flags, nil); err != nil { + return err + } + } + + return c.Expunge(nil) +} + +// Move moves the specified message(s) to the end of the specified destination +// mailbox. +// +// If the server doesn't support the MOVE extension defined in RFC 6851, +// go-imap will fallback to copy, store and expunge. +func (c *Client) Move(seqset *imap.SeqSet, dest string) error { + return c.move(false, seqset, dest) +} + +// UidMove is identical to Move, but seqset is interpreted as containing unique +// identifiers instead of message sequence numbers. +func (c *Client) UidMove(seqset *imap.SeqSet, dest string) error { + return c.move(true, seqset, dest) +} + +// Unselect frees server's resources associated with the selected mailbox and +// returns the server to the authenticated state. This command performs the same +// actions as Close, except that no messages are permanently removed from the +// currently selected mailbox. +// +// If client does not support the UNSELECT extension, ErrExtensionUnsupported +// is returned. +func (c *Client) Unselect() error { + if ok, err := c.Support("UNSELECT"); !ok || err != nil { + return ErrExtensionUnsupported + } + + if c.State() != imap.SelectedState { + return ErrNoMailboxSelected + } + + cmd := &commands.Unselect{} + if status, err := c.Execute(cmd, nil); err != nil { + return err + } else if err := status.Err(); err != nil { + return err + } + + c.SetState(imap.AuthenticatedState, nil) + return nil +} diff --git a/vendor/github.com/emersion/go-imap/client/tag.go b/vendor/github.com/emersion/go-imap/client/tag.go new file mode 100644 index 0000000..01526ab --- /dev/null +++ b/vendor/github.com/emersion/go-imap/client/tag.go @@ -0,0 +1,24 @@ +package client + +import ( + "crypto/rand" + "encoding/base64" +) + +func randomString(n int) (string, error) { + b := make([]byte, n) + _, err := rand.Read(b) + if err != nil { + return "", err + } + + return base64.RawURLEncoding.EncodeToString(b), nil +} + +func generateTag() string { + tag, err := randomString(4) + if err != nil { + panic(err) + } + return tag +} diff --git a/vendor/github.com/emersion/go-imap/command.go b/vendor/github.com/emersion/go-imap/command.go new file mode 100644 index 0000000..dac2696 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/command.go @@ -0,0 +1,57 @@ +package imap + +import ( + "errors" + "strings" +) + +// A value that can be converted to a command. +type Commander interface { + Command() *Command +} + +// A command. +type Command struct { + // The command tag. It acts as a unique identifier for this command. If empty, + // the command is untagged. + Tag string + // The command name. + Name string + // The command arguments. + Arguments []interface{} +} + +// Implements the Commander interface. +func (cmd *Command) Command() *Command { + return cmd +} + +func (cmd *Command) WriteTo(w *Writer) error { + tag := cmd.Tag + if tag == "" { + tag = "*" + } + + fields := []interface{}{RawString(tag), RawString(cmd.Name)} + fields = append(fields, cmd.Arguments...) + return w.writeLine(fields...) +} + +// Parse a command from fields. +func (cmd *Command) Parse(fields []interface{}) error { + if len(fields) < 2 { + return errors.New("imap: cannot parse command: no enough fields") + } + + var ok bool + if cmd.Tag, ok = fields[0].(string); !ok { + return errors.New("imap: cannot parse command: invalid tag") + } + if cmd.Name, ok = fields[1].(string); !ok { + return errors.New("imap: cannot parse command: invalid name") + } + cmd.Name = strings.ToUpper(cmd.Name) // Command names are case-insensitive + + cmd.Arguments = fields[2:] + return nil +} diff --git a/vendor/github.com/emersion/go-imap/commands/append.go b/vendor/github.com/emersion/go-imap/commands/append.go new file mode 100644 index 0000000..d70b584 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/commands/append.go @@ -0,0 +1,93 @@ +package commands + +import ( + "errors" + "time" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/utf7" +) + +// Append is an APPEND command, as defined in RFC 3501 section 6.3.11. +type Append struct { + Mailbox string + Flags []string + Date time.Time + Message imap.Literal +} + +func (cmd *Append) Command() *imap.Command { + var args []interface{} + + mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox) + args = append(args, imap.FormatMailboxName(mailbox)) + + if cmd.Flags != nil { + flags := make([]interface{}, len(cmd.Flags)) + for i, flag := range cmd.Flags { + flags[i] = imap.RawString(flag) + } + args = append(args, flags) + } + + if !cmd.Date.IsZero() { + args = append(args, cmd.Date) + } + + args = append(args, cmd.Message) + + return &imap.Command{ + Name: "APPEND", + Arguments: args, + } +} + +func (cmd *Append) Parse(fields []interface{}) (err error) { + if len(fields) < 2 { + return errors.New("No enough arguments") + } + + // Parse mailbox name + if mailbox, err := imap.ParseString(fields[0]); err != nil { + return err + } else if mailbox, err = utf7.Encoding.NewDecoder().String(mailbox); err != nil { + return err + } else { + cmd.Mailbox = imap.CanonicalMailboxName(mailbox) + } + + // Parse message literal + litIndex := len(fields) - 1 + var ok bool + if cmd.Message, ok = fields[litIndex].(imap.Literal); !ok { + return errors.New("Message must be a literal") + } + + // Remaining fields a optional + fields = fields[1:litIndex] + if len(fields) > 0 { + // Parse flags list + if flags, ok := fields[0].([]interface{}); ok { + if cmd.Flags, err = imap.ParseStringList(flags); err != nil { + return err + } + + for i, flag := range cmd.Flags { + cmd.Flags[i] = imap.CanonicalFlag(flag) + } + + fields = fields[1:] + } + + // Parse date + if len(fields) > 0 { + if date, ok := fields[0].(string); !ok { + return errors.New("Date must be a string") + } else if cmd.Date, err = time.Parse(imap.DateTimeLayout, date); err != nil { + return err + } + } + } + + return +} diff --git a/vendor/github.com/emersion/go-imap/commands/authenticate.go b/vendor/github.com/emersion/go-imap/commands/authenticate.go new file mode 100644 index 0000000..b66f21f --- /dev/null +++ b/vendor/github.com/emersion/go-imap/commands/authenticate.go @@ -0,0 +1,124 @@ +package commands + +import ( + "bufio" + "encoding/base64" + "errors" + "io" + "strings" + + "github.com/emersion/go-imap" + "github.com/emersion/go-sasl" +) + +// AuthenticateConn is a connection that supports IMAP authentication. +type AuthenticateConn interface { + io.Reader + + // WriteResp writes an IMAP response to this connection. + WriteResp(res imap.WriterTo) error +} + +// Authenticate is an AUTHENTICATE command, as defined in RFC 3501 section +// 6.2.2. +type Authenticate struct { + Mechanism string + InitialResponse []byte +} + +func (cmd *Authenticate) Command() *imap.Command { + args := []interface{}{imap.RawString(cmd.Mechanism)} + if cmd.InitialResponse != nil { + var encodedResponse string + if len(cmd.InitialResponse) == 0 { + // Empty initial response should be encoded as "=", not empty + // string. + encodedResponse = "=" + } else { + encodedResponse = base64.StdEncoding.EncodeToString(cmd.InitialResponse) + } + + args = append(args, imap.RawString(encodedResponse)) + } + return &imap.Command{ + Name: "AUTHENTICATE", + Arguments: args, + } +} + +func (cmd *Authenticate) Parse(fields []interface{}) error { + if len(fields) < 1 { + return errors.New("Not enough arguments") + } + + var ok bool + if cmd.Mechanism, ok = fields[0].(string); !ok { + return errors.New("Mechanism must be a string") + } + cmd.Mechanism = strings.ToUpper(cmd.Mechanism) + + if len(fields) != 2 { + return nil + } + + encodedResponse, ok := fields[1].(string) + if !ok { + return errors.New("Initial response must be a string") + } + if encodedResponse == "=" { + cmd.InitialResponse = []byte{} + return nil + } + + var err error + cmd.InitialResponse, err = base64.StdEncoding.DecodeString(encodedResponse) + if err != nil { + return err + } + + return nil +} + +func (cmd *Authenticate) Handle(mechanisms map[string]sasl.Server, conn AuthenticateConn) error { + sasl, ok := mechanisms[cmd.Mechanism] + if !ok { + return errors.New("Unsupported mechanism") + } + + scanner := bufio.NewScanner(conn) + + response := cmd.InitialResponse + for { + challenge, done, err := sasl.Next(response) + if err != nil || done { + return err + } + + encoded := base64.StdEncoding.EncodeToString(challenge) + cont := &imap.ContinuationReq{Info: encoded} + if err := conn.WriteResp(cont); err != nil { + return err + } + + if !scanner.Scan() { + if err := scanner.Err(); err != nil { + return err + } + return errors.New("unexpected EOF") + } + + encoded = scanner.Text() + if encoded != "" { + if encoded == "*" { + return &imap.ErrStatusResp{Resp: &imap.StatusResp{ + Type: imap.StatusRespBad, + Info: "negotiation cancelled", + }} + } + response, err = base64.StdEncoding.DecodeString(encoded) + if err != nil { + return err + } + } + } +} diff --git a/vendor/github.com/emersion/go-imap/commands/capability.go b/vendor/github.com/emersion/go-imap/commands/capability.go new file mode 100644 index 0000000..3359c0a --- /dev/null +++ b/vendor/github.com/emersion/go-imap/commands/capability.go @@ -0,0 +1,18 @@ +package commands + +import ( + "github.com/emersion/go-imap" +) + +// Capability is a CAPABILITY command, as defined in RFC 3501 section 6.1.1. +type Capability struct{} + +func (c *Capability) Command() *imap.Command { + return &imap.Command{ + Name: "CAPABILITY", + } +} + +func (c *Capability) Parse(fields []interface{}) error { + return nil +} diff --git a/vendor/github.com/emersion/go-imap/commands/check.go b/vendor/github.com/emersion/go-imap/commands/check.go new file mode 100644 index 0000000..b90df7c --- /dev/null +++ b/vendor/github.com/emersion/go-imap/commands/check.go @@ -0,0 +1,18 @@ +package commands + +import ( + "github.com/emersion/go-imap" +) + +// Check is a CHECK command, as defined in RFC 3501 section 6.4.1. +type Check struct{} + +func (cmd *Check) Command() *imap.Command { + return &imap.Command{ + Name: "CHECK", + } +} + +func (cmd *Check) Parse(fields []interface{}) error { + return nil +} diff --git a/vendor/github.com/emersion/go-imap/commands/close.go b/vendor/github.com/emersion/go-imap/commands/close.go new file mode 100644 index 0000000..cc60658 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/commands/close.go @@ -0,0 +1,18 @@ +package commands + +import ( + "github.com/emersion/go-imap" +) + +// Close is a CLOSE command, as defined in RFC 3501 section 6.4.2. +type Close struct{} + +func (cmd *Close) Command() *imap.Command { + return &imap.Command{ + Name: "CLOSE", + } +} + +func (cmd *Close) Parse(fields []interface{}) error { + return nil +} diff --git a/vendor/github.com/emersion/go-imap/commands/commands.go b/vendor/github.com/emersion/go-imap/commands/commands.go new file mode 100644 index 0000000..a62b248 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/commands/commands.go @@ -0,0 +1,2 @@ +// Package commands implements IMAP commands defined in RFC 3501. +package commands diff --git a/vendor/github.com/emersion/go-imap/commands/copy.go b/vendor/github.com/emersion/go-imap/commands/copy.go new file mode 100644 index 0000000..5258f35 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/commands/copy.go @@ -0,0 +1,47 @@ +package commands + +import ( + "errors" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/utf7" +) + +// Copy is a COPY command, as defined in RFC 3501 section 6.4.7. +type Copy struct { + SeqSet *imap.SeqSet + Mailbox string +} + +func (cmd *Copy) Command() *imap.Command { + mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox) + + return &imap.Command{ + Name: "COPY", + Arguments: []interface{}{cmd.SeqSet, imap.FormatMailboxName(mailbox)}, + } +} + +func (cmd *Copy) Parse(fields []interface{}) error { + if len(fields) < 2 { + return errors.New("No enough arguments") + } + + if seqSet, ok := fields[0].(string); !ok { + return errors.New("Invalid sequence set") + } else if seqSet, err := imap.ParseSeqSet(seqSet); err != nil { + return err + } else { + cmd.SeqSet = seqSet + } + + if mailbox, err := imap.ParseString(fields[1]); err != nil { + return err + } else if mailbox, err := utf7.Encoding.NewDecoder().String(mailbox); err != nil { + return err + } else { + cmd.Mailbox = imap.CanonicalMailboxName(mailbox) + } + + return nil +} diff --git a/vendor/github.com/emersion/go-imap/commands/create.go b/vendor/github.com/emersion/go-imap/commands/create.go new file mode 100644 index 0000000..a1e6fe2 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/commands/create.go @@ -0,0 +1,38 @@ +package commands + +import ( + "errors" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/utf7" +) + +// Create is a CREATE command, as defined in RFC 3501 section 6.3.3. +type Create struct { + Mailbox string +} + +func (cmd *Create) Command() *imap.Command { + mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox) + + return &imap.Command{ + Name: "CREATE", + Arguments: []interface{}{mailbox}, + } +} + +func (cmd *Create) Parse(fields []interface{}) error { + if len(fields) < 1 { + return errors.New("No enough arguments") + } + + if mailbox, err := imap.ParseString(fields[0]); err != nil { + return err + } else if mailbox, err := utf7.Encoding.NewDecoder().String(mailbox); err != nil { + return err + } else { + cmd.Mailbox = imap.CanonicalMailboxName(mailbox) + } + + return nil +} diff --git a/vendor/github.com/emersion/go-imap/commands/delete.go b/vendor/github.com/emersion/go-imap/commands/delete.go new file mode 100644 index 0000000..60f4da8 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/commands/delete.go @@ -0,0 +1,38 @@ +package commands + +import ( + "errors" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/utf7" +) + +// Delete is a DELETE command, as defined in RFC 3501 section 6.3.3. +type Delete struct { + Mailbox string +} + +func (cmd *Delete) Command() *imap.Command { + mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox) + + return &imap.Command{ + Name: "DELETE", + Arguments: []interface{}{imap.FormatMailboxName(mailbox)}, + } +} + +func (cmd *Delete) Parse(fields []interface{}) error { + if len(fields) < 1 { + return errors.New("No enough arguments") + } + + if mailbox, err := imap.ParseString(fields[0]); err != nil { + return err + } else if mailbox, err := utf7.Encoding.NewDecoder().String(mailbox); err != nil { + return err + } else { + cmd.Mailbox = imap.CanonicalMailboxName(mailbox) + } + + return nil +} diff --git a/vendor/github.com/emersion/go-imap/commands/enable.go b/vendor/github.com/emersion/go-imap/commands/enable.go new file mode 100644 index 0000000..980195e --- /dev/null +++ b/vendor/github.com/emersion/go-imap/commands/enable.go @@ -0,0 +1,23 @@ +package commands + +import ( + "github.com/emersion/go-imap" +) + +// An ENABLE command, defined in RFC 5161 section 3.1. +type Enable struct { + Caps []string +} + +func (cmd *Enable) Command() *imap.Command { + return &imap.Command{ + Name: "ENABLE", + Arguments: imap.FormatStringList(cmd.Caps), + } +} + +func (cmd *Enable) Parse(fields []interface{}) error { + var err error + cmd.Caps, err = imap.ParseStringList(fields) + return err +} diff --git a/vendor/github.com/emersion/go-imap/commands/expunge.go b/vendor/github.com/emersion/go-imap/commands/expunge.go new file mode 100644 index 0000000..af550a4 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/commands/expunge.go @@ -0,0 +1,16 @@ +package commands + +import ( + "github.com/emersion/go-imap" +) + +// Expunge is an EXPUNGE command, as defined in RFC 3501 section 6.4.3. +type Expunge struct{} + +func (cmd *Expunge) Command() *imap.Command { + return &imap.Command{Name: "EXPUNGE"} +} + +func (cmd *Expunge) Parse(fields []interface{}) error { + return nil +} diff --git a/vendor/github.com/emersion/go-imap/commands/fetch.go b/vendor/github.com/emersion/go-imap/commands/fetch.go new file mode 100644 index 0000000..4eb3ab9 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/commands/fetch.go @@ -0,0 +1,63 @@ +package commands + +import ( + "errors" + "strings" + + "github.com/emersion/go-imap" +) + +// Fetch is a FETCH command, as defined in RFC 3501 section 6.4.5. +type Fetch struct { + SeqSet *imap.SeqSet + Items []imap.FetchItem +} + +func (cmd *Fetch) Command() *imap.Command { + // Handle FETCH macros separately as they should not be serialized within parentheses + if len(cmd.Items) == 1 && (cmd.Items[0] == imap.FetchAll || cmd.Items[0] == imap.FetchFast || cmd.Items[0] == imap.FetchFull) { + return &imap.Command{ + Name: "FETCH", + Arguments: []interface{}{cmd.SeqSet, imap.RawString(cmd.Items[0])}, + } + } else { + items := make([]interface{}, len(cmd.Items)) + for i, item := range cmd.Items { + items[i] = imap.RawString(item) + } + + return &imap.Command{ + Name: "FETCH", + Arguments: []interface{}{cmd.SeqSet, items}, + } + } +} + +func (cmd *Fetch) Parse(fields []interface{}) error { + if len(fields) < 2 { + return errors.New("No enough arguments") + } + + var err error + if seqset, ok := fields[0].(string); !ok { + return errors.New("Sequence set must be an atom") + } else if cmd.SeqSet, err = imap.ParseSeqSet(seqset); err != nil { + return err + } + + switch items := fields[1].(type) { + case string: // A macro or a single item + cmd.Items = imap.FetchItem(strings.ToUpper(items)).Expand() + case []interface{}: // A list of items + cmd.Items = make([]imap.FetchItem, 0, len(items)) + for _, v := range items { + itemStr, _ := v.(string) + item := imap.FetchItem(strings.ToUpper(itemStr)) + cmd.Items = append(cmd.Items, item.Expand()...) + } + default: + return errors.New("Items must be either a string or a list") + } + + return nil +} diff --git a/vendor/github.com/emersion/go-imap/commands/idle.go b/vendor/github.com/emersion/go-imap/commands/idle.go new file mode 100644 index 0000000..4d9669f --- /dev/null +++ b/vendor/github.com/emersion/go-imap/commands/idle.go @@ -0,0 +1,17 @@ +package commands + +import ( + "github.com/emersion/go-imap" +) + +// An IDLE command. +// Se RFC 2177 section 3. +type Idle struct{} + +func (cmd *Idle) Command() *imap.Command { + return &imap.Command{Name: "IDLE"} +} + +func (cmd *Idle) Parse(fields []interface{}) error { + return nil +} diff --git a/vendor/github.com/emersion/go-imap/commands/list.go b/vendor/github.com/emersion/go-imap/commands/list.go new file mode 100644 index 0000000..52686e9 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/commands/list.go @@ -0,0 +1,60 @@ +package commands + +import ( + "errors" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/utf7" +) + +// List is a LIST command, as defined in RFC 3501 section 6.3.8. If Subscribed +// is set to true, LSUB will be used instead. +type List struct { + Reference string + Mailbox string + + Subscribed bool +} + +func (cmd *List) Command() *imap.Command { + name := "LIST" + if cmd.Subscribed { + name = "LSUB" + } + + enc := utf7.Encoding.NewEncoder() + ref, _ := enc.String(cmd.Reference) + mailbox, _ := enc.String(cmd.Mailbox) + + return &imap.Command{ + Name: name, + Arguments: []interface{}{ref, mailbox}, + } +} + +func (cmd *List) Parse(fields []interface{}) error { + if len(fields) < 2 { + return errors.New("No enough arguments") + } + + dec := utf7.Encoding.NewDecoder() + + if mailbox, err := imap.ParseString(fields[0]); err != nil { + return err + } else if mailbox, err := dec.String(mailbox); err != nil { + return err + } else { + // TODO: canonical mailbox path + cmd.Reference = imap.CanonicalMailboxName(mailbox) + } + + if mailbox, err := imap.ParseString(fields[1]); err != nil { + return err + } else if mailbox, err := dec.String(mailbox); err != nil { + return err + } else { + cmd.Mailbox = imap.CanonicalMailboxName(mailbox) + } + + return nil +} diff --git a/vendor/github.com/emersion/go-imap/commands/login.go b/vendor/github.com/emersion/go-imap/commands/login.go new file mode 100644 index 0000000..d0af0b5 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/commands/login.go @@ -0,0 +1,36 @@ +package commands + +import ( + "errors" + + "github.com/emersion/go-imap" +) + +// Login is a LOGIN command, as defined in RFC 3501 section 6.2.2. +type Login struct { + Username string + Password string +} + +func (cmd *Login) Command() *imap.Command { + return &imap.Command{ + Name: "LOGIN", + Arguments: []interface{}{cmd.Username, cmd.Password}, + } +} + +func (cmd *Login) Parse(fields []interface{}) error { + if len(fields) < 2 { + return errors.New("Not enough arguments") + } + + var err error + if cmd.Username, err = imap.ParseString(fields[0]); err != nil { + return err + } + if cmd.Password, err = imap.ParseString(fields[1]); err != nil { + return err + } + + return nil +} diff --git a/vendor/github.com/emersion/go-imap/commands/logout.go b/vendor/github.com/emersion/go-imap/commands/logout.go new file mode 100644 index 0000000..e826719 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/commands/logout.go @@ -0,0 +1,18 @@ +package commands + +import ( + "github.com/emersion/go-imap" +) + +// Logout is a LOGOUT command, as defined in RFC 3501 section 6.1.3. +type Logout struct{} + +func (c *Logout) Command() *imap.Command { + return &imap.Command{ + Name: "LOGOUT", + } +} + +func (c *Logout) Parse(fields []interface{}) error { + return nil +} diff --git a/vendor/github.com/emersion/go-imap/commands/move.go b/vendor/github.com/emersion/go-imap/commands/move.go new file mode 100644 index 0000000..613a870 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/commands/move.go @@ -0,0 +1,48 @@ +package commands + +import ( + "errors" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/utf7" +) + +// A MOVE command. +// See RFC 6851 section 3.1. +type Move struct { + SeqSet *imap.SeqSet + Mailbox string +} + +func (cmd *Move) Command() *imap.Command { + mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox) + + return &imap.Command{ + Name: "MOVE", + Arguments: []interface{}{cmd.SeqSet, mailbox}, + } +} + +func (cmd *Move) Parse(fields []interface{}) (err error) { + if len(fields) < 2 { + return errors.New("No enough arguments") + } + + seqset, ok := fields[0].(string) + if !ok { + return errors.New("Invalid sequence set") + } + if cmd.SeqSet, err = imap.ParseSeqSet(seqset); err != nil { + return err + } + + mailbox, ok := fields[1].(string) + if !ok { + return errors.New("Mailbox name must be a string") + } + if cmd.Mailbox, err = utf7.Encoding.NewDecoder().String(mailbox); err != nil { + return err + } + + return +} diff --git a/vendor/github.com/emersion/go-imap/commands/noop.go b/vendor/github.com/emersion/go-imap/commands/noop.go new file mode 100644 index 0000000..da6a1c2 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/commands/noop.go @@ -0,0 +1,18 @@ +package commands + +import ( + "github.com/emersion/go-imap" +) + +// Noop is a NOOP command, as defined in RFC 3501 section 6.1.2. +type Noop struct{} + +func (c *Noop) Command() *imap.Command { + return &imap.Command{ + Name: "NOOP", + } +} + +func (c *Noop) Parse(fields []interface{}) error { + return nil +} diff --git a/vendor/github.com/emersion/go-imap/commands/rename.go b/vendor/github.com/emersion/go-imap/commands/rename.go new file mode 100644 index 0000000..37a5fa7 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/commands/rename.go @@ -0,0 +1,51 @@ +package commands + +import ( + "errors" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/utf7" +) + +// Rename is a RENAME command, as defined in RFC 3501 section 6.3.5. +type Rename struct { + Existing string + New string +} + +func (cmd *Rename) Command() *imap.Command { + enc := utf7.Encoding.NewEncoder() + existingName, _ := enc.String(cmd.Existing) + newName, _ := enc.String(cmd.New) + + return &imap.Command{ + Name: "RENAME", + Arguments: []interface{}{imap.FormatMailboxName(existingName), imap.FormatMailboxName(newName)}, + } +} + +func (cmd *Rename) Parse(fields []interface{}) error { + if len(fields) < 2 { + return errors.New("No enough arguments") + } + + dec := utf7.Encoding.NewDecoder() + + if existingName, err := imap.ParseString(fields[0]); err != nil { + return err + } else if existingName, err := dec.String(existingName); err != nil { + return err + } else { + cmd.Existing = imap.CanonicalMailboxName(existingName) + } + + if newName, err := imap.ParseString(fields[1]); err != nil { + return err + } else if newName, err := dec.String(newName); err != nil { + return err + } else { + cmd.New = imap.CanonicalMailboxName(newName) + } + + return nil +} diff --git a/vendor/github.com/emersion/go-imap/commands/search.go b/vendor/github.com/emersion/go-imap/commands/search.go new file mode 100644 index 0000000..72f026c --- /dev/null +++ b/vendor/github.com/emersion/go-imap/commands/search.go @@ -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, imap.RawString("CHARSET"), imap.RawString(cmd.Charset)) + } + args = append(args, cmd.Criteria.Format()...) + + return &imap.Command{ + Name: "SEARCH", + Arguments: args, + } +} + +func (cmd *Search) Parse(fields []interface{}) error { + if len(fields) == 0 { + return errors.New("Missing search criteria") + } + + // Parse charset + if f, ok := fields[0].(string); ok && strings.EqualFold(f, "CHARSET") { + if len(fields) < 2 { + return errors.New("Missing CHARSET value") + } + if cmd.Charset, ok = fields[1].(string); !ok { + return errors.New("Charset must be a string") + } + fields = fields[2:] + } + + var charsetReader func(io.Reader) io.Reader + charset := strings.ToLower(cmd.Charset) + if charset != "utf-8" && charset != "us-ascii" && charset != "" { + charsetReader = func(r io.Reader) io.Reader { + r, _ = imap.CharsetReader(charset, r) + return r + } + } + + cmd.Criteria = new(imap.SearchCriteria) + return cmd.Criteria.ParseWithCharset(fields, charsetReader) +} diff --git a/vendor/github.com/emersion/go-imap/commands/select.go b/vendor/github.com/emersion/go-imap/commands/select.go new file mode 100644 index 0000000..e881eff --- /dev/null +++ b/vendor/github.com/emersion/go-imap/commands/select.go @@ -0,0 +1,45 @@ +package commands + +import ( + "errors" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/utf7" +) + +// Select is a SELECT command, as defined in RFC 3501 section 6.3.1. If ReadOnly +// is set to true, the EXAMINE command will be used instead. +type Select struct { + Mailbox string + ReadOnly bool +} + +func (cmd *Select) Command() *imap.Command { + name := "SELECT" + if cmd.ReadOnly { + name = "EXAMINE" + } + + mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox) + + return &imap.Command{ + Name: name, + Arguments: []interface{}{imap.FormatMailboxName(mailbox)}, + } +} + +func (cmd *Select) Parse(fields []interface{}) error { + if len(fields) < 1 { + return errors.New("No enough arguments") + } + + if mailbox, err := imap.ParseString(fields[0]); err != nil { + return err + } else if mailbox, err := utf7.Encoding.NewDecoder().String(mailbox); err != nil { + return err + } else { + cmd.Mailbox = imap.CanonicalMailboxName(mailbox) + } + + return nil +} diff --git a/vendor/github.com/emersion/go-imap/commands/starttls.go b/vendor/github.com/emersion/go-imap/commands/starttls.go new file mode 100644 index 0000000..d900e5e --- /dev/null +++ b/vendor/github.com/emersion/go-imap/commands/starttls.go @@ -0,0 +1,18 @@ +package commands + +import ( + "github.com/emersion/go-imap" +) + +// StartTLS is a STARTTLS command, as defined in RFC 3501 section 6.2.1. +type StartTLS struct{} + +func (cmd *StartTLS) Command() *imap.Command { + return &imap.Command{ + Name: "STARTTLS", + } +} + +func (cmd *StartTLS) Parse(fields []interface{}) error { + return nil +} diff --git a/vendor/github.com/emersion/go-imap/commands/status.go b/vendor/github.com/emersion/go-imap/commands/status.go new file mode 100644 index 0000000..672dce5 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/commands/status.go @@ -0,0 +1,58 @@ +package commands + +import ( + "errors" + "strings" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/utf7" +) + +// Status is a STATUS command, as defined in RFC 3501 section 6.3.10. +type Status struct { + Mailbox string + Items []imap.StatusItem +} + +func (cmd *Status) Command() *imap.Command { + mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox) + + items := make([]interface{}, len(cmd.Items)) + for i, item := range cmd.Items { + items[i] = imap.RawString(item) + } + + return &imap.Command{ + Name: "STATUS", + Arguments: []interface{}{imap.FormatMailboxName(mailbox), items}, + } +} + +func (cmd *Status) Parse(fields []interface{}) error { + if len(fields) < 2 { + return errors.New("No enough arguments") + } + + if mailbox, err := imap.ParseString(fields[0]); err != nil { + return err + } else if mailbox, err := utf7.Encoding.NewDecoder().String(mailbox); err != nil { + return err + } else { + cmd.Mailbox = imap.CanonicalMailboxName(mailbox) + } + + items, ok := fields[1].([]interface{}) + if !ok { + return errors.New("STATUS command parameter is not a list") + } + cmd.Items = make([]imap.StatusItem, len(items)) + for i, f := range items { + if s, ok := f.(string); !ok { + return errors.New("Got a non-string field in a STATUS command parameter") + } else { + cmd.Items[i] = imap.StatusItem(strings.ToUpper(s)) + } + } + + return nil +} diff --git a/vendor/github.com/emersion/go-imap/commands/store.go b/vendor/github.com/emersion/go-imap/commands/store.go new file mode 100644 index 0000000..aeee3e6 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/commands/store.go @@ -0,0 +1,50 @@ +package commands + +import ( + "errors" + "strings" + + "github.com/emersion/go-imap" +) + +// Store is a STORE command, as defined in RFC 3501 section 6.4.6. +type Store struct { + SeqSet *imap.SeqSet + Item imap.StoreItem + Value interface{} +} + +func (cmd *Store) Command() *imap.Command { + return &imap.Command{ + Name: "STORE", + Arguments: []interface{}{cmd.SeqSet, imap.RawString(cmd.Item), cmd.Value}, + } +} + +func (cmd *Store) Parse(fields []interface{}) error { + if len(fields) < 3 { + return errors.New("No enough arguments") + } + + seqset, ok := fields[0].(string) + if !ok { + return errors.New("Invalid sequence set") + } + var err error + if cmd.SeqSet, err = imap.ParseSeqSet(seqset); err != nil { + return err + } + + if item, ok := fields[1].(string); !ok { + return errors.New("Item name must be a string") + } else { + cmd.Item = imap.StoreItem(strings.ToUpper(item)) + } + + if len(fields[2:]) == 1 { + cmd.Value = fields[2] + } else { + cmd.Value = fields[2:] + } + return nil +} diff --git a/vendor/github.com/emersion/go-imap/commands/subscribe.go b/vendor/github.com/emersion/go-imap/commands/subscribe.go new file mode 100644 index 0000000..6540969 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/commands/subscribe.go @@ -0,0 +1,63 @@ +package commands + +import ( + "errors" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/utf7" +) + +// Subscribe is a SUBSCRIBE command, as defined in RFC 3501 section 6.3.6. +type Subscribe struct { + Mailbox string +} + +func (cmd *Subscribe) Command() *imap.Command { + mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox) + + return &imap.Command{ + Name: "SUBSCRIBE", + Arguments: []interface{}{imap.FormatMailboxName(mailbox)}, + } +} + +func (cmd *Subscribe) Parse(fields []interface{}) error { + if len(fields) < 0 { + return errors.New("No enough arguments") + } + + if mailbox, err := imap.ParseString(fields[0]); err != nil { + return err + } else if cmd.Mailbox, err = utf7.Encoding.NewDecoder().String(mailbox); err != nil { + return err + } + return nil +} + +// An UNSUBSCRIBE command. +// See RFC 3501 section 6.3.7 +type Unsubscribe struct { + Mailbox string +} + +func (cmd *Unsubscribe) Command() *imap.Command { + mailbox, _ := utf7.Encoding.NewEncoder().String(cmd.Mailbox) + + return &imap.Command{ + Name: "UNSUBSCRIBE", + Arguments: []interface{}{imap.FormatMailboxName(mailbox)}, + } +} + +func (cmd *Unsubscribe) Parse(fields []interface{}) error { + if len(fields) < 0 { + return errors.New("No enogh arguments") + } + + if mailbox, err := imap.ParseString(fields[0]); err != nil { + return err + } else if cmd.Mailbox, err = utf7.Encoding.NewDecoder().String(mailbox); err != nil { + return err + } + return nil +} diff --git a/vendor/github.com/emersion/go-imap/commands/uid.go b/vendor/github.com/emersion/go-imap/commands/uid.go new file mode 100644 index 0000000..979af14 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/commands/uid.go @@ -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{}{imap.RawString(inner.Name)} + args = append(args, inner.Arguments...) + + return &imap.Command{ + Name: "UID", + Arguments: args, + } +} + +func (cmd *Uid) Parse(fields []interface{}) error { + if len(fields) < 0 { + return errors.New("No command name specified") + } + + name, ok := fields[0].(string) + if !ok { + return errors.New("Command name must be a string") + } + + cmd.Cmd = &imap.Command{ + Name: strings.ToUpper(name), // Command names are case-insensitive + Arguments: fields[1:], + } + + return nil +} diff --git a/vendor/github.com/emersion/go-imap/commands/unselect.go b/vendor/github.com/emersion/go-imap/commands/unselect.go new file mode 100644 index 0000000..da5c63d --- /dev/null +++ b/vendor/github.com/emersion/go-imap/commands/unselect.go @@ -0,0 +1,17 @@ +package commands + +import ( + "github.com/emersion/go-imap" +) + +// An UNSELECT command. +// See RFC 3691 section 2. +type Unselect struct{} + +func (cmd *Unselect) Command() *imap.Command { + return &imap.Command{Name: "UNSELECT"} +} + +func (cmd *Unselect) Parse(fields []interface{}) error { + return nil +} diff --git a/vendor/github.com/emersion/go-imap/conn.go b/vendor/github.com/emersion/go-imap/conn.go new file mode 100644 index 0000000..09ce633 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/conn.go @@ -0,0 +1,284 @@ +package imap + +import ( + "bufio" + "crypto/tls" + "io" + "net" + "sync" +) + +// A connection state. +// See RFC 3501 section 3. +type ConnState int + +const ( + // In the connecting state, the server has not yet sent a greeting and no + // command can be issued. + ConnectingState = 0 + + // In the not authenticated state, the client MUST supply + // authentication credentials before most commands will be + // permitted. This state is entered when a connection starts + // unless the connection has been pre-authenticated. + NotAuthenticatedState ConnState = 1 << 0 + + // In the authenticated state, the client is authenticated and MUST + // select a mailbox to access before commands that affect messages + // will be permitted. This state is entered when a + // pre-authenticated connection starts, when acceptable + // authentication credentials have been provided, after an error in + // selecting a mailbox, or after a successful CLOSE command. + AuthenticatedState = 1 << 1 + + // In a selected state, a mailbox has been selected to access. + // This state is entered when a mailbox has been successfully + // selected. + SelectedState = AuthenticatedState + 1<<2 + + // In the logout state, the connection is being terminated. This + // state can be entered as a result of a client request (via the + // LOGOUT command) or by unilateral action on the part of either + // the client or server. + LogoutState = 1 << 3 + + // ConnectedState is either NotAuthenticatedState, AuthenticatedState or + // SelectedState. + ConnectedState = NotAuthenticatedState | AuthenticatedState | SelectedState +) + +// A function that upgrades a connection. +// +// This should only be used by libraries implementing an IMAP extension (e.g. +// COMPRESS). +type ConnUpgrader func(conn net.Conn) (net.Conn, error) + +type Waiter struct { + start sync.WaitGroup + end sync.WaitGroup + finished bool +} + +func NewWaiter() *Waiter { + w := &Waiter{finished: false} + w.start.Add(1) + w.end.Add(1) + return w +} + +func (w *Waiter) Wait() { + if !w.finished { + // Signal that we are ready for upgrade to continue. + w.start.Done() + // Wait for upgrade to finish. + w.end.Wait() + w.finished = true + } +} + +func (w *Waiter) WaitReady() { + if !w.finished { + // Wait for reader/writer goroutine to be ready for upgrade. + w.start.Wait() + } +} + +func (w *Waiter) Close() { + if !w.finished { + // Upgrade is finished, close chanel to release reader/writer + w.end.Done() + } +} + +type LockedWriter struct { + lock sync.Mutex + writer io.Writer +} + +// NewLockedWriter - goroutine safe writer. +func NewLockedWriter(w io.Writer) io.Writer { + return &LockedWriter{writer: w} +} + +func (w *LockedWriter) Write(b []byte) (int, error) { + w.lock.Lock() + defer w.lock.Unlock() + return w.writer.Write(b) +} + +type debugWriter struct { + io.Writer + + local io.Writer + remote io.Writer +} + +// NewDebugWriter creates a new io.Writer that will write local network activity +// to local and remote network activity to remote. +func NewDebugWriter(local, remote io.Writer) io.Writer { + return &debugWriter{Writer: local, local: local, remote: remote} +} + +type multiFlusher struct { + flushers []flusher +} + +func (mf *multiFlusher) Flush() error { + for _, f := range mf.flushers { + if err := f.Flush(); err != nil { + return err + } + } + return nil +} + +func newMultiFlusher(flushers ...flusher) flusher { + return &multiFlusher{flushers} +} + +// Underlying connection state information. +type ConnInfo struct { + RemoteAddr net.Addr + LocalAddr net.Addr + + // nil if connection is not using TLS. + TLS *tls.ConnectionState +} + +// An IMAP connection. +type Conn struct { + net.Conn + *Reader + *Writer + + br *bufio.Reader + bw *bufio.Writer + + waiter *Waiter + + // Print all commands and responses to this io.Writer. + debug io.Writer +} + +// NewConn creates a new IMAP connection. +func NewConn(conn net.Conn, r *Reader, w *Writer) *Conn { + c := &Conn{Conn: conn, Reader: r, Writer: w} + + c.init() + return c +} + +func (c *Conn) createWaiter() *Waiter { + // create new waiter each time. + w := NewWaiter() + c.waiter = w + return w +} + +func (c *Conn) init() { + r := io.Reader(c.Conn) + w := io.Writer(c.Conn) + + if c.debug != nil { + localDebug, remoteDebug := c.debug, c.debug + if debug, ok := c.debug.(*debugWriter); ok { + localDebug, remoteDebug = debug.local, debug.remote + } + // If local and remote are the same, then we need a LockedWriter. + if localDebug == remoteDebug { + localDebug = NewLockedWriter(localDebug) + remoteDebug = localDebug + } + + if localDebug != nil { + w = io.MultiWriter(c.Conn, localDebug) + } + if remoteDebug != nil { + r = io.TeeReader(c.Conn, remoteDebug) + } + } + + if c.br == nil { + c.br = bufio.NewReader(r) + c.Reader.reader = c.br + } else { + c.br.Reset(r) + } + + if c.bw == nil { + c.bw = bufio.NewWriter(w) + c.Writer.Writer = c.bw + } else { + c.bw.Reset(w) + } + + if f, ok := c.Conn.(flusher); ok { + c.Writer.Writer = struct { + io.Writer + flusher + }{ + c.bw, + newMultiFlusher(c.bw, f), + } + } +} + +func (c *Conn) Info() *ConnInfo { + info := &ConnInfo{ + RemoteAddr: c.RemoteAddr(), + LocalAddr: c.LocalAddr(), + } + + tlsConn, ok := c.Conn.(*tls.Conn) + if ok { + state := tlsConn.ConnectionState() + info.TLS = &state + } + + return info +} + +// Write implements io.Writer. +func (c *Conn) Write(b []byte) (n int, err error) { + return c.Writer.Write(b) +} + +// Flush writes any buffered data to the underlying connection. +func (c *Conn) Flush() error { + return c.Writer.Flush() +} + +// Upgrade a connection, e.g. wrap an unencrypted connection with an encrypted +// tunnel. +func (c *Conn) Upgrade(upgrader ConnUpgrader) error { + // Block reads and writes during the upgrading process + w := c.createWaiter() + defer w.Close() + + upgraded, err := upgrader(c.Conn) + if err != nil { + return err + } + + c.Conn = upgraded + c.init() + return nil +} + +// Called by reader/writer goroutines to wait for Upgrade to finish +func (c *Conn) Wait() { + c.waiter.Wait() +} + +// Called by Upgrader to wait for reader/writer goroutines to be ready for +// upgrade. +func (c *Conn) WaitReady() { + c.waiter.WaitReady() +} + +// SetDebug defines an io.Writer to which all network activity will be logged. +// If nil is provided, network activity will not be logged. +func (c *Conn) SetDebug(w io.Writer) { + c.debug = w + c.init() +} diff --git a/vendor/github.com/emersion/go-imap/date.go b/vendor/github.com/emersion/go-imap/date.go new file mode 100644 index 0000000..bf99647 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/date.go @@ -0,0 +1,71 @@ +package imap + +import ( + "fmt" + "regexp" + "time" +) + +// Date and time layouts. +// Dovecot adds a leading zero to dates: +// https://github.com/dovecot/core/blob/4fbd5c5e113078e72f29465ccc96d44955ceadc2/src/lib-imap/imap-date.c#L166 +// Cyrus adds a leading space to dates: +// https://github.com/cyrusimap/cyrus-imapd/blob/1cb805a3bffbdf829df0964f3b802cdc917e76db/lib/times.c#L543 +// GMail doesn't support leading spaces in dates used in SEARCH commands. +const ( + // Defined in RFC 3501 as date-text on page 83. + DateLayout = "_2-Jan-2006" + // Defined in RFC 3501 as date-time on page 83. + DateTimeLayout = "_2-Jan-2006 15:04:05 -0700" + // Defined in RFC 5322 section 3.3, mentioned as env-date in RFC 3501 page 84. + envelopeDateTimeLayout = "Mon, 02 Jan 2006 15:04:05 -0700" + // Use as an example in RFC 3501 page 54. + searchDateLayout = "2-Jan-2006" +) + +// time.Time with a specific layout. +type ( + Date time.Time + DateTime time.Time + envelopeDateTime time.Time + searchDate time.Time +) + +// Permutations of the layouts defined in RFC 5322, section 3.3. +var envelopeDateTimeLayouts = [...]string{ + envelopeDateTimeLayout, // popular, try it first + "_2 Jan 2006 15:04:05 -0700", + "_2 Jan 2006 15:04:05 MST", + "_2 Jan 2006 15:04 -0700", + "_2 Jan 2006 15:04 MST", + "_2 Jan 06 15:04:05 -0700", + "_2 Jan 06 15:04:05 MST", + "_2 Jan 06 15:04 -0700", + "_2 Jan 06 15:04 MST", + "Mon, _2 Jan 2006 15:04:05 -0700", + "Mon, _2 Jan 2006 15:04:05 MST", + "Mon, _2 Jan 2006 15:04 -0700", + "Mon, _2 Jan 2006 15:04 MST", + "Mon, _2 Jan 06 15:04:05 -0700", + "Mon, _2 Jan 06 15:04:05 MST", + "Mon, _2 Jan 06 15:04 -0700", + "Mon, _2 Jan 06 15:04 MST", +} + +// TODO: this is a blunt way to strip any trailing CFWS (comment). A sharper +// one would strip multiple CFWS, and only if really valid according to +// RFC5322. +var commentRE = regexp.MustCompile(`[ \t]+\(.*\)$`) + +// Try parsing the date based on the layouts defined in RFC 5322, section 3.3. +// Inspired by https://github.com/golang/go/blob/master/src/net/mail/message.go +func parseMessageDateTime(maybeDate string) (time.Time, error) { + maybeDate = commentRE.ReplaceAllString(maybeDate, "") + for _, layout := range envelopeDateTimeLayouts { + parsed, err := time.Parse(layout, maybeDate) + if err == nil { + return parsed, nil + } + } + return time.Time{}, fmt.Errorf("date %s could not be parsed", maybeDate) +} diff --git a/vendor/github.com/emersion/go-imap/imap.go b/vendor/github.com/emersion/go-imap/imap.go new file mode 100644 index 0000000..837d78d --- /dev/null +++ b/vendor/github.com/emersion/go-imap/imap.go @@ -0,0 +1,108 @@ +// Package imap implements IMAP4rev1 (RFC 3501). +package imap + +import ( + "errors" + "io" + "strings" +) + +// A StatusItem is a mailbox status data item that can be retrieved with a +// STATUS command. See RFC 3501 section 6.3.10. +type StatusItem string + +const ( + StatusMessages StatusItem = "MESSAGES" + StatusRecent StatusItem = "RECENT" + StatusUidNext StatusItem = "UIDNEXT" + StatusUidValidity StatusItem = "UIDVALIDITY" + StatusUnseen StatusItem = "UNSEEN" + + StatusAppendLimit StatusItem = "APPENDLIMIT" +) + +// A FetchItem is a message data item that can be fetched. +type FetchItem string + +// List of items that can be fetched. +const ( + // Macros + FetchAll FetchItem = "ALL" + FetchFast FetchItem = "FAST" + FetchFull FetchItem = "FULL" + + // Items + FetchBody FetchItem = "BODY" + FetchBodyStructure FetchItem = "BODYSTRUCTURE" + FetchEnvelope FetchItem = "ENVELOPE" + FetchFlags FetchItem = "FLAGS" + FetchInternalDate FetchItem = "INTERNALDATE" + FetchRFC822 FetchItem = "RFC822" + FetchRFC822Header FetchItem = "RFC822.HEADER" + FetchRFC822Size FetchItem = "RFC822.SIZE" + FetchRFC822Text FetchItem = "RFC822.TEXT" + FetchUid FetchItem = "UID" +) + +// Expand expands the item if it's a macro. +func (item FetchItem) Expand() []FetchItem { + switch item { + case FetchAll: + return []FetchItem{FetchFlags, FetchInternalDate, FetchRFC822Size, FetchEnvelope} + case FetchFast: + return []FetchItem{FetchFlags, FetchInternalDate, FetchRFC822Size} + case FetchFull: + return []FetchItem{FetchFlags, FetchInternalDate, FetchRFC822Size, FetchEnvelope, FetchBody} + default: + return []FetchItem{item} + } +} + +// FlagsOp is an operation that will be applied on message flags. +type FlagsOp string + +const ( + // SetFlags replaces existing flags by new ones. + SetFlags FlagsOp = "FLAGS" + // AddFlags adds new flags. + AddFlags = "+FLAGS" + // RemoveFlags removes existing flags. + RemoveFlags = "-FLAGS" +) + +// silentOp can be appended to a FlagsOp to prevent the operation from +// triggering unilateral message updates. +const silentOp = ".SILENT" + +// A StoreItem is a message data item that can be updated. +type StoreItem string + +// FormatFlagsOp returns the StoreItem that executes the flags operation op. +func FormatFlagsOp(op FlagsOp, silent bool) StoreItem { + s := string(op) + if silent { + s += silentOp + } + return StoreItem(s) +} + +// ParseFlagsOp parses a flags operation from StoreItem. +func ParseFlagsOp(item StoreItem) (op FlagsOp, silent bool, err error) { + itemStr := string(item) + silent = strings.HasSuffix(itemStr, silentOp) + if silent { + itemStr = strings.TrimSuffix(itemStr, silentOp) + } + op = FlagsOp(itemStr) + + if op != SetFlags && op != AddFlags && op != RemoveFlags { + err = errors.New("Unsupported STORE operation") + } + return +} + +// CharsetReader, if non-nil, defines a function to generate charset-conversion +// readers, converting from the provided charset into UTF-8. Charsets are always +// lower-case. utf-8 and us-ascii charsets are handled by default. One of the +// the CharsetReader's result values must be non-nil. +var CharsetReader func(charset string, r io.Reader) (io.Reader, error) diff --git a/vendor/github.com/emersion/go-imap/literal.go b/vendor/github.com/emersion/go-imap/literal.go new file mode 100644 index 0000000..b5b7f55 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/literal.go @@ -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 +} diff --git a/vendor/github.com/emersion/go-imap/logger.go b/vendor/github.com/emersion/go-imap/logger.go new file mode 100644 index 0000000..fa96cdc --- /dev/null +++ b/vendor/github.com/emersion/go-imap/logger.go @@ -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{}) +} diff --git a/vendor/github.com/emersion/go-imap/mailbox.go b/vendor/github.com/emersion/go-imap/mailbox.go new file mode 100644 index 0000000..8f12d4d --- /dev/null +++ b/vendor/github.com/emersion/go-imap/mailbox.go @@ -0,0 +1,314 @@ +package imap + +import ( + "errors" + "fmt" + "strings" + "sync" + + "github.com/emersion/go-imap/utf7" +) + +// The primary mailbox, as defined in RFC 3501 section 5.1. +const InboxName = "INBOX" + +// CanonicalMailboxName returns the canonical form of a mailbox name. Mailbox names can be +// case-sensitive or case-insensitive depending on the backend implementation. +// The special INBOX mailbox is case-insensitive. +func CanonicalMailboxName(name string) string { + if strings.EqualFold(name, InboxName) { + return InboxName + } + return name +} + +// Mailbox attributes definied in RFC 3501 section 7.2.2. +const ( + // It is not possible for any child levels of hierarchy to exist under this\ + // name; no child levels exist now and none can be created in the future. + NoInferiorsAttr = "\\Noinferiors" + // It is not possible to use this name as a selectable mailbox. + NoSelectAttr = "\\Noselect" + // The mailbox has been marked "interesting" by the server; the mailbox + // probably contains messages that have been added since the last time the + // mailbox was selected. + MarkedAttr = "\\Marked" + // The mailbox does not contain any additional messages since the last time + // the mailbox was selected. + UnmarkedAttr = "\\Unmarked" +) + +// Mailbox attributes defined in RFC 6154 section 2 (SPECIAL-USE extension). +const ( + // This mailbox presents all messages in the user's message store. + AllAttr = "\\All" + // This mailbox is used to archive messages. + ArchiveAttr = "\\Archive" + // This mailbox is used to hold draft messages -- typically, messages that are + // being composed but have not yet been sent. + DraftsAttr = "\\Drafts" + // This mailbox presents all messages marked in some way as "important". + FlaggedAttr = "\\Flagged" + // This mailbox is where messages deemed to be junk mail are held. + JunkAttr = "\\Junk" + // This mailbox is used to hold copies of messages that have been sent. + SentAttr = "\\Sent" + // This mailbox is used to hold messages that have been deleted or marked for + // deletion. + TrashAttr = "\\Trash" +) + +// Mailbox attributes defined in RFC 3348 (CHILDREN extension) +const ( + // The presence of this attribute indicates that the mailbox has child + // mailboxes. + HasChildrenAttr = "\\HasChildren" + // The presence of this attribute indicates that the mailbox has no child + // mailboxes. + HasNoChildrenAttr = "\\HasNoChildren" +) + +// This mailbox attribute is a signal that the mailbox contains messages that +// are likely important to the user. This attribute is defined in RFC 8457 +// section 3. +const ImportantAttr = "\\Important" + +// Basic mailbox info. +type MailboxInfo struct { + // The mailbox attributes. + Attributes []string + // The server's path separator. + Delimiter string + // The mailbox name. + Name string +} + +// Parse mailbox info from fields. +func (info *MailboxInfo) Parse(fields []interface{}) error { + if len(fields) < 3 { + return errors.New("Mailbox info needs at least 3 fields") + } + + var err error + if info.Attributes, err = ParseStringList(fields[0]); err != nil { + return err + } + + var ok bool + if info.Delimiter, ok = fields[1].(string); !ok { + // The delimiter may be specified as NIL, which gets converted to a nil interface. + if fields[1] != nil { + return errors.New("Mailbox delimiter must be a string") + } + info.Delimiter = "" + } + + if name, err := ParseString(fields[2]); err != nil { + return err + } else if name, err := utf7.Encoding.NewDecoder().String(name); err != nil { + return err + } else { + info.Name = CanonicalMailboxName(name) + } + + return nil +} + +// Format mailbox info to fields. +func (info *MailboxInfo) Format() []interface{} { + name, _ := utf7.Encoding.NewEncoder().String(info.Name) + attrs := make([]interface{}, len(info.Attributes)) + for i, attr := range info.Attributes { + attrs[i] = RawString(attr) + } + + // If the delimiter is NIL, we need to treat it specially by inserting + // a nil field (so that it's later converted to an unquoted NIL atom). + var del interface{} + + if info.Delimiter != "" { + del = info.Delimiter + } + + // Thunderbird doesn't understand delimiters if not quoted + return []interface{}{attrs, del, FormatMailboxName(name)} +} + +// TODO: optimize this +func (info *MailboxInfo) match(name, pattern string) bool { + i := strings.IndexAny(pattern, "*%") + if i == -1 { + // No more wildcards + return name == pattern + } + + // Get parts before and after wildcard + chunk, wildcard, rest := pattern[0:i], pattern[i], pattern[i+1:] + + // Check that name begins with chunk + if len(chunk) > 0 && !strings.HasPrefix(name, chunk) { + return false + } + name = strings.TrimPrefix(name, chunk) + + // Expand wildcard + var j int + for j = 0; j < len(name); j++ { + if wildcard == '%' && string(name[j]) == info.Delimiter { + break // Stop on delimiter if wildcard is % + } + // Try to match the rest from here + if info.match(name[j:], rest) { + return true + } + } + + return info.match(name[j:], rest) +} + +// Match checks if a reference and a pattern matches this mailbox name, as +// defined in RFC 3501 section 6.3.8. +func (info *MailboxInfo) Match(reference, pattern string) bool { + name := info.Name + + if info.Delimiter != "" && strings.HasPrefix(pattern, info.Delimiter) { + reference = "" + pattern = strings.TrimPrefix(pattern, info.Delimiter) + } + if reference != "" { + if info.Delimiter != "" && !strings.HasSuffix(reference, info.Delimiter) { + reference += info.Delimiter + } + if !strings.HasPrefix(name, reference) { + return false + } + name = strings.TrimPrefix(name, reference) + } + + return info.match(name, pattern) +} + +// A mailbox status. +type MailboxStatus struct { + // The mailbox name. + Name string + // True if the mailbox is open in read-only mode. + ReadOnly bool + // The mailbox items that are currently filled in. This map's values + // should not be used directly, they must only be used by libraries + // implementing extensions of the IMAP protocol. + Items map[StatusItem]interface{} + + // The Items map may be accessed in different goroutines. Protect + // concurrent writes. + ItemsLocker sync.Mutex + + // The mailbox flags. + Flags []string + // The mailbox permanent flags. + PermanentFlags []string + // The sequence number of the first unseen message in the mailbox. + UnseenSeqNum uint32 + + // The number of messages in this mailbox. + Messages uint32 + // The number of messages not seen since the last time the mailbox was opened. + Recent uint32 + // The number of unread messages. + Unseen uint32 + // The next UID. + UidNext uint32 + // Together with a UID, it is a unique identifier for a message. + // Must be greater than or equal to 1. + UidValidity uint32 + + // Per-mailbox limit of message size. Set only if server supports the + // APPENDLIMIT extension. + AppendLimit uint32 +} + +// Create a new mailbox status that will contain the specified items. +func NewMailboxStatus(name string, items []StatusItem) *MailboxStatus { + status := &MailboxStatus{ + Name: name, + Items: make(map[StatusItem]interface{}), + } + + for _, k := range items { + status.Items[k] = nil + } + + return status +} + +func (status *MailboxStatus) Parse(fields []interface{}) error { + status.Items = make(map[StatusItem]interface{}) + + var k StatusItem + for i, f := range fields { + if i%2 == 0 { + if kstr, ok := f.(string); !ok { + return fmt.Errorf("cannot parse mailbox status: key is not a string, but a %T", f) + } else { + k = StatusItem(strings.ToUpper(kstr)) + } + } else { + status.Items[k] = nil + + var err error + switch k { + case StatusMessages: + status.Messages, err = ParseNumber(f) + case StatusRecent: + status.Recent, err = ParseNumber(f) + case StatusUnseen: + status.Unseen, err = ParseNumber(f) + case StatusUidNext: + status.UidNext, err = ParseNumber(f) + case StatusUidValidity: + status.UidValidity, err = ParseNumber(f) + case StatusAppendLimit: + status.AppendLimit, err = ParseNumber(f) + default: + status.Items[k] = f + } + + if err != nil { + return err + } + } + } + + return nil +} + +func (status *MailboxStatus) Format() []interface{} { + var fields []interface{} + for k, v := range status.Items { + switch k { + case StatusMessages: + v = status.Messages + case StatusRecent: + v = status.Recent + case StatusUnseen: + v = status.Unseen + case StatusUidNext: + v = status.UidNext + case StatusUidValidity: + v = status.UidValidity + case StatusAppendLimit: + v = status.AppendLimit + } + + fields = append(fields, RawString(k), v) + } + return fields +} + +func FormatMailboxName(name string) interface{} { + // Some e-mails servers don't handle quoted INBOX names correctly so we special-case it. + if strings.EqualFold(name, "INBOX") { + return RawString(name) + } + return name +} diff --git a/vendor/github.com/emersion/go-imap/message.go b/vendor/github.com/emersion/go-imap/message.go new file mode 100644 index 0000000..bd28325 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/message.go @@ -0,0 +1,1186 @@ +package imap + +import ( + "bytes" + "errors" + "fmt" + "io" + "mime" + "strconv" + "strings" + "time" +) + +// System message flags, defined in RFC 3501 section 2.3.2. +const ( + SeenFlag = "\\Seen" + AnsweredFlag = "\\Answered" + FlaggedFlag = "\\Flagged" + DeletedFlag = "\\Deleted" + DraftFlag = "\\Draft" + RecentFlag = "\\Recent" +) + +// ImportantFlag is a message flag to signal that a message is likely important +// to the user. This flag is defined in RFC 8457 section 2. +const ImportantFlag = "$Important" + +// TryCreateFlag is a special flag in MailboxStatus.PermanentFlags indicating +// that it is possible to create new keywords by attempting to store those +// flags in the mailbox. +const TryCreateFlag = "\\*" + +var flags = []string{ + SeenFlag, + AnsweredFlag, + FlaggedFlag, + DeletedFlag, + DraftFlag, + RecentFlag, +} + +// A PartSpecifier specifies which parts of the MIME entity should be returned. +type PartSpecifier string + +// Part specifiers described in RFC 3501 page 55. +const ( + // Refers to the entire part, including headers. + EntireSpecifier PartSpecifier = "" + // Refers to the header of the part. Must include the final CRLF delimiting + // the header and the body. + HeaderSpecifier = "HEADER" + // Refers to the text body of the part, omitting the header. + TextSpecifier = "TEXT" + // Refers to the MIME Internet Message Body header. Must include the final + // CRLF delimiting the header and the body. + MIMESpecifier = "MIME" +) + +// CanonicalFlag returns the canonical form of a flag. Flags are case-insensitive. +// +// If the flag is defined in RFC 3501, it returns the flag with the case of the +// RFC. Otherwise, it returns the lowercase version of the flag. +func CanonicalFlag(flag string) string { + for _, f := range flags { + if strings.EqualFold(f, flag) { + return f + } + } + return strings.ToLower(flag) +} + +func ParseParamList(fields []interface{}) (map[string]string, error) { + params := make(map[string]string) + + var k string + for i, f := range fields { + p, err := ParseString(f) + if err != nil { + return nil, errors.New("Parameter list contains a non-string: " + err.Error()) + } + + if i%2 == 0 { + k = p + } else { + params[k] = p + k = "" + } + } + + if k != "" { + return nil, errors.New("Parameter list contains a key without a value") + } + return params, nil +} + +func FormatParamList(params map[string]string) []interface{} { + var fields []interface{} + for key, value := range params { + fields = append(fields, key, value) + } + return fields +} + +var wordDecoder = &mime.WordDecoder{ + CharsetReader: func(charset string, input io.Reader) (io.Reader, error) { + if CharsetReader != nil { + return CharsetReader(charset, input) + } + return nil, fmt.Errorf("imap: unhandled charset %q", charset) + }, +} + +func decodeHeader(s string) (string, error) { + dec, err := wordDecoder.DecodeHeader(s) + if err != nil { + return s, err + } + return dec, nil +} + +func encodeHeader(s string) string { + return mime.QEncoding.Encode("utf-8", s) +} + +func stringLowered(i interface{}) (string, bool) { + s, ok := i.(string) + return strings.ToLower(s), ok +} + +func parseHeaderParamList(fields []interface{}) (map[string]string, error) { + params, err := ParseParamList(fields) + if err != nil { + return nil, err + } + + for k, v := range params { + if lower := strings.ToLower(k); lower != k { + delete(params, k) + k = lower + } + + params[k], _ = decodeHeader(v) + } + + return params, nil +} + +func formatHeaderParamList(params map[string]string) []interface{} { + encoded := make(map[string]string) + for k, v := range params { + encoded[k] = encodeHeader(v) + } + return FormatParamList(encoded) +} + +// A message. +type Message struct { + // The message sequence number. It must be greater than or equal to 1. + SeqNum uint32 + // 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[FetchItem]interface{} + + // The message envelope. + Envelope *Envelope + // The message body structure (either BODYSTRUCTURE or BODY). + BodyStructure *BodyStructure + // The message flags. + Flags []string + // The date the message was received by the server. + InternalDate time.Time + // The message size. + Size uint32 + // The message unique identifier. It must be greater than or equal to 1. + Uid uint32 + // The message body sections. + Body map[*BodySectionName]Literal + + // The order in which items were requested. This order must be preserved + // because some bad IMAP clients (looking at you, Outlook!) refuse responses + // containing items in a different order. + itemsOrder []FetchItem +} + +// Create a new empty message that will contain the specified items. +func NewMessage(seqNum uint32, items []FetchItem) *Message { + msg := &Message{ + SeqNum: seqNum, + Items: make(map[FetchItem]interface{}), + Body: make(map[*BodySectionName]Literal), + itemsOrder: items, + } + + for _, k := range items { + msg.Items[k] = nil + } + + return msg +} + +// Parse a message from fields. +func (m *Message) Parse(fields []interface{}) error { + m.Items = make(map[FetchItem]interface{}) + m.Body = map[*BodySectionName]Literal{} + m.itemsOrder = nil + + var k FetchItem + for i, f := range fields { + if i%2 == 0 { // It's a key + switch f := f.(type) { + case string: + k = FetchItem(strings.ToUpper(f)) + case RawString: + k = FetchItem(strings.ToUpper(string(f))) + default: + return fmt.Errorf("cannot parse message: key is not a string, but a %T", f) + } + } else { // It's a value + m.Items[k] = nil + m.itemsOrder = append(m.itemsOrder, k) + + switch k { + case FetchBody, FetchBodyStructure: + bs, ok := f.([]interface{}) + if !ok { + return fmt.Errorf("cannot parse message: BODYSTRUCTURE is not a list, but a %T", f) + } + + m.BodyStructure = &BodyStructure{Extended: k == FetchBodyStructure} + if err := m.BodyStructure.Parse(bs); err != nil { + return err + } + case FetchEnvelope: + env, ok := f.([]interface{}) + if !ok { + return fmt.Errorf("cannot parse message: ENVELOPE is not a list, but a %T", f) + } + + m.Envelope = &Envelope{} + if err := m.Envelope.Parse(env); err != nil { + return err + } + case FetchFlags: + flags, ok := f.([]interface{}) + if !ok { + return fmt.Errorf("cannot parse message: FLAGS is not a list, but a %T", f) + } + + m.Flags = make([]string, len(flags)) + for i, flag := range flags { + s, _ := ParseString(flag) + m.Flags[i] = CanonicalFlag(s) + } + case FetchInternalDate: + date, _ := f.(string) + m.InternalDate, _ = time.Parse(DateTimeLayout, date) + case FetchRFC822Size: + m.Size, _ = ParseNumber(f) + case FetchUid: + m.Uid, _ = ParseNumber(f) + default: + // Likely to be a section of the body + // First check that the section name is correct + if section, err := ParseBodySectionName(k); err != nil { + // Not a section name, maybe an attribute defined in an IMAP extension + m.Items[k] = f + } else { + m.Body[section], _ = f.(Literal) + } + } + } + } + + return nil +} + +func (m *Message) formatItem(k FetchItem) []interface{} { + v := m.Items[k] + var kk interface{} = RawString(k) + + switch k { + case FetchBody, FetchBodyStructure: + // Extension data is only returned with the BODYSTRUCTURE fetch + m.BodyStructure.Extended = k == FetchBodyStructure + v = m.BodyStructure.Format() + case FetchEnvelope: + v = m.Envelope.Format() + case FetchFlags: + flags := make([]interface{}, len(m.Flags)) + for i, flag := range m.Flags { + flags[i] = RawString(flag) + } + v = flags + case FetchInternalDate: + v = m.InternalDate + case FetchRFC822Size: + v = m.Size + case FetchUid: + v = m.Uid + default: + for section, literal := range m.Body { + if section.value == k { + // This can contain spaces, so we can't pass it as a string directly + kk = section.resp() + v = literal + break + } + } + } + + return []interface{}{kk, v} +} + +func (m *Message) Format() []interface{} { + var fields []interface{} + + // First send ordered items + processed := make(map[FetchItem]bool) + for _, k := range m.itemsOrder { + if _, ok := m.Items[k]; ok { + fields = append(fields, m.formatItem(k)...) + processed[k] = true + } + } + + // Then send other remaining items + for k := range m.Items { + if !processed[k] { + fields = append(fields, m.formatItem(k)...) + } + } + + return fields +} + +// GetBody gets the body section with the specified name. Returns nil if it's not found. +func (m *Message) GetBody(section *BodySectionName) Literal { + section = section.resp() + + for s, body := range m.Body { + if section.Equal(s) { + if body == nil { + // Server can return nil, we need to treat as empty string per RFC 3501 + body = bytes.NewReader(nil) + } + return body + } + } + return nil +} + +// A body section name. +// See RFC 3501 page 55. +type BodySectionName struct { + BodyPartName + + // If set to true, do not implicitly set the \Seen flag. + Peek bool + // The substring of the section requested. The first value is the position of + // the first desired octet and the second value is the maximum number of + // octets desired. + Partial []int + + value FetchItem +} + +func (section *BodySectionName) parse(s string) error { + section.value = FetchItem(s) + + if s == "RFC822" { + s = "BODY[]" + } + if s == "RFC822.HEADER" { + s = "BODY.PEEK[HEADER]" + } + if s == "RFC822.TEXT" { + s = "BODY[TEXT]" + } + + partStart := strings.Index(s, "[") + if partStart == -1 { + return errors.New("Invalid body section name: must contain an open bracket") + } + + partEnd := strings.LastIndex(s, "]") + if partEnd == -1 { + return errors.New("Invalid body section name: must contain a close bracket") + } + + name := s[:partStart] + part := s[partStart+1 : partEnd] + partial := s[partEnd+1:] + + if name == "BODY.PEEK" { + section.Peek = true + } else if name != "BODY" { + return errors.New("Invalid body section name") + } + + b := bytes.NewBufferString(part + string(cr) + string(lf)) + r := NewReader(b) + fields, err := r.ReadFields() + if err != nil { + return err + } + + if err := section.BodyPartName.parse(fields); err != nil { + return err + } + + if len(partial) > 0 { + if !strings.HasPrefix(partial, "<") || !strings.HasSuffix(partial, ">") { + return errors.New("Invalid body section name: invalid partial") + } + partial = partial[1 : len(partial)-1] + + partialParts := strings.SplitN(partial, ".", 2) + + var from, length int + if from, err = strconv.Atoi(partialParts[0]); err != nil { + return errors.New("Invalid body section name: invalid partial: invalid from: " + err.Error()) + } + section.Partial = []int{from} + + if len(partialParts) == 2 { + if length, err = strconv.Atoi(partialParts[1]); err != nil { + return errors.New("Invalid body section name: invalid partial: invalid length: " + err.Error()) + } + section.Partial = append(section.Partial, length) + } + } + + return nil +} + +func (section *BodySectionName) FetchItem() FetchItem { + if section.value != "" { + return section.value + } + + s := "BODY" + if section.Peek { + s += ".PEEK" + } + + s += "[" + section.BodyPartName.string() + "]" + + if len(section.Partial) > 0 { + s += "<" + s += strconv.Itoa(section.Partial[0]) + + if len(section.Partial) > 1 { + s += "." + s += strconv.Itoa(section.Partial[1]) + } + + s += ">" + } + + return FetchItem(s) +} + +// Equal checks whether two sections are equal. +func (section *BodySectionName) Equal(other *BodySectionName) bool { + if section.Peek != other.Peek { + return false + } + if len(section.Partial) != len(other.Partial) { + return false + } + if len(section.Partial) > 0 && section.Partial[0] != other.Partial[0] { + return false + } + if len(section.Partial) > 1 && section.Partial[1] != other.Partial[1] { + return false + } + return section.BodyPartName.Equal(&other.BodyPartName) +} + +func (section *BodySectionName) resp() *BodySectionName { + resp := *section // Copy section + if resp.Peek { + resp.Peek = false + } + if len(resp.Partial) == 2 { + resp.Partial = []int{resp.Partial[0]} + } + if !strings.HasPrefix(string(resp.value), string(FetchRFC822)) { + resp.value = "" + } + return &resp +} + +// ExtractPartial returns a subset of the specified bytes matching the partial requested in the +// section name. +func (section *BodySectionName) ExtractPartial(b []byte) []byte { + if len(section.Partial) != 2 { + return b + } + + from := section.Partial[0] + length := section.Partial[1] + to := from + length + if from > len(b) { + return nil + } + if to > len(b) { + to = len(b) + } + return b[from:to] +} + +// ParseBodySectionName parses a body section name. +func ParseBodySectionName(s FetchItem) (*BodySectionName, error) { + section := new(BodySectionName) + err := section.parse(string(s)) + return section, err +} + +// A body part name. +type BodyPartName struct { + // The specifier of the requested part. + Specifier PartSpecifier + // The part path. Parts indexes start at 1. + Path []int + // If Specifier is HEADER, contains header fields that will/won't be returned, + // depending of the value of NotFields. + Fields []string + // If set to true, Fields is a blacklist of fields instead of a whitelist. + NotFields bool +} + +func (part *BodyPartName) parse(fields []interface{}) error { + if len(fields) == 0 { + return nil + } + + name, ok := fields[0].(string) + if !ok { + return errors.New("Invalid body section name: part name must be a string") + } + + args := fields[1:] + + path := strings.Split(strings.ToUpper(name), ".") + + end := 0 +loop: + for i, node := range path { + switch PartSpecifier(node) { + case EntireSpecifier, HeaderSpecifier, MIMESpecifier, TextSpecifier: + part.Specifier = PartSpecifier(node) + end = i + 1 + break loop + } + + index, err := strconv.Atoi(node) + if err != nil { + return errors.New("Invalid body part name: " + err.Error()) + } + if index <= 0 { + return errors.New("Invalid body part name: index <= 0") + } + + part.Path = append(part.Path, index) + } + + if part.Specifier == HeaderSpecifier && len(path) > end && path[end] == "FIELDS" && len(args) > 0 { + end++ + if len(path) > end && path[end] == "NOT" { + part.NotFields = true + } + + names, ok := args[0].([]interface{}) + if !ok { + return errors.New("Invalid body part name: HEADER.FIELDS must have a list argument") + } + + for _, namei := range names { + if name, ok := namei.(string); ok { + part.Fields = append(part.Fields, name) + } + } + } + + return nil +} + +func (part *BodyPartName) string() string { + path := make([]string, len(part.Path)) + for i, index := range part.Path { + path[i] = strconv.Itoa(index) + } + + if part.Specifier != EntireSpecifier { + path = append(path, string(part.Specifier)) + } + + if part.Specifier == HeaderSpecifier && len(part.Fields) > 0 { + path = append(path, "FIELDS") + + if part.NotFields { + path = append(path, "NOT") + } + } + + s := strings.Join(path, ".") + + if len(part.Fields) > 0 { + s += " (" + strings.Join(part.Fields, " ") + ")" + } + + return s +} + +// Equal checks whether two body part names are equal. +func (part *BodyPartName) Equal(other *BodyPartName) bool { + if part.Specifier != other.Specifier { + return false + } + if part.NotFields != other.NotFields { + return false + } + if len(part.Path) != len(other.Path) { + return false + } + for i, node := range part.Path { + if node != other.Path[i] { + return false + } + } + if len(part.Fields) != len(other.Fields) { + return false + } + for _, field := range part.Fields { + found := false + for _, f := range other.Fields { + if strings.EqualFold(field, f) { + found = true + break + } + } + if !found { + return false + } + } + return true +} + +// An address. +type Address struct { + // The personal name. + PersonalName string + // The SMTP at-domain-list (source route). + AtDomainList string + // The mailbox name. + MailboxName string + // The host name. + HostName string +} + +// Address returns the mailbox address (e.g. "foo@example.org"). +func (addr *Address) Address() string { + return addr.MailboxName + "@" + addr.HostName +} + +// Parse an address from fields. +func (addr *Address) Parse(fields []interface{}) error { + if len(fields) < 4 { + return errors.New("Address doesn't contain 4 fields") + } + + if s, err := ParseString(fields[0]); err == nil { + addr.PersonalName, _ = decodeHeader(s) + } + if s, err := ParseString(fields[1]); err == nil { + addr.AtDomainList, _ = decodeHeader(s) + } + + s, err := ParseString(fields[2]) + if err != nil { + return errors.New("Mailbox name could not be parsed") + } + addr.MailboxName, _ = decodeHeader(s) + + s, err = ParseString(fields[3]) + if err != nil { + return errors.New("Host name could not be parsed") + } + addr.HostName, _ = decodeHeader(s) + + return nil +} + +// Format an address to fields. +func (addr *Address) Format() []interface{} { + fields := make([]interface{}, 4) + + if addr.PersonalName != "" { + fields[0] = encodeHeader(addr.PersonalName) + } + if addr.AtDomainList != "" { + fields[1] = addr.AtDomainList + } + if addr.MailboxName != "" { + fields[2] = addr.MailboxName + } + if addr.HostName != "" { + fields[3] = addr.HostName + } + + return fields +} + +// Parse an address list from fields. +func ParseAddressList(fields []interface{}) (addrs []*Address) { + for _, f := range fields { + if addrFields, ok := f.([]interface{}); ok { + addr := &Address{} + if err := addr.Parse(addrFields); err == nil { + addrs = append(addrs, addr) + } + } + } + + return +} + +// Format an address list to fields. +func FormatAddressList(addrs []*Address) interface{} { + if len(addrs) == 0 { + return nil + } + + fields := make([]interface{}, len(addrs)) + + for i, addr := range addrs { + fields[i] = addr.Format() + } + + return fields +} + +// A message envelope, ie. message metadata from its headers. +// See RFC 3501 page 77. +type Envelope struct { + // The message date. + Date time.Time + // The message subject. + Subject string + // The From header addresses. + From []*Address + // The message senders. + Sender []*Address + // The Reply-To header addresses. + ReplyTo []*Address + // The To header addresses. + To []*Address + // The Cc header addresses. + Cc []*Address + // The Bcc header addresses. + Bcc []*Address + // The In-Reply-To header. Contains the parent Message-Id. + InReplyTo string + // The Message-Id header. + MessageId string +} + +// Parse an envelope from fields. +func (e *Envelope) Parse(fields []interface{}) error { + if len(fields) < 10 { + return errors.New("ENVELOPE doesn't contain 10 fields") + } + + if date, ok := fields[0].(string); ok { + e.Date, _ = parseMessageDateTime(date) + } + if subject, err := ParseString(fields[1]); err == nil { + e.Subject, _ = decodeHeader(subject) + } + if from, ok := fields[2].([]interface{}); ok { + e.From = ParseAddressList(from) + } + if sender, ok := fields[3].([]interface{}); ok { + e.Sender = ParseAddressList(sender) + } + if replyTo, ok := fields[4].([]interface{}); ok { + e.ReplyTo = ParseAddressList(replyTo) + } + if to, ok := fields[5].([]interface{}); ok { + e.To = ParseAddressList(to) + } + if cc, ok := fields[6].([]interface{}); ok { + e.Cc = ParseAddressList(cc) + } + if bcc, ok := fields[7].([]interface{}); ok { + e.Bcc = ParseAddressList(bcc) + } + if inReplyTo, ok := fields[8].(string); ok { + e.InReplyTo = inReplyTo + } + if msgId, ok := fields[9].(string); ok { + e.MessageId = msgId + } + + return nil +} + +// Format an envelope to fields. +func (e *Envelope) Format() (fields []interface{}) { + fields = make([]interface{}, 0, 10) + fields = append(fields, envelopeDateTime(e.Date)) + if e.Subject != "" { + fields = append(fields, encodeHeader(e.Subject)) + } else { + fields = append(fields, nil) + } + fields = append(fields, + FormatAddressList(e.From), + FormatAddressList(e.Sender), + FormatAddressList(e.ReplyTo), + FormatAddressList(e.To), + FormatAddressList(e.Cc), + FormatAddressList(e.Bcc), + ) + if e.InReplyTo != "" { + fields = append(fields, e.InReplyTo) + } else { + fields = append(fields, nil) + } + if e.MessageId != "" { + fields = append(fields, e.MessageId) + } else { + fields = append(fields, nil) + } + return fields +} + +// A body structure. +// See RFC 3501 page 74. +type BodyStructure struct { + // Basic fields + + // The MIME type (e.g. "text", "image") + MIMEType string + // The MIME subtype (e.g. "plain", "png") + MIMESubType string + // The MIME parameters. + Params map[string]string + + // The Content-Id header. + Id string + // The Content-Description header. + Description string + // The Content-Encoding header. + Encoding string + // The Content-Length header. + Size uint32 + + // Type-specific fields + + // The children parts, if multipart. + Parts []*BodyStructure + // The envelope, if message/rfc822. + Envelope *Envelope + // The body structure, if message/rfc822. + BodyStructure *BodyStructure + // The number of lines, if text or message/rfc822. + Lines uint32 + + // Extension data + + // True if the body structure contains extension data. + Extended bool + + // The Content-Disposition header field value. + Disposition string + // The Content-Disposition header field parameters. + DispositionParams map[string]string + // The Content-Language header field, if multipart. + Language []string + // The content URI, if multipart. + Location []string + + // The MD5 checksum. + MD5 string +} + +func (bs *BodyStructure) Parse(fields []interface{}) error { + if len(fields) == 0 { + return nil + } + + // Initialize params map + bs.Params = make(map[string]string) + + switch fields[0].(type) { + case []interface{}: // A multipart body part + bs.MIMEType = "multipart" + + end := 0 + for i, fi := range fields { + switch f := fi.(type) { + case []interface{}: // A part + part := new(BodyStructure) + if err := part.Parse(f); err != nil { + return err + } + bs.Parts = append(bs.Parts, part) + case string: + end = i + } + + if end > 0 { + break + } + } + + bs.MIMESubType, _ = fields[end].(string) + end++ + + // GMail seems to return only 3 extension data fields. Parse as many fields + // as we can. + if len(fields) > end { + bs.Extended = true // Contains extension data + + params, _ := fields[end].([]interface{}) + bs.Params, _ = parseHeaderParamList(params) + end++ + } + if len(fields) > end { + if disp, ok := fields[end].([]interface{}); ok && len(disp) >= 2 { + if s, ok := disp[0].(string); ok { + bs.Disposition, _ = decodeHeader(s) + bs.Disposition = strings.ToLower(bs.Disposition) + } + if params, ok := disp[1].([]interface{}); ok { + bs.DispositionParams, _ = parseHeaderParamList(params) + } + } + end++ + } + if len(fields) > end { + switch langs := fields[end].(type) { + case string: + bs.Language = []string{langs} + case []interface{}: + bs.Language, _ = ParseStringList(langs) + default: + bs.Language = nil + } + end++ + } + if len(fields) > end { + location, _ := fields[end].([]interface{}) + bs.Location, _ = ParseStringList(location) + end++ + } + case string: // A non-multipart body part + if len(fields) < 7 { + return errors.New("Non-multipart body part doesn't have 7 fields") + } + + bs.MIMEType, _ = stringLowered(fields[0]) + bs.MIMESubType, _ = stringLowered(fields[1]) + + params, _ := fields[2].([]interface{}) + bs.Params, _ = parseHeaderParamList(params) + + bs.Id, _ = fields[3].(string) + if desc, err := ParseString(fields[4]); err == nil { + bs.Description, _ = decodeHeader(desc) + } + bs.Encoding, _ = stringLowered(fields[5]) + bs.Size, _ = ParseNumber(fields[6]) + + end := 7 + + // Type-specific fields + if strings.EqualFold(bs.MIMEType, "message") && strings.EqualFold(bs.MIMESubType, "rfc822") { + if len(fields)-end < 3 { + return errors.New("Missing type-specific fields for message/rfc822") + } + + envelope, _ := fields[end].([]interface{}) + bs.Envelope = new(Envelope) + bs.Envelope.Parse(envelope) + + structure, _ := fields[end+1].([]interface{}) + bs.BodyStructure = new(BodyStructure) + bs.BodyStructure.Parse(structure) + + bs.Lines, _ = ParseNumber(fields[end+2]) + + end += 3 + } + if strings.EqualFold(bs.MIMEType, "text") { + if len(fields)-end < 1 { + return errors.New("Missing type-specific fields for text/*") + } + + bs.Lines, _ = ParseNumber(fields[end]) + end++ + } + + // GMail seems to return only 3 extension data fields. Parse as many fields + // as we can. + if len(fields) > end { + bs.Extended = true // Contains extension data + + bs.MD5, _ = fields[end].(string) + end++ + } + if len(fields) > end { + if disp, ok := fields[end].([]interface{}); ok && len(disp) >= 2 { + if s, ok := disp[0].(string); ok { + bs.Disposition, _ = decodeHeader(s) + bs.Disposition = strings.ToLower(bs.Disposition) + } + if params, ok := disp[1].([]interface{}); ok { + bs.DispositionParams, _ = parseHeaderParamList(params) + } + } + end++ + } + if len(fields) > end { + switch langs := fields[end].(type) { + case string: + bs.Language = []string{langs} + case []interface{}: + bs.Language, _ = ParseStringList(langs) + default: + bs.Language = nil + } + end++ + } + if len(fields) > end { + location, _ := fields[end].([]interface{}) + bs.Location, _ = ParseStringList(location) + end++ + } + } + + return nil +} + +func (bs *BodyStructure) Format() (fields []interface{}) { + if strings.EqualFold(bs.MIMEType, "multipart") { + for _, part := range bs.Parts { + fields = append(fields, part.Format()) + } + + fields = append(fields, bs.MIMESubType) + + if bs.Extended { + extended := make([]interface{}, 4) + + if bs.Params != nil { + extended[0] = formatHeaderParamList(bs.Params) + } + if bs.Disposition != "" { + extended[1] = []interface{}{ + encodeHeader(bs.Disposition), + formatHeaderParamList(bs.DispositionParams), + } + } + if bs.Language != nil { + extended[2] = FormatStringList(bs.Language) + } + if bs.Location != nil { + extended[3] = FormatStringList(bs.Location) + } + + fields = append(fields, extended...) + } + } else { + fields = make([]interface{}, 7) + fields[0] = bs.MIMEType + fields[1] = bs.MIMESubType + fields[2] = formatHeaderParamList(bs.Params) + + if bs.Id != "" { + fields[3] = bs.Id + } + if bs.Description != "" { + fields[4] = encodeHeader(bs.Description) + } + if bs.Encoding != "" { + fields[5] = bs.Encoding + } + + fields[6] = bs.Size + + // Type-specific fields + if strings.EqualFold(bs.MIMEType, "message") && strings.EqualFold(bs.MIMESubType, "rfc822") { + var env interface{} + if bs.Envelope != nil { + env = bs.Envelope.Format() + } + + var bsbs interface{} + if bs.BodyStructure != nil { + bsbs = bs.BodyStructure.Format() + } + + fields = append(fields, env, bsbs, bs.Lines) + } + if strings.EqualFold(bs.MIMEType, "text") { + fields = append(fields, bs.Lines) + } + + // Extension data + if bs.Extended { + extended := make([]interface{}, 4) + + if bs.MD5 != "" { + extended[0] = bs.MD5 + } + if bs.Disposition != "" { + extended[1] = []interface{}{ + encodeHeader(bs.Disposition), + formatHeaderParamList(bs.DispositionParams), + } + } + if bs.Language != nil { + extended[2] = FormatStringList(bs.Language) + } + if bs.Location != nil { + extended[3] = FormatStringList(bs.Location) + } + + fields = append(fields, extended...) + } + } + + return +} + +// Filename parses the body structure's filename, if it's an attachment. An +// empty string is returned if the filename isn't specified. An error is +// returned if and only if a charset error occurs, in which case the undecoded +// filename is returned too. +func (bs *BodyStructure) Filename() (string, error) { + raw, ok := bs.DispositionParams["filename"] + if !ok { + // Using "name" in Content-Type is discouraged + raw = bs.Params["name"] + } + return decodeHeader(raw) +} + +// BodyStructureWalkFunc is the type of the function called for each body +// structure visited by BodyStructure.Walk. The path argument contains the IMAP +// part path (see BodyPartName). +// +// The function should return true to visit all of the part's children or false +// to skip them. +type BodyStructureWalkFunc func(path []int, part *BodyStructure) (walkChildren bool) + +// Walk walks the body structure tree, calling f for each part in the tree, +// including bs itself. The parts are visited in DFS pre-order. +func (bs *BodyStructure) Walk(f BodyStructureWalkFunc) { + // Non-multipart messages only have part 1 + if len(bs.Parts) == 0 { + f([]int{1}, bs) + return + } + + bs.walk(f, nil) +} + +func (bs *BodyStructure) walk(f BodyStructureWalkFunc, path []int) { + if !f(path, bs) { + return + } + + for i, part := range bs.Parts { + num := i + 1 + + partPath := append([]int(nil), path...) + partPath = append(partPath, num) + + part.walk(f, partPath) + } +} diff --git a/vendor/github.com/emersion/go-imap/read.go b/vendor/github.com/emersion/go-imap/read.go new file mode 100644 index 0000000..112ee28 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/read.go @@ -0,0 +1,467 @@ +package imap + +import ( + "bytes" + "errors" + "io" + "strconv" + "strings" +) + +const ( + sp = ' ' + cr = '\r' + lf = '\n' + dquote = '"' + literalStart = '{' + literalEnd = '}' + listStart = '(' + listEnd = ')' + respCodeStart = '[' + respCodeEnd = ']' +) + +const ( + crlf = "\r\n" + nilAtom = "NIL" +) + +// TODO: add CTL to atomSpecials +var ( + quotedSpecials = string([]rune{dquote, '\\'}) + respSpecials = string([]rune{respCodeEnd}) + atomSpecials = string([]rune{listStart, listEnd, literalStart, sp, '%', '*'}) + quotedSpecials + respSpecials +) + +type parseError struct { + error +} + +func newParseError(text string) error { + return &parseError{errors.New(text)} +} + +// IsParseError returns true if the provided error is a parse error produced by +// Reader. +func IsParseError(err error) bool { + _, ok := err.(*parseError) + return ok +} + +// A string reader. +type StringReader interface { + // ReadString reads until the first occurrence of delim in the input, + // returning a string containing the data up to and including the delimiter. + // See https://golang.org/pkg/bufio/#Reader.ReadString + ReadString(delim byte) (line string, err error) +} + +type reader interface { + io.Reader + io.RuneScanner + StringReader +} + +// ParseNumber parses a number. +func ParseNumber(f interface{}) (uint32, error) { + // Useful for tests + if n, ok := f.(uint32); ok { + return n, nil + } + + var s string + switch f := f.(type) { + case RawString: + s = string(f) + case string: + s = f + default: + return 0, newParseError("expected a number, got a non-atom") + } + + nbr, err := strconv.ParseUint(string(s), 10, 32) + if err != nil { + return 0, &parseError{err} + } + + return uint32(nbr), nil +} + +// ParseString parses a string, which is either a literal, a quoted string or an +// atom. +func ParseString(f interface{}) (string, error) { + if s, ok := f.(string); ok { + return s, nil + } + + // Useful for tests + if a, ok := f.(RawString); ok { + return string(a), nil + } + + if l, ok := f.(Literal); ok { + b := make([]byte, l.Len()) + if _, err := io.ReadFull(l, b); err != nil { + return "", err + } + return string(b), nil + } + + return "", newParseError("expected a string") +} + +// Convert a field list to a string list. +func ParseStringList(f interface{}) ([]string, error) { + fields, ok := f.([]interface{}) + if !ok { + return nil, newParseError("expected a string list, got a non-list") + } + + list := make([]string, len(fields)) + for i, f := range fields { + var err error + if list[i], err = ParseString(f); err != nil { + return nil, newParseError("cannot parse string in string list: " + err.Error()) + } + } + return list, nil +} + +func trimSuffix(str string, suffix rune) string { + return str[:len(str)-1] +} + +// An IMAP reader. +type Reader struct { + MaxLiteralSize uint32 // The maximum literal size. + + reader + + continues chan<- bool + + brackets int + inRespCode bool +} + +func (r *Reader) ReadSp() error { + char, _, err := r.ReadRune() + if err != nil { + return err + } + if char != sp { + return newParseError("expected a space") + } + return nil +} + +func (r *Reader) ReadCrlf() (err error) { + var char rune + + if char, _, err = r.ReadRune(); err != nil { + return + } + if char == lf { + return + } + if char != cr { + err = newParseError("line doesn't end with a CR") + return + } + + if char, _, err = r.ReadRune(); err != nil { + return + } + if char != lf { + err = newParseError("line doesn't end with a LF") + } + + return +} + +func (r *Reader) ReadAtom() (interface{}, error) { + r.brackets = 0 + + var atom string + for { + char, _, err := r.ReadRune() + if err != nil { + return nil, err + } + + // TODO: list-wildcards and \ + if r.brackets == 0 && (char == listStart || char == literalStart || char == dquote) { + return nil, newParseError("atom contains forbidden char: " + string(char)) + } + if char == cr || char == lf { + break + } + if r.brackets == 0 && (char == sp || char == listEnd) { + break + } + if char == respCodeEnd { + if r.brackets == 0 { + if r.inRespCode { + break + } else { + return nil, newParseError("atom contains bad brackets nesting") + } + } + r.brackets-- + } + if char == respCodeStart { + r.brackets++ + } + + atom += string(char) + } + + r.UnreadRune() + + if atom == nilAtom { + return nil, nil + } + + return atom, nil +} + +func (r *Reader) ReadLiteral() (Literal, error) { + char, _, err := r.ReadRune() + if err != nil { + return nil, err + } else if char != literalStart { + return nil, newParseError("literal string doesn't start with an open brace") + } + + lstr, err := r.ReadString(byte(literalEnd)) + if err != nil { + return nil, err + } + lstr = trimSuffix(lstr, literalEnd) + + nonSync := strings.HasSuffix(lstr, "+") + if nonSync { + lstr = trimSuffix(lstr, '+') + } + + n, err := strconv.ParseUint(lstr, 10, 32) + if err != nil { + return nil, newParseError("cannot parse literal length: " + err.Error()) + } + if r.MaxLiteralSize > 0 && uint32(n) > r.MaxLiteralSize { + return nil, newParseError("literal exceeding maximum size") + } + + if err := r.ReadCrlf(); err != nil { + return nil, err + } + + // Send continuation request if necessary + if r.continues != nil && !nonSync { + r.continues <- true + } + + // Read literal + b := make([]byte, n) + if _, err := io.ReadFull(r, b); err != nil { + return nil, err + } + return bytes.NewBuffer(b), nil +} + +func (r *Reader) ReadQuotedString() (string, error) { + if char, _, err := r.ReadRune(); err != nil { + return "", err + } else if char != dquote { + return "", newParseError("quoted string doesn't start with a double quote") + } + + var buf bytes.Buffer + var escaped bool + for { + char, _, err := r.ReadRune() + if err != nil { + return "", err + } + + if char == '\\' && !escaped { + escaped = true + } else { + if char == cr || char == lf { + r.UnreadRune() + return "", newParseError("CR or LF not allowed in quoted string") + } + if char == dquote && !escaped { + break + } + + if !strings.ContainsRune(quotedSpecials, char) && escaped { + return "", newParseError("quoted string cannot contain backslash followed by a non-quoted-specials char") + } + + buf.WriteRune(char) + escaped = false + } + } + + return buf.String(), nil +} + +func (r *Reader) ReadFields() (fields []interface{}, err error) { + var char rune + for { + if char, _, err = r.ReadRune(); err != nil { + return + } + if err = r.UnreadRune(); err != nil { + return + } + + var field interface{} + ok := true + switch char { + case literalStart: + field, err = r.ReadLiteral() + case dquote: + field, err = r.ReadQuotedString() + case listStart: + field, err = r.ReadList() + case listEnd: + ok = false + case cr: + return + default: + field, err = r.ReadAtom() + } + + if err != nil { + return + } + if ok { + fields = append(fields, field) + } + + if char, _, err = r.ReadRune(); err != nil { + return + } + if char == cr || char == lf || char == listEnd || char == respCodeEnd { + if char == cr || char == lf { + r.UnreadRune() + } + return + } + if char == listStart { + r.UnreadRune() + continue + } + if char != sp { + err = newParseError("fields are not separated by a space") + return + } + } +} + +func (r *Reader) ReadList() (fields []interface{}, err error) { + char, _, err := r.ReadRune() + if err != nil { + return + } + if char != listStart { + err = newParseError("list doesn't start with an open parenthesis") + return + } + + fields, err = r.ReadFields() + if err != nil { + return + } + + r.UnreadRune() + if char, _, err = r.ReadRune(); err != nil { + return + } + if char != listEnd { + err = newParseError("list doesn't end with a close parenthesis") + } + return +} + +func (r *Reader) ReadLine() (fields []interface{}, err error) { + fields, err = r.ReadFields() + if err != nil { + return + } + + r.UnreadRune() + err = r.ReadCrlf() + return +} + +func (r *Reader) ReadRespCode() (code StatusRespCode, fields []interface{}, err error) { + char, _, err := r.ReadRune() + if err != nil { + return + } + if char != respCodeStart { + err = newParseError("response code doesn't start with an open bracket") + return + } + + r.inRespCode = true + fields, err = r.ReadFields() + r.inRespCode = false + if err != nil { + return + } + + if len(fields) == 0 { + err = newParseError("response code doesn't contain any field") + return + } + + codeStr, ok := fields[0].(string) + if !ok { + err = newParseError("response code doesn't start with a string atom") + return + } + if codeStr == "" { + err = newParseError("response code is empty") + return + } + code = StatusRespCode(strings.ToUpper(codeStr)) + + fields = fields[1:] + + r.UnreadRune() + char, _, err = r.ReadRune() + if err != nil { + return + } + if char != respCodeEnd { + err = newParseError("response code doesn't end with a close bracket") + } + return +} + +func (r *Reader) ReadInfo() (info string, err error) { + info, err = r.ReadString(byte(lf)) + if err != nil { + return + } + info = strings.TrimSuffix(info, string(lf)) + info = strings.TrimSuffix(info, string(cr)) + info = strings.TrimLeft(info, " ") + + return +} + +func NewReader(r reader) *Reader { + return &Reader{reader: r} +} + +func NewServerReader(r reader, continues chan<- bool) *Reader { + return &Reader{reader: r, continues: continues} +} + +type Parser interface { + Parse(fields []interface{}) error +} diff --git a/vendor/github.com/emersion/go-imap/response.go b/vendor/github.com/emersion/go-imap/response.go new file mode 100644 index 0000000..611d03e --- /dev/null +++ b/vendor/github.com/emersion/go-imap/response.go @@ -0,0 +1,181 @@ +package imap + +import ( + "strings" +) + +// Resp is an IMAP response. It is either a *DataResp, a +// *ContinuationReq or a *StatusResp. +type Resp interface { + resp() +} + +// ReadResp reads a single response from a Reader. +func ReadResp(r *Reader) (Resp, error) { + atom, err := r.ReadAtom() + if err != nil { + return nil, err + } + tag, ok := atom.(string) + if !ok { + return nil, newParseError("response tag is not an atom") + } + + if tag == "+" { + if err := r.ReadSp(); err != nil { + r.UnreadRune() + } + + resp := &ContinuationReq{} + resp.Info, err = r.ReadInfo() + if err != nil { + return nil, err + } + + return resp, nil + } + + if err := r.ReadSp(); err != nil { + return nil, err + } + + // Can be either data or status + // Try to parse a status + var fields []interface{} + if atom, err := r.ReadAtom(); err == nil { + fields = append(fields, atom) + + if err := r.ReadSp(); err == nil { + if name, ok := atom.(string); ok { + status := StatusRespType(name) + switch status { + case StatusRespOk, StatusRespNo, StatusRespBad, StatusRespPreauth, StatusRespBye: + resp := &StatusResp{ + Tag: tag, + Type: status, + } + + char, _, err := r.ReadRune() + if err != nil { + return nil, err + } + r.UnreadRune() + + if char == '[' { + // Contains code & arguments + resp.Code, resp.Arguments, err = r.ReadRespCode() + if err != nil { + return nil, err + } + } + + resp.Info, err = r.ReadInfo() + if err != nil { + return nil, err + } + + return resp, nil + } + } + } else { + r.UnreadRune() + } + } else { + r.UnreadRune() + } + + // Not a status so it's data + resp := &DataResp{Tag: tag} + + var remaining []interface{} + remaining, err = r.ReadLine() + if err != nil { + return nil, err + } + + resp.Fields = append(fields, remaining...) + return resp, nil +} + +// DataResp is an IMAP response containing data. +type DataResp struct { + // The response tag. Can be either "" for untagged responses, "+" for continuation + // requests or a previous command's tag. + Tag string + // The parsed response fields. + Fields []interface{} +} + +// NewUntaggedResp creates a new untagged response. +func NewUntaggedResp(fields []interface{}) *DataResp { + return &DataResp{ + Tag: "*", + Fields: fields, + } +} + +func (r *DataResp) resp() {} + +func (r *DataResp) WriteTo(w *Writer) error { + tag := RawString(r.Tag) + if tag == "" { + tag = RawString("*") + } + + fields := []interface{}{RawString(tag)} + fields = append(fields, r.Fields...) + return w.writeLine(fields...) +} + +// ContinuationReq is a continuation request response. +type ContinuationReq struct { + // The info message sent with the continuation request. + Info string +} + +func (r *ContinuationReq) resp() {} + +func (r *ContinuationReq) WriteTo(w *Writer) error { + if err := w.writeString("+"); err != nil { + return err + } + + if r.Info != "" { + if err := w.writeString(string(sp) + r.Info); err != nil { + return err + } + } + + return w.writeCrlf() +} + +// ParseNamedResp attempts to parse a named data response. +func ParseNamedResp(resp Resp) (name string, fields []interface{}, ok bool) { + data, ok := resp.(*DataResp) + if !ok || len(data.Fields) == 0 { + return + } + + // Some responses (namely EXISTS and RECENT) are formatted like so: + // [num] [name] [...] + // Which is fucking stupid. But we handle that here by checking if the + // response name is a number and then rearranging it. + if len(data.Fields) > 1 { + name, ok := data.Fields[1].(string) + if ok { + if _, err := ParseNumber(data.Fields[0]); err == nil { + fields := []interface{}{data.Fields[0]} + fields = append(fields, data.Fields[2:]...) + return strings.ToUpper(name), fields, true + } + } + } + + // IMAP commands are formatted like this: + // [name] [...] + name, ok = data.Fields[0].(string) + if !ok { + return + } + return strings.ToUpper(name), data.Fields[1:], true +} diff --git a/vendor/github.com/emersion/go-imap/responses/authenticate.go b/vendor/github.com/emersion/go-imap/responses/authenticate.go new file mode 100644 index 0000000..8e134cb --- /dev/null +++ b/vendor/github.com/emersion/go-imap/responses/authenticate.go @@ -0,0 +1,61 @@ +package responses + +import ( + "encoding/base64" + + "github.com/emersion/go-imap" + "github.com/emersion/go-sasl" +) + +// An AUTHENTICATE response. +type Authenticate struct { + Mechanism sasl.Client + InitialResponse []byte + RepliesCh chan []byte +} + +// Implements +func (r *Authenticate) Replies() <-chan []byte { + return r.RepliesCh +} + +func (r *Authenticate) writeLine(l string) error { + r.RepliesCh <- []byte(l + "\r\n") + return nil +} + +func (r *Authenticate) cancel() error { + return r.writeLine("*") +} + +func (r *Authenticate) Handle(resp imap.Resp) error { + cont, ok := resp.(*imap.ContinuationReq) + if !ok { + return ErrUnhandled + } + + // Empty challenge, send initial response as stated in RFC 2222 section 5.1 + if cont.Info == "" && r.InitialResponse != nil { + encoded := base64.StdEncoding.EncodeToString(r.InitialResponse) + if err := r.writeLine(encoded); err != nil { + return err + } + r.InitialResponse = nil + return nil + } + + challenge, err := base64.StdEncoding.DecodeString(cont.Info) + if err != nil { + r.cancel() + return err + } + + reply, err := r.Mechanism.Next(challenge) + if err != nil { + r.cancel() + return err + } + + encoded := base64.StdEncoding.EncodeToString(reply) + return r.writeLine(encoded) +} diff --git a/vendor/github.com/emersion/go-imap/responses/capability.go b/vendor/github.com/emersion/go-imap/responses/capability.go new file mode 100644 index 0000000..483cb2e --- /dev/null +++ b/vendor/github.com/emersion/go-imap/responses/capability.go @@ -0,0 +1,20 @@ +package responses + +import ( + "github.com/emersion/go-imap" +) + +// A CAPABILITY response. +// See RFC 3501 section 7.2.1 +type Capability struct { + Caps []string +} + +func (r *Capability) WriteTo(w *imap.Writer) error { + fields := []interface{}{imap.RawString("CAPABILITY")} + for _, cap := range r.Caps { + fields = append(fields, imap.RawString(cap)) + } + + return imap.NewUntaggedResp(fields).WriteTo(w) +} diff --git a/vendor/github.com/emersion/go-imap/responses/enabled.go b/vendor/github.com/emersion/go-imap/responses/enabled.go new file mode 100644 index 0000000..fc4e27b --- /dev/null +++ b/vendor/github.com/emersion/go-imap/responses/enabled.go @@ -0,0 +1,33 @@ +package responses + +import ( + "github.com/emersion/go-imap" +) + +// An ENABLED response, defined in RFC 5161 section 3.2. +type Enabled struct { + Caps []string +} + +func (r *Enabled) Handle(resp imap.Resp) error { + name, fields, ok := imap.ParseNamedResp(resp) + if !ok || name != "ENABLED" { + return ErrUnhandled + } + + if caps, err := imap.ParseStringList(fields); err != nil { + return err + } else { + r.Caps = append(r.Caps, caps...) + } + + return nil +} + +func (r *Enabled) WriteTo(w *imap.Writer) error { + fields := []interface{}{imap.RawString("ENABLED")} + for _, cap := range r.Caps { + fields = append(fields, imap.RawString(cap)) + } + return imap.NewUntaggedResp(fields).WriteTo(w) +} diff --git a/vendor/github.com/emersion/go-imap/responses/expunge.go b/vendor/github.com/emersion/go-imap/responses/expunge.go new file mode 100644 index 0000000..bce3bf1 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/responses/expunge.go @@ -0,0 +1,43 @@ +package responses + +import ( + "github.com/emersion/go-imap" +) + +const expungeName = "EXPUNGE" + +// An EXPUNGE response. +// See RFC 3501 section 7.4.1 +type Expunge struct { + SeqNums chan uint32 +} + +func (r *Expunge) Handle(resp imap.Resp) error { + name, fields, ok := imap.ParseNamedResp(resp) + if !ok || name != expungeName { + return ErrUnhandled + } + + if len(fields) == 0 { + return errNotEnoughFields + } + + seqNum, err := imap.ParseNumber(fields[0]) + if err != nil { + return err + } + + r.SeqNums <- seqNum + return nil +} + +func (r *Expunge) WriteTo(w *imap.Writer) error { + for seqNum := range r.SeqNums { + resp := imap.NewUntaggedResp([]interface{}{seqNum, imap.RawString(expungeName)}) + if err := resp.WriteTo(w); err != nil { + return err + } + } + + return nil +} diff --git a/vendor/github.com/emersion/go-imap/responses/fetch.go b/vendor/github.com/emersion/go-imap/responses/fetch.go new file mode 100644 index 0000000..691ebcb --- /dev/null +++ b/vendor/github.com/emersion/go-imap/responses/fetch.go @@ -0,0 +1,70 @@ +package responses + +import ( + "github.com/emersion/go-imap" +) + +const fetchName = "FETCH" + +// A FETCH response. +// See RFC 3501 section 7.4.2 +type Fetch struct { + Messages chan *imap.Message + SeqSet *imap.SeqSet + Uid bool +} + +func (r *Fetch) Handle(resp imap.Resp) error { + name, fields, ok := imap.ParseNamedResp(resp) + if !ok || name != fetchName { + return ErrUnhandled + } else if len(fields) < 1 { + return errNotEnoughFields + } + + seqNum, err := imap.ParseNumber(fields[0]) + if err != nil { + return err + } + + msgFields, _ := fields[1].([]interface{}) + msg := &imap.Message{SeqNum: seqNum} + if err := msg.Parse(msgFields); err != nil { + return err + } + + if r.Uid && msg.Uid == 0 { + // we requested UIDs and got a message without one --> unilateral update --> ignore + return ErrUnhandled + } + + var num uint32 + if r.Uid { + num = msg.Uid + } else { + num = seqNum + } + + // Check whether we obtained a result we requested with our SeqSet + // If the result is not contained in our SeqSet we have to handle an additional special case: + // In case we requested UIDs with a dynamic sequence (i.e. * or n:*) and the maximum UID of the mailbox + // is less then our n, the server will supply us with the max UID (cf. RFC 3501 §6.4.8 and §9 `seq-range`). + // Thus, such a result is correct and has to be returned by us. + if !r.SeqSet.Contains(num) && (!r.Uid || !r.SeqSet.Dynamic()) { + return ErrUnhandled + } + + r.Messages <- msg + return nil +} + +func (r *Fetch) WriteTo(w *imap.Writer) error { + var err error + for msg := range r.Messages { + resp := imap.NewUntaggedResp([]interface{}{msg.SeqNum, imap.RawString(fetchName), msg.Format()}) + if err == nil { + err = resp.WriteTo(w) + } + } + return err +} diff --git a/vendor/github.com/emersion/go-imap/responses/idle.go b/vendor/github.com/emersion/go-imap/responses/idle.go new file mode 100644 index 0000000..b5efcac --- /dev/null +++ b/vendor/github.com/emersion/go-imap/responses/idle.go @@ -0,0 +1,38 @@ +package responses + +import ( + "github.com/emersion/go-imap" +) + +// An IDLE response. +type Idle struct { + RepliesCh chan []byte + Stop <-chan struct{} + + gotContinuationReq bool +} + +func (r *Idle) Replies() <-chan []byte { + return r.RepliesCh +} + +func (r *Idle) stop() { + r.RepliesCh <- []byte("DONE\r\n") +} + +func (r *Idle) Handle(resp imap.Resp) error { + // Wait for a continuation request + if _, ok := resp.(*imap.ContinuationReq); ok && !r.gotContinuationReq { + r.gotContinuationReq = true + + // We got a continuation request, wait for r.Stop to be closed + go func() { + <-r.Stop + r.stop() + }() + + return nil + } + + return ErrUnhandled +} diff --git a/vendor/github.com/emersion/go-imap/responses/list.go b/vendor/github.com/emersion/go-imap/responses/list.go new file mode 100644 index 0000000..e080fc1 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/responses/list.go @@ -0,0 +1,57 @@ +package responses + +import ( + "github.com/emersion/go-imap" +) + +const ( + listName = "LIST" + lsubName = "LSUB" +) + +// A LIST response. +// If Subscribed is set to true, LSUB will be used instead. +// See RFC 3501 section 7.2.2 +type List struct { + Mailboxes chan *imap.MailboxInfo + Subscribed bool +} + +func (r *List) Name() string { + if r.Subscribed { + return lsubName + } else { + return listName + } +} + +func (r *List) Handle(resp imap.Resp) error { + name, fields, ok := imap.ParseNamedResp(resp) + if !ok || name != r.Name() { + return ErrUnhandled + } + + mbox := &imap.MailboxInfo{} + if err := mbox.Parse(fields); err != nil { + return err + } + + r.Mailboxes <- mbox + return nil +} + +func (r *List) WriteTo(w *imap.Writer) error { + respName := r.Name() + + for mbox := range r.Mailboxes { + fields := []interface{}{imap.RawString(respName)} + fields = append(fields, mbox.Format()...) + + resp := imap.NewUntaggedResp(fields) + if err := resp.WriteTo(w); err != nil { + return err + } + } + + return nil +} diff --git a/vendor/github.com/emersion/go-imap/responses/responses.go b/vendor/github.com/emersion/go-imap/responses/responses.go new file mode 100644 index 0000000..4d035ee --- /dev/null +++ b/vendor/github.com/emersion/go-imap/responses/responses.go @@ -0,0 +1,35 @@ +// IMAP responses defined in RFC 3501. +package responses + +import ( + "errors" + + "github.com/emersion/go-imap" +) + +// ErrUnhandled is used when a response hasn't been handled. +var ErrUnhandled = errors.New("imap: unhandled response") + +var errNotEnoughFields = errors.New("imap: not enough fields in response") + +// Handler handles responses. +type Handler interface { + // Handle processes a response. If the response cannot be processed, + // ErrUnhandledResp must be returned. + Handle(resp imap.Resp) error +} + +// HandlerFunc is a function that handles responses. +type HandlerFunc func(resp imap.Resp) error + +// Handle implements Handler. +func (f HandlerFunc) Handle(resp imap.Resp) error { + return f(resp) +} + +// Replier is a Handler that needs to send raw data (for instance +// AUTHENTICATE). +type Replier interface { + Handler + Replies() <-chan []byte +} diff --git a/vendor/github.com/emersion/go-imap/responses/search.go b/vendor/github.com/emersion/go-imap/responses/search.go new file mode 100644 index 0000000..028dbc7 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/responses/search.go @@ -0,0 +1,41 @@ +package responses + +import ( + "github.com/emersion/go-imap" +) + +const searchName = "SEARCH" + +// A SEARCH response. +// See RFC 3501 section 7.2.5 +type Search struct { + Ids []uint32 +} + +func (r *Search) Handle(resp imap.Resp) error { + name, fields, ok := imap.ParseNamedResp(resp) + if !ok || name != searchName { + return ErrUnhandled + } + + r.Ids = make([]uint32, len(fields)) + for i, f := range fields { + if id, err := imap.ParseNumber(f); err != nil { + return err + } else { + r.Ids[i] = id + } + } + + return nil +} + +func (r *Search) WriteTo(w *imap.Writer) (err error) { + fields := []interface{}{imap.RawString(searchName)} + for _, id := range r.Ids { + fields = append(fields, id) + } + + resp := imap.NewUntaggedResp(fields) + return resp.WriteTo(w) +} diff --git a/vendor/github.com/emersion/go-imap/responses/select.go b/vendor/github.com/emersion/go-imap/responses/select.go new file mode 100644 index 0000000..e450963 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/responses/select.go @@ -0,0 +1,142 @@ +package responses + +import ( + "fmt" + + "github.com/emersion/go-imap" +) + +// A SELECT response. +type Select struct { + Mailbox *imap.MailboxStatus +} + +func (r *Select) Handle(resp imap.Resp) error { + if r.Mailbox == nil { + r.Mailbox = &imap.MailboxStatus{Items: make(map[imap.StatusItem]interface{})} + } + mbox := r.Mailbox + + switch resp := resp.(type) { + case *imap.DataResp: + name, fields, ok := imap.ParseNamedResp(resp) + if !ok || name != "FLAGS" { + return ErrUnhandled + } else if len(fields) < 1 { + return errNotEnoughFields + } + + flags, _ := fields[0].([]interface{}) + mbox.Flags, _ = imap.ParseStringList(flags) + case *imap.StatusResp: + if len(resp.Arguments) < 1 { + return ErrUnhandled + } + + var item imap.StatusItem + switch resp.Code { + case "UNSEEN": + mbox.UnseenSeqNum, _ = imap.ParseNumber(resp.Arguments[0]) + case "PERMANENTFLAGS": + flags, _ := resp.Arguments[0].([]interface{}) + mbox.PermanentFlags, _ = imap.ParseStringList(flags) + case "UIDNEXT": + mbox.UidNext, _ = imap.ParseNumber(resp.Arguments[0]) + item = imap.StatusUidNext + case "UIDVALIDITY": + mbox.UidValidity, _ = imap.ParseNumber(resp.Arguments[0]) + item = imap.StatusUidValidity + default: + return ErrUnhandled + } + + if item != "" { + mbox.ItemsLocker.Lock() + mbox.Items[item] = nil + mbox.ItemsLocker.Unlock() + } + default: + return ErrUnhandled + } + return nil +} + +func (r *Select) WriteTo(w *imap.Writer) error { + mbox := r.Mailbox + + if mbox.Flags != nil { + flags := make([]interface{}, len(mbox.Flags)) + for i, f := range mbox.Flags { + flags[i] = imap.RawString(f) + } + res := imap.NewUntaggedResp([]interface{}{imap.RawString("FLAGS"), flags}) + if err := res.WriteTo(w); err != nil { + return err + } + } + + if mbox.PermanentFlags != nil { + flags := make([]interface{}, len(mbox.PermanentFlags)) + for i, f := range mbox.PermanentFlags { + flags[i] = imap.RawString(f) + } + statusRes := &imap.StatusResp{ + Type: imap.StatusRespOk, + Code: imap.CodePermanentFlags, + Arguments: []interface{}{flags}, + Info: "Flags permitted.", + } + if err := statusRes.WriteTo(w); err != nil { + return err + } + } + + if mbox.UnseenSeqNum > 0 { + statusRes := &imap.StatusResp{ + Type: imap.StatusRespOk, + Code: imap.CodeUnseen, + Arguments: []interface{}{mbox.UnseenSeqNum}, + Info: fmt.Sprintf("Message %d is first unseen", mbox.UnseenSeqNum), + } + if err := statusRes.WriteTo(w); err != nil { + return err + } + } + + for k := range r.Mailbox.Items { + switch k { + case imap.StatusMessages: + res := imap.NewUntaggedResp([]interface{}{mbox.Messages, imap.RawString("EXISTS")}) + if err := res.WriteTo(w); err != nil { + return err + } + case imap.StatusRecent: + res := imap.NewUntaggedResp([]interface{}{mbox.Recent, imap.RawString("RECENT")}) + if err := res.WriteTo(w); err != nil { + return err + } + case imap.StatusUidNext: + statusRes := &imap.StatusResp{ + Type: imap.StatusRespOk, + Code: imap.CodeUidNext, + Arguments: []interface{}{mbox.UidNext}, + Info: "Predicted next UID", + } + if err := statusRes.WriteTo(w); err != nil { + return err + } + case imap.StatusUidValidity: + statusRes := &imap.StatusResp{ + Type: imap.StatusRespOk, + Code: imap.CodeUidValidity, + Arguments: []interface{}{mbox.UidValidity}, + Info: "UIDs valid", + } + if err := statusRes.WriteTo(w); err != nil { + return err + } + } + } + + return nil +} diff --git a/vendor/github.com/emersion/go-imap/responses/status.go b/vendor/github.com/emersion/go-imap/responses/status.go new file mode 100644 index 0000000..6a8570c --- /dev/null +++ b/vendor/github.com/emersion/go-imap/responses/status.go @@ -0,0 +1,53 @@ +package responses + +import ( + "errors" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/utf7" +) + +const statusName = "STATUS" + +// A STATUS response. +// See RFC 3501 section 7.2.4 +type Status struct { + Mailbox *imap.MailboxStatus +} + +func (r *Status) Handle(resp imap.Resp) error { + if r.Mailbox == nil { + r.Mailbox = &imap.MailboxStatus{} + } + mbox := r.Mailbox + + name, fields, ok := imap.ParseNamedResp(resp) + if !ok || name != statusName { + return ErrUnhandled + } else if len(fields) < 2 { + return errNotEnoughFields + } + + if name, err := imap.ParseString(fields[0]); err != nil { + return err + } else if name, err := utf7.Encoding.NewDecoder().String(name); err != nil { + return err + } else { + mbox.Name = imap.CanonicalMailboxName(name) + } + + var items []interface{} + if items, ok = fields[1].([]interface{}); !ok { + return errors.New("STATUS response expects a list as second argument") + } + + mbox.Items = nil + return mbox.Parse(items) +} + +func (r *Status) WriteTo(w *imap.Writer) error { + mbox := r.Mailbox + name, _ := utf7.Encoding.NewEncoder().String(mbox.Name) + fields := []interface{}{imap.RawString(statusName), imap.FormatMailboxName(name), mbox.Format()} + return imap.NewUntaggedResp(fields).WriteTo(w) +} diff --git a/vendor/github.com/emersion/go-imap/search.go b/vendor/github.com/emersion/go-imap/search.go new file mode 100644 index 0000000..0ecb24d --- /dev/null +++ b/vendor/github.com/emersion/go-imap/search.go @@ -0,0 +1,371 @@ +package imap + +import ( + "errors" + "fmt" + "io" + "net/textproto" + "strings" + "time" +) + +func maybeString(mystery interface{}) string { + if s, ok := mystery.(string); ok { + return s + } + return "" +} + +func convertField(f interface{}, charsetReader func(io.Reader) io.Reader) string { + // An IMAP string contains only 7-bit data, no need to decode it + if s, ok := f.(string); ok { + return s + } + + // If no charset is provided, getting directly the string is faster + if charsetReader == nil { + if stringer, ok := f.(fmt.Stringer); ok { + return stringer.String() + } + } + + // Not a string, it must be a literal + l, ok := f.(Literal) + if !ok { + return "" + } + + var r io.Reader = l + if charsetReader != nil { + if dec := charsetReader(r); dec != nil { + r = dec + } + } + + b := make([]byte, l.Len()) + if _, err := io.ReadFull(r, b); err != nil { + return "" + } + return string(b) +} + +func popSearchField(fields []interface{}) (interface{}, []interface{}, error) { + if len(fields) == 0 { + return nil, nil, errors.New("imap: no enough fields for search key") + } + return fields[0], fields[1:], nil +} + +// SearchCriteria is a search criteria. A message matches the criteria if and +// only if it matches each one of its fields. +type SearchCriteria struct { + SeqNum *SeqSet // Sequence number is in sequence set + Uid *SeqSet // UID is in sequence set + + // Time and timezone are ignored + Since time.Time // Internal date is since this date + Before time.Time // Internal date is before this date + SentSince time.Time // Date header field is since this date + SentBefore time.Time // Date header field is before this date + + Header textproto.MIMEHeader // Each header field value is present + Body []string // Each string is in the body + Text []string // Each string is in the text (header + body) + + WithFlags []string // Each flag is present + WithoutFlags []string // Each flag is not present + + Larger uint32 // Size is larger than this number + Smaller uint32 // Size is smaller than this number + + Not []*SearchCriteria // Each criteria doesn't match + Or [][2]*SearchCriteria // Each criteria pair has at least one match of two +} + +// NewSearchCriteria creates a new search criteria. +func NewSearchCriteria() *SearchCriteria { + return &SearchCriteria{Header: make(textproto.MIMEHeader)} +} + +func (c *SearchCriteria) parseField(fields []interface{}, charsetReader func(io.Reader) io.Reader) ([]interface{}, error) { + if len(fields) == 0 { + return nil, nil + } + + f := fields[0] + fields = fields[1:] + + if subfields, ok := f.([]interface{}); ok { + return fields, c.ParseWithCharset(subfields, charsetReader) + } + + key, ok := f.(string) + if !ok { + return nil, fmt.Errorf("imap: invalid search criteria field type: %T", f) + } + key = strings.ToUpper(key) + + var err error + switch key { + case "ALL": + // Nothing to do + case "ANSWERED", "DELETED", "DRAFT", "FLAGGED", "RECENT", "SEEN": + c.WithFlags = append(c.WithFlags, CanonicalFlag("\\"+key)) + case "BCC", "CC", "FROM", "SUBJECT", "TO": + if f, fields, err = popSearchField(fields); err != nil { + return nil, err + } + if c.Header == nil { + c.Header = make(textproto.MIMEHeader) + } + c.Header.Add(key, convertField(f, charsetReader)) + case "BEFORE": + if f, fields, err = popSearchField(fields); err != nil { + return nil, err + } else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil { + return nil, err + } else if c.Before.IsZero() || t.Before(c.Before) { + c.Before = t + } + case "BODY": + if f, fields, err = popSearchField(fields); err != nil { + return nil, err + } else { + c.Body = append(c.Body, convertField(f, charsetReader)) + } + case "HEADER": + var f1, f2 interface{} + if f1, fields, err = popSearchField(fields); err != nil { + return nil, err + } else if f2, fields, err = popSearchField(fields); err != nil { + return nil, err + } else { + if c.Header == nil { + c.Header = make(textproto.MIMEHeader) + } + c.Header.Add(maybeString(f1), convertField(f2, charsetReader)) + } + case "KEYWORD": + if f, fields, err = popSearchField(fields); err != nil { + return nil, err + } else { + c.WithFlags = append(c.WithFlags, CanonicalFlag(maybeString(f))) + } + case "LARGER": + if f, fields, err = popSearchField(fields); err != nil { + return nil, err + } else if n, err := ParseNumber(f); err != nil { + return nil, err + } else if c.Larger == 0 || n > c.Larger { + c.Larger = n + } + case "NEW": + c.WithFlags = append(c.WithFlags, RecentFlag) + c.WithoutFlags = append(c.WithoutFlags, SeenFlag) + case "NOT": + not := new(SearchCriteria) + if fields, err = not.parseField(fields, charsetReader); err != nil { + return nil, err + } + c.Not = append(c.Not, not) + case "OLD": + c.WithoutFlags = append(c.WithoutFlags, RecentFlag) + case "ON": + if f, fields, err = popSearchField(fields); err != nil { + return nil, err + } else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil { + return nil, err + } else { + c.Since = t + c.Before = t.Add(24 * time.Hour) + } + case "OR": + c1, c2 := new(SearchCriteria), new(SearchCriteria) + if fields, err = c1.parseField(fields, charsetReader); err != nil { + return nil, err + } else if fields, err = c2.parseField(fields, charsetReader); err != nil { + return nil, err + } + c.Or = append(c.Or, [2]*SearchCriteria{c1, c2}) + case "SENTBEFORE": + if f, fields, err = popSearchField(fields); err != nil { + return nil, err + } else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil { + return nil, err + } else if c.SentBefore.IsZero() || t.Before(c.SentBefore) { + c.SentBefore = t + } + case "SENTON": + if f, fields, err = popSearchField(fields); err != nil { + return nil, err + } else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil { + return nil, err + } else { + c.SentSince = t + c.SentBefore = t.Add(24 * time.Hour) + } + case "SENTSINCE": + if f, fields, err = popSearchField(fields); err != nil { + return nil, err + } else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil { + return nil, err + } else if c.SentSince.IsZero() || t.After(c.SentSince) { + c.SentSince = t + } + case "SINCE": + if f, fields, err = popSearchField(fields); err != nil { + return nil, err + } else if t, err := time.Parse(DateLayout, maybeString(f)); err != nil { + return nil, err + } else if c.Since.IsZero() || t.After(c.Since) { + c.Since = t + } + case "SMALLER": + if f, fields, err = popSearchField(fields); err != nil { + return nil, err + } else if n, err := ParseNumber(f); err != nil { + return nil, err + } else if c.Smaller == 0 || n < c.Smaller { + c.Smaller = n + } + case "TEXT": + if f, fields, err = popSearchField(fields); err != nil { + return nil, err + } else { + c.Text = append(c.Text, convertField(f, charsetReader)) + } + case "UID": + if f, fields, err = popSearchField(fields); err != nil { + return nil, err + } else if c.Uid, err = ParseSeqSet(maybeString(f)); err != nil { + return nil, err + } + case "UNANSWERED", "UNDELETED", "UNDRAFT", "UNFLAGGED", "UNSEEN": + unflag := strings.TrimPrefix(key, "UN") + c.WithoutFlags = append(c.WithoutFlags, CanonicalFlag("\\"+unflag)) + case "UNKEYWORD": + if f, fields, err = popSearchField(fields); err != nil { + return nil, err + } else { + c.WithoutFlags = append(c.WithoutFlags, CanonicalFlag(maybeString(f))) + } + default: // Try to parse a sequence set + if c.SeqNum, err = ParseSeqSet(key); err != nil { + return nil, err + } + } + + return fields, nil +} + +// ParseWithCharset parses a search criteria from the provided fields. +// charsetReader is an optional function that converts from the fields charset +// to UTF-8. +func (c *SearchCriteria) ParseWithCharset(fields []interface{}, charsetReader func(io.Reader) io.Reader) error { + for len(fields) > 0 { + var err error + if fields, err = c.parseField(fields, charsetReader); err != nil { + return err + } + } + return nil +} + +// Format formats search criteria to fields. UTF-8 is used. +func (c *SearchCriteria) Format() []interface{} { + var fields []interface{} + + if c.SeqNum != nil { + fields = append(fields, c.SeqNum) + } + if c.Uid != nil { + fields = append(fields, RawString("UID"), c.Uid) + } + + if !c.Since.IsZero() && !c.Before.IsZero() && c.Before.Sub(c.Since) == 24*time.Hour { + fields = append(fields, RawString("ON"), searchDate(c.Since)) + } else { + if !c.Since.IsZero() { + fields = append(fields, RawString("SINCE"), searchDate(c.Since)) + } + if !c.Before.IsZero() { + fields = append(fields, RawString("BEFORE"), searchDate(c.Before)) + } + } + if !c.SentSince.IsZero() && !c.SentBefore.IsZero() && c.SentBefore.Sub(c.SentSince) == 24*time.Hour { + fields = append(fields, RawString("SENTON"), searchDate(c.SentSince)) + } else { + if !c.SentSince.IsZero() { + fields = append(fields, RawString("SENTSINCE"), searchDate(c.SentSince)) + } + if !c.SentBefore.IsZero() { + fields = append(fields, RawString("SENTBEFORE"), searchDate(c.SentBefore)) + } + } + + for key, values := range c.Header { + var prefields []interface{} + switch key { + case "Bcc", "Cc", "From", "Subject", "To": + prefields = []interface{}{RawString(strings.ToUpper(key))} + default: + prefields = []interface{}{RawString("HEADER"), key} + } + for _, value := range values { + fields = append(fields, prefields...) + fields = append(fields, value) + } + } + + for _, value := range c.Body { + fields = append(fields, RawString("BODY"), value) + } + for _, value := range c.Text { + fields = append(fields, RawString("TEXT"), value) + } + + for _, flag := range c.WithFlags { + var subfields []interface{} + switch flag { + case AnsweredFlag, DeletedFlag, DraftFlag, FlaggedFlag, RecentFlag, SeenFlag: + subfields = []interface{}{RawString(strings.ToUpper(strings.TrimPrefix(flag, "\\")))} + default: + subfields = []interface{}{RawString("KEYWORD"), RawString(flag)} + } + fields = append(fields, subfields...) + } + for _, flag := range c.WithoutFlags { + var subfields []interface{} + switch flag { + case AnsweredFlag, DeletedFlag, DraftFlag, FlaggedFlag, SeenFlag: + subfields = []interface{}{RawString("UN" + strings.ToUpper(strings.TrimPrefix(flag, "\\")))} + case RecentFlag: + subfields = []interface{}{RawString("OLD")} + default: + subfields = []interface{}{RawString("UNKEYWORD"), RawString(flag)} + } + fields = append(fields, subfields...) + } + + if c.Larger > 0 { + fields = append(fields, RawString("LARGER"), c.Larger) + } + if c.Smaller > 0 { + fields = append(fields, RawString("SMALLER"), c.Smaller) + } + + for _, not := range c.Not { + fields = append(fields, RawString("NOT"), not.Format()) + } + + for _, or := range c.Or { + fields = append(fields, RawString("OR"), or[0].Format(), or[1].Format()) + } + + // Not a single criteria given, add ALL criteria as fallback + if len(fields) == 0 { + fields = append(fields, RawString("ALL")) + } + + return fields +} diff --git a/vendor/github.com/emersion/go-imap/seqset.go b/vendor/github.com/emersion/go-imap/seqset.go new file mode 100644 index 0000000..abe6afc --- /dev/null +++ b/vendor/github.com/emersion/go-imap/seqset.go @@ -0,0 +1,289 @@ +package imap + +import ( + "fmt" + "strconv" + "strings" +) + +// ErrBadSeqSet is used to report problems with the format of a sequence set +// value. +type ErrBadSeqSet string + +func (err ErrBadSeqSet) Error() string { + return fmt.Sprintf("imap: bad sequence set value %q", string(err)) +} + +// Seq represents a single seq-number or seq-range value (RFC 3501 ABNF). Values +// may be static (e.g. "1", "2:4") or dynamic (e.g. "*", "1:*"). A seq-number is +// represented by setting Start = Stop. Zero is used to represent "*", which is +// safe because seq-number uses nz-number rule. The order of values is always +// Start <= Stop, except when representing "n:*", where Start = n and Stop = 0. +type Seq struct { + Start, Stop uint32 +} + +// parseSeqNumber parses a single seq-number value (non-zero uint32 or "*"). +func parseSeqNumber(v string) (uint32, error) { + if n, err := strconv.ParseUint(v, 10, 32); err == nil && v[0] != '0' { + return uint32(n), nil + } else if v == "*" { + return 0, nil + } + return 0, ErrBadSeqSet(v) +} + +// parseSeq creates a new seq instance by parsing strings in the format "n" or +// "n:m", where n and/or m may be "*". An error is returned for invalid values. +func parseSeq(v string) (s Seq, err error) { + if sep := strings.IndexRune(v, ':'); sep < 0 { + s.Start, err = parseSeqNumber(v) + s.Stop = s.Start + return + } else if s.Start, err = parseSeqNumber(v[:sep]); err == nil { + if s.Stop, err = parseSeqNumber(v[sep+1:]); err == nil { + if (s.Stop < s.Start && s.Stop != 0) || s.Start == 0 { + s.Start, s.Stop = s.Stop, s.Start + } + return + } + } + return s, ErrBadSeqSet(v) +} + +// Contains returns true if the seq-number q is contained in sequence value s. +// The dynamic value "*" contains only other "*" values, the dynamic range "n:*" +// contains "*" and all numbers >= n. +func (s Seq) Contains(q uint32) bool { + if q == 0 { + return s.Stop == 0 // "*" is contained only in "*" and "n:*" + } + return s.Start != 0 && s.Start <= q && (q <= s.Stop || s.Stop == 0) +} + +// Less returns true if s precedes and does not contain seq-number q. +func (s Seq) Less(q uint32) bool { + return (s.Stop < q || q == 0) && s.Stop != 0 +} + +// Merge combines sequence values s and t into a single union if the two +// intersect or one is a superset of the other. The order of s and t does not +// matter. If the values cannot be merged, s is returned unmodified and ok is +// set to false. +func (s Seq) Merge(t Seq) (union Seq, ok bool) { + if union = s; s == t { + ok = true + return + } + if s.Start != 0 && t.Start != 0 { + // s and t are any combination of "n", "n:m", or "n:*" + if s.Start > t.Start { + s, t = t, s + } + // s starts at or before t, check where it ends + if (s.Stop >= t.Stop && t.Stop != 0) || s.Stop == 0 { + return s, true // s is a superset of t + } + // s is "n" or "n:m", if m == ^uint32(0) then t is "n:*" + if s.Stop+1 >= t.Start || s.Stop == ^uint32(0) { + return Seq{s.Start, t.Stop}, true // s intersects or touches t + } + return + } + // exactly one of s and t is "*" + if s.Start == 0 { + if t.Stop == 0 { + return t, true // s is "*", t is "n:*" + } + } else if s.Stop == 0 { + return s, true // s is "n:*", t is "*" + } + return +} + +// String returns sequence value s as a seq-number or seq-range string. +func (s Seq) String() string { + if s.Start == s.Stop { + if s.Start == 0 { + return "*" + } + return strconv.FormatUint(uint64(s.Start), 10) + } + b := strconv.AppendUint(make([]byte, 0, 24), uint64(s.Start), 10) + if s.Stop == 0 { + return string(append(b, ':', '*')) + } + return string(strconv.AppendUint(append(b, ':'), uint64(s.Stop), 10)) +} + +// SeqSet is used to represent a set of message sequence numbers or UIDs (see +// sequence-set ABNF rule). The zero value is an empty set. +type SeqSet struct { + Set []Seq +} + +// ParseSeqSet returns a new SeqSet instance after parsing the set string. +func ParseSeqSet(set string) (s *SeqSet, err error) { + s = new(SeqSet) + return s, s.Add(set) +} + +// Add inserts new sequence values into the set. The string format is described +// by RFC 3501 sequence-set ABNF rule. If an error is encountered, all values +// inserted successfully prior to the error remain in the set. +func (s *SeqSet) Add(set string) error { + for _, sv := range strings.Split(set, ",") { + v, err := parseSeq(sv) + if err != nil { + return err + } + s.insert(v) + } + return nil +} + +// AddNum inserts new sequence numbers into the set. The value 0 represents "*". +func (s *SeqSet) AddNum(q ...uint32) { + for _, v := range q { + s.insert(Seq{v, v}) + } +} + +// AddRange inserts a new sequence range into the set. +func (s *SeqSet) AddRange(Start, Stop uint32) { + if (Stop < Start && Stop != 0) || Start == 0 { + s.insert(Seq{Stop, Start}) + } else { + s.insert(Seq{Start, Stop}) + } +} + +// AddSet inserts all values from t into s. +func (s *SeqSet) AddSet(t *SeqSet) { + for _, v := range t.Set { + s.insert(v) + } +} + +// Clear removes all values from the set. +func (s *SeqSet) Clear() { + s.Set = s.Set[:0] +} + +// Empty returns true if the sequence set does not contain any values. +func (s SeqSet) Empty() bool { + return len(s.Set) == 0 +} + +// Dynamic returns true if the set contains "*" or "n:*" values. +func (s SeqSet) Dynamic() bool { + return len(s.Set) > 0 && s.Set[len(s.Set)-1].Stop == 0 +} + +// Contains returns true if the non-zero sequence number or UID q is contained +// in the set. The dynamic range "n:*" contains all q >= n. It is the caller's +// responsibility to handle the special case where q is the maximum UID in the +// mailbox and q < n (i.e. the set cannot match UIDs against "*:n" or "*" since +// it doesn't know what the maximum value is). +func (s SeqSet) Contains(q uint32) bool { + if _, ok := s.search(q); ok { + return q != 0 + } + return false +} + +// String returns a sorted representation of all contained sequence values. +func (s SeqSet) String() string { + if len(s.Set) == 0 { + return "" + } + b := make([]byte, 0, 64) + for _, v := range s.Set { + b = append(b, ',') + if v.Start == 0 { + b = append(b, '*') + continue + } + b = strconv.AppendUint(b, uint64(v.Start), 10) + if v.Start != v.Stop { + if v.Stop == 0 { + b = append(b, ':', '*') + continue + } + b = strconv.AppendUint(append(b, ':'), uint64(v.Stop), 10) + } + } + return string(b[1:]) +} + +// insert adds sequence value v to the set. +func (s *SeqSet) insert(v Seq) { + i, _ := s.search(v.Start) + merged := false + if i > 0 { + // try merging with the preceding entry (e.g. "1,4".insert(2), i == 1) + s.Set[i-1], merged = s.Set[i-1].Merge(v) + } + if i == len(s.Set) { + // v was either merged with the last entry or needs to be appended + if !merged { + s.insertAt(i, v) + } + return + } else if merged { + i-- + } else if s.Set[i], merged = s.Set[i].Merge(v); !merged { + s.insertAt(i, v) // insert in the middle (e.g. "1,5".insert(3), i == 1) + return + } + // v was merged with s.Set[i], continue trying to merge until the end + for j := i + 1; j < len(s.Set); j++ { + if s.Set[i], merged = s.Set[i].Merge(s.Set[j]); !merged { + if j > i+1 { + // cut out all entries between i and j that were merged + s.Set = append(s.Set[:i+1], s.Set[j:]...) + } + return + } + } + // everything after s.Set[i] was merged + s.Set = s.Set[:i+1] +} + +// insertAt inserts a new sequence value v at index i, resizing s.Set as needed. +func (s *SeqSet) insertAt(i int, v Seq) { + if n := len(s.Set); i == n { + // insert at the end + s.Set = append(s.Set, v) + return + } else if n < cap(s.Set) { + // enough space, shift everything at and after i to the right + s.Set = s.Set[:n+1] + copy(s.Set[i+1:], s.Set[i:]) + } else { + // allocate new slice and copy everything, n is at least 1 + set := make([]Seq, n+1, n*2) + copy(set, s.Set[:i]) + copy(set[i+1:], s.Set[i:]) + s.Set = set + } + s.Set[i] = v +} + +// search attempts to find the index of the sequence set value that contains q. +// If no values contain q, the returned index is the position where q should be +// inserted and ok is set to false. +func (s SeqSet) search(q uint32) (i int, ok bool) { + min, max := 0, len(s.Set)-1 + for min < max { + if mid := (min + max) >> 1; s.Set[mid].Less(q) { + min = mid + 1 + } else { + max = mid + } + } + if max < 0 || s.Set[min].Less(q) { + return len(s.Set), false // q is the new largest value + } + return min, s.Set[min].Contains(q) +} diff --git a/vendor/github.com/emersion/go-imap/status.go b/vendor/github.com/emersion/go-imap/status.go new file mode 100644 index 0000000..81ffd1b --- /dev/null +++ b/vendor/github.com/emersion/go-imap/status.go @@ -0,0 +1,136 @@ +package imap + +import ( + "errors" +) + +// A status response type. +type StatusRespType string + +// Status response types defined in RFC 3501 section 7.1. +const ( + // The OK response indicates an information message from the server. When + // tagged, it indicates successful completion of the associated command. + // The untagged form indicates an information-only message. + StatusRespOk StatusRespType = "OK" + + // The NO response indicates an operational error message from the + // server. When tagged, it indicates unsuccessful completion of the + // associated command. The untagged form indicates a warning; the + // command can still complete successfully. + StatusRespNo StatusRespType = "NO" + + // The BAD response indicates an error message from the server. When + // tagged, it reports a protocol-level error in the client's command; + // the tag indicates the command that caused the error. The untagged + // form indicates a protocol-level error for which the associated + // command can not be determined; it can also indicate an internal + // server failure. + StatusRespBad StatusRespType = "BAD" + + // The PREAUTH response is always untagged, and is one of three + // possible greetings at connection startup. It indicates that the + // connection has already been authenticated by external means; thus + // no LOGIN command is needed. + StatusRespPreauth StatusRespType = "PREAUTH" + + // The BYE response is always untagged, and indicates that the server + // is about to close the connection. + StatusRespBye StatusRespType = "BYE" +) + +type StatusRespCode string + +// Status response codes defined in RFC 3501 section 7.1. +const ( + CodeAlert StatusRespCode = "ALERT" + CodeBadCharset StatusRespCode = "BADCHARSET" + CodeCapability StatusRespCode = "CAPABILITY" + CodeParse StatusRespCode = "PARSE" + CodePermanentFlags StatusRespCode = "PERMANENTFLAGS" + CodeReadOnly StatusRespCode = "READ-ONLY" + CodeReadWrite StatusRespCode = "READ-WRITE" + CodeTryCreate StatusRespCode = "TRYCREATE" + CodeUidNext StatusRespCode = "UIDNEXT" + CodeUidValidity StatusRespCode = "UIDVALIDITY" + CodeUnseen StatusRespCode = "UNSEEN" +) + +// A status response. +// See RFC 3501 section 7.1 +type StatusResp struct { + // The response tag. If empty, it defaults to *. + Tag string + // The status type. + Type StatusRespType + // The status code. + // See https://www.iana.org/assignments/imap-response-codes/imap-response-codes.xhtml + Code StatusRespCode + // Arguments provided with the status code. + Arguments []interface{} + // The status info. + Info string +} + +func (r *StatusResp) resp() {} + +// If this status is NO or BAD, returns an error with the status info. +// Otherwise, returns nil. +func (r *StatusResp) Err() error { + if r == nil { + // No status response, connection closed before we get one + return errors.New("imap: connection closed during command execution") + } + + if r.Type == StatusRespNo || r.Type == StatusRespBad { + return errors.New(r.Info) + } + return nil +} + +func (r *StatusResp) WriteTo(w *Writer) error { + tag := RawString(r.Tag) + if tag == "" { + tag = "*" + } + + if err := w.writeFields([]interface{}{RawString(tag), RawString(r.Type)}); err != nil { + return err + } + + if err := w.writeString(string(sp)); err != nil { + return err + } + + if r.Code != "" { + if err := w.writeRespCode(r.Code, r.Arguments); err != nil { + return err + } + + if err := w.writeString(string(sp)); err != nil { + return err + } + } + + if err := w.writeString(r.Info); err != nil { + return err + } + + return w.writeCrlf() +} + +// ErrStatusResp can be returned by a server.Handler to replace the default status +// response. The response tag must be empty. +// +// To suppress default response, Resp should be set to nil. +type ErrStatusResp struct { + // Response to send instead of default. + Resp *StatusResp +} + +func (err *ErrStatusResp) Error() string { + if err.Resp == nil { + return "imap: suppressed response" + } + return err.Resp.Info +} diff --git a/vendor/github.com/emersion/go-imap/utf7/decoder.go b/vendor/github.com/emersion/go-imap/utf7/decoder.go new file mode 100644 index 0000000..cfcba8c --- /dev/null +++ b/vendor/github.com/emersion/go-imap/utf7/decoder.go @@ -0,0 +1,149 @@ +package utf7 + +import ( + "errors" + "unicode/utf16" + "unicode/utf8" + + "golang.org/x/text/transform" +) + +// ErrInvalidUTF7 means that a transformer encountered invalid UTF-7. +var ErrInvalidUTF7 = errors.New("utf7: invalid UTF-7") + +type decoder struct { + ascii bool +} + +func (d *decoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) { + for i := 0; i < len(src); i++ { + ch := src[i] + + if ch < min || ch > max { // Illegal code point in ASCII mode + err = ErrInvalidUTF7 + return + } + + if ch != '&' { + if nDst+1 > len(dst) { + err = transform.ErrShortDst + return + } + + nSrc++ + + dst[nDst] = ch + nDst++ + + d.ascii = true + continue + } + + // Find the end of the Base64 or "&-" segment + start := i + 1 + for i++; i < len(src) && src[i] != '-'; i++ { + if src[i] == '\r' || src[i] == '\n' { // base64 package ignores CR and LF + err = ErrInvalidUTF7 + return + } + } + + if i == len(src) { // Implicit shift ("&...") + if atEOF { + err = ErrInvalidUTF7 + } else { + err = transform.ErrShortSrc + } + return + } + + var b []byte + if i == start { // Escape sequence "&-" + b = []byte{'&'} + d.ascii = true + } else { // Control or non-ASCII code points in base64 + if !d.ascii { // Null shift ("&...-&...-") + err = ErrInvalidUTF7 + return + } + + b = decode(src[start:i]) + d.ascii = false + } + + if len(b) == 0 { // Bad encoding + err = ErrInvalidUTF7 + return + } + + if nDst+len(b) > len(dst) { + d.ascii = true + err = transform.ErrShortDst + return + } + + nSrc = i + 1 + + for _, ch := range b { + dst[nDst] = ch + nDst++ + } + } + + if atEOF { + d.ascii = true + } + + return +} + +func (d *decoder) Reset() { + d.ascii = true +} + +// Extracts UTF-16-BE bytes from base64 data and converts them to UTF-8. +// A nil slice is returned if the encoding is invalid. +func decode(b64 []byte) []byte { + var b []byte + + // Allocate a single block of memory large enough to store the Base64 data + // (if padding is required), UTF-16-BE bytes, and decoded UTF-8 bytes. + // Since a 2-byte UTF-16 sequence may expand into a 3-byte UTF-8 sequence, + // double the space allocation for UTF-8. + if n := len(b64); b64[n-1] == '=' { + return nil + } else if n&3 == 0 { + b = make([]byte, b64Enc.DecodedLen(n)*3) + } else { + n += 4 - n&3 + b = make([]byte, n+b64Enc.DecodedLen(n)*3) + copy(b[copy(b, b64):n], []byte("==")) + b64, b = b[:n], b[n:] + } + + // Decode Base64 into the first 1/3rd of b + n, err := b64Enc.Decode(b, b64) + if err != nil || n&1 == 1 { + return nil + } + + // Decode UTF-16-BE into the remaining 2/3rds of b + b, s := b[:n], b[n:] + j := 0 + for i := 0; i < n; i += 2 { + r := rune(b[i])<<8 | rune(b[i+1]) + if utf16.IsSurrogate(r) { + if i += 2; i == n { + return nil + } + r2 := rune(b[i])<<8 | rune(b[i+1]) + if r = utf16.DecodeRune(r, r2); r == repl { + return nil + } + } else if min <= r && r <= max { + return nil + } + j += utf8.EncodeRune(s[j:], r) + } + return s[:j] +} diff --git a/vendor/github.com/emersion/go-imap/utf7/encoder.go b/vendor/github.com/emersion/go-imap/utf7/encoder.go new file mode 100644 index 0000000..8414d10 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/utf7/encoder.go @@ -0,0 +1,91 @@ +package utf7 + +import ( + "unicode/utf16" + "unicode/utf8" + + "golang.org/x/text/transform" +) + +type encoder struct{} + +func (e *encoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) { + for i := 0; i < len(src); { + ch := src[i] + + var b []byte + if min <= ch && ch <= max { + b = []byte{ch} + if ch == '&' { + b = append(b, '-') + } + + i++ + } else { + start := i + + // Find the next printable ASCII code point + i++ + for i < len(src) && (src[i] < min || src[i] > max) { + i++ + } + + if !atEOF && i == len(src) { + err = transform.ErrShortSrc + return + } + + b = encode(src[start:i]) + } + + if nDst+len(b) > len(dst) { + err = transform.ErrShortDst + return + } + + nSrc = i + + for _, ch := range b { + dst[nDst] = ch + nDst++ + } + } + + return +} + +func (e *encoder) Reset() {} + +// Converts string s from UTF-8 to UTF-16-BE, encodes the result as base64, +// removes the padding, and adds UTF-7 shifts. +func encode(s []byte) []byte { + // len(s) is sufficient for UTF-8 to UTF-16 conversion if there are no + // control code points (see table below). + b := make([]byte, 0, len(s)+4) + for len(s) > 0 { + r, size := utf8.DecodeRune(s) + if r > utf8.MaxRune { + r, size = utf8.RuneError, 1 // Bug fix (issue 3785) + } + s = s[size:] + if r1, r2 := utf16.EncodeRune(r); r1 != repl { + b = append(b, byte(r1>>8), byte(r1)) + r = r2 + } + b = append(b, byte(r>>8), byte(r)) + } + + // Encode as base64 + n := b64Enc.EncodedLen(len(b)) + 2 + b64 := make([]byte, n) + b64Enc.Encode(b64[1:], b) + + // Strip padding + n -= 2 - (len(b)+2)%3 + b64 = b64[:n] + + // Add UTF-7 shifts + b64[0] = '&' + b64[n-1] = '-' + return b64 +} diff --git a/vendor/github.com/emersion/go-imap/utf7/utf7.go b/vendor/github.com/emersion/go-imap/utf7/utf7.go new file mode 100644 index 0000000..b9dd962 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/utf7/utf7.go @@ -0,0 +1,34 @@ +// Package utf7 implements modified UTF-7 encoding defined in RFC 3501 section 5.1.3 +package utf7 + +import ( + "encoding/base64" + + "golang.org/x/text/encoding" +) + +const ( + min = 0x20 // Minimum self-representing UTF-7 value + max = 0x7E // Maximum self-representing UTF-7 value + + repl = '\uFFFD' // Unicode replacement code point +) + +var b64Enc = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,") + +type enc struct{} + +func (e enc) NewDecoder() *encoding.Decoder { + return &encoding.Decoder{ + Transformer: &decoder{true}, + } +} + +func (e enc) NewEncoder() *encoding.Encoder { + return &encoding.Encoder{ + Transformer: &encoder{}, + } +} + +// Encoding is the modified UTF-7 encoding. +var Encoding encoding.Encoding = enc{} diff --git a/vendor/github.com/emersion/go-imap/write.go b/vendor/github.com/emersion/go-imap/write.go new file mode 100644 index 0000000..c295e4e --- /dev/null +++ b/vendor/github.com/emersion/go-imap/write.go @@ -0,0 +1,255 @@ +package imap + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "strconv" + "time" + "unicode" +) + +type flusher interface { + Flush() error +} + +type ( + // A raw string. + RawString string +) + +type WriterTo interface { + WriteTo(w *Writer) error +} + +func formatNumber(num uint32) string { + return strconv.FormatUint(uint64(num), 10) +} + +// Convert a string list to a field list. +func FormatStringList(list []string) (fields []interface{}) { + fields = make([]interface{}, len(list)) + for i, v := range list { + fields[i] = v + } + return +} + +// Check if a string is 8-bit clean. +func isAscii(s string) bool { + for _, c := range s { + if c > unicode.MaxASCII || unicode.IsControl(c) { + return false + } + } + return true +} + +// An IMAP writer. +type Writer struct { + io.Writer + + AllowAsyncLiterals bool + + continues <-chan bool +} + +// Helper function to write a string to w. +func (w *Writer) writeString(s string) error { + _, err := io.WriteString(w.Writer, s) + return err +} + +func (w *Writer) writeCrlf() error { + if err := w.writeString(crlf); err != nil { + return err + } + + return w.Flush() +} + +func (w *Writer) writeNumber(num uint32) error { + return w.writeString(formatNumber(num)) +} + +func (w *Writer) writeQuoted(s string) error { + return w.writeString(strconv.Quote(s)) +} + +func (w *Writer) writeQuotedOrLiteral(s string) error { + if !isAscii(s) { + // IMAP doesn't allow 8-bit data outside literals + return w.writeLiteral(bytes.NewBufferString(s)) + } + + return w.writeQuoted(s) +} + +func (w *Writer) writeDateTime(t time.Time, layout string) error { + if t.IsZero() { + return w.writeString(nilAtom) + } + return w.writeQuoted(t.Format(layout)) +} + +func (w *Writer) writeFields(fields []interface{}) error { + for i, field := range fields { + if i > 0 { // Write separator + if err := w.writeString(string(sp)); err != nil { + return err + } + } + + if err := w.writeField(field); err != nil { + return err + } + } + + return nil +} + +func (w *Writer) writeList(fields []interface{}) error { + if err := w.writeString(string(listStart)); err != nil { + return err + } + + if err := w.writeFields(fields); err != nil { + return err + } + + return w.writeString(string(listEnd)) +} + +// LiteralLengthErr is returned when the Len() of the Literal object does not +// match the actual length of the byte stream. +type LiteralLengthErr struct { + Actual int + Expected int +} + +func (e LiteralLengthErr) Error() string { + return fmt.Sprintf("imap: size of Literal is not equal to Len() (%d != %d)", e.Expected, e.Actual) +} + +func (w *Writer) writeLiteral(l Literal) error { + if l == nil { + return w.writeString(nilAtom) + } + + unsyncLiteral := w.AllowAsyncLiterals && l.Len() <= 4096 + + header := string(literalStart) + strconv.Itoa(l.Len()) + if unsyncLiteral { + header += string('+') + } + header += string(literalEnd) + crlf + if err := w.writeString(header); err != nil { + return err + } + + // If a channel is available, wait for a continuation request before sending data + if !unsyncLiteral && w.continues != nil { + // Make sure to flush the writer, otherwise we may never receive a continuation request + if err := w.Flush(); err != nil { + return err + } + + if !<-w.continues { + return fmt.Errorf("imap: cannot send literal: no continuation request received") + } + } + + // In case of bufio.Buffer, it will be 0 after io.Copy. + literalLen := int64(l.Len()) + + n, err := io.CopyN(w, l, literalLen) + if err != nil { + if err == io.EOF && n != literalLen { + return LiteralLengthErr{int(n), l.Len()} + } + return err + } + extra, _ := io.Copy(ioutil.Discard, l) + if extra != 0 { + return LiteralLengthErr{int(n + extra), l.Len()} + } + + return nil +} + +func (w *Writer) writeField(field interface{}) error { + if field == nil { + return w.writeString(nilAtom) + } + + switch field := field.(type) { + case RawString: + return w.writeString(string(field)) + case string: + return w.writeQuotedOrLiteral(field) + case int: + return w.writeNumber(uint32(field)) + case uint32: + return w.writeNumber(field) + case Literal: + return w.writeLiteral(field) + case []interface{}: + return w.writeList(field) + case envelopeDateTime: + return w.writeDateTime(time.Time(field), envelopeDateTimeLayout) + case searchDate: + return w.writeDateTime(time.Time(field), searchDateLayout) + case Date: + return w.writeDateTime(time.Time(field), DateLayout) + case DateTime: + return w.writeDateTime(time.Time(field), DateTimeLayout) + case time.Time: + return w.writeDateTime(field, DateTimeLayout) + case *SeqSet: + return w.writeString(field.String()) + case *BodySectionName: + // Can contain spaces - that's why we don't just pass it as a string + return w.writeString(string(field.FetchItem())) + } + + return fmt.Errorf("imap: cannot format field: %v", field) +} + +func (w *Writer) writeRespCode(code StatusRespCode, args []interface{}) error { + if err := w.writeString(string(respCodeStart)); err != nil { + return err + } + + fields := []interface{}{RawString(code)} + fields = append(fields, args...) + + if err := w.writeFields(fields); err != nil { + return err + } + + return w.writeString(string(respCodeEnd)) +} + +func (w *Writer) writeLine(fields ...interface{}) error { + if err := w.writeFields(fields); err != nil { + return err + } + + return w.writeCrlf() +} + +func (w *Writer) Flush() error { + if f, ok := w.Writer.(flusher); ok { + return f.Flush() + } + return nil +} + +func NewWriter(w io.Writer) *Writer { + return &Writer{Writer: w} +} + +func NewClientWriter(w io.Writer, continues <-chan bool) *Writer { + return &Writer{Writer: w, continues: continues} +} diff --git a/vendor/github.com/emersion/go-sasl/.build.yml b/vendor/github.com/emersion/go-sasl/.build.yml new file mode 100644 index 0000000..daa6006 --- /dev/null +++ b/vendor/github.com/emersion/go-sasl/.build.yml @@ -0,0 +1,19 @@ +image: alpine/latest +packages: + - go + # Required by codecov + - bash + - findutils +sources: + - https://github.com/emersion/go-sasl +tasks: + - build: | + cd go-sasl + go build -v ./... + - test: | + cd go-sasl + go test -coverprofile=coverage.txt -covermode=atomic ./... + - upload-coverage: | + cd go-sasl + export CODECOV_TOKEN=3f257f71-a128-4834-8f68-2b534e9f4cb1 + curl -s https://codecov.io/bash | bash diff --git a/vendor/github.com/emersion/go-sasl/.gitignore b/vendor/github.com/emersion/go-sasl/.gitignore new file mode 100644 index 0000000..daf913b --- /dev/null +++ b/vendor/github.com/emersion/go-sasl/.gitignore @@ -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 diff --git a/vendor/github.com/emersion/go-sasl/LICENSE b/vendor/github.com/emersion/go-sasl/LICENSE new file mode 100644 index 0000000..dc1922e --- /dev/null +++ b/vendor/github.com/emersion/go-sasl/LICENSE @@ -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. diff --git a/vendor/github.com/emersion/go-sasl/README.md b/vendor/github.com/emersion/go-sasl/README.md new file mode 100644 index 0000000..1f8a682 --- /dev/null +++ b/vendor/github.com/emersion/go-sasl/README.md @@ -0,0 +1,17 @@ +# go-sasl + +[![GoDoc](https://godoc.org/github.com/emersion/go-sasl?status.svg)](https://godoc.org/github.com/emersion/go-sasl) +[![Build Status](https://travis-ci.org/emersion/go-sasl.svg?branch=master)](https://travis-ci.org/emersion/go-sasl) + +A [SASL](https://tools.ietf.org/html/rfc4422) library written in Go. + +Implemented mechanisms: +* [ANONYMOUS](https://tools.ietf.org/html/rfc4505) +* [EXTERNAL](https://tools.ietf.org/html/rfc4422#appendix-A) +* [LOGIN](https://tools.ietf.org/html/draft-murchison-sasl-login-00) (obsolete, use PLAIN instead) +* [PLAIN](https://tools.ietf.org/html/rfc4616) +* [OAUTHBEARER](https://tools.ietf.org/html/rfc7628) + +## License + +MIT diff --git a/vendor/github.com/emersion/go-sasl/anonymous.go b/vendor/github.com/emersion/go-sasl/anonymous.go new file mode 100644 index 0000000..8ccb817 --- /dev/null +++ b/vendor/github.com/emersion/go-sasl/anonymous.go @@ -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} +} diff --git a/vendor/github.com/emersion/go-sasl/external.go b/vendor/github.com/emersion/go-sasl/external.go new file mode 100644 index 0000000..da070c8 --- /dev/null +++ b/vendor/github.com/emersion/go-sasl/external.go @@ -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} +} diff --git a/vendor/github.com/emersion/go-sasl/login.go b/vendor/github.com/emersion/go-sasl/login.go new file mode 100644 index 0000000..3847ee1 --- /dev/null +++ b/vendor/github.com/emersion/go-sasl/login.go @@ -0,0 +1,89 @@ +package sasl + +import ( + "bytes" +) + +// The LOGIN mechanism name. +const Login = "LOGIN" + +var expectedChallenge = []byte("Password:") + +type loginClient struct { + Username string + Password string +} + +func (a *loginClient) Start() (mech string, ir []byte, err error) { + mech = "LOGIN" + ir = []byte(a.Username) + return +} + +func (a *loginClient) Next(challenge []byte) (response []byte, err error) { + if bytes.Compare(challenge, expectedChallenge) != 0 { + return nil, ErrUnexpectedServerChallenge + } else { + return []byte(a.Password), nil + } +} + +// A client implementation of the LOGIN authentication mechanism for SMTP, +// as described in http://www.iana.org/go/draft-murchison-sasl-login +// +// It is considered obsolete, and should not be used when other mechanisms are +// available. For plaintext password authentication use PLAIN mechanism. +func NewLoginClient(username, password string) Client { + return &loginClient{username, password} +} + +// Authenticates users with an username and a password. +type LoginAuthenticator func(username, password string) error + +type loginState int + +const ( + loginNotStarted loginState = iota + loginWaitingUsername + loginWaitingPassword +) + +type loginServer struct { + state loginState + username, password string + authenticate LoginAuthenticator +} + +// A server implementation of the LOGIN authentication mechanism, as described +// in https://tools.ietf.org/html/draft-murchison-sasl-login-00. +// +// LOGIN is obsolete and should only be enabled for legacy clients that cannot +// be updated to use PLAIN. +func NewLoginServer(authenticator LoginAuthenticator) Server { + return &loginServer{authenticate: authenticator} +} + +func (a *loginServer) Next(response []byte) (challenge []byte, done bool, err error) { + switch a.state { + case loginNotStarted: + // Check for initial response field, as per RFC4422 section 3 + if response == nil { + challenge = []byte("Username:") + break + } + a.state++ + fallthrough + case loginWaitingUsername: + a.username = string(response) + challenge = []byte("Password:") + case loginWaitingPassword: + a.password = string(response) + err = a.authenticate(a.username, a.password) + done = true + default: + err = ErrUnexpectedClientResponse + } + + a.state++ + return +} diff --git a/vendor/github.com/emersion/go-sasl/oauthbearer.go b/vendor/github.com/emersion/go-sasl/oauthbearer.go new file mode 100644 index 0000000..a0639b1 --- /dev/null +++ b/vendor/github.com/emersion/go-sasl/oauthbearer.go @@ -0,0 +1,191 @@ +package sasl + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" +) + +// The OAUTHBEARER mechanism name. +const OAuthBearer = "OAUTHBEARER" + +type OAuthBearerError struct { + Status string `json:"status"` + Schemes string `json:"schemes"` + Scope string `json:"scope"` +} + +type OAuthBearerOptions struct { + Username string + Token string + Host string + Port int +} + +// Implements error +func (err *OAuthBearerError) Error() string { + return fmt.Sprintf("OAUTHBEARER authentication error (%v)", err.Status) +} + +type oauthBearerClient struct { + OAuthBearerOptions +} + +func (a *oauthBearerClient) Start() (mech string, ir []byte, err error) { + mech = OAuthBearer + var str = "n,a=" + a.Username + "," + + if a.Host != "" { + str += "\x01host=" + a.Host + } + + if a.Port != 0 { + str += "\x01port=" + strconv.Itoa(a.Port) + } + str += "\x01auth=Bearer " + a.Token + "\x01\x01" + ir = []byte(str) + return +} + +func (a *oauthBearerClient) Next(challenge []byte) ([]byte, error) { + authBearerErr := &OAuthBearerError{} + if err := json.Unmarshal(challenge, authBearerErr); err != nil { + return nil, err + } else { + return nil, authBearerErr + } +} + +// An implementation of the OAUTHBEARER authentication mechanism, as +// described in RFC 7628. +func NewOAuthBearerClient(opt *OAuthBearerOptions) Client { + return &oauthBearerClient{*opt} +} + +type OAuthBearerAuthenticator func(opts OAuthBearerOptions) *OAuthBearerError + +type oauthBearerServer struct { + done bool + failErr error + authenticate OAuthBearerAuthenticator +} + +func (a *oauthBearerServer) fail(descr string) ([]byte, bool, error) { + blob, err := json.Marshal(OAuthBearerError{ + Status: "invalid_request", + Schemes: "bearer", + }) + if err != nil { + panic(err) // wtf + } + a.failErr = errors.New(descr) + return blob, false, nil +} + +func (a *oauthBearerServer) Next(response []byte) (challenge []byte, done bool, err error) { + // Per RFC, we cannot just send an error, we need to return JSON-structured + // value as a challenge and then after getting dummy response from the + // client stop the exchange. + if a.failErr != nil { + // Server libraries (go-smtp, go-imap) will not call Next on + // protocol-specific SASL cancel response ('*'). However, GS2 (and + // indirectly OAUTHBEARER) defines a protocol-independent way to do so + // using 0x01. + if len(response) != 1 && response[0] != 0x01 { + return nil, true, errors.New("unexpected response") + } + return nil, true, a.failErr + } + + if a.done { + err = ErrUnexpectedClientResponse + return + } + + // Generate empty challenge. + if response == nil { + return []byte{}, false, nil + } + + a.done = true + + // Cut n,a=username,\x01host=...\x01auth=... + // into + // n + // a=username + // \x01host=...\x01auth=...\x01\x01 + parts := bytes.SplitN(response, []byte{','}, 3) + if len(parts) != 3 { + return a.fail("Invalid response") + } + if !bytes.Equal(parts[0], []byte{'n'}) { + return a.fail("Invalid response, missing 'n'") + } + opts := OAuthBearerOptions{} + if !bytes.HasPrefix(parts[1], []byte("a=")) { + return a.fail("Invalid response, missing 'a'") + } + opts.Username = string(bytes.TrimPrefix(parts[1], []byte("a="))) + + // Cut \x01host=...\x01auth=...\x01\x01 + // into + // *empty* + // host=... + // auth=... + // *empty* + // + // Note that this code does not do a lot of checks to make sure the input + // follows the exact format specified by RFC. + params := bytes.Split(parts[2], []byte{0x01}) + for _, p := range params { + // Skip empty fields (one at start and end). + if len(p) == 0 { + continue + } + + pParts := bytes.SplitN(p, []byte{'='}, 2) + if len(pParts) != 2 { + return a.fail("Invalid response, missing '='") + } + + switch string(pParts[0]) { + case "host": + opts.Host = string(pParts[1]) + case "port": + port, err := strconv.ParseUint(string(pParts[1]), 10, 16) + if err != nil { + return a.fail("Invalid response, malformed 'port' value") + } + opts.Port = int(port) + case "auth": + const prefix = "bearer " + strValue := string(pParts[1]) + // Token type is case-insensitive. + if !strings.HasPrefix(strings.ToLower(strValue), prefix) { + return a.fail("Unsupported token type") + } + opts.Token = strValue[len(prefix):] + default: + return a.fail("Invalid response, unknown parameter: " + string(pParts[0])) + } + } + + authzErr := a.authenticate(opts) + if authzErr != nil { + blob, err := json.Marshal(authzErr) + if err != nil { + panic(err) // wtf + } + a.failErr = authzErr + return blob, false, nil + } + + return nil, true, nil +} + +func NewOAuthBearerServer(auth OAuthBearerAuthenticator) Server { + return &oauthBearerServer{authenticate: auth} +} diff --git a/vendor/github.com/emersion/go-sasl/plain.go b/vendor/github.com/emersion/go-sasl/plain.go new file mode 100644 index 0000000..344ed17 --- /dev/null +++ b/vendor/github.com/emersion/go-sasl/plain.go @@ -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} +} diff --git a/vendor/github.com/emersion/go-sasl/sasl.go b/vendor/github.com/emersion/go-sasl/sasl.go new file mode 100644 index 0000000..c209144 --- /dev/null +++ b/vendor/github.com/emersion/go-sasl/sasl.go @@ -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) +} diff --git a/vendor/github.com/emersion/go-smtp/.build.yml b/vendor/github.com/emersion/go-smtp/.build.yml new file mode 100644 index 0000000..46854b9 --- /dev/null +++ b/vendor/github.com/emersion/go-smtp/.build.yml @@ -0,0 +1,17 @@ +image: alpine/edge +packages: + - go +sources: + - https://github.com/emersion/go-smtp +artifacts: + - coverage.html +tasks: + - build: | + cd go-smtp + go build -v ./... + - test: | + cd go-smtp + go test -coverprofile=coverage.txt -covermode=atomic ./... + - coverage: | + cd go-smtp + go tool cover -html=coverage.txt -o ~/coverage.html diff --git a/vendor/github.com/emersion/go-smtp/.gitignore b/vendor/github.com/emersion/go-smtp/.gitignore new file mode 100644 index 0000000..dc3e55d --- /dev/null +++ b/vendor/github.com/emersion/go-smtp/.gitignore @@ -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 diff --git a/vendor/github.com/emersion/go-smtp/LICENSE b/vendor/github.com/emersion/go-smtp/LICENSE new file mode 100644 index 0000000..92ce700 --- /dev/null +++ b/vendor/github.com/emersion/go-smtp/LICENSE @@ -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. diff --git a/vendor/github.com/emersion/go-smtp/README.md b/vendor/github.com/emersion/go-smtp/README.md new file mode 100644 index 0000000..6992498 --- /dev/null +++ b/vendor/github.com/emersion/go-smtp/README.md @@ -0,0 +1,181 @@ +# go-smtp + +[![godocs.io](https://godocs.io/github.com/emersion/go-smtp?status.svg)](https://godocs.io/github.com/emersion/go-smtp) +[![builds.sr.ht status](https://builds.sr.ht/~emersion/go-smtp/commits.svg)](https://builds.sr.ht/~emersion/go-smtp/commits?) + +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 +* [LMTP](https://tools.ietf.org/html/rfc2033) support + +## Usage + +### Client + +```go +package main + +import ( + "log" + "strings" + + "github.com/emersion/go-sasl" + "github.com/emersion/go-smtp" +) + +func main() { + // Setup 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. For example, if you +want to send an email via a server without TLS or auth support, you can do +something like this: + +```go +package main + +import ( + "log" + "strings" + + "github.com/emersion/go-smtp" +) + +func main() { + // Setup an unencrypted connection to a local mail server. + c, err := smtp.Dial("localhost:25") + if err != nil { + return err + } + defer c.Close() + + // 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 := c.SendMail("sender@example.org", to, msg) + if err != nil { + log.Fatal(err) + } +} +``` + +### Server + +```go +package main + +import ( + "errors" + "io" + "io/ioutil" + "log" + "time" + + "github.com/emersion/go-smtp" +) + +// The Backend implements SMTP server methods. +type Backend struct{} + +func (bkd *Backend) NewSession(_ *smtp.Conn) (smtp.Session, error) { + return &Session{}, nil +} + +// A Session is returned after EHLO. +type Session struct{} + +func (s *Session) AuthPlain(username, password string) error { + if username != "username" || password != "password" { + return errors.New("Invalid username or password") + } + return nil +} + +func (s *Session) Mail(from string, opts *smtp.MailOptions) error { + log.Println("Mail from:", from) + return nil +} + +func (s *Session) Rcpt(to string) error { + log.Println("Rcpt to:", to) + return nil +} + +func (s *Session) Data(r io.Reader) error { + if b, err := ioutil.ReadAll(r); err != nil { + return err + } else { + log.Println("Data:", string(b)) + } + return nil +} + +func (s *Session) Reset() {} + +func (s *Session) Logout() error { + return nil +} + +func main() { + be := &Backend{} + + s := smtp.NewServer(be) + + s.Addr = ":1025" + s.Domain = "localhost" + s.ReadTimeout = 10 * time.Second + s.WriteTimeout = 10 * time.Second + 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: +RCPT TO: +DATA +Hey <3 +. +``` + +## Relationship with net/smtp + +The Go standard library provides a SMTP client implementation in `net/smtp`. +However `net/smtp` is frozen: it's not getting any new features. go-smtp +provides a server implementation and a number of client improvements. + +## Licence + +MIT diff --git a/vendor/github.com/emersion/go-smtp/backend.go b/vendor/github.com/emersion/go-smtp/backend.go new file mode 100644 index 0000000..59cea3a --- /dev/null +++ b/vendor/github.com/emersion/go-smtp/backend.go @@ -0,0 +1,108 @@ +package smtp + +import ( + "io" +) + +var ( + ErrAuthRequired = &SMTPError{ + Code: 502, + EnhancedCode: EnhancedCode{5, 7, 0}, + Message: "Please authenticate first", + } + ErrAuthUnsupported = &SMTPError{ + Code: 502, + EnhancedCode: EnhancedCode{5, 7, 0}, + Message: "Authentication not supported", + } +) + +// A SMTP server backend. +type Backend interface { + NewSession(c *Conn) (Session, error) +} + +type BodyType string + +const ( + Body7Bit BodyType = "7BIT" + Body8BitMIME BodyType = "8BITMIME" + BodyBinaryMIME BodyType = "BINARYMIME" +) + +// MailOptions contains custom arguments that were +// passed as an argument to the MAIL command. +type MailOptions struct { + // Value of BODY= argument, 7BIT, 8BITMIME or BINARYMIME. + Body BodyType + + // Size of the body. Can be 0 if not specified by client. + Size int + + // TLS is required for the message transmission. + // + // The message should be rejected if it can't be transmitted + // with TLS. + RequireTLS bool + + // The message envelope or message header contains UTF-8-encoded strings. + // This flag is set by SMTPUTF8-aware (RFC 6531) client. + UTF8 bool + + // The authorization identity asserted by the message sender in decoded + // form with angle brackets stripped. + // + // nil value indicates missing AUTH, non-nil empty string indicates + // AUTH=<>. + // + // Defined in RFC 4954. + Auth *string +} + +// Session is used by servers to respond to an SMTP client. +// +// The methods are called when the remote client issues the matching command. +type Session interface { + // Discard currently processed message. + Reset() + + // Free all resources associated with session. + Logout() error + + // Authenticate the user using SASL PLAIN. + AuthPlain(username, password string) error + + // Set return path for currently processed message. + Mail(from string, opts *MailOptions) error + // Add recipient for currently processed message. + Rcpt(to string) error + // Set currently processed message contents and send it. + // + // r must be consumed before Data returns. + Data(r io.Reader) error +} + +// LMTPSession is an add-on interface for Session. It can be implemented by +// LMTP servers to provide extra functionality. +type LMTPSession interface { + // LMTPData is the LMTP-specific version of Data method. + // It can be optionally implemented by the backend to provide + // per-recipient status information when it is used over LMTP + // protocol. + // + // LMTPData implementation sets status information using passed + // StatusCollector by calling SetStatus once per each AddRcpt + // call, even if AddRcpt was called multiple times with + // the same argument. SetStatus must not be called after + // LMTPData returns. + // + // Return value of LMTPData itself is used as a status for + // recipients that got no status set before using StatusCollector. + LMTPData(r io.Reader, status StatusCollector) error +} + +// StatusCollector allows a backend to provide per-recipient status +// information. +type StatusCollector interface { + SetStatus(rcptTo string, err error) +} diff --git a/vendor/github.com/emersion/go-smtp/client.go b/vendor/github.com/emersion/go-smtp/client.go new file mode 100644 index 0000000..2b455ba --- /dev/null +++ b/vendor/github.com/emersion/go-smtp/client.go @@ -0,0 +1,722 @@ +// 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" + "fmt" + "io" + "net" + "net/textproto" + "strconv" + "strings" + "time" + + "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 + lmtp bool + // map of supported extensions + ext map[string]string + // supported auth mechanisms + auth []string + localName string // the name to use in HELO/EHLO/LHLO + didHello bool // whether we've said HELO/EHLO/LHLO + helloError error // the error from the hello + rcpts []string // recipients accumulated for the current session + + // Time to wait for command responses (this includes 3xx reply to DATA). + CommandTimeout time.Duration + // Time to wait for responses after final dot. + SubmissionTimeout time.Duration + + // Logger for all network activity. + DebugWriter io.Writer +} + +// 30 seconds was chosen as it's the +// same duration as http.DefaultTransport's timeout. +var defaultTimeout = 30 * time.Second + +// 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.DialTimeout("tcp", addr, defaultTimeout) + if err != nil { + return nil, err + } + host, _, _ := net.SplitHostPort(addr) + return NewClient(conn, host) +} + +// DialTLS returns a new Client connected to an SMTP server via TLS at addr. +// The addr must include a port, as in "mail.example.com:smtps". +// +// A nil tlsConfig is equivalent to a zero tls.Config. +func DialTLS(addr string, tlsConfig *tls.Config) (*Client, error) { + tlsDialer := tls.Dialer{ + NetDialer: &net.Dialer{ + Timeout: defaultTimeout, + }, + Config: tlsConfig, + } + conn, err := tlsDialer.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) { + c := &Client{ + serverName: host, + localName: "localhost", + // As recommended by RFC 5321. For DATA command reply (3xx one) RFC + // recommends a slightly shorter timeout but we do not bother + // differentiating these. + CommandTimeout: 5 * time.Minute, + // 10 minutes + 2 minute buffer in case the server is doing transparent + // forwarding and also follows recommended timeouts. + SubmissionTimeout: 12 * time.Minute, + } + + c.setConn(conn) + + // Initial greeting timeout. RFC 5321 recommends 5 minutes. + c.conn.SetDeadline(time.Now().Add(5 * time.Minute)) + defer c.conn.SetDeadline(time.Time{}) + + _, _, err := c.Text.ReadResponse(220) + if err != nil { + c.Text.Close() + if protoErr, ok := err.(*textproto.Error); ok { + return nil, toSMTPErr(protoErr) + } + return nil, err + } + + return c, nil +} + +// NewClientLMTP returns a new LMTP Client (as defined in RFC 2033) using an +// existing connection and host as a server name to be used when authenticating. +func NewClientLMTP(conn net.Conn, host string) (*Client, error) { + c, err := NewClient(conn, host) + if err != nil { + return nil, err + } + c.lmtp = true + return c, nil +} + +// setConn sets the underlying network connection for the client. +func (c *Client) setConn(conn net.Conn) { + c.conn = conn + + var r io.Reader = conn + var w io.Writer = conn + + r = &lineLimitReader{ + R: conn, + // Doubled maximum line length per RFC 5321 (Section 4.5.3.1.6) + LineLimit: 2000, + } + + r = io.TeeReader(r, clientDebugWriter{c}) + w = io.MultiWriter(w, clientDebugWriter{c}) + + rwc := struct { + io.Reader + io.Writer + io.Closer + }{ + Reader: r, + Writer: w, + Closer: conn, + } + c.Text = textproto.NewConn(rwc) + + _, isTLS := conn.(*tls.Conn) + c.tls = isTLS +} + +// 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. +// +// If server returns an error, it will be of type *SMTPError. +func (c *Client) Hello(localName string) error { + if err := validateLine(localName); err != nil { + return err + } + 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 +// textproto.Error returned by c.Text.ReadResponse is converted into SMTPError. +func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) { + c.conn.SetDeadline(time.Now().Add(c.CommandTimeout)) + defer c.conn.SetDeadline(time.Time{}) + + 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) + if err != nil { + if protoErr, ok := err.(*textproto.Error); ok { + smtpErr := toSMTPErr(protoErr) + return code, smtpErr.Message, smtpErr + } + return code, msg, err + } + return code, msg, nil +} + +// 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 { + cmd := "EHLO" + if c.lmtp { + cmd = "LHLO" + } + + _, msg, err := c.cmd(250, "%s %s", cmd, 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. +// +// A nil config is equivalent to a zero tls.Config. +// +// If server returns an error, it will be of type *SMTPError. +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 + } + if config == nil { + config = &tls.Config{} + } + if config.ServerName == "" { + // Make a copy to avoid polluting argument + config = config.Clone() + config.ServerName = c.serverName + } + if testHookStartTLS != nil { + testHookStartTLS(config) + } + c.setConn(tls.Client(c.conn, config)) + 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. +// +// If server returns an error, it will be of type *SMTPError. +func (c *Client) Verify(addr string) error { + if err := validateLine(addr); err != nil { + return err + } + 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. +// Only servers that advertise the AUTH extension support this function. +// +// If server returns an error, it will be of type *SMTPError. +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 { + return err + } + resp64 := make([]byte, encoding.EncodedLen(len(resp))) + encoding.Encode(resp64, resp) + code, msg64, err := c.cmd(0, strings.TrimSpace(fmt.Sprintf("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 = toSMTPErr(&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, "*") + 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. +// +// If opts is not nil, MAIL arguments provided in the structure will be added +// to the command. Handling of unsupported options depends on the extension. +// +// If server returns an error, it will be of type *SMTPError. +func (c *Client) Mail(from string, opts *MailOptions) error { + if err := validateLine(from); err != nil { + return err + } + if err := c.hello(); err != nil { + return err + } + cmdStr := "MAIL FROM:<%s>" + if _, ok := c.ext["8BITMIME"]; ok { + cmdStr += " BODY=8BITMIME" + } + if _, ok := c.ext["SIZE"]; ok && opts != nil && opts.Size != 0 { + cmdStr += " SIZE=" + strconv.Itoa(opts.Size) + } + if opts != nil && opts.RequireTLS { + if _, ok := c.ext["REQUIRETLS"]; ok { + cmdStr += " REQUIRETLS" + } else { + return errors.New("smtp: server does not support REQUIRETLS") + } + } + if opts != nil && opts.UTF8 { + if _, ok := c.ext["SMTPUTF8"]; ok { + cmdStr += " SMTPUTF8" + } else { + return errors.New("smtp: server does not support SMTPUTF8") + } + } + if opts != nil && opts.Auth != nil { + if _, ok := c.ext["AUTH"]; ok { + cmdStr += " AUTH=" + encodeXtext(*opts.Auth) + } + // We can safely discard parameter if server does not support AUTH. + } + _, _, 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. +// +// If server returns an error, it will be of type *SMTPError. +func (c *Client) Rcpt(to string) error { + if err := validateLine(to); err != nil { + return err + } + if _, _, err := c.cmd(25, "RCPT TO:<%s>", to); err != nil { + return err + } + c.rcpts = append(c.rcpts, to) + return nil +} + +type dataCloser struct { + c *Client + io.WriteCloser + statusCb func(rcpt string, status *SMTPError) + closed bool +} + +func (d *dataCloser) Close() error { + if d.closed { + return fmt.Errorf("smtp: data writer closed twice") + } + + if err := d.WriteCloser.Close(); err != nil { + return err + } + + d.c.conn.SetDeadline(time.Now().Add(d.c.SubmissionTimeout)) + defer d.c.conn.SetDeadline(time.Time{}) + + expectedResponses := len(d.c.rcpts) + if d.c.lmtp { + for expectedResponses > 0 { + rcpt := d.c.rcpts[len(d.c.rcpts)-expectedResponses] + if _, _, err := d.c.Text.ReadResponse(250); err != nil { + if protoErr, ok := err.(*textproto.Error); ok { + if d.statusCb != nil { + d.statusCb(rcpt, toSMTPErr(protoErr)) + } + } else { + return err + } + } else if d.statusCb != nil { + d.statusCb(rcpt, nil) + } + expectedResponses-- + } + } else { + _, _, err := d.c.Text.ReadResponse(250) + if err != nil { + if protoErr, ok := err.(*textproto.Error); ok { + return toSMTPErr(protoErr) + } + return err + } + } + + d.closed = true + return nil +} + +// 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. +// +// If server returns an error, it will be of type *SMTPError. +func (c *Client) Data() (io.WriteCloser, error) { + _, _, err := c.cmd(354, "DATA") + if err != nil { + return nil, err + } + return &dataCloser{c: c, WriteCloser: c.Text.DotWriter()}, nil +} + +// LMTPData is the LMTP-specific version of the Data method. It accepts a callback +// that will be called for each status response received from the server. +// +// Status callback will receive a SMTPError argument for each negative server +// reply and nil for each positive reply. I/O errors will not be reported using +// callback and instead will be returned by the Close method of io.WriteCloser. +// Callback will be called for each successfull Rcpt call done before in the +// same order. +func (c *Client) LMTPData(statusCb func(rcpt string, status *SMTPError)) (io.WriteCloser, error) { + if !c.lmtp { + return nil, errors.New("smtp: not a LMTP client") + } + + _, _, err := c.cmd(354, "DATA") + if err != nil { + return nil, err + } + return &dataCloser{c: c, WriteCloser: c.Text.DotWriter(), statusCb: statusCb}, nil +} + +// SendMail will use an existing connection to send an email from +// address from, to addresses to, with message r. +// +// This function does not start TLS, nor does it perform authentication. Use +// StartTLS and Auth before-hand if desirable. +// +// 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. +func (c *Client) SendMail(from string, to []string, r io.Reader) error { + var err error + + if err = c.Mail(from, nil); 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() +} + +var testHookStartTLS func(*tls.Config) // nil, except for tests + +// SendMail connects to the server at addr, switches to TLS, authenticates with +// the optional SASL client, 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. +// +// SendMail is intended to be used for very simple use-cases. If you want to +// customize SendMail's behavior, use a Client instead. +// +// The SendMail function and the go-smtp package are low-level +// mechanisms and provide no support for DKIM signing (see go-msgauth), MIME +// attachments (see the mime/multipart package or the go-message package), or +// other mail functionality. +func SendMail(addr string, a sasl.Client, from string, to []string, r io.Reader) error { + if err := validateLine(from); err != nil { + return err + } + for _, recp := range to { + if err := validateLine(recp); err != nil { + return err + } + } + 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 { + return errors.New("smtp: server doesn't support STARTTLS") + } + if err = c.StartTLS(nil); err != nil { + return err + } + if a != nil && c.ext != nil { + if _, ok := c.ext["AUTH"]; !ok { + return errors.New("smtp: server doesn't support AUTH") + } + if err = c.Auth(a); err != nil { + return err + } + } + return c.SendMail(from, to, r) +} + +// 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 + } + if _, _, err := c.cmd(250, "RSET"); err != nil { + return err + } + c.rcpts = nil + return nil +} + +// Noop sends the NOOP command to the server. It does nothing but check +// that the connection to the server is okay. +func (c *Client) Noop() error { + if err := c.hello(); err != nil { + return err + } + _, _, err := c.cmd(250, "NOOP") + return err +} + +// Quit sends the QUIT command and closes the connection to the server. +// +// If Quit fails the connection is not closed, Close should be used +// in this case. +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() +} + +func parseEnhancedCode(s string) (EnhancedCode, error) { + parts := strings.Split(s, ".") + if len(parts) != 3 { + return EnhancedCode{}, fmt.Errorf("wrong amount of enhanced code parts") + } + + code := EnhancedCode{} + for i, part := range parts { + num, err := strconv.Atoi(part) + if err != nil { + return code, err + } + code[i] = num + } + return code, nil +} + +// toSMTPErr converts textproto.Error into SMTPError, parsing +// enhanced status code if it is present. +func toSMTPErr(protoErr *textproto.Error) *SMTPError { + if protoErr == nil { + return nil + } + smtpErr := &SMTPError{ + Code: protoErr.Code, + Message: protoErr.Msg, + } + + parts := strings.SplitN(protoErr.Msg, " ", 2) + if len(parts) != 2 { + return smtpErr + } + + enchCode, err := parseEnhancedCode(parts[0]) + if err != nil { + return smtpErr + } + + msg := parts[1] + + // Per RFC 2034, enhanced code should be prepended to each line. + msg = strings.ReplaceAll(msg, "\n"+parts[0]+" ", "\n") + + smtpErr.EnhancedCode = enchCode + smtpErr.Message = msg + return smtpErr +} + +type clientDebugWriter struct { + c *Client +} + +func (cdw clientDebugWriter) Write(b []byte) (int, error) { + if cdw.c.DebugWriter == nil { + return len(b), nil + } + return cdw.c.DebugWriter.Write(b) +} diff --git a/vendor/github.com/emersion/go-smtp/conn.go b/vendor/github.com/emersion/go-smtp/conn.go new file mode 100644 index 0000000..72a67d8 --- /dev/null +++ b/vendor/github.com/emersion/go-smtp/conn.go @@ -0,0 +1,986 @@ +package smtp + +import ( + "crypto/tls" + "encoding/base64" + "errors" + "fmt" + "io" + "io/ioutil" + "net" + "net/textproto" + "regexp" + "runtime/debug" + "strconv" + "strings" + "sync" + "time" +) + +// Number of errors we'll tolerate per connection before closing. Defaults to 3. +const errThreshold = 3 + +type Conn struct { + conn net.Conn + text *textproto.Conn + server *Server + helo string + + // Number of errors witnessed on this connection + errCount int + + session Session + locker sync.Mutex + binarymime bool + + lineLimitReader *lineLimitReader + bdatPipe *io.PipeWriter + bdatStatus *statusCollector // used for BDAT on LMTP + dataResult chan error + bytesReceived int // counts total size of chunks when BDAT is used + + fromReceived bool + recipients []string + didAuth bool +} + +func newConn(c net.Conn, s *Server) *Conn { + sc := &Conn{ + server: s, + conn: c, + } + + sc.init() + return sc +} + +func (c *Conn) init() { + c.lineLimitReader = &lineLimitReader{ + R: c.conn, + LineLimit: c.server.MaxLineLength, + } + rwc := struct { + io.Reader + io.Writer + io.Closer + }{ + Reader: c.lineLimitReader, + Writer: c.conn, + Closer: c.conn, + } + + if c.server.Debug != nil { + rwc = struct { + io.Reader + io.Writer + io.Closer + }{ + io.TeeReader(rwc.Reader, c.server.Debug), + io.MultiWriter(rwc.Writer, c.server.Debug), + rwc.Closer, + } + } + + c.text = textproto.NewConn(rwc) +} + +// Commands are dispatched to the appropriate handler functions. +func (c *Conn) handle(cmd string, arg string) { + // If panic happens during command handling - send 421 response + // and close connection. + defer func() { + if err := recover(); err != nil { + c.writeResponse(421, EnhancedCode{4, 0, 0}, "Internal server error") + c.Close() + + stack := debug.Stack() + c.server.ErrorLog.Printf("panic serving %v: %v\n%s", c.conn.RemoteAddr(), err, stack) + } + }() + + if cmd == "" { + c.protocolError(500, EnhancedCode{5, 5, 2}, "Error: bad syntax") + return + } + + cmd = strings.ToUpper(cmd) + switch cmd { + case "SEND", "SOML", "SAML", "EXPN", "HELP", "TURN": + // These commands are not implemented in any state + c.writeResponse(502, EnhancedCode{5, 5, 1}, fmt.Sprintf("%v command not implemented", cmd)) + case "HELO", "EHLO", "LHLO": + lmtp := cmd == "LHLO" + enhanced := lmtp || cmd == "EHLO" + if c.server.LMTP && !lmtp { + c.writeResponse(500, EnhancedCode{5, 5, 1}, "This is a LMTP server, use LHLO") + return + } + if !c.server.LMTP && lmtp { + c.writeResponse(500, EnhancedCode{5, 5, 1}, "This is not a LMTP server") + return + } + c.handleGreet(enhanced, arg) + case "MAIL": + c.handleMail(arg) + case "RCPT": + c.handleRcpt(arg) + case "VRFY": + c.writeResponse(252, EnhancedCode{2, 5, 0}, "Cannot VRFY user, but will accept message") + case "NOOP": + c.writeResponse(250, EnhancedCode{2, 0, 0}, "I have sucessfully done nothing") + case "RSET": // Reset session + c.reset() + c.writeResponse(250, EnhancedCode{2, 0, 0}, "Session reset") + case "BDAT": + c.handleBdat(arg) + case "DATA": + c.handleData(arg) + case "QUIT": + c.writeResponse(221, EnhancedCode{2, 0, 0}, "Bye") + c.Close() + case "AUTH": + if c.server.AuthDisabled { + c.protocolError(500, EnhancedCode{5, 5, 2}, "Syntax error, AUTH command unrecognized") + } else { + c.handleAuth(arg) + } + case "STARTTLS": + c.handleStartTLS() + default: + msg := fmt.Sprintf("Syntax errors, %v command unrecognized", cmd) + c.protocolError(500, EnhancedCode{5, 5, 2}, msg) + } +} + +func (c *Conn) Server() *Server { + return c.server +} + +func (c *Conn) Session() Session { + c.locker.Lock() + defer c.locker.Unlock() + return c.session +} + +func (c *Conn) setSession(session Session) { + c.locker.Lock() + defer c.locker.Unlock() + c.session = session +} + +func (c *Conn) Close() error { + c.locker.Lock() + defer c.locker.Unlock() + + if c.bdatPipe != nil { + c.bdatPipe.CloseWithError(ErrDataReset) + c.bdatPipe = nil + } + + if c.session != nil { + c.session.Logout() + c.session = nil + } + + return c.conn.Close() +} + +// TLSConnectionState returns the connection's TLS connection state. +// Zero values are returned if the connection doesn't use TLS. +func (c *Conn) TLSConnectionState() (state tls.ConnectionState, ok bool) { + tc, ok := c.conn.(*tls.Conn) + if !ok { + return + } + return tc.ConnectionState(), true +} + +func (c *Conn) Hostname() string { + return c.helo +} + +func (c *Conn) Conn() net.Conn { + return c.conn +} + +func (c *Conn) authAllowed() bool { + _, isTLS := c.TLSConnectionState() + return !c.server.AuthDisabled && (isTLS || c.server.AllowInsecureAuth) +} + +// protocolError writes errors responses and closes the connection once too many +// have occurred. +func (c *Conn) protocolError(code int, ec EnhancedCode, msg string) { + c.writeResponse(code, ec, msg) + + c.errCount++ + if c.errCount > errThreshold { + c.writeResponse(500, EnhancedCode{5, 5, 1}, "Too many errors. Quiting now") + c.Close() + } +} + +// GREET state -> waiting for HELO +func (c *Conn) handleGreet(enhanced bool, arg string) { + domain, err := parseHelloArgument(arg) + if err != nil { + c.writeResponse(501, EnhancedCode{5, 5, 2}, "Domain/address argument required for HELO") + return + } + c.helo = domain + + sess, err := c.server.Backend.NewSession(c) + if err != nil { + if smtpErr, ok := err.(*SMTPError); ok { + c.writeResponse(smtpErr.Code, smtpErr.EnhancedCode, smtpErr.Message) + return + } + c.writeResponse(451, EnhancedCode{4, 0, 0}, err.Error()) + return + } + c.setSession(sess) + + if !enhanced { + c.writeResponse(250, EnhancedCode{2, 0, 0}, fmt.Sprintf("Hello %s", domain)) + return + } + + caps := []string{} + caps = append(caps, c.server.caps...) + if _, isTLS := c.TLSConnectionState(); c.server.TLSConfig != nil && !isTLS { + caps = append(caps, "STARTTLS") + } + if c.authAllowed() { + authCap := "AUTH" + for name := range c.server.auths { + authCap += " " + name + } + + caps = append(caps, authCap) + } + if c.server.EnableSMTPUTF8 { + caps = append(caps, "SMTPUTF8") + } + if _, isTLS := c.TLSConnectionState(); isTLS && c.server.EnableREQUIRETLS { + caps = append(caps, "REQUIRETLS") + } + if c.server.EnableBINARYMIME { + caps = append(caps, "BINARYMIME") + } + if c.server.MaxMessageBytes > 0 { + caps = append(caps, fmt.Sprintf("SIZE %v", c.server.MaxMessageBytes)) + } else { + caps = append(caps, "SIZE") + } + + args := []string{"Hello " + domain} + args = append(args, caps...) + c.writeResponse(250, NoEnhancedCode, args...) +} + +// READY state -> waiting for MAIL +func (c *Conn) handleMail(arg string) { + if c.helo == "" { + c.writeResponse(502, EnhancedCode{2, 5, 1}, "Please introduce yourself first.") + return + } + if c.bdatPipe != nil { + c.writeResponse(502, EnhancedCode{5, 5, 1}, "MAIL not allowed during message transfer") + return + } + + if len(arg) < 6 || strings.ToUpper(arg[0:5]) != "FROM:" { + c.writeResponse(501, EnhancedCode{5, 5, 2}, "Was expecting MAIL arg syntax of FROM:
") + return + } + fromArgs := strings.Split(strings.Trim(arg[5:], " "), " ") + if c.server.Strict { + if !strings.HasPrefix(fromArgs[0], "<") || !strings.HasSuffix(fromArgs[0], ">") { + c.writeResponse(501, EnhancedCode{5, 5, 2}, "Was expecting MAIL arg syntax of FROM:
") + return + } + } + from := fromArgs[0] + if from == "" { + c.writeResponse(501, EnhancedCode{5, 5, 2}, "Was expecting MAIL arg syntax of FROM:
") + return + } + from = strings.Trim(from, "<>") + + opts := &MailOptions{} + + c.binarymime = false + // 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 len(fromArgs) > 1 { + args, err := parseArgs(fromArgs[1:]) + if err != nil { + c.writeResponse(501, EnhancedCode{5, 5, 4}, "Unable to parse MAIL ESMTP parameters") + return + } + + for key, value := range args { + switch key { + case "SIZE": + size, err := strconv.ParseInt(value, 10, 32) + if err != nil { + c.writeResponse(501, EnhancedCode{5, 5, 4}, "Unable to parse SIZE as an integer") + return + } + + if c.server.MaxMessageBytes > 0 && int(size) > c.server.MaxMessageBytes { + c.writeResponse(552, EnhancedCode{5, 3, 4}, "Max message size exceeded") + return + } + + opts.Size = int(size) + case "SMTPUTF8": + if !c.server.EnableSMTPUTF8 { + c.writeResponse(504, EnhancedCode{5, 5, 4}, "SMTPUTF8 is not implemented") + return + } + opts.UTF8 = true + case "REQUIRETLS": + if !c.server.EnableREQUIRETLS { + c.writeResponse(504, EnhancedCode{5, 5, 4}, "REQUIRETLS is not implemented") + return + } + opts.RequireTLS = true + case "BODY": + switch value { + case "BINARYMIME": + if !c.server.EnableBINARYMIME { + c.writeResponse(504, EnhancedCode{5, 5, 4}, "BINARYMIME is not implemented") + return + } + c.binarymime = true + case "7BIT", "8BITMIME": + default: + c.writeResponse(500, EnhancedCode{5, 5, 4}, "Unknown BODY value") + return + } + opts.Body = BodyType(value) + case "AUTH": + value, err := decodeXtext(value) + if err != nil { + c.writeResponse(500, EnhancedCode{5, 5, 4}, "Malformed AUTH parameter value") + return + } + if !strings.HasPrefix(value, "<") { + c.writeResponse(500, EnhancedCode{5, 5, 4}, "Missing opening angle bracket") + return + } + if !strings.HasSuffix(value, ">") { + c.writeResponse(500, EnhancedCode{5, 5, 4}, "Missing closing angle bracket") + return + } + decodedMbox := value[1 : len(value)-1] + opts.Auth = &decodedMbox + default: + c.writeResponse(500, EnhancedCode{5, 5, 4}, "Unknown MAIL FROM argument") + return + } + } + } + + if err := c.Session().Mail(from, opts); err != nil { + if smtpErr, ok := err.(*SMTPError); ok { + c.writeResponse(smtpErr.Code, smtpErr.EnhancedCode, smtpErr.Message) + return + } + c.writeResponse(451, EnhancedCode{4, 0, 0}, err.Error()) + return + } + + c.writeResponse(250, EnhancedCode{2, 0, 0}, fmt.Sprintf("Roger, accepting mail from <%v>", from)) + c.fromReceived = true +} + +// This regexp matches 'hexchar' token defined in +// https://tools.ietf.org/html/rfc4954#section-8 however it is intentionally +// relaxed by requiring only '+' to be present. It allows us to detect +// malformed values such as +A or +HH and report them appropriately. +var hexcharRe = regexp.MustCompile(`\+[0-9A-F]?[0-9A-F]?`) + +func decodeXtext(val string) (string, error) { + if !strings.Contains(val, "+") { + return val, nil + } + + var replaceErr error + decoded := hexcharRe.ReplaceAllStringFunc(val, func(match string) string { + if len(match) != 3 { + replaceErr = errors.New("incomplete hexchar") + return "" + } + char, err := strconv.ParseInt(match, 16, 8) + if err != nil { + replaceErr = err + return "" + } + + return string(rune(char)) + }) + if replaceErr != nil { + return "", replaceErr + } + + return decoded, nil +} + +func encodeXtext(raw string) string { + var out strings.Builder + out.Grow(len(raw)) + + for _, ch := range raw { + if ch == '+' || ch == '=' { + out.WriteRune('+') + out.WriteString(strings.ToUpper(strconv.FormatInt(int64(ch), 16))) + } + if ch > '!' && ch < '~' { // printable non-space US-ASCII + out.WriteRune(ch) + } + // Non-ASCII. + out.WriteRune('+') + out.WriteString(strings.ToUpper(strconv.FormatInt(int64(ch), 16))) + } + return out.String() +} + +// MAIL state -> waiting for RCPTs followed by DATA +func (c *Conn) handleRcpt(arg string) { + if !c.fromReceived { + c.writeResponse(502, EnhancedCode{5, 5, 1}, "Missing MAIL FROM command.") + return + } + if c.bdatPipe != nil { + c.writeResponse(502, EnhancedCode{5, 5, 1}, "RCPT not allowed during message transfer") + return + } + + if (len(arg) < 4) || (strings.ToUpper(arg[0:3]) != "TO:") { + c.writeResponse(501, EnhancedCode{5, 5, 2}, "Was expecting RCPT arg syntax of TO:
") + return + } + + // TODO: This trim is probably too forgiving + recipient := strings.Trim(arg[3:], "<> ") + + if c.server.MaxRecipients > 0 && len(c.recipients) >= c.server.MaxRecipients { + c.writeResponse(552, EnhancedCode{5, 5, 3}, fmt.Sprintf("Maximum limit of %v recipients reached", c.server.MaxRecipients)) + return + } + + if err := c.Session().Rcpt(recipient); err != nil { + if smtpErr, ok := err.(*SMTPError); ok { + c.writeResponse(smtpErr.Code, smtpErr.EnhancedCode, smtpErr.Message) + return + } + c.writeResponse(451, EnhancedCode{4, 0, 0}, err.Error()) + return + } + c.recipients = append(c.recipients, recipient) + c.writeResponse(250, EnhancedCode{2, 0, 0}, fmt.Sprintf("I'll make sure <%v> gets this", recipient)) +} + +func (c *Conn) handleAuth(arg string) { + if c.helo == "" { + c.writeResponse(502, EnhancedCode{5, 5, 1}, "Please introduce yourself first.") + return + } + if c.didAuth { + c.writeResponse(503, EnhancedCode{5, 5, 1}, "Already authenticated") + return + } + + parts := strings.Fields(arg) + if len(parts) == 0 { + c.writeResponse(502, EnhancedCode{5, 5, 4}, "Missing parameter") + return + } + + if _, isTLS := c.TLSConnectionState(); !isTLS && !c.server.AllowInsecureAuth { + c.writeResponse(523, EnhancedCode{5, 7, 10}, "TLS is required") + return + } + + 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, EnhancedCode{5, 7, 4}, "Unsupported authentication mechanism") + return + } + + sasl := newSasl(c) + + response := ir + for { + challenge, done, err := sasl.Next(response) + if err != nil { + if smtpErr, ok := err.(*SMTPError); ok { + c.writeResponse(smtpErr.Code, smtpErr.EnhancedCode, smtpErr.Message) + return + } + c.writeResponse(454, EnhancedCode{4, 7, 0}, err.Error()) + return + } + + if done { + break + } + + encoded := "" + if len(challenge) > 0 { + encoded = base64.StdEncoding.EncodeToString(challenge) + } + c.writeResponse(334, NoEnhancedCode, encoded) + + encoded, err = c.readLine() + if err != nil { + return // TODO: error handling + } + + if encoded == "*" { + // https://tools.ietf.org/html/rfc4954#page-4 + c.writeResponse(501, EnhancedCode{5, 0, 0}, "Negotiation cancelled") + return + } + + response, err = base64.StdEncoding.DecodeString(encoded) + if err != nil { + c.writeResponse(454, EnhancedCode{4, 7, 0}, "Invalid base64 data") + return + } + } + + c.writeResponse(235, EnhancedCode{2, 0, 0}, "Authentication succeeded") + c.didAuth = true +} + +func (c *Conn) handleStartTLS() { + if _, isTLS := c.TLSConnectionState(); isTLS { + c.writeResponse(502, EnhancedCode{5, 5, 1}, "Already running in TLS") + return + } + + if c.server.TLSConfig == nil { + c.writeResponse(502, EnhancedCode{5, 5, 1}, "TLS not supported") + return + } + + c.writeResponse(220, EnhancedCode{2, 0, 0}, "Ready to start TLS") + + // Upgrade to TLS + tlsConn := tls.Server(c.conn, c.server.TLSConfig) + + if err := tlsConn.Handshake(); err != nil { + c.writeResponse(550, EnhancedCode{5, 0, 0}, "Handshake error") + return + } + + c.conn = tlsConn + c.init() + + // Reset all state and close the previous Session. + // This is different from just calling reset() since we want the Backend to + // be able to see the information about TLS connection in the + // ConnectionState object passed to it. + if session := c.Session(); session != nil { + session.Logout() + c.setSession(nil) + } + c.helo = "" + c.didAuth = false + c.reset() +} + +// DATA +func (c *Conn) handleData(arg string) { + if arg != "" { + c.writeResponse(501, EnhancedCode{5, 5, 4}, "DATA command should not have any arguments") + return + } + if c.bdatPipe != nil { + c.writeResponse(502, EnhancedCode{5, 5, 1}, "DATA not allowed during message transfer") + return + } + if c.binarymime { + c.writeResponse(502, EnhancedCode{5, 5, 1}, "DATA not allowed for BINARYMIME messages") + return + } + + if !c.fromReceived || len(c.recipients) == 0 { + c.writeResponse(502, EnhancedCode{5, 5, 1}, "Missing RCPT TO command.") + return + } + + // We have recipients, go to accept data + c.writeResponse(354, EnhancedCode{2, 0, 0}, "Go ahead. End your data with .") + + defer c.reset() + + if c.server.LMTP { + c.handleDataLMTP() + return + } + + r := newDataReader(c) + code, enhancedCode, msg := toSMTPStatus(c.Session().Data(r)) + r.limited = false + io.Copy(ioutil.Discard, r) // Make sure all the data has been consumed + c.writeResponse(code, enhancedCode, msg) +} + +func (c *Conn) handleBdat(arg string) { + args := strings.Fields(arg) + if len(args) == 0 { + c.writeResponse(501, EnhancedCode{5, 5, 4}, "Missing chunk size argument") + return + } + if len(args) > 2 { + c.writeResponse(501, EnhancedCode{5, 5, 4}, "Too many arguments") + return + } + + if !c.fromReceived || len(c.recipients) == 0 { + c.writeResponse(502, EnhancedCode{5, 5, 1}, "Missing RCPT TO command.") + return + } + + last := false + if len(args) == 2 { + if !strings.EqualFold(args[1], "LAST") { + c.writeResponse(501, EnhancedCode{5, 5, 4}, "Unknown BDAT argument") + return + } + last = true + } + + // ParseUint instead of Atoi so we will not accept negative values. + size, err := strconv.ParseUint(args[0], 10, 32) + if err != nil { + c.writeResponse(501, EnhancedCode{5, 5, 4}, "Malformed size argument") + return + } + + if c.server.MaxMessageBytes != 0 && c.bytesReceived+int(size) > c.server.MaxMessageBytes { + c.writeResponse(552, EnhancedCode{5, 3, 4}, "Max message size exceeded") + + // Discard chunk itself without passing it to backend. + io.Copy(ioutil.Discard, io.LimitReader(c.text.R, int64(size))) + + c.reset() + return + } + + if c.bdatStatus == nil && c.server.LMTP { + c.bdatStatus = c.createStatusCollector() + } + + if c.bdatPipe == nil { + var r *io.PipeReader + r, c.bdatPipe = io.Pipe() + + c.dataResult = make(chan error, 1) + + go func() { + defer func() { + if err := recover(); err != nil { + c.handlePanic(err, c.bdatStatus) + + c.dataResult <- errPanic + r.CloseWithError(errPanic) + } + }() + + var err error + if !c.server.LMTP { + err = c.Session().Data(r) + } else { + lmtpSession, ok := c.Session().(LMTPSession) + if !ok { + err = c.Session().Data(r) + for _, rcpt := range c.recipients { + c.bdatStatus.SetStatus(rcpt, err) + } + } else { + err = lmtpSession.LMTPData(r, c.bdatStatus) + } + } + + c.dataResult <- err + r.CloseWithError(err) + }() + } + + c.lineLimitReader.LineLimit = 0 + + chunk := io.LimitReader(c.text.R, int64(size)) + _, err = io.Copy(c.bdatPipe, chunk) + if err != nil { + // Backend might return an error early using CloseWithError without consuming + // the whole chunk. + io.Copy(ioutil.Discard, chunk) + + c.writeResponse(toSMTPStatus(err)) + + if err == errPanic { + c.Close() + } + + c.reset() + c.lineLimitReader.LineLimit = c.server.MaxLineLength + return + } + + c.bytesReceived += int(size) + + if last { + c.lineLimitReader.LineLimit = c.server.MaxLineLength + + c.bdatPipe.Close() + + err := <-c.dataResult + + if c.server.LMTP { + c.bdatStatus.fillRemaining(err) + for i, rcpt := range c.recipients { + code, enchCode, msg := toSMTPStatus(<-c.bdatStatus.status[i]) + c.writeResponse(code, enchCode, "<"+rcpt+"> "+msg) + } + } else { + c.writeResponse(toSMTPStatus(err)) + } + + if err == errPanic { + c.Close() + return + } + + c.reset() + } else { + c.writeResponse(250, EnhancedCode{2, 0, 0}, "Continue") + } +} + +// ErrDataReset is returned by Reader pased to Data function if client does not +// send another BDAT command and instead closes connection or issues RSET command. +var ErrDataReset = errors.New("smtp: message transmission aborted") + +var errPanic = &SMTPError{ + Code: 421, + EnhancedCode: EnhancedCode{4, 0, 0}, + Message: "Internal server error", +} + +func (c *Conn) handlePanic(err interface{}, status *statusCollector) { + if status != nil { + status.fillRemaining(errPanic) + } + + stack := debug.Stack() + c.server.ErrorLog.Printf("panic serving %v: %v\n%s", c.conn.RemoteAddr(), err, stack) +} + +func (c *Conn) createStatusCollector() *statusCollector { + rcptCounts := make(map[string]int, len(c.recipients)) + + status := &statusCollector{ + statusMap: make(map[string]chan error, len(c.recipients)), + status: make([]chan error, 0, len(c.recipients)), + } + for _, rcpt := range c.recipients { + rcptCounts[rcpt]++ + } + // Create channels with buffer sizes necessary to fit all + // statuses for a single recipient to avoid deadlocks. + for rcpt, count := range rcptCounts { + status.statusMap[rcpt] = make(chan error, count) + } + for _, rcpt := range c.recipients { + status.status = append(status.status, status.statusMap[rcpt]) + } + + return status +} + +type statusCollector struct { + // Contains map from recipient to list of channels that are used for that + // recipient. + statusMap map[string]chan error + + // Contains channels from statusMap, in the same + // order as Conn.recipients. + status []chan error +} + +// fillRemaining sets status for all recipients SetStatus was not called for before. +func (s *statusCollector) fillRemaining(err error) { + // Amount of times certain recipient was specified is indicated by the channel + // buffer size, so once we fill it, we can be confident that we sent + // at least as much statuses as needed. Extra statuses will be ignored anyway. +chLoop: + for _, ch := range s.statusMap { + for { + select { + case ch <- err: + default: + continue chLoop + } + } + } +} + +func (s *statusCollector) SetStatus(rcptTo string, err error) { + ch := s.statusMap[rcptTo] + if ch == nil { + panic("SetStatus is called for recipient that was not specified before") + } + + select { + case ch <- err: + default: + // There enough buffer space to fit all statuses at once, if this is + // not the case - backend is doing something wrong. + panic("SetStatus is called more times than particular recipient was specified") + } +} + +func (c *Conn) handleDataLMTP() { + r := newDataReader(c) + status := c.createStatusCollector() + + done := make(chan bool, 1) + + lmtpSession, ok := c.Session().(LMTPSession) + if !ok { + // Fallback to using a single status for all recipients. + err := c.Session().Data(r) + io.Copy(ioutil.Discard, r) // Make sure all the data has been consumed + for _, rcpt := range c.recipients { + status.SetStatus(rcpt, err) + } + done <- true + } else { + go func() { + defer func() { + if err := recover(); err != nil { + status.fillRemaining(&SMTPError{ + Code: 421, + EnhancedCode: EnhancedCode{4, 0, 0}, + Message: "Internal server error", + }) + + stack := debug.Stack() + c.server.ErrorLog.Printf("panic serving %v: %v\n%s", c.conn.RemoteAddr(), err, stack) + done <- false + } + }() + + status.fillRemaining(lmtpSession.LMTPData(r, status)) + io.Copy(ioutil.Discard, r) // Make sure all the data has been consumed + done <- true + }() + } + + for i, rcpt := range c.recipients { + code, enchCode, msg := toSMTPStatus(<-status.status[i]) + c.writeResponse(code, enchCode, "<"+rcpt+"> "+msg) + } + + // If done gets false, the panic occured in LMTPData and the connection + // should be closed. + if !<-done { + c.Close() + } +} + +func toSMTPStatus(err error) (code int, enchCode EnhancedCode, msg string) { + if err != nil { + if smtperr, ok := err.(*SMTPError); ok { + return smtperr.Code, smtperr.EnhancedCode, smtperr.Message + } else { + return 554, EnhancedCode{5, 0, 0}, "Error: transaction failed, blame it on the weather: " + err.Error() + } + } + + return 250, EnhancedCode{2, 0, 0}, "OK: queued" +} + +func (c *Conn) Reject() { + c.writeResponse(421, EnhancedCode{4, 4, 5}, "Too busy. Try again later.") + c.Close() +} + +func (c *Conn) greet() { + c.writeResponse(220, NoEnhancedCode, fmt.Sprintf("%v ESMTP Service Ready", c.server.Domain)) +} + +func (c *Conn) writeResponse(code int, enhCode EnhancedCode, text ...string) { + // TODO: error handling + if c.server.WriteTimeout != 0 { + c.conn.SetWriteDeadline(time.Now().Add(c.server.WriteTimeout)) + } + + // All responses must include an enhanced code, if it is missing - use + // a generic code X.0.0. + if enhCode == EnhancedCodeNotSet { + cat := code / 100 + switch cat { + case 2, 4, 5: + enhCode = EnhancedCode{cat, 0, 0} + default: + enhCode = NoEnhancedCode + } + } + + for i := 0; i < len(text)-1; i++ { + c.text.PrintfLine("%d-%v", code, text[i]) + } + if enhCode == NoEnhancedCode { + c.text.PrintfLine("%d %v", code, text[len(text)-1]) + } else { + c.text.PrintfLine("%d %v.%v.%v %v", code, enhCode[0], enhCode[1], enhCode[2], text[len(text)-1]) + } +} + +// Reads a line of input +func (c *Conn) readLine() (string, error) { + if c.server.ReadTimeout != 0 { + if err := c.conn.SetReadDeadline(time.Now().Add(c.server.ReadTimeout)); err != nil { + return "", err + } + } + + return c.text.ReadLine() +} + +func (c *Conn) reset() { + c.locker.Lock() + defer c.locker.Unlock() + + if c.bdatPipe != nil { + c.bdatPipe.CloseWithError(ErrDataReset) + c.bdatPipe = nil + } + c.bdatStatus = nil + c.bytesReceived = 0 + + if c.session != nil { + c.session.Reset() + } + + c.fromReceived = false + c.recipients = nil +} diff --git a/vendor/github.com/emersion/go-smtp/data.go b/vendor/github.com/emersion/go-smtp/data.go new file mode 100644 index 0000000..c338455 --- /dev/null +++ b/vendor/github.com/emersion/go-smtp/data.go @@ -0,0 +1,147 @@ +package smtp + +import ( + "bufio" + "io" +) + +type EnhancedCode [3]int + +// SMTPError specifies the error code, enhanced error code (if any) and +// message returned by the server. +type SMTPError struct { + Code int + EnhancedCode EnhancedCode + Message string +} + +// NoEnhancedCode is used to indicate that enhanced error code should not be +// included in response. +// +// Note that RFC 2034 requires an enhanced code to be included in all 2xx, 4xx +// and 5xx responses. This constant is exported for use by extensions, you +// should probably use EnhancedCodeNotSet instead. +var NoEnhancedCode = EnhancedCode{-1, -1, -1} + +// EnhancedCodeNotSet is a nil value of EnhancedCode field in SMTPError, used +// to indicate that backend failed to provide enhanced status code. X.0.0 will +// be used (X is derived from error code). +var EnhancedCodeNotSet = EnhancedCode{0, 0, 0} + +func (err *SMTPError) Error() string { + return err.Message +} + +func (err *SMTPError) Temporary() bool { + return err.Code/100 == 4 +} + +var ErrDataTooLarge = &SMTPError{ + Code: 552, + EnhancedCode: EnhancedCode{5, 3, 4}, + Message: "Maximum message size exceeded", +} + +type dataReader struct { + r *bufio.Reader + state int + + limited bool + n int64 // Maximum bytes remaining +} + +func newDataReader(c *Conn) *dataReader { + dr := &dataReader{ + r: c.text.R, + } + + 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] + } + } + + // Code below is taken from net/textproto with only one modification to + // not rewrite CRLF -> LF. + + // Run data through a simple state machine to + // elide leading dots and detect ending .\r\n line. + const ( + stateBeginLine = iota // beginning of line; initial state; must be zero + stateDot // read . at beginning of line + stateDotCR // read .\r at beginning of line + stateCR // read \r (possibly at end of line) + stateData // reading data in middle of line + stateEOF // reached .\r\n end marker line + ) + for n < len(b) && r.state != stateEOF { + var c byte + c, err = r.r.ReadByte() + if err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + break + } + switch r.state { + case stateBeginLine: + if c == '.' { + r.state = stateDot + continue + } + r.state = stateData + case stateDot: + if c == '\r' { + r.state = stateDotCR + continue + } + if c == '\n' { + r.state = stateEOF + continue + } + + r.state = stateData + case stateDotCR: + if c == '\n' { + r.state = stateEOF + continue + } + r.state = stateData + case stateCR: + if c == '\n' { + r.state = stateBeginLine + break + } + r.state = stateData + case stateData: + if c == '\r' { + r.state = stateCR + } + if c == '\n' { + r.state = stateBeginLine + } + } + b[n] = c + n++ + } + if err == nil && r.state == stateEOF { + err = io.EOF + } + + if r.limited { + r.n -= int64(n) + } + return +} diff --git a/vendor/github.com/emersion/go-smtp/lengthlimit_reader.go b/vendor/github.com/emersion/go-smtp/lengthlimit_reader.go new file mode 100644 index 0000000..1513e56 --- /dev/null +++ b/vendor/github.com/emersion/go-smtp/lengthlimit_reader.go @@ -0,0 +1,47 @@ +package smtp + +import ( + "errors" + "io" +) + +var ErrTooLongLine = errors.New("smtp: too long a line in input stream") + +// lineLimitReader reads from the underlying Reader but restricts +// line length of lines in input stream to a certain length. +// +// If line length exceeds the limit - Read returns ErrTooLongLine +type lineLimitReader struct { + R io.Reader + LineLimit int + + curLineLength int +} + +func (r *lineLimitReader) Read(b []byte) (int, error) { + if r.curLineLength > r.LineLimit && r.LineLimit > 0 { + return 0, ErrTooLongLine + } + + n, err := r.R.Read(b) + if err != nil { + return n, err + } + + if r.LineLimit == 0 { + return n, nil + } + + for _, chr := range b[:n] { + if chr == '\n' { + r.curLineLength = 0 + } + r.curLineLength++ + + if r.curLineLength > r.LineLimit { + return 0, ErrTooLongLine + } + } + + return n, nil +} diff --git a/vendor/github.com/emersion/go-smtp/parse.go b/vendor/github.com/emersion/go-smtp/parse.go new file mode 100644 index 0000000..dc7e77f --- /dev/null +++ b/vendor/github.com/emersion/go-smtp/parse.go @@ -0,0 +1,72 @@ +package smtp + +import ( + "fmt" + "strings" +) + +func parseCmd(line string) (cmd string, arg string, err error) { + line = strings.TrimRight(line, "\r\n") + + l := len(line) + switch { + case strings.HasPrefix(strings.ToUpper(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 SMTPUTF8" +// +// The leading space is mandatory. +func parseArgs(args []string) (map[string]string, error) { + argMap := map[string]string{} + for _, arg := range args { + if arg == "" { + continue + } + m := strings.Split(arg, "=") + switch len(m) { + case 2: + argMap[strings.ToUpper(m[0])] = m[1] + case 1: + argMap[strings.ToUpper(m[0])] = "" + default: + return nil, fmt.Errorf("Failed to parse arg string: %q", arg) + } + } + return argMap, 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 +} diff --git a/vendor/github.com/emersion/go-smtp/server.go b/vendor/github.com/emersion/go-smtp/server.go new file mode 100644 index 0000000..82cc422 --- /dev/null +++ b/vendor/github.com/emersion/go-smtp/server.go @@ -0,0 +1,292 @@ +package smtp + +import ( + "crypto/tls" + "errors" + "io" + "log" + "net" + "os" + "sync" + "time" + + "github.com/emersion/go-sasl" +) + +var errTCPAndLMTP = errors.New("smtp: cannot start LMTP server listening on a TCP socket") + +// A function that creates SASL servers. +type SaslServerFactory func(conn *Conn) sasl.Server + +// Logger interface is used by Server to report unexpected internal errors. +type Logger interface { + Printf(format string, v ...interface{}) + Println(v ...interface{}) +} + +// A SMTP server. +type Server struct { + // TCP or Unix address to listen on. + Addr string + // The server TLS configuration. + TLSConfig *tls.Config + // Enable LMTP mode, as defined in RFC 2033. LMTP mode cannot be used with a + // TCP listener. + LMTP bool + + Domain string + MaxRecipients int + MaxMessageBytes int + MaxLineLength int + AllowInsecureAuth bool + Strict bool + Debug io.Writer + ErrorLog Logger + ReadTimeout time.Duration + WriteTimeout time.Duration + + // Advertise SMTPUTF8 (RFC 6531) capability. + // Should be used only if backend supports it. + EnableSMTPUTF8 bool + + // Advertise REQUIRETLS (RFC 8689) capability. + // Should be used only if backend supports it. + EnableREQUIRETLS bool + + // Advertise BINARYMIME (RFC 3030) capability. + // Should be used only if backend supports it. + EnableBINARYMIME bool + + // If set, the AUTH command will not be advertised and authentication + // attempts will be rejected. This setting overrides AllowInsecureAuth. + AuthDisabled bool + + // The server backend. + Backend Backend + + caps []string + auths map[string]SaslServerFactory + done chan struct{} + + locker sync.Mutex + listeners []net.Listener + conns map[*Conn]struct{} +} + +// New creates a new SMTP server. +func NewServer(be Backend) *Server { + return &Server{ + // Doubled maximum line length per RFC 5321 (Section 4.5.3.1.6) + MaxLineLength: 2000, + + Backend: be, + done: make(chan struct{}, 1), + ErrorLog: log.New(os.Stderr, "smtp/server ", log.LstdFlags), + caps: []string{"PIPELINING", "8BITMIME", "ENHANCEDSTATUSCODES", "CHUNKING"}, + 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") + } + + sess := conn.Session() + if sess == nil { + panic("No session when AUTH is called") + } + + return sess.AuthPlain(username, password) + }) + }, + }, + conns: make(map[*Conn]struct{}), + } +} + +// Serve accepts incoming connections on the Listener l. +func (s *Server) Serve(l net.Listener) error { + s.locker.Lock() + s.listeners = append(s.listeners, l) + s.locker.Unlock() + + var tempDelay time.Duration // how long to sleep on accept failure + + for { + c, err := l.Accept() + if err != nil { + select { + case <-s.done: + // we called Close() + return nil + default: + } + if ne, ok := err.(net.Error); ok && ne.Temporary() { + if tempDelay == 0 { + tempDelay = 5 * time.Millisecond + } else { + tempDelay *= 2 + } + if max := 1 * time.Second; tempDelay > max { + tempDelay = max + } + s.ErrorLog.Printf("accept error: %s; retrying in %s", err, tempDelay) + time.Sleep(tempDelay) + continue + } + return err + } + go func() { + err := s.handleConn(newConn(c, s)) + if err != nil { + s.ErrorLog.Printf("handler error: %s", err) + } + }() + } +} + +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() + }() + + if tlsConn, ok := c.conn.(*tls.Conn); ok { + if d := s.ReadTimeout; d != 0 { + c.conn.SetReadDeadline(time.Now().Add(d)) + } + if d := s.WriteTimeout; d != 0 { + c.conn.SetWriteDeadline(time.Now().Add(d)) + } + if err := tlsConn.Handshake(); err != nil { + return err + } + } + + c.greet() + + for { + line, err := c.readLine() + if err == nil { + cmd, arg, err := parseCmd(line) + if err != nil { + c.protocolError(501, EnhancedCode{5, 5, 2}, "Bad command") + continue + } + + c.handle(cmd, arg) + } else { + if err == io.EOF || errors.Is(err, net.ErrClosed) { + return nil + } + if err == ErrTooLongLine { + c.writeResponse(500, EnhancedCode{5, 4, 0}, "Too long line, closing connection") + return nil + } + + if neterr, ok := err.(net.Error); ok && neterr.Timeout() { + c.writeResponse(221, EnhancedCode{2, 4, 2}, "Idle timeout, bye bye") + return nil + } + + c.writeResponse(221, EnhancedCode{2, 4, 0}, "Connection error, sorry") + return err + } + } +} + +// ListenAndServe listens on the network address s.Addr and then calls Serve +// to handle requests on incoming connections. +// +// If s.Addr is blank and LMTP is disabled, ":smtp" is used. +func (s *Server) ListenAndServe() error { + network := "tcp" + if s.LMTP { + network = "unix" + } + + addr := s.Addr + if !s.LMTP && addr == "" { + addr = ":smtp" + } + + l, err := net.Listen(network, 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 { + if s.LMTP { + return errTCPAndLMTP + } + + 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 immediately closes all active listeners and connections. +// +// Close returns any error returned from closing the server's underlying +// listener(s). +func (s *Server) Close() error { + select { + case <-s.done: + return errors.New("smtp: server already closed") + default: + close(s.done) + } + + var err error + s.locker.Lock() + for _, l := range s.listeners { + if lerr := l.Close(); lerr != nil && err == nil { + err = lerr + } + } + + for conn := range s.conns { + conn.Close() + } + s.locker.Unlock() + + return err +} + +// 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) + } +} diff --git a/vendor/github.com/emersion/go-smtp/smtp.go b/vendor/github.com/emersion/go-smtp/smtp.go new file mode 100644 index 0000000..36963cb --- /dev/null +++ b/vendor/github.com/emersion/go-smtp/smtp.go @@ -0,0 +1,30 @@ +// 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 +// ENHANCEDSTATUSCODES: RFC 2034 +// SMTPUTF8: RFC 6531 +// REQUIRETLS: RFC 8689 +// CHUNKING: RFC 3030 +// BINARYMIME: RFC 3030 +// +// LMTP (RFC 2033) is also supported. +// +// Additional extensions may be handled by other packages. +package smtp + +import ( + "errors" + "strings" +) + +// validateLine checks to see if a line has CR or LF as per RFC 5321 +func validateLine(line string) error { + if strings.ContainsAny(line, "\n\r") { + return errors.New("smtp: A line must not contain CR or LF") + } + return nil +} diff --git a/vendor/golang.org/x/text/AUTHORS b/vendor/golang.org/x/text/AUTHORS new file mode 100644 index 0000000..15167cd --- /dev/null +++ b/vendor/golang.org/x/text/AUTHORS @@ -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. diff --git a/vendor/golang.org/x/text/CONTRIBUTORS b/vendor/golang.org/x/text/CONTRIBUTORS new file mode 100644 index 0000000..1c4577e --- /dev/null +++ b/vendor/golang.org/x/text/CONTRIBUTORS @@ -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. diff --git a/vendor/golang.org/x/text/LICENSE b/vendor/golang.org/x/text/LICENSE new file mode 100644 index 0000000..6a66aea --- /dev/null +++ b/vendor/golang.org/x/text/LICENSE @@ -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. diff --git a/vendor/golang.org/x/text/PATENTS b/vendor/golang.org/x/text/PATENTS new file mode 100644 index 0000000..7330990 --- /dev/null +++ b/vendor/golang.org/x/text/PATENTS @@ -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. diff --git a/vendor/golang.org/x/text/encoding/encoding.go b/vendor/golang.org/x/text/encoding/encoding.go new file mode 100644 index 0000000..a0bd7cd --- /dev/null +++ b/vendor/golang.org/x/text/encoding/encoding.go @@ -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 +// https://unicode.org/reports/tr36/#Text_Comparison +const ASCIISub = '\x1a' + +// Nop is the nop encoding. Its transformed bytes are the same as the source +// bytes; it does not replace invalid UTF-8 sequences. +var Nop Encoding = nop{} + +type nop struct{} + +func (nop) NewDecoder() *Decoder { + return &Decoder{Transformer: transform.Nop} +} +func (nop) NewEncoder() *Encoder { + return &Encoder{Transformer: transform.Nop} +} + +// Replacement is the replacement encoding. Decoding from the replacement +// encoding yields a single '\uFFFD' replacement rune. Encoding from UTF-8 to +// the replacement encoding yields the same as the source bytes except that +// invalid UTF-8 is converted to '\uFFFD'. +// +// It is defined at http://encoding.spec.whatwg.org/#replacement +var Replacement Encoding = replacement{} + +type replacement struct{} + +func (replacement) NewDecoder() *Decoder { + return &Decoder{Transformer: replacementDecoder{}} +} + +func (replacement) NewEncoder() *Encoder { + return &Encoder{Transformer: replacementEncoder{}} +} + +func (replacement) ID() (mib identifier.MIB, other string) { + return identifier.Replacement, "" +} + +type replacementDecoder struct{ transform.NopResetter } + +func (replacementDecoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) { + if len(dst) < 3 { + return 0, 0, transform.ErrShortDst + } + if atEOF { + const fffd = "\ufffd" + dst[0] = fffd[0] + dst[1] = fffd[1] + dst[2] = fffd[2] + nDst = 3 + } + return nDst, len(src), nil +} + +type replacementEncoder struct{ transform.NopResetter } + +func (replacementEncoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) { + r, size := rune(0), 0 + + for ; nSrc < len(src); nSrc += size { + r = rune(src[nSrc]) + + // Decode a 1-byte rune. + if r < utf8.RuneSelf { + size = 1 + + } else { + // Decode a multi-byte rune. + r, size = utf8.DecodeRune(src[nSrc:]) + if size == 1 { + // All valid runes of size 1 (those below utf8.RuneSelf) were + // handled above. We have invalid UTF-8 or we haven't seen the + // full character yet. + if !atEOF && !utf8.FullRune(src[nSrc:]) { + err = transform.ErrShortSrc + break + } + r = '\ufffd' + } + } + + if nDst+utf8.RuneLen(r) > len(dst) { + err = transform.ErrShortDst + break + } + nDst += utf8.EncodeRune(dst[nDst:], r) + } + return nDst, nSrc, err +} + +// HTMLEscapeUnsupported wraps encoders to replace source runes outside the +// repertoire of the destination encoding with HTML escape sequences. +// +// This wrapper exists to comply to URL and HTML forms requiring a +// non-terminating legacy encoder. The produced sequences may lead to data +// loss as they are indistinguishable from legitimate input. To avoid this +// issue, use UTF-8 encodings whenever possible. +func HTMLEscapeUnsupported(e *Encoder) *Encoder { + return &Encoder{Transformer: &errorHandler{e, errorToHTML}} +} + +// ReplaceUnsupported wraps encoders to replace source runes outside the +// repertoire of the destination encoding with an encoding-specific +// replacement. +// +// This wrapper is only provided for backwards compatibility and legacy +// handling. Its use is strongly discouraged. Use UTF-8 whenever possible. +func ReplaceUnsupported(e *Encoder) *Encoder { + return &Encoder{Transformer: &errorHandler{e, errorToReplacement}} +} + +type errorHandler struct { + *Encoder + handler func(dst []byte, r rune, err repertoireError) (n int, ok bool) +} + +// TODO: consider making this error public in some form. +type repertoireError interface { + Replacement() byte +} + +func (h errorHandler) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) { + nDst, nSrc, err = h.Transformer.Transform(dst, src, atEOF) + for err != nil { + rerr, ok := err.(repertoireError) + if !ok { + return nDst, nSrc, err + } + r, sz := utf8.DecodeRune(src[nSrc:]) + n, ok := h.handler(dst[nDst:], r, rerr) + if !ok { + return nDst, nSrc, transform.ErrShortDst + } + err = nil + nDst += n + if nSrc += sz; nSrc < len(src) { + var dn, sn int + dn, sn, err = h.Transformer.Transform(dst[nDst:], src[nSrc:], atEOF) + nDst += dn + nSrc += sn + } + } + return nDst, nSrc, err +} + +func errorToHTML(dst []byte, r rune, err repertoireError) (n int, ok bool) { + buf := [8]byte{} + b := strconv.AppendUint(buf[:0], uint64(r), 10) + if n = len(b) + len("&#;"); n >= len(dst) { + return 0, false + } + dst[0] = '&' + dst[1] = '#' + dst[copy(dst[2:], b)+2] = ';' + return n, true +} + +func errorToReplacement(dst []byte, r rune, err repertoireError) (n int, ok bool) { + if len(dst) == 0 { + return 0, false + } + dst[0] = err.Replacement() + return 1, true +} + +// ErrInvalidUTF8 means that a transformer encountered invalid UTF-8. +var ErrInvalidUTF8 = errors.New("encoding: invalid UTF-8") + +// UTF8Validator is a transformer that returns ErrInvalidUTF8 on the first +// input byte that is not valid UTF-8. +var UTF8Validator transform.Transformer = utf8Validator{} + +type utf8Validator struct{ transform.NopResetter } + +func (utf8Validator) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) { + n := len(src) + if n > len(dst) { + n = len(dst) + } + for i := 0; i < n; { + if c := src[i]; c < utf8.RuneSelf { + dst[i] = c + i++ + continue + } + _, size := utf8.DecodeRune(src[i:]) + if size == 1 { + // All valid runes of size 1 (those below utf8.RuneSelf) were + // handled above. We have invalid UTF-8 or we haven't seen the + // full character yet. + err = ErrInvalidUTF8 + if !atEOF && !utf8.FullRune(src[i:]) { + err = transform.ErrShortSrc + } + return i, i, err + } + if i+size > len(dst) { + return i, i, transform.ErrShortDst + } + for ; size > 0; size-- { + dst[i] = src[i] + i++ + } + } + if len(src) > len(dst) { + err = transform.ErrShortDst + } + return n, n, err +} diff --git a/vendor/golang.org/x/text/encoding/internal/identifier/identifier.go b/vendor/golang.org/x/text/encoding/internal/identifier/identifier.go new file mode 100644 index 0000000..5c9b85c --- /dev/null +++ b/vendor/golang.org/x/text/encoding/internal/identifier/identifier.go @@ -0,0 +1,81 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:generate go run gen.go + +// Package identifier defines the contract between implementations of Encoding +// and Index by defining identifiers that uniquely identify standardized coded +// character sets (CCS) and character encoding schemes (CES), which we will +// together refer to as encodings, for which Encoding implementations provide +// converters to and from UTF-8. This package is typically only of concern to +// implementers of Indexes and Encodings. +// +// One part of the identifier is the MIB code, which is defined by IANA and +// uniquely identifies a CCS or CES. Each code is associated with data that +// references authorities, official documentation as well as aliases and MIME +// names. +// +// Not all CESs are covered by the IANA registry. The "other" string that is +// returned by ID can be used to identify other character sets or versions of +// existing ones. +// +// It is recommended that each package that provides a set of Encodings provide +// the All and Common variables to reference all supported encodings and +// commonly used subset. This allows Index implementations to include all +// available encodings without explicitly referencing or knowing about them. +package identifier + +// Note: this package is internal, but could be made public if there is a need +// for writing third-party Indexes and Encodings. + +// References: +// - http://source.icu-project.org/repos/icu/icu/trunk/source/data/mappings/convrtrs.txt +// - http://www.iana.org/assignments/character-sets/character-sets.xhtml +// - http://www.iana.org/assignments/ianacharset-mib/ianacharset-mib +// - http://www.ietf.org/rfc/rfc2978.txt +// - https://www.unicode.org/reports/tr22/ +// - http://www.w3.org/TR/encoding/ +// - https://encoding.spec.whatwg.org/ +// - https://encoding.spec.whatwg.org/encodings.json +// - https://tools.ietf.org/html/rfc6657#section-5 + +// Interface can be implemented by Encodings to define the CCS or CES for which +// it implements conversions. +type Interface interface { + // ID returns an encoding identifier. Exactly one of the mib and other + // values should be non-zero. + // + // In the usual case it is only necessary to indicate the MIB code. The + // other string can be used to specify encodings for which there is no MIB, + // such as "x-mac-dingbat". + // + // The other string may only contain the characters a-z, A-Z, 0-9, - and _. + ID() (mib MIB, other string) + + // NOTE: the restrictions on the encoding are to allow extending the syntax + // with additional information such as versions, vendors and other variants. +} + +// A MIB identifies an encoding. It is derived from the IANA MIB codes and adds +// some identifiers for some encodings that are not covered by the IANA +// standard. +// +// See http://www.iana.org/assignments/ianacharset-mib. +type MIB uint16 + +// These additional MIB types are not defined in IANA. They are added because +// they are common and defined within the text repo. +const ( + // Unofficial marks the start of encodings not registered by IANA. + Unofficial MIB = 10000 + iota + + // Replacement is the WhatWG replacement encoding. + Replacement + + // XUserDefined is the code for x-user-defined. + XUserDefined + + // MacintoshCyrillic is the code for x-mac-cyrillic. + MacintoshCyrillic +) diff --git a/vendor/golang.org/x/text/encoding/internal/identifier/mib.go b/vendor/golang.org/x/text/encoding/internal/identifier/mib.go new file mode 100644 index 0000000..fc7df1b --- /dev/null +++ b/vendor/golang.org/x/text/encoding/internal/identifier/mib.go @@ -0,0 +1,1619 @@ +// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. + +package identifier + +const ( + // ASCII is the MIB identifier with IANA name US-ASCII (MIME: US-ASCII). + // + // ANSI X3.4-1986 + // Reference: RFC2046 + ASCII MIB = 3 + + // ISOLatin1 is the MIB identifier with IANA name ISO_8859-1:1987 (MIME: ISO-8859-1). + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISOLatin1 MIB = 4 + + // ISOLatin2 is the MIB identifier with IANA name ISO_8859-2:1987 (MIME: ISO-8859-2). + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISOLatin2 MIB = 5 + + // ISOLatin3 is the MIB identifier with IANA name ISO_8859-3:1988 (MIME: ISO-8859-3). + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISOLatin3 MIB = 6 + + // ISOLatin4 is the MIB identifier with IANA name ISO_8859-4:1988 (MIME: ISO-8859-4). + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISOLatin4 MIB = 7 + + // ISOLatinCyrillic is the MIB identifier with IANA name ISO_8859-5:1988 (MIME: ISO-8859-5). + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISOLatinCyrillic MIB = 8 + + // ISOLatinArabic is the MIB identifier with IANA name ISO_8859-6:1987 (MIME: ISO-8859-6). + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISOLatinArabic MIB = 9 + + // ISOLatinGreek is the MIB identifier with IANA name ISO_8859-7:1987 (MIME: ISO-8859-7). + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1947 + // Reference: RFC1345 + ISOLatinGreek MIB = 10 + + // ISOLatinHebrew is the MIB identifier with IANA name ISO_8859-8:1988 (MIME: ISO-8859-8). + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISOLatinHebrew MIB = 11 + + // ISOLatin5 is the MIB identifier with IANA name ISO_8859-9:1989 (MIME: ISO-8859-9). + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISOLatin5 MIB = 12 + + // ISOLatin6 is the MIB identifier with IANA name ISO-8859-10 (MIME: ISO-8859-10). + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISOLatin6 MIB = 13 + + // ISOTextComm is the MIB identifier with IANA name ISO_6937-2-add. + // + // ISO-IR: International Register of Escape Sequences and ISO 6937-2:1983 + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISOTextComm MIB = 14 + + // HalfWidthKatakana is the MIB identifier with IANA name JIS_X0201. + // + // JIS X 0201-1976. One byte only, this is equivalent to + // JIS/Roman (similar to ASCII) plus eight-bit half-width + // Katakana + // Reference: RFC1345 + HalfWidthKatakana MIB = 15 + + // JISEncoding is the MIB identifier with IANA name JIS_Encoding. + // + // JIS X 0202-1991. Uses ISO 2022 escape sequences to + // shift code sets as documented in JIS X 0202-1991. + JISEncoding MIB = 16 + + // ShiftJIS is the MIB identifier with IANA name Shift_JIS (MIME: Shift_JIS). + // + // This charset is an extension of csHalfWidthKatakana by + // adding graphic characters in JIS X 0208. The CCS's are + // JIS X0201:1997 and JIS X0208:1997. The + // complete definition is shown in Appendix 1 of JIS + // X0208:1997. + // This charset can be used for the top-level media type "text". + ShiftJIS MIB = 17 + + // EUCPkdFmtJapanese is the MIB identifier with IANA name Extended_UNIX_Code_Packed_Format_for_Japanese (MIME: EUC-JP). + // + // Standardized by OSF, UNIX International, and UNIX Systems + // Laboratories Pacific. Uses ISO 2022 rules to select + // code set 0: US-ASCII (a single 7-bit byte set) + // code set 1: JIS X0208-1990 (a double 8-bit byte set) + // restricted to A0-FF in both bytes + // code set 2: Half Width Katakana (a single 7-bit byte set) + // requiring SS2 as the character prefix + // code set 3: JIS X0212-1990 (a double 7-bit byte set) + // restricted to A0-FF in both bytes + // requiring SS3 as the character prefix + EUCPkdFmtJapanese MIB = 18 + + // EUCFixWidJapanese is the MIB identifier with IANA name Extended_UNIX_Code_Fixed_Width_for_Japanese. + // + // Used in Japan. Each character is 2 octets. + // code set 0: US-ASCII (a single 7-bit byte set) + // 1st byte = 00 + // 2nd byte = 20-7E + // code set 1: JIS X0208-1990 (a double 7-bit byte set) + // restricted to A0-FF in both bytes + // code set 2: Half Width Katakana (a single 7-bit byte set) + // 1st byte = 00 + // 2nd byte = A0-FF + // code set 3: JIS X0212-1990 (a double 7-bit byte set) + // restricted to A0-FF in + // the first byte + // and 21-7E in the second byte + EUCFixWidJapanese MIB = 19 + + // ISO4UnitedKingdom is the MIB identifier with IANA name BS_4730. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO4UnitedKingdom MIB = 20 + + // ISO11SwedishForNames is the MIB identifier with IANA name SEN_850200_C. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO11SwedishForNames MIB = 21 + + // ISO15Italian is the MIB identifier with IANA name IT. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO15Italian MIB = 22 + + // ISO17Spanish is the MIB identifier with IANA name ES. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO17Spanish MIB = 23 + + // ISO21German is the MIB identifier with IANA name DIN_66003. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO21German MIB = 24 + + // ISO60Norwegian1 is the MIB identifier with IANA name NS_4551-1. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO60Norwegian1 MIB = 25 + + // ISO69French is the MIB identifier with IANA name NF_Z_62-010. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO69French MIB = 26 + + // ISO10646UTF1 is the MIB identifier with IANA name ISO-10646-UTF-1. + // + // Universal Transfer Format (1), this is the multibyte + // encoding, that subsets ASCII-7. It does not have byte + // ordering issues. + ISO10646UTF1 MIB = 27 + + // ISO646basic1983 is the MIB identifier with IANA name ISO_646.basic:1983. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO646basic1983 MIB = 28 + + // INVARIANT is the MIB identifier with IANA name INVARIANT. + // + // Reference: RFC1345 + INVARIANT MIB = 29 + + // ISO2IntlRefVersion is the MIB identifier with IANA name ISO_646.irv:1983. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO2IntlRefVersion MIB = 30 + + // NATSSEFI is the MIB identifier with IANA name NATS-SEFI. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + NATSSEFI MIB = 31 + + // NATSSEFIADD is the MIB identifier with IANA name NATS-SEFI-ADD. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + NATSSEFIADD MIB = 32 + + // NATSDANO is the MIB identifier with IANA name NATS-DANO. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + NATSDANO MIB = 33 + + // NATSDANOADD is the MIB identifier with IANA name NATS-DANO-ADD. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + NATSDANOADD MIB = 34 + + // ISO10Swedish is the MIB identifier with IANA name SEN_850200_B. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO10Swedish MIB = 35 + + // KSC56011987 is the MIB identifier with IANA name KS_C_5601-1987. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + KSC56011987 MIB = 36 + + // ISO2022KR is the MIB identifier with IANA name ISO-2022-KR (MIME: ISO-2022-KR). + // + // rfc1557 (see also KS_C_5601-1987) + // Reference: RFC1557 + ISO2022KR MIB = 37 + + // EUCKR is the MIB identifier with IANA name EUC-KR (MIME: EUC-KR). + // + // rfc1557 (see also KS_C_5861-1992) + // Reference: RFC1557 + EUCKR MIB = 38 + + // ISO2022JP is the MIB identifier with IANA name ISO-2022-JP (MIME: ISO-2022-JP). + // + // rfc1468 (see also rfc2237 ) + // Reference: RFC1468 + ISO2022JP MIB = 39 + + // ISO2022JP2 is the MIB identifier with IANA name ISO-2022-JP-2 (MIME: ISO-2022-JP-2). + // + // rfc1554 + // Reference: RFC1554 + ISO2022JP2 MIB = 40 + + // ISO13JISC6220jp is the MIB identifier with IANA name JIS_C6220-1969-jp. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO13JISC6220jp MIB = 41 + + // ISO14JISC6220ro is the MIB identifier with IANA name JIS_C6220-1969-ro. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO14JISC6220ro MIB = 42 + + // ISO16Portuguese is the MIB identifier with IANA name PT. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO16Portuguese MIB = 43 + + // ISO18Greek7Old is the MIB identifier with IANA name greek7-old. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO18Greek7Old MIB = 44 + + // ISO19LatinGreek is the MIB identifier with IANA name latin-greek. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO19LatinGreek MIB = 45 + + // ISO25French is the MIB identifier with IANA name NF_Z_62-010_(1973). + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO25French MIB = 46 + + // ISO27LatinGreek1 is the MIB identifier with IANA name Latin-greek-1. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO27LatinGreek1 MIB = 47 + + // ISO5427Cyrillic is the MIB identifier with IANA name ISO_5427. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO5427Cyrillic MIB = 48 + + // ISO42JISC62261978 is the MIB identifier with IANA name JIS_C6226-1978. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO42JISC62261978 MIB = 49 + + // ISO47BSViewdata is the MIB identifier with IANA name BS_viewdata. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO47BSViewdata MIB = 50 + + // ISO49INIS is the MIB identifier with IANA name INIS. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO49INIS MIB = 51 + + // ISO50INIS8 is the MIB identifier with IANA name INIS-8. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO50INIS8 MIB = 52 + + // ISO51INISCyrillic is the MIB identifier with IANA name INIS-cyrillic. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO51INISCyrillic MIB = 53 + + // ISO54271981 is the MIB identifier with IANA name ISO_5427:1981. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO54271981 MIB = 54 + + // ISO5428Greek is the MIB identifier with IANA name ISO_5428:1980. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO5428Greek MIB = 55 + + // ISO57GB1988 is the MIB identifier with IANA name GB_1988-80. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO57GB1988 MIB = 56 + + // ISO58GB231280 is the MIB identifier with IANA name GB_2312-80. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO58GB231280 MIB = 57 + + // ISO61Norwegian2 is the MIB identifier with IANA name NS_4551-2. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO61Norwegian2 MIB = 58 + + // ISO70VideotexSupp1 is the MIB identifier with IANA name videotex-suppl. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO70VideotexSupp1 MIB = 59 + + // ISO84Portuguese2 is the MIB identifier with IANA name PT2. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO84Portuguese2 MIB = 60 + + // ISO85Spanish2 is the MIB identifier with IANA name ES2. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO85Spanish2 MIB = 61 + + // ISO86Hungarian is the MIB identifier with IANA name MSZ_7795.3. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO86Hungarian MIB = 62 + + // ISO87JISX0208 is the MIB identifier with IANA name JIS_C6226-1983. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO87JISX0208 MIB = 63 + + // ISO88Greek7 is the MIB identifier with IANA name greek7. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO88Greek7 MIB = 64 + + // ISO89ASMO449 is the MIB identifier with IANA name ASMO_449. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO89ASMO449 MIB = 65 + + // ISO90 is the MIB identifier with IANA name iso-ir-90. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO90 MIB = 66 + + // ISO91JISC62291984a is the MIB identifier with IANA name JIS_C6229-1984-a. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO91JISC62291984a MIB = 67 + + // ISO92JISC62991984b is the MIB identifier with IANA name JIS_C6229-1984-b. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO92JISC62991984b MIB = 68 + + // ISO93JIS62291984badd is the MIB identifier with IANA name JIS_C6229-1984-b-add. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO93JIS62291984badd MIB = 69 + + // ISO94JIS62291984hand is the MIB identifier with IANA name JIS_C6229-1984-hand. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO94JIS62291984hand MIB = 70 + + // ISO95JIS62291984handadd is the MIB identifier with IANA name JIS_C6229-1984-hand-add. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO95JIS62291984handadd MIB = 71 + + // ISO96JISC62291984kana is the MIB identifier with IANA name JIS_C6229-1984-kana. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO96JISC62291984kana MIB = 72 + + // ISO2033 is the MIB identifier with IANA name ISO_2033-1983. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO2033 MIB = 73 + + // ISO99NAPLPS is the MIB identifier with IANA name ANSI_X3.110-1983. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO99NAPLPS MIB = 74 + + // ISO102T617bit is the MIB identifier with IANA name T.61-7bit. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO102T617bit MIB = 75 + + // ISO103T618bit is the MIB identifier with IANA name T.61-8bit. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO103T618bit MIB = 76 + + // ISO111ECMACyrillic is the MIB identifier with IANA name ECMA-cyrillic. + // + // ISO registry + ISO111ECMACyrillic MIB = 77 + + // ISO121Canadian1 is the MIB identifier with IANA name CSA_Z243.4-1985-1. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO121Canadian1 MIB = 78 + + // ISO122Canadian2 is the MIB identifier with IANA name CSA_Z243.4-1985-2. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO122Canadian2 MIB = 79 + + // ISO123CSAZ24341985gr is the MIB identifier with IANA name CSA_Z243.4-1985-gr. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO123CSAZ24341985gr MIB = 80 + + // ISO88596E is the MIB identifier with IANA name ISO_8859-6-E (MIME: ISO-8859-6-E). + // + // rfc1556 + // Reference: RFC1556 + ISO88596E MIB = 81 + + // ISO88596I is the MIB identifier with IANA name ISO_8859-6-I (MIME: ISO-8859-6-I). + // + // rfc1556 + // Reference: RFC1556 + ISO88596I MIB = 82 + + // ISO128T101G2 is the MIB identifier with IANA name T.101-G2. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO128T101G2 MIB = 83 + + // ISO88598E is the MIB identifier with IANA name ISO_8859-8-E (MIME: ISO-8859-8-E). + // + // rfc1556 + // Reference: RFC1556 + ISO88598E MIB = 84 + + // ISO88598I is the MIB identifier with IANA name ISO_8859-8-I (MIME: ISO-8859-8-I). + // + // rfc1556 + // Reference: RFC1556 + ISO88598I MIB = 85 + + // ISO139CSN369103 is the MIB identifier with IANA name CSN_369103. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO139CSN369103 MIB = 86 + + // ISO141JUSIB1002 is the MIB identifier with IANA name JUS_I.B1.002. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO141JUSIB1002 MIB = 87 + + // ISO143IECP271 is the MIB identifier with IANA name IEC_P27-1. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO143IECP271 MIB = 88 + + // ISO146Serbian is the MIB identifier with IANA name JUS_I.B1.003-serb. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO146Serbian MIB = 89 + + // ISO147Macedonian is the MIB identifier with IANA name JUS_I.B1.003-mac. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO147Macedonian MIB = 90 + + // ISO150GreekCCITT is the MIB identifier with IANA name greek-ccitt. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO150GreekCCITT MIB = 91 + + // ISO151Cuba is the MIB identifier with IANA name NC_NC00-10:81. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO151Cuba MIB = 92 + + // ISO6937Add is the MIB identifier with IANA name ISO_6937-2-25. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO6937Add MIB = 93 + + // ISO153GOST1976874 is the MIB identifier with IANA name GOST_19768-74. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO153GOST1976874 MIB = 94 + + // ISO8859Supp is the MIB identifier with IANA name ISO_8859-supp. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO8859Supp MIB = 95 + + // ISO10367Box is the MIB identifier with IANA name ISO_10367-box. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO10367Box MIB = 96 + + // ISO158Lap is the MIB identifier with IANA name latin-lap. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO158Lap MIB = 97 + + // ISO159JISX02121990 is the MIB identifier with IANA name JIS_X0212-1990. + // + // ISO-IR: International Register of Escape Sequences + // Note: The current registration authority is IPSJ/ITSCJ, Japan. + // Reference: RFC1345 + ISO159JISX02121990 MIB = 98 + + // ISO646Danish is the MIB identifier with IANA name DS_2089. + // + // Danish Standard, DS 2089, February 1974 + // Reference: RFC1345 + ISO646Danish MIB = 99 + + // USDK is the MIB identifier with IANA name us-dk. + // + // Reference: RFC1345 + USDK MIB = 100 + + // DKUS is the MIB identifier with IANA name dk-us. + // + // Reference: RFC1345 + DKUS MIB = 101 + + // KSC5636 is the MIB identifier with IANA name KSC5636. + // + // Reference: RFC1345 + KSC5636 MIB = 102 + + // Unicode11UTF7 is the MIB identifier with IANA name UNICODE-1-1-UTF-7. + // + // rfc1642 + // Reference: RFC1642 + Unicode11UTF7 MIB = 103 + + // ISO2022CN is the MIB identifier with IANA name ISO-2022-CN. + // + // rfc1922 + // Reference: RFC1922 + ISO2022CN MIB = 104 + + // ISO2022CNEXT is the MIB identifier with IANA name ISO-2022-CN-EXT. + // + // rfc1922 + // Reference: RFC1922 + ISO2022CNEXT MIB = 105 + + // UTF8 is the MIB identifier with IANA name UTF-8. + // + // rfc3629 + // Reference: RFC3629 + UTF8 MIB = 106 + + // ISO885913 is the MIB identifier with IANA name ISO-8859-13. + // + // ISO See https://www.iana.org/assignments/charset-reg/ISO-8859-13 https://www.iana.org/assignments/charset-reg/ISO-8859-13 + ISO885913 MIB = 109 + + // ISO885914 is the MIB identifier with IANA name ISO-8859-14. + // + // ISO See https://www.iana.org/assignments/charset-reg/ISO-8859-14 + ISO885914 MIB = 110 + + // ISO885915 is the MIB identifier with IANA name ISO-8859-15. + // + // ISO + // Please see: https://www.iana.org/assignments/charset-reg/ISO-8859-15 + ISO885915 MIB = 111 + + // ISO885916 is the MIB identifier with IANA name ISO-8859-16. + // + // ISO + ISO885916 MIB = 112 + + // GBK is the MIB identifier with IANA name GBK. + // + // Chinese IT Standardization Technical Committee + // Please see: https://www.iana.org/assignments/charset-reg/GBK + GBK MIB = 113 + + // GB18030 is the MIB identifier with IANA name GB18030. + // + // Chinese IT Standardization Technical Committee + // Please see: https://www.iana.org/assignments/charset-reg/GB18030 + GB18030 MIB = 114 + + // OSDEBCDICDF0415 is the MIB identifier with IANA name OSD_EBCDIC_DF04_15. + // + // Fujitsu-Siemens standard mainframe EBCDIC encoding + // Please see: https://www.iana.org/assignments/charset-reg/OSD-EBCDIC-DF04-15 + OSDEBCDICDF0415 MIB = 115 + + // OSDEBCDICDF03IRV is the MIB identifier with IANA name OSD_EBCDIC_DF03_IRV. + // + // Fujitsu-Siemens standard mainframe EBCDIC encoding + // Please see: https://www.iana.org/assignments/charset-reg/OSD-EBCDIC-DF03-IRV + OSDEBCDICDF03IRV MIB = 116 + + // OSDEBCDICDF041 is the MIB identifier with IANA name OSD_EBCDIC_DF04_1. + // + // Fujitsu-Siemens standard mainframe EBCDIC encoding + // Please see: https://www.iana.org/assignments/charset-reg/OSD-EBCDIC-DF04-1 + OSDEBCDICDF041 MIB = 117 + + // ISO115481 is the MIB identifier with IANA name ISO-11548-1. + // + // See https://www.iana.org/assignments/charset-reg/ISO-11548-1 + ISO115481 MIB = 118 + + // KZ1048 is the MIB identifier with IANA name KZ-1048. + // + // See https://www.iana.org/assignments/charset-reg/KZ-1048 + KZ1048 MIB = 119 + + // Unicode is the MIB identifier with IANA name ISO-10646-UCS-2. + // + // the 2-octet Basic Multilingual Plane, aka Unicode + // this needs to specify network byte order: the standard + // does not specify (it is a 16-bit integer space) + Unicode MIB = 1000 + + // UCS4 is the MIB identifier with IANA name ISO-10646-UCS-4. + // + // the full code space. (same comment about byte order, + // these are 31-bit numbers. + UCS4 MIB = 1001 + + // UnicodeASCII is the MIB identifier with IANA name ISO-10646-UCS-Basic. + // + // ASCII subset of Unicode. Basic Latin = collection 1 + // See ISO 10646, Appendix A + UnicodeASCII MIB = 1002 + + // UnicodeLatin1 is the MIB identifier with IANA name ISO-10646-Unicode-Latin1. + // + // ISO Latin-1 subset of Unicode. Basic Latin and Latin-1 + // Supplement = collections 1 and 2. See ISO 10646, + // Appendix A. See rfc1815 . + UnicodeLatin1 MIB = 1003 + + // UnicodeJapanese is the MIB identifier with IANA name ISO-10646-J-1. + // + // ISO 10646 Japanese, see rfc1815 . + UnicodeJapanese MIB = 1004 + + // UnicodeIBM1261 is the MIB identifier with IANA name ISO-Unicode-IBM-1261. + // + // IBM Latin-2, -3, -5, Extended Presentation Set, GCSGID: 1261 + UnicodeIBM1261 MIB = 1005 + + // UnicodeIBM1268 is the MIB identifier with IANA name ISO-Unicode-IBM-1268. + // + // IBM Latin-4 Extended Presentation Set, GCSGID: 1268 + UnicodeIBM1268 MIB = 1006 + + // UnicodeIBM1276 is the MIB identifier with IANA name ISO-Unicode-IBM-1276. + // + // IBM Cyrillic Greek Extended Presentation Set, GCSGID: 1276 + UnicodeIBM1276 MIB = 1007 + + // UnicodeIBM1264 is the MIB identifier with IANA name ISO-Unicode-IBM-1264. + // + // IBM Arabic Presentation Set, GCSGID: 1264 + UnicodeIBM1264 MIB = 1008 + + // UnicodeIBM1265 is the MIB identifier with IANA name ISO-Unicode-IBM-1265. + // + // IBM Hebrew Presentation Set, GCSGID: 1265 + UnicodeIBM1265 MIB = 1009 + + // Unicode11 is the MIB identifier with IANA name UNICODE-1-1. + // + // rfc1641 + // Reference: RFC1641 + Unicode11 MIB = 1010 + + // SCSU is the MIB identifier with IANA name SCSU. + // + // SCSU See https://www.iana.org/assignments/charset-reg/SCSU + SCSU MIB = 1011 + + // UTF7 is the MIB identifier with IANA name UTF-7. + // + // rfc2152 + // Reference: RFC2152 + UTF7 MIB = 1012 + + // UTF16BE is the MIB identifier with IANA name UTF-16BE. + // + // rfc2781 + // Reference: RFC2781 + UTF16BE MIB = 1013 + + // UTF16LE is the MIB identifier with IANA name UTF-16LE. + // + // rfc2781 + // Reference: RFC2781 + UTF16LE MIB = 1014 + + // UTF16 is the MIB identifier with IANA name UTF-16. + // + // rfc2781 + // Reference: RFC2781 + UTF16 MIB = 1015 + + // CESU8 is the MIB identifier with IANA name CESU-8. + // + // https://www.unicode.org/reports/tr26 + CESU8 MIB = 1016 + + // UTF32 is the MIB identifier with IANA name UTF-32. + // + // https://www.unicode.org/reports/tr19/ + UTF32 MIB = 1017 + + // UTF32BE is the MIB identifier with IANA name UTF-32BE. + // + // https://www.unicode.org/reports/tr19/ + UTF32BE MIB = 1018 + + // UTF32LE is the MIB identifier with IANA name UTF-32LE. + // + // https://www.unicode.org/reports/tr19/ + UTF32LE MIB = 1019 + + // BOCU1 is the MIB identifier with IANA name BOCU-1. + // + // https://www.unicode.org/notes/tn6/ + BOCU1 MIB = 1020 + + // Windows30Latin1 is the MIB identifier with IANA name ISO-8859-1-Windows-3.0-Latin-1. + // + // Extended ISO 8859-1 Latin-1 for Windows 3.0. + // PCL Symbol Set id: 9U + Windows30Latin1 MIB = 2000 + + // Windows31Latin1 is the MIB identifier with IANA name ISO-8859-1-Windows-3.1-Latin-1. + // + // Extended ISO 8859-1 Latin-1 for Windows 3.1. + // PCL Symbol Set id: 19U + Windows31Latin1 MIB = 2001 + + // Windows31Latin2 is the MIB identifier with IANA name ISO-8859-2-Windows-Latin-2. + // + // Extended ISO 8859-2. Latin-2 for Windows 3.1. + // PCL Symbol Set id: 9E + Windows31Latin2 MIB = 2002 + + // Windows31Latin5 is the MIB identifier with IANA name ISO-8859-9-Windows-Latin-5. + // + // Extended ISO 8859-9. Latin-5 for Windows 3.1 + // PCL Symbol Set id: 5T + Windows31Latin5 MIB = 2003 + + // HPRoman8 is the MIB identifier with IANA name hp-roman8. + // + // LaserJet IIP Printer User's Manual, + // HP part no 33471-90901, Hewlet-Packard, June 1989. + // Reference: RFC1345 + HPRoman8 MIB = 2004 + + // AdobeStandardEncoding is the MIB identifier with IANA name Adobe-Standard-Encoding. + // + // PostScript Language Reference Manual + // PCL Symbol Set id: 10J + AdobeStandardEncoding MIB = 2005 + + // VenturaUS is the MIB identifier with IANA name Ventura-US. + // + // Ventura US. ASCII plus characters typically used in + // publishing, like pilcrow, copyright, registered, trade mark, + // section, dagger, and double dagger in the range A0 (hex) + // to FF (hex). + // PCL Symbol Set id: 14J + VenturaUS MIB = 2006 + + // VenturaInternational is the MIB identifier with IANA name Ventura-International. + // + // Ventura International. ASCII plus coded characters similar + // to Roman8. + // PCL Symbol Set id: 13J + VenturaInternational MIB = 2007 + + // DECMCS is the MIB identifier with IANA name DEC-MCS. + // + // VAX/VMS User's Manual, + // Order Number: AI-Y517A-TE, April 1986. + // Reference: RFC1345 + DECMCS MIB = 2008 + + // PC850Multilingual is the MIB identifier with IANA name IBM850. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + PC850Multilingual MIB = 2009 + + // PC8DanishNorwegian is the MIB identifier with IANA name PC8-Danish-Norwegian. + // + // PC Danish Norwegian + // 8-bit PC set for Danish Norwegian + // PCL Symbol Set id: 11U + PC8DanishNorwegian MIB = 2012 + + // PC862LatinHebrew is the MIB identifier with IANA name IBM862. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + PC862LatinHebrew MIB = 2013 + + // PC8Turkish is the MIB identifier with IANA name PC8-Turkish. + // + // PC Latin Turkish. PCL Symbol Set id: 9T + PC8Turkish MIB = 2014 + + // IBMSymbols is the MIB identifier with IANA name IBM-Symbols. + // + // Presentation Set, CPGID: 259 + IBMSymbols MIB = 2015 + + // IBMThai is the MIB identifier with IANA name IBM-Thai. + // + // Presentation Set, CPGID: 838 + IBMThai MIB = 2016 + + // HPLegal is the MIB identifier with IANA name HP-Legal. + // + // PCL 5 Comparison Guide, Hewlett-Packard, + // HP part number 5961-0510, October 1992 + // PCL Symbol Set id: 1U + HPLegal MIB = 2017 + + // HPPiFont is the MIB identifier with IANA name HP-Pi-font. + // + // PCL 5 Comparison Guide, Hewlett-Packard, + // HP part number 5961-0510, October 1992 + // PCL Symbol Set id: 15U + HPPiFont MIB = 2018 + + // HPMath8 is the MIB identifier with IANA name HP-Math8. + // + // PCL 5 Comparison Guide, Hewlett-Packard, + // HP part number 5961-0510, October 1992 + // PCL Symbol Set id: 8M + HPMath8 MIB = 2019 + + // HPPSMath is the MIB identifier with IANA name Adobe-Symbol-Encoding. + // + // PostScript Language Reference Manual + // PCL Symbol Set id: 5M + HPPSMath MIB = 2020 + + // HPDesktop is the MIB identifier with IANA name HP-DeskTop. + // + // PCL 5 Comparison Guide, Hewlett-Packard, + // HP part number 5961-0510, October 1992 + // PCL Symbol Set id: 7J + HPDesktop MIB = 2021 + + // VenturaMath is the MIB identifier with IANA name Ventura-Math. + // + // PCL 5 Comparison Guide, Hewlett-Packard, + // HP part number 5961-0510, October 1992 + // PCL Symbol Set id: 6M + VenturaMath MIB = 2022 + + // MicrosoftPublishing is the MIB identifier with IANA name Microsoft-Publishing. + // + // PCL 5 Comparison Guide, Hewlett-Packard, + // HP part number 5961-0510, October 1992 + // PCL Symbol Set id: 6J + MicrosoftPublishing MIB = 2023 + + // Windows31J is the MIB identifier with IANA name Windows-31J. + // + // Windows Japanese. A further extension of Shift_JIS + // to include NEC special characters (Row 13), NEC + // selection of IBM extensions (Rows 89 to 92), and IBM + // extensions (Rows 115 to 119). The CCS's are + // JIS X0201:1997, JIS X0208:1997, and these extensions. + // This charset can be used for the top-level media type "text", + // but it is of limited or specialized use (see rfc2278 ). + // PCL Symbol Set id: 19K + Windows31J MIB = 2024 + + // GB2312 is the MIB identifier with IANA name GB2312 (MIME: GB2312). + // + // Chinese for People's Republic of China (PRC) mixed one byte, + // two byte set: + // 20-7E = one byte ASCII + // A1-FE = two byte PRC Kanji + // See GB 2312-80 + // PCL Symbol Set Id: 18C + GB2312 MIB = 2025 + + // Big5 is the MIB identifier with IANA name Big5 (MIME: Big5). + // + // Chinese for Taiwan Multi-byte set. + // PCL Symbol Set Id: 18T + Big5 MIB = 2026 + + // Macintosh is the MIB identifier with IANA name macintosh. + // + // The Unicode Standard ver1.0, ISBN 0-201-56788-1, Oct 1991 + // Reference: RFC1345 + Macintosh MIB = 2027 + + // IBM037 is the MIB identifier with IANA name IBM037. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + IBM037 MIB = 2028 + + // IBM038 is the MIB identifier with IANA name IBM038. + // + // IBM 3174 Character Set Ref, GA27-3831-02, March 1990 + // Reference: RFC1345 + IBM038 MIB = 2029 + + // IBM273 is the MIB identifier with IANA name IBM273. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + IBM273 MIB = 2030 + + // IBM274 is the MIB identifier with IANA name IBM274. + // + // IBM 3174 Character Set Ref, GA27-3831-02, March 1990 + // Reference: RFC1345 + IBM274 MIB = 2031 + + // IBM275 is the MIB identifier with IANA name IBM275. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + IBM275 MIB = 2032 + + // IBM277 is the MIB identifier with IANA name IBM277. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + IBM277 MIB = 2033 + + // IBM278 is the MIB identifier with IANA name IBM278. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + IBM278 MIB = 2034 + + // IBM280 is the MIB identifier with IANA name IBM280. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + IBM280 MIB = 2035 + + // IBM281 is the MIB identifier with IANA name IBM281. + // + // IBM 3174 Character Set Ref, GA27-3831-02, March 1990 + // Reference: RFC1345 + IBM281 MIB = 2036 + + // IBM284 is the MIB identifier with IANA name IBM284. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + IBM284 MIB = 2037 + + // IBM285 is the MIB identifier with IANA name IBM285. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + IBM285 MIB = 2038 + + // IBM290 is the MIB identifier with IANA name IBM290. + // + // IBM 3174 Character Set Ref, GA27-3831-02, March 1990 + // Reference: RFC1345 + IBM290 MIB = 2039 + + // IBM297 is the MIB identifier with IANA name IBM297. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + IBM297 MIB = 2040 + + // IBM420 is the MIB identifier with IANA name IBM420. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990, + // IBM NLS RM p 11-11 + // Reference: RFC1345 + IBM420 MIB = 2041 + + // IBM423 is the MIB identifier with IANA name IBM423. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + IBM423 MIB = 2042 + + // IBM424 is the MIB identifier with IANA name IBM424. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + IBM424 MIB = 2043 + + // PC8CodePage437 is the MIB identifier with IANA name IBM437. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + PC8CodePage437 MIB = 2011 + + // IBM500 is the MIB identifier with IANA name IBM500. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + IBM500 MIB = 2044 + + // IBM851 is the MIB identifier with IANA name IBM851. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + IBM851 MIB = 2045 + + // PCp852 is the MIB identifier with IANA name IBM852. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + PCp852 MIB = 2010 + + // IBM855 is the MIB identifier with IANA name IBM855. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + IBM855 MIB = 2046 + + // IBM857 is the MIB identifier with IANA name IBM857. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + IBM857 MIB = 2047 + + // IBM860 is the MIB identifier with IANA name IBM860. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + IBM860 MIB = 2048 + + // IBM861 is the MIB identifier with IANA name IBM861. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + IBM861 MIB = 2049 + + // IBM863 is the MIB identifier with IANA name IBM863. + // + // IBM Keyboard layouts and code pages, PN 07G4586 June 1991 + // Reference: RFC1345 + IBM863 MIB = 2050 + + // IBM864 is the MIB identifier with IANA name IBM864. + // + // IBM Keyboard layouts and code pages, PN 07G4586 June 1991 + // Reference: RFC1345 + IBM864 MIB = 2051 + + // IBM865 is the MIB identifier with IANA name IBM865. + // + // IBM DOS 3.3 Ref (Abridged), 94X9575 (Feb 1987) + // Reference: RFC1345 + IBM865 MIB = 2052 + + // IBM868 is the MIB identifier with IANA name IBM868. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + IBM868 MIB = 2053 + + // IBM869 is the MIB identifier with IANA name IBM869. + // + // IBM Keyboard layouts and code pages, PN 07G4586 June 1991 + // Reference: RFC1345 + IBM869 MIB = 2054 + + // IBM870 is the MIB identifier with IANA name IBM870. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + IBM870 MIB = 2055 + + // IBM871 is the MIB identifier with IANA name IBM871. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + IBM871 MIB = 2056 + + // IBM880 is the MIB identifier with IANA name IBM880. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + IBM880 MIB = 2057 + + // IBM891 is the MIB identifier with IANA name IBM891. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + IBM891 MIB = 2058 + + // IBM903 is the MIB identifier with IANA name IBM903. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + IBM903 MIB = 2059 + + // IBBM904 is the MIB identifier with IANA name IBM904. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + IBBM904 MIB = 2060 + + // IBM905 is the MIB identifier with IANA name IBM905. + // + // IBM 3174 Character Set Ref, GA27-3831-02, March 1990 + // Reference: RFC1345 + IBM905 MIB = 2061 + + // IBM918 is the MIB identifier with IANA name IBM918. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + IBM918 MIB = 2062 + + // IBM1026 is the MIB identifier with IANA name IBM1026. + // + // IBM NLS RM Vol2 SE09-8002-01, March 1990 + // Reference: RFC1345 + IBM1026 MIB = 2063 + + // IBMEBCDICATDE is the MIB identifier with IANA name EBCDIC-AT-DE. + // + // IBM 3270 Char Set Ref Ch 10, GA27-2837-9, April 1987 + // Reference: RFC1345 + IBMEBCDICATDE MIB = 2064 + + // EBCDICATDEA is the MIB identifier with IANA name EBCDIC-AT-DE-A. + // + // IBM 3270 Char Set Ref Ch 10, GA27-2837-9, April 1987 + // Reference: RFC1345 + EBCDICATDEA MIB = 2065 + + // EBCDICCAFR is the MIB identifier with IANA name EBCDIC-CA-FR. + // + // IBM 3270 Char Set Ref Ch 10, GA27-2837-9, April 1987 + // Reference: RFC1345 + EBCDICCAFR MIB = 2066 + + // EBCDICDKNO is the MIB identifier with IANA name EBCDIC-DK-NO. + // + // IBM 3270 Char Set Ref Ch 10, GA27-2837-9, April 1987 + // Reference: RFC1345 + EBCDICDKNO MIB = 2067 + + // EBCDICDKNOA is the MIB identifier with IANA name EBCDIC-DK-NO-A. + // + // IBM 3270 Char Set Ref Ch 10, GA27-2837-9, April 1987 + // Reference: RFC1345 + EBCDICDKNOA MIB = 2068 + + // EBCDICFISE is the MIB identifier with IANA name EBCDIC-FI-SE. + // + // IBM 3270 Char Set Ref Ch 10, GA27-2837-9, April 1987 + // Reference: RFC1345 + EBCDICFISE MIB = 2069 + + // EBCDICFISEA is the MIB identifier with IANA name EBCDIC-FI-SE-A. + // + // IBM 3270 Char Set Ref Ch 10, GA27-2837-9, April 1987 + // Reference: RFC1345 + EBCDICFISEA MIB = 2070 + + // EBCDICFR is the MIB identifier with IANA name EBCDIC-FR. + // + // IBM 3270 Char Set Ref Ch 10, GA27-2837-9, April 1987 + // Reference: RFC1345 + EBCDICFR MIB = 2071 + + // EBCDICIT is the MIB identifier with IANA name EBCDIC-IT. + // + // IBM 3270 Char Set Ref Ch 10, GA27-2837-9, April 1987 + // Reference: RFC1345 + EBCDICIT MIB = 2072 + + // EBCDICPT is the MIB identifier with IANA name EBCDIC-PT. + // + // IBM 3270 Char Set Ref Ch 10, GA27-2837-9, April 1987 + // Reference: RFC1345 + EBCDICPT MIB = 2073 + + // EBCDICES is the MIB identifier with IANA name EBCDIC-ES. + // + // IBM 3270 Char Set Ref Ch 10, GA27-2837-9, April 1987 + // Reference: RFC1345 + EBCDICES MIB = 2074 + + // EBCDICESA is the MIB identifier with IANA name EBCDIC-ES-A. + // + // IBM 3270 Char Set Ref Ch 10, GA27-2837-9, April 1987 + // Reference: RFC1345 + EBCDICESA MIB = 2075 + + // EBCDICESS is the MIB identifier with IANA name EBCDIC-ES-S. + // + // IBM 3270 Char Set Ref Ch 10, GA27-2837-9, April 1987 + // Reference: RFC1345 + EBCDICESS MIB = 2076 + + // EBCDICUK is the MIB identifier with IANA name EBCDIC-UK. + // + // IBM 3270 Char Set Ref Ch 10, GA27-2837-9, April 1987 + // Reference: RFC1345 + EBCDICUK MIB = 2077 + + // EBCDICUS is the MIB identifier with IANA name EBCDIC-US. + // + // IBM 3270 Char Set Ref Ch 10, GA27-2837-9, April 1987 + // Reference: RFC1345 + EBCDICUS MIB = 2078 + + // Unknown8BiT is the MIB identifier with IANA name UNKNOWN-8BIT. + // + // Reference: RFC1428 + Unknown8BiT MIB = 2079 + + // Mnemonic is the MIB identifier with IANA name MNEMONIC. + // + // rfc1345 , also known as "mnemonic+ascii+38" + // Reference: RFC1345 + Mnemonic MIB = 2080 + + // Mnem is the MIB identifier with IANA name MNEM. + // + // rfc1345 , also known as "mnemonic+ascii+8200" + // Reference: RFC1345 + Mnem MIB = 2081 + + // VISCII is the MIB identifier with IANA name VISCII. + // + // rfc1456 + // Reference: RFC1456 + VISCII MIB = 2082 + + // VIQR is the MIB identifier with IANA name VIQR. + // + // rfc1456 + // Reference: RFC1456 + VIQR MIB = 2083 + + // KOI8R is the MIB identifier with IANA name KOI8-R (MIME: KOI8-R). + // + // rfc1489 , based on GOST-19768-74, ISO-6937/8, + // INIS-Cyrillic, ISO-5427. + // Reference: RFC1489 + KOI8R MIB = 2084 + + // HZGB2312 is the MIB identifier with IANA name HZ-GB-2312. + // + // rfc1842 , rfc1843 rfc1843 rfc1842 + HZGB2312 MIB = 2085 + + // IBM866 is the MIB identifier with IANA name IBM866. + // + // IBM NLDG Volume 2 (SE09-8002-03) August 1994 + IBM866 MIB = 2086 + + // PC775Baltic is the MIB identifier with IANA name IBM775. + // + // HP PCL 5 Comparison Guide (P/N 5021-0329) pp B-13, 1996 + PC775Baltic MIB = 2087 + + // KOI8U is the MIB identifier with IANA name KOI8-U. + // + // rfc2319 + // Reference: RFC2319 + KOI8U MIB = 2088 + + // IBM00858 is the MIB identifier with IANA name IBM00858. + // + // IBM See https://www.iana.org/assignments/charset-reg/IBM00858 + IBM00858 MIB = 2089 + + // IBM00924 is the MIB identifier with IANA name IBM00924. + // + // IBM See https://www.iana.org/assignments/charset-reg/IBM00924 + IBM00924 MIB = 2090 + + // IBM01140 is the MIB identifier with IANA name IBM01140. + // + // IBM See https://www.iana.org/assignments/charset-reg/IBM01140 + IBM01140 MIB = 2091 + + // IBM01141 is the MIB identifier with IANA name IBM01141. + // + // IBM See https://www.iana.org/assignments/charset-reg/IBM01141 + IBM01141 MIB = 2092 + + // IBM01142 is the MIB identifier with IANA name IBM01142. + // + // IBM See https://www.iana.org/assignments/charset-reg/IBM01142 + IBM01142 MIB = 2093 + + // IBM01143 is the MIB identifier with IANA name IBM01143. + // + // IBM See https://www.iana.org/assignments/charset-reg/IBM01143 + IBM01143 MIB = 2094 + + // IBM01144 is the MIB identifier with IANA name IBM01144. + // + // IBM See https://www.iana.org/assignments/charset-reg/IBM01144 + IBM01144 MIB = 2095 + + // IBM01145 is the MIB identifier with IANA name IBM01145. + // + // IBM See https://www.iana.org/assignments/charset-reg/IBM01145 + IBM01145 MIB = 2096 + + // IBM01146 is the MIB identifier with IANA name IBM01146. + // + // IBM See https://www.iana.org/assignments/charset-reg/IBM01146 + IBM01146 MIB = 2097 + + // IBM01147 is the MIB identifier with IANA name IBM01147. + // + // IBM See https://www.iana.org/assignments/charset-reg/IBM01147 + IBM01147 MIB = 2098 + + // IBM01148 is the MIB identifier with IANA name IBM01148. + // + // IBM See https://www.iana.org/assignments/charset-reg/IBM01148 + IBM01148 MIB = 2099 + + // IBM01149 is the MIB identifier with IANA name IBM01149. + // + // IBM See https://www.iana.org/assignments/charset-reg/IBM01149 + IBM01149 MIB = 2100 + + // Big5HKSCS is the MIB identifier with IANA name Big5-HKSCS. + // + // See https://www.iana.org/assignments/charset-reg/Big5-HKSCS + Big5HKSCS MIB = 2101 + + // IBM1047 is the MIB identifier with IANA name IBM1047. + // + // IBM1047 (EBCDIC Latin 1/Open Systems) https://www-1.ibm.com/servers/eserver/iseries/software/globalization/pdf/cp01047z.pdf + IBM1047 MIB = 2102 + + // PTCP154 is the MIB identifier with IANA name PTCP154. + // + // See https://www.iana.org/assignments/charset-reg/PTCP154 + PTCP154 MIB = 2103 + + // Amiga1251 is the MIB identifier with IANA name Amiga-1251. + // + // See https://www.amiga.ultranet.ru/Amiga-1251.html + Amiga1251 MIB = 2104 + + // KOI7switched is the MIB identifier with IANA name KOI7-switched. + // + // See https://www.iana.org/assignments/charset-reg/KOI7-switched + KOI7switched MIB = 2105 + + // BRF is the MIB identifier with IANA name BRF. + // + // See https://www.iana.org/assignments/charset-reg/BRF + BRF MIB = 2106 + + // TSCII is the MIB identifier with IANA name TSCII. + // + // See https://www.iana.org/assignments/charset-reg/TSCII + TSCII MIB = 2107 + + // CP51932 is the MIB identifier with IANA name CP51932. + // + // See https://www.iana.org/assignments/charset-reg/CP51932 + CP51932 MIB = 2108 + + // Windows874 is the MIB identifier with IANA name windows-874. + // + // See https://www.iana.org/assignments/charset-reg/windows-874 + Windows874 MIB = 2109 + + // Windows1250 is the MIB identifier with IANA name windows-1250. + // + // Microsoft https://www.iana.org/assignments/charset-reg/windows-1250 + Windows1250 MIB = 2250 + + // Windows1251 is the MIB identifier with IANA name windows-1251. + // + // Microsoft https://www.iana.org/assignments/charset-reg/windows-1251 + Windows1251 MIB = 2251 + + // Windows1252 is the MIB identifier with IANA name windows-1252. + // + // Microsoft https://www.iana.org/assignments/charset-reg/windows-1252 + Windows1252 MIB = 2252 + + // Windows1253 is the MIB identifier with IANA name windows-1253. + // + // Microsoft https://www.iana.org/assignments/charset-reg/windows-1253 + Windows1253 MIB = 2253 + + // Windows1254 is the MIB identifier with IANA name windows-1254. + // + // Microsoft https://www.iana.org/assignments/charset-reg/windows-1254 + Windows1254 MIB = 2254 + + // Windows1255 is the MIB identifier with IANA name windows-1255. + // + // Microsoft https://www.iana.org/assignments/charset-reg/windows-1255 + Windows1255 MIB = 2255 + + // Windows1256 is the MIB identifier with IANA name windows-1256. + // + // Microsoft https://www.iana.org/assignments/charset-reg/windows-1256 + Windows1256 MIB = 2256 + + // Windows1257 is the MIB identifier with IANA name windows-1257. + // + // Microsoft https://www.iana.org/assignments/charset-reg/windows-1257 + Windows1257 MIB = 2257 + + // Windows1258 is the MIB identifier with IANA name windows-1258. + // + // Microsoft https://www.iana.org/assignments/charset-reg/windows-1258 + Windows1258 MIB = 2258 + + // TIS620 is the MIB identifier with IANA name TIS-620. + // + // Thai Industrial Standards Institute (TISI) + TIS620 MIB = 2259 + + // CP50220 is the MIB identifier with IANA name CP50220. + // + // See https://www.iana.org/assignments/charset-reg/CP50220 + CP50220 MIB = 2260 +) diff --git a/vendor/golang.org/x/text/transform/transform.go b/vendor/golang.org/x/text/transform/transform.go new file mode 100644 index 0000000..48ec64b --- /dev/null +++ b/vendor/golang.org/x/text/transform/transform.go @@ -0,0 +1,709 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package transform provides reader and writer wrappers that transform the +// bytes passing through as well as various transformations. Example +// transformations provided by other packages include normalization and +// conversion between character sets. +package transform // import "golang.org/x/text/transform" + +import ( + "bytes" + "errors" + "io" + "unicode/utf8" +) + +var ( + // ErrShortDst means that the destination buffer was too short to + // receive all of the transformed bytes. + ErrShortDst = errors.New("transform: short destination buffer") + + // ErrShortSrc means that the source buffer has insufficient data to + // complete the transformation. + ErrShortSrc = errors.New("transform: short source buffer") + + // ErrEndOfSpan means that the input and output (the transformed input) + // are not identical. + ErrEndOfSpan = errors.New("transform: input and output are not identical") + + // errInconsistentByteCount means that Transform returned success (nil + // error) but also returned nSrc inconsistent with the src argument. + errInconsistentByteCount = errors.New("transform: inconsistent byte count returned") + + // errShortInternal means that an internal buffer is not large enough + // to make progress and the Transform operation must be aborted. + errShortInternal = errors.New("transform: short internal buffer") +) + +// Transformer transforms bytes. +type Transformer interface { + // Transform writes to dst the transformed bytes read from src, and + // returns the number of dst bytes written and src bytes read. The + // atEOF argument tells whether src represents the last bytes of the + // input. + // + // Callers should always process the nDst bytes produced and account + // for the nSrc bytes consumed before considering the error err. + // + // A nil error means that all of the transformed bytes (whether freshly + // transformed from src or left over from previous Transform calls) + // were written to dst. A nil error can be returned regardless of + // whether atEOF is true. If err is nil then nSrc must equal len(src); + // the converse is not necessarily true. + // + // ErrShortDst means that dst was too short to receive all of the + // transformed bytes. ErrShortSrc means that src had insufficient data + // to complete the transformation. If both conditions apply, then + // either error may be returned. Other than the error conditions listed + // here, implementations are free to report other errors that arise. + Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) + + // Reset resets the state and allows a Transformer to be reused. + Reset() +} + +// SpanningTransformer extends the Transformer interface with a Span method +// that determines how much of the input already conforms to the Transformer. +type SpanningTransformer interface { + Transformer + + // Span returns a position in src such that transforming src[:n] results in + // identical output src[:n] for these bytes. It does not necessarily return + // the largest such n. The atEOF argument tells whether src represents the + // last bytes of the input. + // + // Callers should always account for the n bytes consumed before + // considering the error err. + // + // A nil error means that all input bytes are known to be identical to the + // output produced by the Transformer. A nil error can be returned + // regardless of whether atEOF is true. If err is nil, then n must + // equal len(src); the converse is not necessarily true. + // + // ErrEndOfSpan means that the Transformer output may differ from the + // input after n bytes. Note that n may be len(src), meaning that the output + // would contain additional bytes after otherwise identical output. + // ErrShortSrc means that src had insufficient data to determine whether the + // remaining bytes would change. Other than the error conditions listed + // here, implementations are free to report other errors that arise. + // + // Calling Span can modify the Transformer state as a side effect. In + // effect, it does the transformation just as calling Transform would, only + // without copying to a destination buffer and only up to a point it can + // determine the input and output bytes are the same. This is obviously more + // limited than calling Transform, but can be more efficient in terms of + // copying and allocating buffers. Calls to Span and Transform may be + // interleaved. + Span(src []byte, atEOF bool) (n int, err error) +} + +// NopResetter can be embedded by implementations of Transformer to add a nop +// Reset method. +type NopResetter struct{} + +// Reset implements the Reset method of the Transformer interface. +func (NopResetter) Reset() {} + +// Reader wraps another io.Reader by transforming the bytes read. +type Reader struct { + r io.Reader + t Transformer + err error + + // dst[dst0:dst1] contains bytes that have been transformed by t but + // not yet copied out via Read. + dst []byte + dst0, dst1 int + + // src[src0:src1] contains bytes that have been read from r but not + // yet transformed through t. + src []byte + src0, src1 int + + // transformComplete is whether the transformation is complete, + // regardless of whether or not it was successful. + transformComplete bool +} + +const defaultBufSize = 4096 + +// NewReader returns a new Reader that wraps r by transforming the bytes read +// via t. It calls Reset on t. +func NewReader(r io.Reader, t Transformer) *Reader { + t.Reset() + return &Reader{ + r: r, + t: t, + dst: make([]byte, defaultBufSize), + src: make([]byte, defaultBufSize), + } +} + +// Read implements the io.Reader interface. +func (r *Reader) Read(p []byte) (int, error) { + n, err := 0, error(nil) + for { + // Copy out any transformed bytes and return the final error if we are done. + if r.dst0 != r.dst1 { + n = copy(p, r.dst[r.dst0:r.dst1]) + r.dst0 += n + if r.dst0 == r.dst1 && r.transformComplete { + return n, r.err + } + return n, nil + } else if r.transformComplete { + return 0, r.err + } + + // Try to transform some source bytes, or to flush the transformer if we + // are out of source bytes. We do this even if r.r.Read returned an error. + // As the io.Reader documentation says, "process the n > 0 bytes returned + // before considering the error". + if r.src0 != r.src1 || r.err != nil { + r.dst0 = 0 + r.dst1, n, err = r.t.Transform(r.dst, r.src[r.src0:r.src1], r.err == io.EOF) + r.src0 += n + + switch { + case err == nil: + if r.src0 != r.src1 { + r.err = errInconsistentByteCount + } + // The Transform call was successful; we are complete if we + // cannot read more bytes into src. + r.transformComplete = r.err != nil + continue + case err == ErrShortDst && (r.dst1 != 0 || n != 0): + // Make room in dst by copying out, and try again. + continue + case err == ErrShortSrc && r.src1-r.src0 != len(r.src) && r.err == nil: + // Read more bytes into src via the code below, and try again. + default: + r.transformComplete = true + // The reader error (r.err) takes precedence over the + // transformer error (err) unless r.err is nil or io.EOF. + if r.err == nil || r.err == io.EOF { + r.err = err + } + continue + } + } + + // Move any untransformed source bytes to the start of the buffer + // and read more bytes. + if r.src0 != 0 { + r.src0, r.src1 = 0, copy(r.src, r.src[r.src0:r.src1]) + } + n, r.err = r.r.Read(r.src[r.src1:]) + r.src1 += n + } +} + +// TODO: implement ReadByte (and ReadRune??). + +// Writer wraps another io.Writer by transforming the bytes read. +// The user needs to call Close to flush unwritten bytes that may +// be buffered. +type Writer struct { + w io.Writer + t Transformer + dst []byte + + // src[:n] contains bytes that have not yet passed through t. + src []byte + n int +} + +// NewWriter returns a new Writer that wraps w by transforming the bytes written +// via t. It calls Reset on t. +func NewWriter(w io.Writer, t Transformer) *Writer { + t.Reset() + return &Writer{ + w: w, + t: t, + dst: make([]byte, defaultBufSize), + src: make([]byte, defaultBufSize), + } +} + +// Write implements the io.Writer interface. If there are not enough +// bytes available to complete a Transform, the bytes will be buffered +// for the next write. Call Close to convert the remaining bytes. +func (w *Writer) Write(data []byte) (n int, err error) { + src := data + if w.n > 0 { + // Append bytes from data to the last remainder. + // TODO: limit the amount copied on first try. + n = copy(w.src[w.n:], data) + w.n += n + src = w.src[:w.n] + } + for { + nDst, nSrc, err := w.t.Transform(w.dst, src, false) + if _, werr := w.w.Write(w.dst[:nDst]); werr != nil { + return n, werr + } + src = src[nSrc:] + if w.n == 0 { + n += nSrc + } else if len(src) <= n { + // Enough bytes from w.src have been consumed. We make src point + // to data instead to reduce the copying. + w.n = 0 + n -= len(src) + src = data[n:] + if n < len(data) && (err == nil || err == ErrShortSrc) { + continue + } + } + switch err { + case ErrShortDst: + // This error is okay as long as we are making progress. + if nDst > 0 || nSrc > 0 { + continue + } + case ErrShortSrc: + if len(src) < len(w.src) { + m := copy(w.src, src) + // If w.n > 0, bytes from data were already copied to w.src and n + // was already set to the number of bytes consumed. + if w.n == 0 { + n += m + } + w.n = m + err = nil + } else if nDst > 0 || nSrc > 0 { + // Not enough buffer to store the remainder. Keep processing as + // long as there is progress. Without this case, transforms that + // require a lookahead larger than the buffer may result in an + // error. This is not something one may expect to be common in + // practice, but it may occur when buffers are set to small + // sizes during testing. + continue + } + case nil: + if w.n > 0 { + err = errInconsistentByteCount + } + } + return n, err + } +} + +// Close implements the io.Closer interface. +func (w *Writer) Close() error { + src := w.src[:w.n] + for { + nDst, nSrc, err := w.t.Transform(w.dst, src, true) + if _, werr := w.w.Write(w.dst[:nDst]); werr != nil { + return werr + } + if err != ErrShortDst { + return err + } + src = src[nSrc:] + } +} + +type nop struct{ NopResetter } + +func (nop) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) { + n := copy(dst, src) + if n < len(src) { + err = ErrShortDst + } + return n, n, err +} + +func (nop) Span(src []byte, atEOF bool) (n int, err error) { + return len(src), nil +} + +type discard struct{ NopResetter } + +func (discard) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) { + return 0, len(src), nil +} + +var ( + // Discard is a Transformer for which all Transform calls succeed + // by consuming all bytes and writing nothing. + Discard Transformer = discard{} + + // Nop is a SpanningTransformer that copies src to dst. + Nop SpanningTransformer = nop{} +) + +// chain is a sequence of links. A chain with N Transformers has N+1 links and +// N+1 buffers. Of those N+1 buffers, the first and last are the src and dst +// buffers given to chain.Transform and the middle N-1 buffers are intermediate +// buffers owned by the chain. The i'th link transforms bytes from the i'th +// buffer chain.link[i].b at read offset chain.link[i].p to the i+1'th buffer +// chain.link[i+1].b at write offset chain.link[i+1].n, for i in [0, N). +type chain struct { + link []link + err error + // errStart is the index at which the error occurred plus 1. Processing + // errStart at this level at the next call to Transform. As long as + // errStart > 0, chain will not consume any more source bytes. + errStart int +} + +func (c *chain) fatalError(errIndex int, err error) { + if i := errIndex + 1; i > c.errStart { + c.errStart = i + c.err = err + } +} + +type link struct { + t Transformer + // b[p:n] holds the bytes to be transformed by t. + b []byte + p int + n int +} + +func (l *link) src() []byte { + return l.b[l.p:l.n] +} + +func (l *link) dst() []byte { + return l.b[l.n:] +} + +// Chain returns a Transformer that applies t in sequence. +func Chain(t ...Transformer) Transformer { + if len(t) == 0 { + return nop{} + } + c := &chain{link: make([]link, len(t)+1)} + for i, tt := range t { + c.link[i].t = tt + } + // Allocate intermediate buffers. + b := make([][defaultBufSize]byte, len(t)-1) + for i := range b { + c.link[i+1].b = b[i][:] + } + return c +} + +// Reset resets the state of Chain. It calls Reset on all the Transformers. +func (c *chain) Reset() { + for i, l := range c.link { + if l.t != nil { + l.t.Reset() + } + c.link[i].p, c.link[i].n = 0, 0 + } +} + +// TODO: make chain use Span (is going to be fun to implement!) + +// Transform applies the transformers of c in sequence. +func (c *chain) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) { + // Set up src and dst in the chain. + srcL := &c.link[0] + dstL := &c.link[len(c.link)-1] + srcL.b, srcL.p, srcL.n = src, 0, len(src) + dstL.b, dstL.n = dst, 0 + var lastFull, needProgress bool // for detecting progress + + // i is the index of the next Transformer to apply, for i in [low, high]. + // low is the lowest index for which c.link[low] may still produce bytes. + // high is the highest index for which c.link[high] has a Transformer. + // The error returned by Transform determines whether to increase or + // decrease i. We try to completely fill a buffer before converting it. + for low, i, high := c.errStart, c.errStart, len(c.link)-2; low <= i && i <= high; { + in, out := &c.link[i], &c.link[i+1] + nDst, nSrc, err0 := in.t.Transform(out.dst(), in.src(), atEOF && low == i) + out.n += nDst + in.p += nSrc + if i > 0 && in.p == in.n { + in.p, in.n = 0, 0 + } + needProgress, lastFull = lastFull, false + switch err0 { + case ErrShortDst: + // Process the destination buffer next. Return if we are already + // at the high index. + if i == high { + return dstL.n, srcL.p, ErrShortDst + } + if out.n != 0 { + i++ + // If the Transformer at the next index is not able to process any + // source bytes there is nothing that can be done to make progress + // and the bytes will remain unprocessed. lastFull is used to + // detect this and break out of the loop with a fatal error. + lastFull = true + continue + } + // The destination buffer was too small, but is completely empty. + // Return a fatal error as this transformation can never complete. + c.fatalError(i, errShortInternal) + case ErrShortSrc: + if i == 0 { + // Save ErrShortSrc in err. All other errors take precedence. + err = ErrShortSrc + break + } + // Source bytes were depleted before filling up the destination buffer. + // Verify we made some progress, move the remaining bytes to the errStart + // and try to get more source bytes. + if needProgress && nSrc == 0 || in.n-in.p == len(in.b) { + // There were not enough source bytes to proceed while the source + // buffer cannot hold any more bytes. Return a fatal error as this + // transformation can never complete. + c.fatalError(i, errShortInternal) + break + } + // in.b is an internal buffer and we can make progress. + in.p, in.n = 0, copy(in.b, in.src()) + fallthrough + case nil: + // if i == low, we have depleted the bytes at index i or any lower levels. + // In that case we increase low and i. In all other cases we decrease i to + // fetch more bytes before proceeding to the next index. + if i > low { + i-- + continue + } + default: + c.fatalError(i, err0) + } + // Exhausted level low or fatal error: increase low and continue + // to process the bytes accepted so far. + i++ + low = i + } + + // If c.errStart > 0, this means we found a fatal error. We will clear + // all upstream buffers. At this point, no more progress can be made + // downstream, as Transform would have bailed while handling ErrShortDst. + if c.errStart > 0 { + for i := 1; i < c.errStart; i++ { + c.link[i].p, c.link[i].n = 0, 0 + } + err, c.errStart, c.err = c.err, 0, nil + } + return dstL.n, srcL.p, err +} + +// Deprecated: Use runes.Remove instead. +func RemoveFunc(f func(r rune) bool) Transformer { + return removeF(f) +} + +type removeF func(r rune) bool + +func (removeF) Reset() {} + +// Transform implements the Transformer interface. +func (t removeF) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) { + for r, sz := rune(0), 0; len(src) > 0; src = src[sz:] { + + if r = rune(src[0]); r < utf8.RuneSelf { + sz = 1 + } else { + r, sz = utf8.DecodeRune(src) + + if sz == 1 { + // Invalid rune. + if !atEOF && !utf8.FullRune(src) { + err = ErrShortSrc + break + } + // We replace illegal bytes with RuneError. Not doing so might + // otherwise turn a sequence of invalid UTF-8 into valid UTF-8. + // The resulting byte sequence may subsequently contain runes + // for which t(r) is true that were passed unnoticed. + if !t(r) { + if nDst+3 > len(dst) { + err = ErrShortDst + break + } + nDst += copy(dst[nDst:], "\uFFFD") + } + nSrc++ + continue + } + } + + if !t(r) { + if nDst+sz > len(dst) { + err = ErrShortDst + break + } + nDst += copy(dst[nDst:], src[:sz]) + } + nSrc += sz + } + return +} + +// grow returns a new []byte that is longer than b, and copies the first n bytes +// of b to the start of the new slice. +func grow(b []byte, n int) []byte { + m := len(b) + if m <= 32 { + m = 64 + } else if m <= 256 { + m *= 2 + } else { + m += m >> 1 + } + buf := make([]byte, m) + copy(buf, b[:n]) + return buf +} + +const initialBufSize = 128 + +// String returns a string with the result of converting s[:n] using t, where +// n <= len(s). If err == nil, n will be len(s). It calls Reset on t. +func String(t Transformer, s string) (result string, n int, err error) { + t.Reset() + if s == "" { + // Fast path for the common case for empty input. Results in about a + // 86% reduction of running time for BenchmarkStringLowerEmpty. + if _, _, err := t.Transform(nil, nil, true); err == nil { + return "", 0, nil + } + } + + // Allocate only once. Note that both dst and src escape when passed to + // Transform. + buf := [2 * initialBufSize]byte{} + dst := buf[:initialBufSize:initialBufSize] + src := buf[initialBufSize : 2*initialBufSize] + + // The input string s is transformed in multiple chunks (starting with a + // chunk size of initialBufSize). nDst and nSrc are per-chunk (or + // per-Transform-call) indexes, pDst and pSrc are overall indexes. + nDst, nSrc := 0, 0 + pDst, pSrc := 0, 0 + + // pPrefix is the length of a common prefix: the first pPrefix bytes of the + // result will equal the first pPrefix bytes of s. It is not guaranteed to + // be the largest such value, but if pPrefix, len(result) and len(s) are + // all equal after the final transform (i.e. calling Transform with atEOF + // being true returned nil error) then we don't need to allocate a new + // result string. + pPrefix := 0 + for { + // Invariant: pDst == pPrefix && pSrc == pPrefix. + + n := copy(src, s[pSrc:]) + nDst, nSrc, err = t.Transform(dst, src[:n], pSrc+n == len(s)) + pDst += nDst + pSrc += nSrc + + // TODO: let transformers implement an optional Spanner interface, akin + // to norm's QuickSpan. This would even allow us to avoid any allocation. + if !bytes.Equal(dst[:nDst], src[:nSrc]) { + break + } + pPrefix = pSrc + if err == ErrShortDst { + // A buffer can only be short if a transformer modifies its input. + break + } else if err == ErrShortSrc { + if nSrc == 0 { + // No progress was made. + break + } + // Equal so far and !atEOF, so continue checking. + } else if err != nil || pPrefix == len(s) { + return string(s[:pPrefix]), pPrefix, err + } + } + // Post-condition: pDst == pPrefix + nDst && pSrc == pPrefix + nSrc. + + // We have transformed the first pSrc bytes of the input s to become pDst + // transformed bytes. Those transformed bytes are discontiguous: the first + // pPrefix of them equal s[:pPrefix] and the last nDst of them equal + // dst[:nDst]. We copy them around, into a new dst buffer if necessary, so + // that they become one contiguous slice: dst[:pDst]. + if pPrefix != 0 { + newDst := dst + if pDst > len(newDst) { + newDst = make([]byte, len(s)+nDst-nSrc) + } + copy(newDst[pPrefix:pDst], dst[:nDst]) + copy(newDst[:pPrefix], s[:pPrefix]) + dst = newDst + } + + // Prevent duplicate Transform calls with atEOF being true at the end of + // the input. Also return if we have an unrecoverable error. + if (err == nil && pSrc == len(s)) || + (err != nil && err != ErrShortDst && err != ErrShortSrc) { + return string(dst[:pDst]), pSrc, err + } + + // Transform the remaining input, growing dst and src buffers as necessary. + for { + n := copy(src, s[pSrc:]) + atEOF := pSrc+n == len(s) + nDst, nSrc, err := t.Transform(dst[pDst:], src[:n], atEOF) + pDst += nDst + pSrc += nSrc + + // If we got ErrShortDst or ErrShortSrc, do not grow as long as we can + // make progress. This may avoid excessive allocations. + if err == ErrShortDst { + if nDst == 0 { + dst = grow(dst, pDst) + } + } else if err == ErrShortSrc { + if atEOF { + return string(dst[:pDst]), pSrc, err + } + if nSrc == 0 { + src = grow(src, 0) + } + } else if err != nil || pSrc == len(s) { + return string(dst[:pDst]), pSrc, err + } + } +} + +// Bytes returns a new byte slice with the result of converting b[:n] using t, +// where n <= len(b). If err == nil, n will be len(b). It calls Reset on t. +func Bytes(t Transformer, b []byte) (result []byte, n int, err error) { + return doAppend(t, 0, make([]byte, len(b)), b) +} + +// Append appends the result of converting src[:n] using t to dst, where +// n <= len(src), If err == nil, n will be len(src). It calls Reset on t. +func Append(t Transformer, dst, src []byte) (result []byte, n int, err error) { + if len(dst) == cap(dst) { + n := len(src) + len(dst) // It is okay for this to be 0. + b := make([]byte, n) + dst = b[:copy(b, dst)] + } + return doAppend(t, len(dst), dst[:cap(dst)], src) +} + +func doAppend(t Transformer, pDst int, dst, src []byte) (result []byte, n int, err error) { + t.Reset() + pSrc := 0 + for { + nDst, nSrc, err := t.Transform(dst[pDst:], src[pSrc:], true) + pDst += nDst + pSrc += nSrc + if err != ErrShortDst { + return dst[:pDst], pSrc, err + } + + // Grow the destination buffer, but do not grow as long as we can make + // progress. This may avoid excessive allocations. + if nDst == 0 { + dst = grow(dst, pDst) + } + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt new file mode 100644 index 0000000..d81c542 --- /dev/null +++ b/vendor/modules.txt @@ -0,0 +1,18 @@ +# github.com/emersion/go-imap v1.2.1 +## explicit; go 1.13 +github.com/emersion/go-imap +github.com/emersion/go-imap/client +github.com/emersion/go-imap/commands +github.com/emersion/go-imap/responses +github.com/emersion/go-imap/utf7 +# github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 +## explicit; go 1.12 +github.com/emersion/go-sasl +# github.com/emersion/go-smtp v0.16.0 +## explicit; go 1.13 +github.com/emersion/go-smtp +# golang.org/x/text v0.3.7 +## explicit; go 1.17 +golang.org/x/text/encoding +golang.org/x/text/encoding/internal/identifier +golang.org/x/text/transform -- cgit v1.2.3-54-g00ecf