initial commit for getimap
This commit is contained in:
commit
a9c57e76ba
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/getimap
|
||||||
|
/*.ini
|
14
.gitlab-ci.yml
Normal file
14
.gitlab-ci.yml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
stages:
|
||||||
|
- build
|
||||||
|
|
||||||
|
build:
|
||||||
|
stage: build
|
||||||
|
tags:
|
||||||
|
- docker
|
||||||
|
image: golang:1.9.2
|
||||||
|
script:
|
||||||
|
- make
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- bin/get_imap_attachments
|
||||||
|
expire_in: 1 day
|
29
Makefile
Normal file
29
Makefile
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# getimap Makefile
|
||||||
|
|
||||||
|
GOCMD=go
|
||||||
|
GOBUILD=$(GOCMD) build
|
||||||
|
GOCLEAN=$(GOCMD) clean
|
||||||
|
GOTEST=$(GOCMD) test
|
||||||
|
GOGET=$(GOCMD) get
|
||||||
|
GOLINT=golint
|
||||||
|
GOOPTIONS=-mod=vendor -ldflags="-s -w"
|
||||||
|
RMCMD=rm
|
||||||
|
|
||||||
|
BINNAME=getimap
|
||||||
|
|
||||||
|
SRCFILES=cmd/getimap/*.go
|
||||||
|
|
||||||
|
all: build
|
||||||
|
|
||||||
|
get:
|
||||||
|
$(GOGET) -u golang.org/x/lint/golint
|
||||||
|
$(GOGET) -d -t -v
|
||||||
|
|
||||||
|
build:
|
||||||
|
$(GOBUILD) $(GOOPTIONS) -o $(BINNAME) $(SRCFILES)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
$(RMCMD) -f $(BINNAME)
|
||||||
|
|
||||||
|
lint:
|
||||||
|
$(GOLINT)
|
76
README.md
Normal file
76
README.md
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# getimap
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
getimap is a tool written in Golang that fetchs mails from imap servers and write attachments to disk
|
||||||
|
|
||||||
|
## Howto
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample config in getimap.ini
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[imap]
|
||||||
|
hostname=imap.paulbsd.com
|
||||||
|
port=993
|
||||||
|
username=paul@paulbsd.com
|
||||||
|
password=password
|
||||||
|
path=INBOX
|
||||||
|
tls=true
|
||||||
|
|
||||||
|
[search]
|
||||||
|
#from="bob@paulbsd.com"
|
||||||
|
to="paul@paulbsd.com"
|
||||||
|
#subject="Final invoice"
|
||||||
|
|
||||||
|
[output]
|
||||||
|
regexp=".*invoice.*"
|
||||||
|
path=/tmp
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./getimap getimap.ini
|
||||||
|
```
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
- tests
|
||||||
|
- flag management
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
```text
|
||||||
|
Copyright (c) 2019, 2020 PaulBSD
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
2. 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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
The views and conclusions contained in the software and documentation are those
|
||||||
|
of the authors and should not be interpreted as representing official policies,
|
||||||
|
either expressed or implied, of this project.
|
||||||
|
```
|
67
cmd/getimap/getimap.go
Normal file
67
cmd/getimap/getimap.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// imports
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
goimap "github.com/emersion/go-imap"
|
||||||
|
|
||||||
|
"git.paulbsd.com/paulbsd/getimap/src/config"
|
||||||
|
"git.paulbsd.com/paulbsd/getimap/src/getimaplib"
|
||||||
|
"git.paulbsd.com/paulbsd/getimap/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// main function
|
||||||
|
func main() {
|
||||||
|
var configfile string
|
||||||
|
var gi *getimaplib.GetImap
|
||||||
|
var err error
|
||||||
|
|
||||||
|
gi = new(getimaplib.GetImap)
|
||||||
|
|
||||||
|
if len(os.Args) > 1 {
|
||||||
|
configfile = os.Args[1]
|
||||||
|
} else {
|
||||||
|
utils.Usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
utils.HandleFatalError(err)
|
||||||
|
|
||||||
|
mainconfig := config.GetConfig(configfile)
|
||||||
|
gi.Config = mainconfig
|
||||||
|
|
||||||
|
gi.Client = gi.Clt()
|
||||||
|
defer gi.Client.Logout()
|
||||||
|
|
||||||
|
mbox, err := gi.Client.Select(mainconfig.ImapConfig.Path, false)
|
||||||
|
utils.HandleFatalError(err)
|
||||||
|
|
||||||
|
if mbox.Messages == 0 {
|
||||||
|
log.Fatal("No message in mailbox")
|
||||||
|
}
|
||||||
|
|
||||||
|
seqset := new(goimap.SeqSet)
|
||||||
|
seqset.AddRange(1, mbox.Messages)
|
||||||
|
|
||||||
|
messages := make(chan *goimap.Message, mbox.Messages)
|
||||||
|
donemsgs := make(chan error, 1)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
donemsgs <- gi.Client.Fetch(seqset, []goimap.FetchItem{goimap.FetchEnvelope, goimap.FetchFlags}, messages)
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = <-donemsgs
|
||||||
|
utils.HandleError(err)
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
gi.ProcessMessages(messages)
|
||||||
|
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
273
draft/getimap.sample.go
Normal file
273
draft/getimap.sample.go
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/DusanKasan/parsemail"
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
"github.com/emersion/go-imap/client"
|
||||||
|
"github.com/emersion/go-message/mail"
|
||||||
|
"gopkg.in/ini.v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var imapconfig ImapConfig
|
||||||
|
var searchconfig SearchConfig
|
||||||
|
var outputconfig OutputConfig
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
type ImapConfig struct {
|
||||||
|
Hostname string
|
||||||
|
Port int
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchConfig struct {
|
||||||
|
From string
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutputConfig struct {
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func usage() {
|
||||||
|
fmt.Fprintf(os.Stderr, "usage: imap [inputfile]\n")
|
||||||
|
flag.PrintDefaults()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetConfig() {
|
||||||
|
flag.Usage = usage
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
var configfile string
|
||||||
|
|
||||||
|
if len(os.Args) > 1 {
|
||||||
|
configfile = os.Args[1]
|
||||||
|
} else {
|
||||||
|
usage()
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleError(err)
|
||||||
|
|
||||||
|
config, err := ini.Load(configfile)
|
||||||
|
HandleError(err)
|
||||||
|
|
||||||
|
imapconfig_section := config.Section("imap")
|
||||||
|
imapconfig.Hostname = imapconfig_section.Key("hostname").String()
|
||||||
|
imapconfig.Port, err = imapconfig_section.Key("port").Int()
|
||||||
|
imapconfig.Username = imapconfig_section.Key("username").String()
|
||||||
|
imapconfig.Password = imapconfig_section.Key("password").String()
|
||||||
|
imapconfig.Path = imapconfig_section.Key("path").String()
|
||||||
|
|
||||||
|
searchconfig_section := config.Section("search")
|
||||||
|
searchconfig.From = searchconfig_section.Key("from").String()
|
||||||
|
|
||||||
|
outputconfig_section := config.Section("output")
|
||||||
|
outputconfig.Path = outputconfig_section.Key("path").String()
|
||||||
|
|
||||||
|
HandleError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Connect() *client.Client {
|
||||||
|
tr := &tls.Config{InsecureSkipVerify: true}
|
||||||
|
|
||||||
|
c, err := client.DialTLS(fmt.Sprintf("%s:%d", imapconfig.Hostname, imapconfig.Port), tr)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.Login(imapconfig.Username, imapconfig.Password); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListMailboxes(c *client.Client) bool {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetInboxMessages() []*imap.Message {
|
||||||
|
var e []*imap.Message
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetMessage(id uint32) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func Search() {
|
||||||
|
/* criteria := new(imap.SearchCriteria)
|
||||||
|
criteria.WithoutFlags = []string{imap.UnSeenFlag}
|
||||||
|
criteria.Header = textproto.MIMEHeader{"From": {"si-france@logarius.com"}}
|
||||||
|
fromcrit := make(textproto.MIMEHeader)
|
||||||
|
fromcrit.Get("From")
|
||||||
|
criteria.Header = fromcrit
|
||||||
|
|
||||||
|
log.Println("ids : ")
|
||||||
|
ids, err := c.Search(criteria)
|
||||||
|
|
||||||
|
log.Println(ids)
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
GetConfig()
|
||||||
|
|
||||||
|
var c = Connect()
|
||||||
|
defer c.Logout()
|
||||||
|
|
||||||
|
//ListMailboxes(c)
|
||||||
|
|
||||||
|
mbox, err := c.Select(imapconfig.Path, false)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
seqset := new(imap.SeqSet)
|
||||||
|
seqset.AddRange(1, mbox.Messages)
|
||||||
|
//seqSet.Add("1:*")
|
||||||
|
|
||||||
|
messages := make(chan *imap.Message, 1)
|
||||||
|
|
||||||
|
done := make(chan error, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
done <- c.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope}, messages)
|
||||||
|
}()
|
||||||
|
|
||||||
|
var id uint32
|
||||||
|
|
||||||
|
for msg := range messages {
|
||||||
|
from_addr_struct := msg.Envelope.From[0]
|
||||||
|
from_addr := fmt.Sprintf("%s@%s", from_addr_struct.MailboxName, from_addr_struct.HostName)
|
||||||
|
//log.Println(from_addr)
|
||||||
|
if from_addr == searchconfig.From {
|
||||||
|
//log.Println(msg.SeqNum)
|
||||||
|
id = msg.SeqNum
|
||||||
|
//log.Println(fmt.Sprint(msg.SeqNum))
|
||||||
|
}
|
||||||
|
//log.Println("*** " + from_addr + ", " + msg.Envelope.Subject + " " + fmt.Sprint(msg.SeqNum))
|
||||||
|
}
|
||||||
|
|
||||||
|
msgseqset := new(imap.SeqSet)
|
||||||
|
msgseqset.AddRange(id, id)
|
||||||
|
|
||||||
|
section := &imap.BodySectionName{}
|
||||||
|
items := []imap.FetchItem{section.FetchItem()}
|
||||||
|
|
||||||
|
message := make(chan *imap.Message, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
done <- c.Fetch(msgseqset, items, message)
|
||||||
|
}()
|
||||||
|
|
||||||
|
msg := <-message
|
||||||
|
if msg == nil {
|
||||||
|
log.Fatal("Server didn't returned message")
|
||||||
|
}
|
||||||
|
|
||||||
|
r := msg.GetBody(section)
|
||||||
|
if r == nil {
|
||||||
|
log.Fatal("Server didn't returned message body")
|
||||||
|
}
|
||||||
|
|
||||||
|
email, err := parsemail.Parse(r)
|
||||||
|
for _, a := range email.Attachments {
|
||||||
|
/*log.Println("aaaa",a.Filename)
|
||||||
|
log.Println("aaaa",a.ContentType)*/
|
||||||
|
var destpath = fmt.Sprintf("%s/%s", outputconfig.Path, a.Filename)
|
||||||
|
log.Println(fmt.Sprintf("Writing attachement : %s", destpath))
|
||||||
|
WriteFile(destpath, a.Data)
|
||||||
|
//and read a.Data
|
||||||
|
}
|
||||||
|
|
||||||
|
mr, err := mail.CreateReader(r)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
header := mr.Header
|
||||||
|
if date, err := header.Date(); err == nil {
|
||||||
|
log.Println("Date:", date)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := <-done; err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
for {
|
||||||
|
p, err := mr.NextPart()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch h := p.Header.(type) {
|
||||||
|
case mail.AttachmentHeader:
|
||||||
|
// This is an attachment
|
||||||
|
filename, _ := h.Filename()
|
||||||
|
log.Println(fmt.Sprintf("Got attachment: %v", filename))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteFile(destpath string, data io.Reader) {
|
||||||
|
|
||||||
|
fo, err := os.Create(destpath)
|
||||||
|
HandleError(err)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := fo.Close(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
for {
|
||||||
|
n, err := data.Read(buf)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if _, err := fo.Write(buf[:n]); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleError(err error) {
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
16
getimap.ini.sample
Normal file
16
getimap.ini.sample
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[imap]
|
||||||
|
hostname=imap.paulbsd.com
|
||||||
|
port=993
|
||||||
|
username=paul@paulbsd.com
|
||||||
|
password=password
|
||||||
|
path=INBOX
|
||||||
|
tls=true
|
||||||
|
|
||||||
|
[search]
|
||||||
|
from="paul@paulbsd.com"
|
||||||
|
#to="bob@example.com"
|
||||||
|
#subject="invoice"
|
||||||
|
|
||||||
|
[output]
|
||||||
|
regexp="CUR*"
|
||||||
|
path=/tmp
|
12
go.mod
Normal file
12
go.mod
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
module git.paulbsd.com/paulbsd/getimap
|
||||||
|
|
||||||
|
go 1.14
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/DusanKasan/parsemail v0.0.0-20190115161936-abc648830b9a
|
||||||
|
github.com/emersion/go-imap v1.0.0
|
||||||
|
github.com/emersion/go-message v0.10.4-0.20190609165112-592ace5bc1ca
|
||||||
|
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||||
|
github.com/stretchr/testify v1.5.1 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.42.0
|
||||||
|
)
|
42
go.sum
Normal file
42
go.sum
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
github.com/DusanKasan/parsemail v0.0.0-20190115161936-abc648830b9a h1:vgKGGcwqf3gjwSwgtz0r94vxTzk4Hmjnv9N4L71clkU=
|
||||||
|
github.com/DusanKasan/parsemail v0.0.0-20190115161936-abc648830b9a/go.mod h1:X2gHR36ajhLdcOtFd638L5CutXdN/TDzppa7v2ckcK8=
|
||||||
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/emersion/go-imap v1.0.0 h1:/7HHNiSOk13DErenBZaQfTBmUy+quc6X7s3RNnuVtUM=
|
||||||
|
github.com/emersion/go-imap v1.0.0/go.mod h1:MEiDDwwQFcZ+L45Pa68jNGv0qU9kbW+SJzwDpvSfX1s=
|
||||||
|
github.com/emersion/go-message v0.10.4-0.20190609165112-592ace5bc1ca h1:OYhqtJI4eOLvGtRIsUfP87VMJ1J/o6ks1tah9DlYkn4=
|
||||||
|
github.com/emersion/go-message v0.10.4-0.20190609165112-592ace5bc1ca/go.mod h1:3h+HsGTCFHmk4ngJ2IV/YPhdlaOcR6hcgqM3yca9v7c=
|
||||||
|
github.com/emersion/go-sasl v0.0.0-20190520160400-47d427600317 h1:tYZxAY8nu3JJQKios9f27Sbvbkfm4XHXT476gVtszu0=
|
||||||
|
github.com/emersion/go-sasl v0.0.0-20190520160400-47d427600317/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
|
||||||
|
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe h1:40SWqY0zE3qCi6ZrtTf5OUdNm5lDnGnjRSq9GgmeTrg=
|
||||||
|
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
|
github.com/martinlindhe/base36 v0.0.0-20190418230009-7c6542dfbb41 h1:CVsnY46BCLkX9XOhALJ/S7yb9ayc4eqjXSXO3tyB66A=
|
||||||
|
github.com/martinlindhe/base36 v0.0.0-20190418230009-7c6542dfbb41/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
|
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||||
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk=
|
||||||
|
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
60
src/config/config.go
Normal file
60
src/config/config.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
|
||||||
|
ini "gopkg.in/ini.v1"
|
||||||
|
|
||||||
|
"git.paulbsd.com/paulbsd/getimap/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetConfig fetch the configuration from ini file
|
||||||
|
func GetConfig(configfile string) *Config {
|
||||||
|
flag.Usage = utils.Usage
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
cfg, err := ini.Load(configfile)
|
||||||
|
utils.HandleFatalError(err)
|
||||||
|
|
||||||
|
var mc = new(Config)
|
||||||
|
|
||||||
|
imapconfigSection := cfg.Section("imap")
|
||||||
|
mc.ImapConfig.Hostname = imapconfigSection.Key("hostname").String()
|
||||||
|
mc.ImapConfig.Port, err = imapconfigSection.Key("port").Int()
|
||||||
|
mc.ImapConfig.Username = imapconfigSection.Key("username").String()
|
||||||
|
mc.ImapConfig.Password = imapconfigSection.Key("password").String()
|
||||||
|
mc.ImapConfig.Path = imapconfigSection.Key("path").String()
|
||||||
|
mc.ImapConfig.TLS, err = imapconfigSection.Key("tls").Bool()
|
||||||
|
utils.HandleError(err)
|
||||||
|
|
||||||
|
searchconfigSection := cfg.Section("search")
|
||||||
|
mc.SearchConfig.From = searchconfigSection.Key("from").String()
|
||||||
|
mc.SearchConfig.To = searchconfigSection.Key("to").String()
|
||||||
|
mc.SearchConfig.Subject = searchconfigSection.Key("subject").String()
|
||||||
|
|
||||||
|
outputconfigSection := cfg.Section("output")
|
||||||
|
mc.OutputConfig.RegExp = outputconfigSection.Key("regexp").String()
|
||||||
|
mc.OutputConfig.Path = outputconfigSection.Key("path").String()
|
||||||
|
|
||||||
|
return mc
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
ImapConfig struct {
|
||||||
|
Hostname string
|
||||||
|
Port int
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
Path string
|
||||||
|
TLS bool
|
||||||
|
}
|
||||||
|
SearchConfig struct {
|
||||||
|
From string
|
||||||
|
To string
|
||||||
|
Subject string
|
||||||
|
}
|
||||||
|
OutputConfig struct {
|
||||||
|
RegExp string
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
}
|
225
src/getimaplib/getimaplib.go
Normal file
225
src/getimaplib/getimaplib.go
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
package getimaplib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"git.paulbsd.com/paulbsd/getimap/src/config"
|
||||||
|
"git.paulbsd.com/paulbsd/getimap/src/parsemail"
|
||||||
|
"git.paulbsd.com/paulbsd/getimap/utils"
|
||||||
|
goimap "github.com/emersion/go-imap"
|
||||||
|
"github.com/emersion/go-imap/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProcessMessages treats fetch list
|
||||||
|
func (gi *GetImap) ProcessMessages(messages chan *goimap.Message) {
|
||||||
|
log.Println(fmt.Sprintf("Ready to process %d messages in %s ...", len(messages), gi.Config.ImapConfig.Path))
|
||||||
|
for msgitem := range messages {
|
||||||
|
isSeen := false
|
||||||
|
|
||||||
|
for flag := range msgitem.Flags {
|
||||||
|
if msgitem.Flags[flag] == goimap.SeenFlag {
|
||||||
|
isSeen = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(msgitem.Envelope.From) > 0 {
|
||||||
|
gi.Fromaddr = fmt.Sprintf("%s@%s", msgitem.Envelope.From[0].MailboxName, msgitem.Envelope.From[0].HostName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(msgitem.Envelope.To) > 0 {
|
||||||
|
gi.Toaddr = fmt.Sprintf("%s@%s", msgitem.Envelope.To[0].MailboxName, msgitem.Envelope.To[0].HostName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if msgitem.Envelope.Subject != "" {
|
||||||
|
gi.Subject = msgitem.Envelope.Subject
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isSeen {
|
||||||
|
msgseqset := gi.FilterMail(msgitem, gi.Fromaddr, gi.Toaddr, gi.Subject)
|
||||||
|
if msgseqset != nil {
|
||||||
|
emailbody := gi.GetEmailBody(msgseqset)
|
||||||
|
|
||||||
|
parsedemail, err := parsemail.Parse(emailbody)
|
||||||
|
utils.HandleError(err)
|
||||||
|
|
||||||
|
gi.ProcessAttachments(&parsedemail, msgitem, gi.Fromaddr, gi.Toaddr)
|
||||||
|
gi.MsgsSum++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Println(fmt.Sprintf("Processed %d messages, %d attachments", gi.MsgsSum, gi.AttachmentsSum))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterMail filters mail with from and to params in config search section
|
||||||
|
func (gi *GetImap) FilterMail(msgitem *goimap.Message, fromaddr string, toaddr string, subject string) *goimap.SeqSet {
|
||||||
|
msgseqset := new(goimap.SeqSet)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
var fromMatch bool
|
||||||
|
var toMatch bool
|
||||||
|
var subjectMatch bool
|
||||||
|
|
||||||
|
fmt.Println(fromaddr, toaddr, subject)
|
||||||
|
|
||||||
|
if gi.Config.SearchConfig.From != "" {
|
||||||
|
fromMatch, err = regexp.MatchString(gi.Config.SearchConfig.From, fromaddr)
|
||||||
|
utils.HandleError(err)
|
||||||
|
}
|
||||||
|
if gi.Config.SearchConfig.To != "" {
|
||||||
|
toMatch, err = regexp.MatchString(gi.Config.SearchConfig.To, toaddr)
|
||||||
|
utils.HandleError(err)
|
||||||
|
}
|
||||||
|
if gi.Config.SearchConfig.Subject != "" {
|
||||||
|
subjectMatch, err = regexp.MatchString(gi.Config.SearchConfig.Subject, subject)
|
||||||
|
utils.HandleError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fromMatch || toMatch || subjectMatch {
|
||||||
|
msgseqset.AddNum(msgitem.SeqNum)
|
||||||
|
} else {
|
||||||
|
msgseqset = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return msgseqset
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEmailBody returns the body of mail from imap as a io.Reader (imap.Literal)
|
||||||
|
func (gi *GetImap) GetEmailBody(msgseqset *goimap.SeqSet) goimap.Literal {
|
||||||
|
donemsg := make(chan error, 1)
|
||||||
|
section := &goimap.BodySectionName{}
|
||||||
|
items := []goimap.FetchItem{section.FetchItem()}
|
||||||
|
messages := make(chan *goimap.Message, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
donemsg <- gi.Client.Fetch(msgseqset, items, messages)
|
||||||
|
}()
|
||||||
|
|
||||||
|
msgitem := <-messages
|
||||||
|
if msgitem == nil {
|
||||||
|
log.Fatal("Server didn't returned message")
|
||||||
|
}
|
||||||
|
|
||||||
|
body := msgitem.GetBody(section)
|
||||||
|
if body == nil {
|
||||||
|
log.Fatal("Server didn't returned message body")
|
||||||
|
}
|
||||||
|
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessAttachments processes the attachments in each matching mail
|
||||||
|
func (gi *GetImap) ProcessAttachments(email *parsemail.Email, msg *goimap.Message, fromaddr string, toaddr string) {
|
||||||
|
for _, attachment := range email.Attachments {
|
||||||
|
|
||||||
|
if attachment.Filename != "" {
|
||||||
|
matched, err := regexp.MatchString(gi.Config.OutputConfig.RegExp, attachment.Filename)
|
||||||
|
utils.HandleError(err)
|
||||||
|
|
||||||
|
if matched {
|
||||||
|
destpath := fmt.Sprintf("%s/%s", gi.Config.OutputConfig.Path, attachment.Filename)
|
||||||
|
log.Println(fmt.Sprintf("Mail from %s, to %s, seqnum %d, found attachment %s", fromaddr, toaddr, msg.SeqNum, attachment.Filename))
|
||||||
|
gi.WriteFile(destpath, attachment.Data)
|
||||||
|
gi.AttachmentsSum++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Println(fmt.Sprintf("Mail from %s, to %s, seqnum %d, no found attachments, passing", fromaddr, toaddr, msg.SeqNum))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteFile writes attachment as a file to specified output
|
||||||
|
func (gi *GetImap) WriteFile(destpath string, data io.Reader) bool {
|
||||||
|
_, err := os.Stat(destpath)
|
||||||
|
var written bool
|
||||||
|
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
log.Println(fmt.Sprintf("Creating file %s", destpath))
|
||||||
|
fo, err := os.Create(destpath)
|
||||||
|
utils.HandleError(err)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := fo.Close(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
buf := make([]byte, 4096)
|
||||||
|
for {
|
||||||
|
n, err := data.Read(buf)
|
||||||
|
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if _, err := fo.Write(buf[:n]); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
written = true
|
||||||
|
gi.WrittenSum++
|
||||||
|
} else {
|
||||||
|
log.Println(fmt.Sprintf("File %s exists, not creating", destpath))
|
||||||
|
}
|
||||||
|
|
||||||
|
return written
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clt establish the client connection to the imap server
|
||||||
|
func (gi *GetImap) Clt() *client.Client {
|
||||||
|
var clt *client.Client
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if gi.Config.ImapConfig.TLS {
|
||||||
|
tr := &tls.Config{InsecureSkipVerify: true}
|
||||||
|
clt, err = client.DialTLS(fmt.Sprintf("%s:%d", gi.Config.ImapConfig.Hostname, gi.Config.ImapConfig.Port), tr)
|
||||||
|
} else {
|
||||||
|
clt, err = client.Dial(fmt.Sprintf("%s:%d", gi.Config.ImapConfig.Hostname, gi.Config.ImapConfig.Port))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = clt.Login(gi.Config.ImapConfig.Username, gi.Config.ImapConfig.Password)
|
||||||
|
utils.HandleFatalError(err)
|
||||||
|
|
||||||
|
return clt
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListMailboxes func not yet used
|
||||||
|
func (gi *GetImap) ListMailboxes() (ret bool) {
|
||||||
|
mailboxes := make(chan *goimap.MailboxInfo, 10)
|
||||||
|
done := make(chan error, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
done <- gi.Client.List("", "*", mailboxes)
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Printf("Mailboxes:")
|
||||||
|
for m := range mailboxes {
|
||||||
|
log.Println("* " + m.Name)
|
||||||
|
}
|
||||||
|
ret = true
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetImap is the main struct
|
||||||
|
type GetImap struct {
|
||||||
|
Config *config.Config
|
||||||
|
Client *client.Client
|
||||||
|
Fromaddr string
|
||||||
|
Toaddr string
|
||||||
|
Subject string
|
||||||
|
MsgsSum int
|
||||||
|
AttachmentsSum int
|
||||||
|
WrittenSum int
|
||||||
|
}
|
469
src/parsemail/parsemail.go
Normal file
469
src/parsemail/parsemail.go
Normal file
@ -0,0 +1,469 @@
|
|||||||
|
package parsemail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"mime"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/mail"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const contentTypeMultipartMixed = "multipart/mixed"
|
||||||
|
const contentTypeMultipartAlternative = "multipart/alternative"
|
||||||
|
const contentTypeMultipartRelated = "multipart/related"
|
||||||
|
const contentTypeTextHTML = "text/html"
|
||||||
|
const contentTypeTextPlain = "text/plain"
|
||||||
|
|
||||||
|
// Parse an email message read from io.Reader into parsemail.Email struct
|
||||||
|
func Parse(r io.Reader) (email Email, err error) {
|
||||||
|
msg, err := mail.ReadMessage(r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
email, err = createEmailFromHeader(msg.Header)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType, params, err := parseContentType(msg.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch contentType {
|
||||||
|
case contentTypeMultipartMixed:
|
||||||
|
email.TextBody, email.HTMLBody, email.Attachments, email.EmbeddedFiles, err = parseMultipartMixed(msg.Body, params["boundary"])
|
||||||
|
case contentTypeMultipartAlternative:
|
||||||
|
email.TextBody, email.HTMLBody, email.EmbeddedFiles, err = parseMultipartAlternative(msg.Body, params["boundary"])
|
||||||
|
case contentTypeTextPlain:
|
||||||
|
message, _ := ioutil.ReadAll(msg.Body)
|
||||||
|
email.TextBody = strings.TrimSuffix(string(message[:]), "\n")
|
||||||
|
case contentTypeTextHTML:
|
||||||
|
message, _ := ioutil.ReadAll(msg.Body)
|
||||||
|
email.HTMLBody = strings.TrimSuffix(string(message[:]), "\n")
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("Unknown top level mime type: %s", contentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func createEmailFromHeader(header mail.Header) (email Email, err error) {
|
||||||
|
hp := headerParser{header: &header}
|
||||||
|
|
||||||
|
email.Subject = decodeMimeSentence(header.Get("Subject"))
|
||||||
|
email.From = hp.parseAddressList(header.Get("From"))
|
||||||
|
email.Sender = hp.parseAddress(header.Get("Sender"))
|
||||||
|
email.ReplyTo = hp.parseAddressList(header.Get("Reply-To"))
|
||||||
|
email.To = hp.parseAddressList(header.Get("To"))
|
||||||
|
email.Cc = hp.parseAddressList(header.Get("Cc"))
|
||||||
|
email.Bcc = hp.parseAddressList(header.Get("Bcc"))
|
||||||
|
email.Date = hp.parseTime(header.Get("Date"))
|
||||||
|
email.ResentFrom = hp.parseAddressList(header.Get("Resent-From"))
|
||||||
|
email.ResentSender = hp.parseAddress(header.Get("Resent-Sender"))
|
||||||
|
email.ResentTo = hp.parseAddressList(header.Get("Resent-To"))
|
||||||
|
email.ResentCc = hp.parseAddressList(header.Get("Resent-Cc"))
|
||||||
|
email.ResentBcc = hp.parseAddressList(header.Get("Resent-Bcc"))
|
||||||
|
email.ResentMessageID = hp.parseMessageID(header.Get("Resent-Message-ID"))
|
||||||
|
email.MessageID = hp.parseMessageID(header.Get("Message-ID"))
|
||||||
|
email.InReplyTo = hp.parseMessageIDList(header.Get("In-Reply-To"))
|
||||||
|
email.References = hp.parseMessageIDList(header.Get("References"))
|
||||||
|
email.ResentDate = hp.parseTime(header.Get("Resent-Date"))
|
||||||
|
|
||||||
|
if hp.err != nil {
|
||||||
|
err = hp.err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//decode whole header for easier access to extra fields
|
||||||
|
//todo: should we decode? aren't only standard fields mime encoded?
|
||||||
|
email.Header, err = decodeHeaderMime(header)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseContentType(contentTypeHeader string) (contentType string, params map[string]string, err error) {
|
||||||
|
if contentTypeHeader == "" {
|
||||||
|
contentType = contentTypeTextPlain
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return mime.ParseMediaType(contentTypeHeader)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMultipartRelated(msg io.Reader, boundary string) (textBody, htmlBody string, embeddedFiles []EmbeddedFile, err error) {
|
||||||
|
pmr := multipart.NewReader(msg, boundary)
|
||||||
|
for {
|
||||||
|
part, err := pmr.NextPart()
|
||||||
|
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType, params, err := mime.ParseMediaType(part.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch contentType {
|
||||||
|
case contentTypeTextPlain:
|
||||||
|
ppContent, err := ioutil.ReadAll(part)
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
textBody += strings.TrimSuffix(string(ppContent[:]), "\n")
|
||||||
|
case contentTypeTextHTML:
|
||||||
|
ppContent, err := ioutil.ReadAll(part)
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlBody += strings.TrimSuffix(string(ppContent[:]), "\n")
|
||||||
|
case contentTypeMultipartAlternative:
|
||||||
|
tb, hb, ef, err := parseMultipartAlternative(part, params["boundary"])
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlBody += hb
|
||||||
|
textBody += tb
|
||||||
|
embeddedFiles = append(embeddedFiles, ef...)
|
||||||
|
default:
|
||||||
|
if isEmbeddedFile(part) {
|
||||||
|
ef, err := decodeEmbeddedFile(part)
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
embeddedFiles = append(embeddedFiles, ef)
|
||||||
|
} else {
|
||||||
|
return textBody, htmlBody, embeddedFiles, fmt.Errorf("Can't process multipart/related inner mime type: %s", contentType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMultipartAlternative(msg io.Reader, boundary string) (textBody, htmlBody string, embeddedFiles []EmbeddedFile, err error) {
|
||||||
|
pmr := multipart.NewReader(msg, boundary)
|
||||||
|
for {
|
||||||
|
part, err := pmr.NextPart()
|
||||||
|
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType, params, err := mime.ParseMediaType(part.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch contentType {
|
||||||
|
case contentTypeTextPlain:
|
||||||
|
ppContent, err := ioutil.ReadAll(part)
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
textBody += strings.TrimSuffix(string(ppContent[:]), "\n")
|
||||||
|
case contentTypeTextHTML:
|
||||||
|
ppContent, err := ioutil.ReadAll(part)
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlBody += strings.TrimSuffix(string(ppContent[:]), "\n")
|
||||||
|
case contentTypeMultipartRelated:
|
||||||
|
tb, hb, ef, err := parseMultipartRelated(part, params["boundary"])
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlBody += hb
|
||||||
|
textBody += tb
|
||||||
|
embeddedFiles = append(embeddedFiles, ef...)
|
||||||
|
default:
|
||||||
|
if isEmbeddedFile(part) {
|
||||||
|
ef, err := decodeEmbeddedFile(part)
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
embeddedFiles = append(embeddedFiles, ef)
|
||||||
|
} else {
|
||||||
|
return textBody, htmlBody, embeddedFiles, fmt.Errorf("Can't process multipart/alternative inner mime type: %s", contentType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMultipartMixed(msg io.Reader, boundary string) (textBody, htmlBody string, attachments []Attachment, embeddedFiles []EmbeddedFile, err error) {
|
||||||
|
mr := multipart.NewReader(msg, boundary)
|
||||||
|
for {
|
||||||
|
part, err := mr.NextPart()
|
||||||
|
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return textBody, htmlBody, attachments, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType, params, err := mime.ParseMediaType(part.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, attachments, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if contentType == contentTypeMultipartAlternative {
|
||||||
|
textBody, htmlBody, embeddedFiles, err = parseMultipartAlternative(part, params["boundary"])
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, attachments, embeddedFiles, err
|
||||||
|
}
|
||||||
|
} else if contentType == contentTypeMultipartRelated {
|
||||||
|
textBody, htmlBody, embeddedFiles, err = parseMultipartRelated(part, params["boundary"])
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, attachments, embeddedFiles, err
|
||||||
|
}
|
||||||
|
} else if contentType == contentTypeTextPlain {
|
||||||
|
at, _ := decodeAttachment(part)
|
||||||
|
attachments = append(attachments, at)
|
||||||
|
} else if isAttachment(part) {
|
||||||
|
at, _ := decodeAttachment(part)
|
||||||
|
attachments = append(attachments, at)
|
||||||
|
} else {
|
||||||
|
return textBody, htmlBody, attachments, embeddedFiles, fmt.Errorf("Unknown multipart/mixed nested mime type: %s", contentType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return textBody, htmlBody, attachments, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeMimeSentence(s string) string {
|
||||||
|
result := []string{}
|
||||||
|
ss := strings.Split(s, " ")
|
||||||
|
|
||||||
|
for _, word := range ss {
|
||||||
|
dec := new(mime.WordDecoder)
|
||||||
|
w, err := dec.Decode(word)
|
||||||
|
if err != nil {
|
||||||
|
if len(result) == 0 {
|
||||||
|
w = word
|
||||||
|
} else {
|
||||||
|
w = " " + word
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(result, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeHeaderMime(header mail.Header) (mail.Header, error) {
|
||||||
|
parsedHeader := map[string][]string{}
|
||||||
|
|
||||||
|
for headerName, headerData := range header {
|
||||||
|
|
||||||
|
parsedHeaderData := []string{}
|
||||||
|
for _, headerValue := range headerData {
|
||||||
|
parsedHeaderData = append(parsedHeaderData, decodeMimeSentence(headerValue))
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedHeader[headerName] = parsedHeaderData
|
||||||
|
}
|
||||||
|
|
||||||
|
return mail.Header(parsedHeader), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodePartData(part *multipart.Part) (io.Reader, error) {
|
||||||
|
encoding := part.Header.Get("Content-Transfer-Encoding")
|
||||||
|
|
||||||
|
if strings.EqualFold(encoding, "base64") {
|
||||||
|
dr := base64.NewDecoder(base64.StdEncoding, part)
|
||||||
|
dd, err := ioutil.ReadAll(dr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return bytes.NewReader(dd), nil
|
||||||
|
} else if encoding == "7bit" {
|
||||||
|
dd, err := ioutil.ReadAll(part)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return bytes.NewReader(dd), nil
|
||||||
|
} else if encoding == "" {
|
||||||
|
dd, err := ioutil.ReadAll(part)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return bytes.NewReader(dd), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("Unknown encoding: %s", encoding)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isEmbeddedFile(part *multipart.Part) bool {
|
||||||
|
return part.Header.Get("Content-Transfer-Encoding") != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeEmbeddedFile(part *multipart.Part) (ef EmbeddedFile, err error) {
|
||||||
|
cid := decodeMimeSentence(part.Header.Get("Content-Id"))
|
||||||
|
decoded, err := decodePartData(part)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ef.CID = strings.Trim(cid, "<>")
|
||||||
|
ef.Data = decoded
|
||||||
|
ef.ContentType = part.Header.Get("Content-Type")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAttachment(part *multipart.Part) bool {
|
||||||
|
return part.FileName() != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeAttachment(part *multipart.Part) (at Attachment, err error) {
|
||||||
|
filename := part.FileName()
|
||||||
|
decoded, err := decodePartData(part)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
at.Filename = filename
|
||||||
|
at.Data = decoded
|
||||||
|
at.ContentType = strings.Split(part.Header.Get("Content-Type"), ";")[0]
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type headerParser struct {
|
||||||
|
header *mail.Header
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hp headerParser) parseAddress(s string) (ma *mail.Address) {
|
||||||
|
if hp.err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Trim(s, " \n") != "" {
|
||||||
|
ma, hp.err = mail.ParseAddress(s)
|
||||||
|
|
||||||
|
return ma
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hp headerParser) parseAddressList(s string) (ma []*mail.Address) {
|
||||||
|
if hp.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Trim(s, " \n") != "" {
|
||||||
|
ma, hp.err = mail.ParseAddressList(s)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hp headerParser) parseTime(s string) (t time.Time) {
|
||||||
|
if hp.err != nil || s == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t, hp.err = time.Parse(time.RFC1123Z, s)
|
||||||
|
if hp.err == nil {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
t, hp.err = time.Parse("Mon, 2 Jan 2006 15:04:05 -0700", s)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hp headerParser) parseMessageID(s string) string {
|
||||||
|
if hp.err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Trim(s, "<> ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hp headerParser) parseMessageIDList(s string) (result []string) {
|
||||||
|
if hp.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range strings.Split(s, " ") {
|
||||||
|
if strings.Trim(p, " \n") != "" {
|
||||||
|
result = append(result, hp.parseMessageID(p))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attachment with filename, content type and data (as a io.Reader)
|
||||||
|
type Attachment struct {
|
||||||
|
Filename string
|
||||||
|
ContentType string
|
||||||
|
Data io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmbeddedFile with content id, content type and data (as a io.Reader)
|
||||||
|
type EmbeddedFile struct {
|
||||||
|
CID string
|
||||||
|
ContentType string
|
||||||
|
Data io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email with fields for all the headers defined in RFC5322 with it's attachments and
|
||||||
|
type Email struct {
|
||||||
|
Header mail.Header
|
||||||
|
|
||||||
|
Subject string
|
||||||
|
Sender *mail.Address
|
||||||
|
From []*mail.Address
|
||||||
|
ReplyTo []*mail.Address
|
||||||
|
To []*mail.Address
|
||||||
|
Cc []*mail.Address
|
||||||
|
Bcc []*mail.Address
|
||||||
|
Date time.Time
|
||||||
|
MessageID string
|
||||||
|
InReplyTo []string
|
||||||
|
References []string
|
||||||
|
|
||||||
|
ResentFrom []*mail.Address
|
||||||
|
ResentSender *mail.Address
|
||||||
|
ResentTo []*mail.Address
|
||||||
|
ResentDate time.Time
|
||||||
|
ResentCc []*mail.Address
|
||||||
|
ResentBcc []*mail.Address
|
||||||
|
ResentMessageID string
|
||||||
|
|
||||||
|
HTMLBody string
|
||||||
|
TextBody string
|
||||||
|
|
||||||
|
Attachments []Attachment
|
||||||
|
EmbeddedFiles []EmbeddedFile
|
||||||
|
}
|
672
src/parsemail/parsemail_test.go
Normal file
672
src/parsemail/parsemail_test.go
Normal file
@ -0,0 +1,672 @@
|
|||||||
|
package parsemail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/mail"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseEmail(t *testing.T) {
|
||||||
|
var testData = map[int]struct {
|
||||||
|
mailData string
|
||||||
|
|
||||||
|
subject string
|
||||||
|
date time.Time
|
||||||
|
from []mail.Address
|
||||||
|
sender mail.Address
|
||||||
|
to []mail.Address
|
||||||
|
replyTo []mail.Address
|
||||||
|
cc []mail.Address
|
||||||
|
bcc []mail.Address
|
||||||
|
messageID string
|
||||||
|
resentDate time.Time
|
||||||
|
resentFrom []mail.Address
|
||||||
|
resentSender mail.Address
|
||||||
|
resentTo []mail.Address
|
||||||
|
resentReplyTo []mail.Address
|
||||||
|
resentCc []mail.Address
|
||||||
|
resentBcc []mail.Address
|
||||||
|
resentMessageID string
|
||||||
|
inReplyTo []string
|
||||||
|
references []string
|
||||||
|
htmlBody string
|
||||||
|
textBody string
|
||||||
|
attachments []attachmentData
|
||||||
|
embeddedFiles []embeddedFileData
|
||||||
|
headerCheck func(mail.Header, *testing.T)
|
||||||
|
}{
|
||||||
|
1: {
|
||||||
|
mailData: rfc5322exampleA11,
|
||||||
|
subject: "Saying Hello",
|
||||||
|
from: []mail.Address{
|
||||||
|
{
|
||||||
|
Name: "John Doe",
|
||||||
|
Address: "jdoe@machine.example",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
to: []mail.Address{
|
||||||
|
{
|
||||||
|
Name: "Mary Smith",
|
||||||
|
Address: "mary@example.net",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sender: mail.Address{
|
||||||
|
Name: "Michael Jones",
|
||||||
|
Address: "mjones@machine.example",
|
||||||
|
},
|
||||||
|
messageID: "1234@local.machine.example",
|
||||||
|
date: parseDate("Fri, 21 Nov 1997 09:55:06 -0600"),
|
||||||
|
textBody: `This is a message just to say hello.
|
||||||
|
So, "Hello".`,
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
mailData: rfc5322exampleA12,
|
||||||
|
from: []mail.Address{
|
||||||
|
{
|
||||||
|
Name: "Joe Q. Public",
|
||||||
|
Address: "john.q.public@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
to: []mail.Address{
|
||||||
|
{
|
||||||
|
Name: "Mary Smith",
|
||||||
|
Address: "mary@x.test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "",
|
||||||
|
Address: "jdoe@example.org",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Who?",
|
||||||
|
Address: "one@y.test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cc: []mail.Address{
|
||||||
|
{
|
||||||
|
Name: "",
|
||||||
|
Address: "boss@nil.test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Giant; \"Big\" Box",
|
||||||
|
Address: "sysservices@example.net",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
messageID: "5678.21-Nov-1997@example.com",
|
||||||
|
date: parseDate("Tue, 01 Jul 2003 10:52:37 +0200"),
|
||||||
|
textBody: `Hi everyone.`,
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
mailData: rfc5322exampleA2a,
|
||||||
|
subject: "Re: Saying Hello",
|
||||||
|
from: []mail.Address{
|
||||||
|
{
|
||||||
|
Name: "Mary Smith",
|
||||||
|
Address: "mary@example.net",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
replyTo: []mail.Address{
|
||||||
|
{
|
||||||
|
Name: "Mary Smith: Personal Account",
|
||||||
|
Address: "smith@home.example",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
to: []mail.Address{
|
||||||
|
{
|
||||||
|
Name: "John Doe",
|
||||||
|
Address: "jdoe@machine.example",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
messageID: "3456@example.net",
|
||||||
|
inReplyTo: []string{"1234@local.machine.example"},
|
||||||
|
references: []string{"1234@local.machine.example"},
|
||||||
|
date: parseDate("Fri, 21 Nov 1997 10:01:10 -0600"),
|
||||||
|
textBody: `This is a reply to your hello.`,
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
mailData: rfc5322exampleA2b,
|
||||||
|
subject: "Re: Saying Hello",
|
||||||
|
from: []mail.Address{
|
||||||
|
{
|
||||||
|
Name: "John Doe",
|
||||||
|
Address: "jdoe@machine.example",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
to: []mail.Address{
|
||||||
|
{
|
||||||
|
Name: "Mary Smith: Personal Account",
|
||||||
|
Address: "smith@home.example",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
messageID: "abcd.1234@local.machine.test",
|
||||||
|
inReplyTo: []string{"3456@example.net"},
|
||||||
|
references: []string{"1234@local.machine.example", "3456@example.net"},
|
||||||
|
date: parseDate("Fri, 21 Nov 1997 11:00:00 -0600"),
|
||||||
|
textBody: `This is a reply to your reply.`,
|
||||||
|
},
|
||||||
|
5: {
|
||||||
|
mailData: rfc5322exampleA3,
|
||||||
|
subject: "Saying Hello",
|
||||||
|
from: []mail.Address{
|
||||||
|
{
|
||||||
|
Name: "John Doe",
|
||||||
|
Address: "jdoe@machine.example",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
to: []mail.Address{
|
||||||
|
{
|
||||||
|
Name: "Mary Smith",
|
||||||
|
Address: "mary@example.net",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
messageID: "1234@local.machine.example",
|
||||||
|
date: parseDate("Fri, 21 Nov 1997 09:55:06 -0600"),
|
||||||
|
resentFrom: []mail.Address{
|
||||||
|
{
|
||||||
|
Name: "Mary Smith",
|
||||||
|
Address: "mary@example.net",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resentTo: []mail.Address{
|
||||||
|
{
|
||||||
|
Name: "Jane Brown",
|
||||||
|
Address: "j-brown@other.example",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resentMessageID: "78910@example.net",
|
||||||
|
resentDate: parseDate("Mon, 24 Nov 1997 14:22:01 -0800"),
|
||||||
|
textBody: `This is a message just to say hello.
|
||||||
|
So, "Hello".`,
|
||||||
|
},
|
||||||
|
6: {
|
||||||
|
mailData: data1,
|
||||||
|
subject: "Peter Paholík",
|
||||||
|
from: []mail.Address{
|
||||||
|
{
|
||||||
|
Name: "Peter Paholík",
|
||||||
|
Address: "peter.paholik@gmail.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
to: []mail.Address{
|
||||||
|
{
|
||||||
|
Name: "",
|
||||||
|
Address: "dusan@kasan.sk",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
messageID: "CACtgX4kNXE7T5XKSKeH_zEcfUUmf2vXVASxYjaaK9cCn-3zb_g@mail.gmail.com",
|
||||||
|
date: parseDate("Fri, 07 Apr 2017 09:17:26 +0200"),
|
||||||
|
htmlBody: "<div dir=\"ltr\"><br></div>",
|
||||||
|
attachments: []attachmentData{
|
||||||
|
{
|
||||||
|
filename: "Peter Paholík 1 4 2017 2017-04-07.pdf",
|
||||||
|
contentType: "application/pdf",
|
||||||
|
base64data: "JVBERi0xLjQNCiW1tbW1DQoxIDAgb2JqDQo8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFIvTGFuZyhlbi1VUykgL1N0cnVjdFRyZWVSb290IDY3IDAgUi9NYXJrSW5mbzw8L01hcmtlZCB0cnVlPj4vT3V0cHV0SW50ZW50c1s8PC9UeXBlL091dHB1dEludGVudC9TL0dUU19QREZBMS9PdXRwdXRDb25kZXYgMzk1MzYyDQo+Pg0Kc3RhcnR4cmVmDQo0MTk4ODUNCiUlRU9GDQo=",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
7: {
|
||||||
|
mailData: data2,
|
||||||
|
subject: "Re: Test Subject 2",
|
||||||
|
from: []mail.Address{
|
||||||
|
{
|
||||||
|
Name: "Sender Man",
|
||||||
|
Address: "sender@domain.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
to: []mail.Address{
|
||||||
|
{
|
||||||
|
Name: "",
|
||||||
|
Address: "info@receiver.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cc: []mail.Address{
|
||||||
|
{
|
||||||
|
Name: "Cc Man",
|
||||||
|
Address: "ccman@gmail.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
messageID: "0e9a21b4-01dc-e5c1-dcd6-58ce5aa61f4f@receiver.com",
|
||||||
|
inReplyTo: []string{"9ff38d03-c4ab-89b7-9328-e99d5e24e3ba@receiver.eu"},
|
||||||
|
references: []string{"2f6b7595-c01e-46e5-42bc-f263e1c4282d@receiver.com", "9ff38d03-c4ab-89b7-9328-e99d5e24e3ba@domain.com"},
|
||||||
|
date: parseDate("Fri, 07 Apr 2017 12:59:55 +0200"),
|
||||||
|
htmlBody: `<html>data<img src="part2.9599C449.04E5EC81@develhell.com"/></html>`,
|
||||||
|
textBody: `First level
|
||||||
|
> Second level
|
||||||
|
>> Third level
|
||||||
|
>
|
||||||
|
`,
|
||||||
|
embeddedFiles: []embeddedFileData{
|
||||||
|
{
|
||||||
|
cid: "part2.9599C449.04E5EC81@develhell.com",
|
||||||
|
contentType: "image/png",
|
||||||
|
base64data: "iVBORw0KGgoAAAANSUhEUgAAAQEAAAAYCAIAAAB1IN9NAAAACXBIWXMAAAsTAAALEwEAmpwYYKUKF+Os3baUndC0pDnwNAmLy1SUr2Gw0luxQuV/AwC6cEhVV5VRrwAAAABJRU5ErkJggg==",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for index, td := range testData {
|
||||||
|
m, err := mail.ReadMessage(strings.NewReader(td.mailData))
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
e, err := Parse(m)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if td.subject != e.Subject {
|
||||||
|
t.Errorf("[Test Case %v] Wrong subject. Expected: %s, Got: %s", index, td.subject, e.Subject)
|
||||||
|
}
|
||||||
|
|
||||||
|
if td.messageID != e.MessageID {
|
||||||
|
t.Errorf("[Test Case %v] Wrong messageID. Expected: '%s', Got: '%s'", index, td.messageID, e.MessageID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !td.date.Equal(e.Date) {
|
||||||
|
t.Errorf("[Test Case %v] Wrong date. Expected: %v, Got: %v", index, td.date, e.Date)
|
||||||
|
}
|
||||||
|
|
||||||
|
d := dereferenceAddressList(e.From)
|
||||||
|
if !assertAddressListEq(td.from, d) {
|
||||||
|
t.Errorf("[Test Case %v] Wrong from. Expected: %s, Got: %s", index, td.from, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
var sender mail.Address
|
||||||
|
if e.Sender != nil {
|
||||||
|
sender = *e.Sender
|
||||||
|
}
|
||||||
|
if td.sender != sender {
|
||||||
|
t.Errorf("[Test Case %v] Wrong sender. Expected: %s, Got: %s", index, td.sender, sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
d = dereferenceAddressList(e.To)
|
||||||
|
if !assertAddressListEq(td.to, d) {
|
||||||
|
t.Errorf("[Test Case %v] Wrong to. Expected: %s, Got: %s", index, td.to, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
d = dereferenceAddressList(e.Cc)
|
||||||
|
if !assertAddressListEq(td.cc, d) {
|
||||||
|
t.Errorf("[Test Case %v] Wrong cc. Expected: %s, Got: %s", index, td.cc, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
d = dereferenceAddressList(e.Bcc)
|
||||||
|
if !assertAddressListEq(td.bcc, d) {
|
||||||
|
t.Errorf("[Test Case %v] Wrong bcc. Expected: %s, Got: %s", index, td.bcc, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
if td.resentMessageID != e.ResentMessageID {
|
||||||
|
t.Errorf("[Test Case %v] Wrong resent messageID. Expected: '%s', Got: '%s'", index, td.resentMessageID, e.ResentMessageID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !td.resentDate.Equal(e.ResentDate) && !td.resentDate.IsZero() && !e.ResentDate.IsZero() {
|
||||||
|
t.Errorf("[Test Case %v] Wrong resent date. Expected: %v, Got: %v", index, td.resentDate, e.ResentDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
d = dereferenceAddressList(e.ResentFrom)
|
||||||
|
if !assertAddressListEq(td.resentFrom, d) {
|
||||||
|
t.Errorf("[Test Case %v] Wrong resent from. Expected: %s, Got: %s", index, td.resentFrom, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resentSender mail.Address
|
||||||
|
if e.ResentSender != nil {
|
||||||
|
resentSender = *e.ResentSender
|
||||||
|
}
|
||||||
|
if td.resentSender != resentSender {
|
||||||
|
t.Errorf("[Test Case %v] Wrong resent sender. Expected: %s, Got: %s", index, td.resentSender, resentSender)
|
||||||
|
}
|
||||||
|
|
||||||
|
d = dereferenceAddressList(e.ResentTo)
|
||||||
|
if !assertAddressListEq(td.resentTo, d) {
|
||||||
|
t.Errorf("[Test Case %v] Wrong resent to. Expected: %s, Got: %s", index, td.resentTo, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
d = dereferenceAddressList(e.ResentCc)
|
||||||
|
if !assertAddressListEq(td.resentCc, d) {
|
||||||
|
t.Errorf("[Test Case %v] Wrong resent cc. Expected: %s, Got: %s", index, td.resentCc, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
d = dereferenceAddressList(e.ResentBcc)
|
||||||
|
if !assertAddressListEq(td.resentBcc, d) {
|
||||||
|
t.Errorf("[Test Case %v] Wrong resent bcc. Expected: %s, Got: %s", index, td.resentBcc, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !assertSliceEq(td.inReplyTo, e.InReplyTo) {
|
||||||
|
t.Errorf("[Test Case %v] Wrong in reply to. Expected: %s, Got: %s", index, td.inReplyTo, e.InReplyTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !assertSliceEq(td.references, e.References) {
|
||||||
|
t.Errorf("[Test Case %v] Wrong references. Expected: %s, Got: %s", index, td.references, e.References)
|
||||||
|
}
|
||||||
|
|
||||||
|
d = dereferenceAddressList(e.ReplyTo)
|
||||||
|
if !assertAddressListEq(td.replyTo, d) {
|
||||||
|
t.Errorf("[Test Case %v] Wrong reply to. Expected: %s, Got: %s", index, td.replyTo, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
if td.htmlBody != e.HTMLBody {
|
||||||
|
t.Errorf("[Test Case %v] Wrong html body. Expected: '%s', Got: '%s'", index, td.htmlBody, e.HTMLBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
if td.textBody != e.TextBody {
|
||||||
|
t.Errorf("[Test Case %v] Wrong text body. Expected: '%s', Got: '%s'", index, td.textBody, e.TextBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(td.attachments) != len(e.Attachments) {
|
||||||
|
t.Errorf("[Test Case %v] Incorrect number of attachments! Expected: %v, Got: %v.", index, len(td.attachments), len(e.Attachments))
|
||||||
|
} else {
|
||||||
|
attachs := e.Attachments[:]
|
||||||
|
|
||||||
|
for _, ad := range td.attachments {
|
||||||
|
found := false
|
||||||
|
|
||||||
|
for i, ra := range attachs {
|
||||||
|
b, err := ioutil.ReadAll(ra.Data)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded := base64.StdEncoding.EncodeToString(b)
|
||||||
|
if ra.Filename == ad.filename && encoded == ad.base64data && ra.ContentType == ad.contentType {
|
||||||
|
found = true
|
||||||
|
attachs = append(attachs[:i], attachs[i+1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
t.Errorf("[Test Case %v] Attachment not found: %s", index, ad.filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(attachs) != 0 {
|
||||||
|
t.Errorf("[Test Case %v] Email contains %v unexpected attachments: %v", index, len(attachs), attachs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(td.embeddedFiles) != len(e.EmbeddedFiles) {
|
||||||
|
t.Errorf("[Test Case %v] Incorrect number of embedded files! Expected: %v, Got: %v.", index, len(td.embeddedFiles), len(e.EmbeddedFiles))
|
||||||
|
} else {
|
||||||
|
embeds := e.EmbeddedFiles[:]
|
||||||
|
|
||||||
|
for _, ad := range td.embeddedFiles {
|
||||||
|
found := false
|
||||||
|
|
||||||
|
for i, ra := range embeds {
|
||||||
|
b, err := ioutil.ReadAll(ra.Data)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded := base64.StdEncoding.EncodeToString(b)
|
||||||
|
|
||||||
|
if ra.CID == ad.cid && encoded == ad.base64data && ra.ContentType == ad.contentType {
|
||||||
|
found = true
|
||||||
|
embeds = append(embeds[:i], embeds[i+1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
t.Errorf("[Test Case %v] Embedded file not found: %s", index, ad.cid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(embeds) != 0 {
|
||||||
|
t.Errorf("[Test Case %v] Email contains %v unexpected embedded files: %v", index, len(embeds), embeds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDate(in string) time.Time {
|
||||||
|
out, err := time.Parse(time.RFC1123Z, in)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
type attachmentData struct {
|
||||||
|
filename string
|
||||||
|
contentType string
|
||||||
|
base64data string
|
||||||
|
}
|
||||||
|
|
||||||
|
type embeddedFileData struct {
|
||||||
|
cid string
|
||||||
|
contentType string
|
||||||
|
base64data string
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertSliceEq(a, b []string) bool {
|
||||||
|
if len(a) == len(b) && len(a) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if a == nil && b == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if a == nil || b == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range a {
|
||||||
|
if a[i] != b[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertAddressListEq(a, b []mail.Address) bool {
|
||||||
|
if len(a) == len(b) && len(a) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if a == nil && b == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if a == nil || b == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range a {
|
||||||
|
if a[i] != b[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func dereferenceAddressList(al []*mail.Address) (result []mail.Address) {
|
||||||
|
for _, a := range al {
|
||||||
|
result = append(result, *a)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var data1 = `From: =?UTF-8?Q?Peter_Pahol=C3=ADk?= <peter.paholik@gmail.com>
|
||||||
|
Date: Fri, 7 Apr 2017 09:17:26 +0200
|
||||||
|
Message-ID: <CACtgX4kNXE7T5XKSKeH_zEcfUUmf2vXVASxYjaaK9cCn-3zb_g@mail.gmail.com>
|
||||||
|
Subject: =?UTF-8?Q?Peter_Pahol=C3=ADk?=
|
||||||
|
To: dusan@kasan.sk
|
||||||
|
Content-Type: multipart/mixed; boundary=f403045f1dcc043a44054c8e6bbf
|
||||||
|
|
||||||
|
--f403045f1dcc043a44054c8e6bbf
|
||||||
|
Content-Type: multipart/alternative; boundary=f403045f1dcc043a3f054c8e6bbd
|
||||||
|
|
||||||
|
--f403045f1dcc043a3f054c8e6bbd
|
||||||
|
Content-Type: text/plain; charset=UTF-8
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
--f403045f1dcc043a3f054c8e6bbd
|
||||||
|
Content-Type: text/html; charset=UTF-8
|
||||||
|
|
||||||
|
<div dir="ltr"><br></div>
|
||||||
|
|
||||||
|
--f403045f1dcc043a3f054c8e6bbd--
|
||||||
|
--f403045f1dcc043a44054c8e6bbf
|
||||||
|
Content-Type: application/pdf;
|
||||||
|
name="=?UTF-8?Q?Peter_Paholi=CC=81k_1?=
|
||||||
|
=?UTF-8?Q?_4_2017_2017=2D04=2D07=2Epdf?="
|
||||||
|
Content-Disposition: attachment;
|
||||||
|
filename="=?UTF-8?Q?Peter_Paholi=CC=81k_1?=
|
||||||
|
=?UTF-8?Q?_4_2017_2017=2D04=2D07=2Epdf?="
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
X-Attachment-Id: f_j17i0f0d0
|
||||||
|
|
||||||
|
JVBERi0xLjQNCiW1tbW1DQoxIDAgb2JqDQo8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFIvTGFu
|
||||||
|
Zyhlbi1VUykgL1N0cnVjdFRyZWVSb290IDY3IDAgUi9NYXJrSW5mbzw8L01hcmtlZCB0cnVlPj4v
|
||||||
|
T3V0cHV0SW50ZW50c1s8PC9UeXBlL091dHB1dEludGVudC9TL0dUU19QREZBMS9PdXRwdXRDb25k
|
||||||
|
ZXYgMzk1MzYyDQo+Pg0Kc3RhcnR4cmVmDQo0MTk4ODUNCiUlRU9GDQo=
|
||||||
|
--f403045f1dcc043a44054c8e6bbf--
|
||||||
|
`
|
||||||
|
|
||||||
|
var data2 = `Subject: Re: Test Subject 2
|
||||||
|
To: info@receiver.com
|
||||||
|
References: <2f6b7595-c01e-46e5-42bc-f263e1c4282d@receiver.com>
|
||||||
|
<9ff38d03-c4ab-89b7-9328-e99d5e24e3ba@domain.com>
|
||||||
|
Cc: Cc Man <ccman@gmail.com>
|
||||||
|
From: Sender Man <sender@domain.com>
|
||||||
|
Message-ID: <0e9a21b4-01dc-e5c1-dcd6-58ce5aa61f4f@receiver.com>
|
||||||
|
Date: Fri, 7 Apr 2017 12:59:55 +0200
|
||||||
|
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:45.0)
|
||||||
|
Gecko/20100101 Thunderbird/45.8.0
|
||||||
|
MIME-Version: 1.0
|
||||||
|
In-Reply-To: <9ff38d03-c4ab-89b7-9328-e99d5e24e3ba@receiver.eu>
|
||||||
|
Content-Type: multipart/alternative;
|
||||||
|
boundary="------------C70C0458A558E585ACB75FB4"
|
||||||
|
|
||||||
|
This is a multi-part message in MIME format.
|
||||||
|
--------------C70C0458A558E585ACB75FB4
|
||||||
|
Content-Type: text/plain; charset=utf-8; format=flowed
|
||||||
|
Content-Transfer-Encoding: 8bit
|
||||||
|
|
||||||
|
First level
|
||||||
|
> Second level
|
||||||
|
>> Third level
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
--------------C70C0458A558E585ACB75FB4
|
||||||
|
Content-Type: multipart/related;
|
||||||
|
boundary="------------5DB4A1356834BB602A5F88B2"
|
||||||
|
|
||||||
|
|
||||||
|
--------------5DB4A1356834BB602A5F88B2
|
||||||
|
Content-Type: text/html; charset=utf-8
|
||||||
|
Content-Transfer-Encoding: 8bit
|
||||||
|
|
||||||
|
<html>data<img src="part2.9599C449.04E5EC81@develhell.com"/></html>
|
||||||
|
|
||||||
|
--------------5DB4A1356834BB602A5F88B2
|
||||||
|
Content-Type: image/png
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
Content-ID: <part2.9599C449.04E5EC81@develhell.com>
|
||||||
|
|
||||||
|
iVBORw0KGgoAAAANSUhEUgAAAQEAAAAYCAIAAAB1IN9NAAAACXBIWXMAAAsTAAALEwEAmpwY
|
||||||
|
YKUKF+Os3baUndC0pDnwNAmLy1SUr2Gw0luxQuV/AwC6cEhVV5VRrwAAAABJRU5ErkJggg==
|
||||||
|
--------------5DB4A1356834BB602A5F88B2
|
||||||
|
|
||||||
|
--------------C70C0458A558E585ACB75FB4--
|
||||||
|
`
|
||||||
|
|
||||||
|
var rfc5322exampleA11 = `From: John Doe <jdoe@machine.example>
|
||||||
|
Sender: Michael Jones <mjones@machine.example>
|
||||||
|
To: Mary Smith <mary@example.net>
|
||||||
|
Subject: Saying Hello
|
||||||
|
Date: Fri, 21 Nov 1997 09:55:06 -0600
|
||||||
|
Message-ID: <1234@local.machine.example>
|
||||||
|
|
||||||
|
This is a message just to say hello.
|
||||||
|
So, "Hello".
|
||||||
|
`
|
||||||
|
|
||||||
|
var rfc5322exampleA12 = `From: "Joe Q. Public" <john.q.public@example.com>
|
||||||
|
To: Mary Smith <mary@x.test>, jdoe@example.org, Who? <one@y.test>
|
||||||
|
Cc: <boss@nil.test>, "Giant; \"Big\" Box" <sysservices@example.net>
|
||||||
|
Date: Tue, 1 Jul 2003 10:52:37 +0200
|
||||||
|
Message-ID: <5678.21-Nov-1997@example.com>
|
||||||
|
|
||||||
|
Hi everyone.
|
||||||
|
`
|
||||||
|
|
||||||
|
//todo: not yet implemented in net/mail
|
||||||
|
//once there is support for this, add it
|
||||||
|
var rfc5322exampleA13 = `From: Pete <pete@silly.example>
|
||||||
|
To: A Group:Ed Jones <c@a.test>,joe@where.test,John <jdoe@one.test>;
|
||||||
|
Cc: Undisclosed recipients:;
|
||||||
|
Date: Thu, 13 Feb 1969 23:32:54 -0330
|
||||||
|
Message-ID: <testabcd.1234@silly.example>
|
||||||
|
|
||||||
|
Testing.
|
||||||
|
`
|
||||||
|
|
||||||
|
//we skipped the first message bcause it's the same as A 1.1
|
||||||
|
var rfc5322exampleA2a = `From: Mary Smith <mary@example.net>
|
||||||
|
To: John Doe <jdoe@machine.example>
|
||||||
|
Reply-To: "Mary Smith: Personal Account" <smith@home.example>
|
||||||
|
Subject: Re: Saying Hello
|
||||||
|
Date: Fri, 21 Nov 1997 10:01:10 -0600
|
||||||
|
Message-ID: <3456@example.net>
|
||||||
|
In-Reply-To: <1234@local.machine.example>
|
||||||
|
References: <1234@local.machine.example>
|
||||||
|
|
||||||
|
This is a reply to your hello.
|
||||||
|
`
|
||||||
|
|
||||||
|
var rfc5322exampleA2b = `To: "Mary Smith: Personal Account" <smith@home.example>
|
||||||
|
From: John Doe <jdoe@machine.example>
|
||||||
|
Subject: Re: Saying Hello
|
||||||
|
Date: Fri, 21 Nov 1997 11:00:00 -0600
|
||||||
|
Message-ID: <abcd.1234@local.machine.test>
|
||||||
|
In-Reply-To: <3456@example.net>
|
||||||
|
References: <1234@local.machine.example> <3456@example.net>
|
||||||
|
|
||||||
|
This is a reply to your reply.
|
||||||
|
`
|
||||||
|
|
||||||
|
var rfc5322exampleA3 = `Resent-From: Mary Smith <mary@example.net>
|
||||||
|
Resent-To: Jane Brown <j-brown@other.example>
|
||||||
|
Resent-Date: Mon, 24 Nov 1997 14:22:01 -0800
|
||||||
|
Resent-Message-ID: <78910@example.net>
|
||||||
|
From: John Doe <jdoe@machine.example>
|
||||||
|
To: Mary Smith <mary@example.net>
|
||||||
|
Subject: Saying Hello
|
||||||
|
Date: Fri, 21 Nov 1997 09:55:06 -0600
|
||||||
|
Message-ID: <1234@local.machine.example>
|
||||||
|
|
||||||
|
This is a message just to say hello.
|
||||||
|
So, "Hello".`
|
||||||
|
|
||||||
|
var rfc5322exampleA4 = `Received: from x.y.test
|
||||||
|
by example.net
|
||||||
|
via TCP
|
||||||
|
with ESMTP
|
||||||
|
id ABC12345
|
||||||
|
for <mary@example.net>; 21 Nov 1997 10:05:43 -0600
|
||||||
|
Received: from node.example by x.y.test; 21 Nov 1997 10:01:22 -0600
|
||||||
|
From: John Doe <jdoe@node.example>
|
||||||
|
To: Mary Smith <mary@example.net>
|
||||||
|
Subject: Saying Hello
|
||||||
|
Date: Fri, 21 Nov 1997 09:55:06 -0600
|
||||||
|
Message-ID: <1234@local.node.example>
|
||||||
|
|
||||||
|
This is a message just to say hello.
|
||||||
|
So, "Hello".`
|
30
utils/utils.go
Normal file
30
utils/utils.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandleError no blocking errors
|
||||||
|
func HandleError(err error) {
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleFatalError fatal errors
|
||||||
|
func HandleFatalError(err error) {
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage displays possible arguments
|
||||||
|
func Usage() {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage: getimap [inputfile]\n")
|
||||||
|
flag.PrintDefaults()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
3
vendor/github.com/DusanKasan/parsemail/CHANGELOG.md
generated
vendored
Normal file
3
vendor/github.com/DusanKasan/parsemail/CHANGELOG.md
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## No versions tagged yet
|
19
vendor/github.com/DusanKasan/parsemail/CONTRIBUTING.md
generated
vendored
Normal file
19
vendor/github.com/DusanKasan/parsemail/CONTRIBUTING.md
generated
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
## How to contribute
|
||||||
|
|
||||||
|
This project is open to contribution from anyone, as long as you cover your changes with tests. Your pull requests will be merged after your code passe CI and manual code review.
|
||||||
|
|
||||||
|
Every change merges to master. No development is done in other branches.
|
||||||
|
|
||||||
|
## Typical contribution use case
|
||||||
|
|
||||||
|
- You need a feature that is not implemented yet
|
||||||
|
- Search for open/closed issues relating to what you need
|
||||||
|
- If you don't find anything, create new issue
|
||||||
|
- Fork this repository and create fix/feature in the fork
|
||||||
|
- Write tests for your change
|
||||||
|
- If you changed API, document the change in README
|
||||||
|
- Create pull request, describe what you did
|
||||||
|
- Wait for CI to verify you didn't break anything
|
||||||
|
- If you did, rewrite it
|
||||||
|
- If CI passes, wait for manual review by repo's owner
|
||||||
|
- Your pull request will be merged into master
|
21
vendor/github.com/DusanKasan/parsemail/LICENSE.md
generated
vendored
Normal file
21
vendor/github.com/DusanKasan/parsemail/LICENSE.md
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2017 Dusan Kasan
|
||||||
|
|
||||||
|
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.
|
58
vendor/github.com/DusanKasan/parsemail/README.md
generated
vendored
Normal file
58
vendor/github.com/DusanKasan/parsemail/README.md
generated
vendored
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# Parsemail - simple email parsing Go library
|
||||||
|
|
||||||
|
[![Build Status](https://circleci.com/gh/DusanKasan/parsemail.svg?style=shield&circle-token=:circle-token)](https://circleci.com/gh/DusanKasan/parsemail) [![Coverage Status](https://coveralls.io/repos/github/DusanKasan/Parsemail/badge.svg?branch=master)](https://coveralls.io/github/DusanKasan/Parsemail?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/DusanKasan/parsemail)](https://goreportcard.com/report/github.com/DusanKasan/parsemail)
|
||||||
|
|
||||||
|
This library allows for parsing an email message into a more convenient form than the `net/mail` provides. Where the `net/mail` just gives you a map of header fields and a `io.Reader` of its body, Parsemail allows access to all the standard header fields set in [RFC5322](https://tools.ietf.org/html/rfc5322), html/text body as well as attachements/embedded content as binary streams with metadata.
|
||||||
|
|
||||||
|
## Simple usage
|
||||||
|
|
||||||
|
You just parse a io.Reader that holds the email data. The returned Email struct contains all the standard email information/headers as public fields.
|
||||||
|
|
||||||
|
```go
|
||||||
|
var reader io.Reader // this reads an email message
|
||||||
|
email, err := parsemail.Parse(reader) // returns Email struct and error
|
||||||
|
if err != nil {
|
||||||
|
// handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(email.Subject)
|
||||||
|
fmt.Println(email.From)
|
||||||
|
fmt.Println(email.To)
|
||||||
|
fmt.Println(email.HTMLBody)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Retrieving attachments
|
||||||
|
|
||||||
|
Attachments are a easily accessible as `Attachment` type, containing their mime type, filename and data stream.
|
||||||
|
|
||||||
|
```go
|
||||||
|
var reader io.Reader
|
||||||
|
email, err := parsemail.Parse(reader)
|
||||||
|
if err != nil {
|
||||||
|
// handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, a := range(email.Attachments) {
|
||||||
|
fmt.Println(a.Filename)
|
||||||
|
fmt.Println(a.ContentType)
|
||||||
|
//and read a.Data
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Retrieving embedded files
|
||||||
|
|
||||||
|
You can access embedded files in the same way you can access attachments. They contain the mime type, data stream and content id that is used to reference them through the email.
|
||||||
|
|
||||||
|
```go
|
||||||
|
var reader io.Reader
|
||||||
|
email, err := parsemail.Parse(reader)
|
||||||
|
if err != nil {
|
||||||
|
// handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, a := range(email.EmbeddedFiles) {
|
||||||
|
fmt.Println(a.CID)
|
||||||
|
fmt.Println(a.ContentType)
|
||||||
|
//and read a.Data
|
||||||
|
}
|
||||||
|
```
|
458
vendor/github.com/DusanKasan/parsemail/parsemail.go
generated
vendored
Normal file
458
vendor/github.com/DusanKasan/parsemail/parsemail.go
generated
vendored
Normal file
@ -0,0 +1,458 @@
|
|||||||
|
package parsemail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"mime"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/mail"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const contentTypeMultipartMixed = "multipart/mixed"
|
||||||
|
const contentTypeMultipartAlternative = "multipart/alternative"
|
||||||
|
const contentTypeMultipartRelated = "multipart/related"
|
||||||
|
const contentTypeTextHtml = "text/html"
|
||||||
|
const contentTypeTextPlain = "text/plain"
|
||||||
|
|
||||||
|
// Parse an email message read from io.Reader into parsemail.Email struct
|
||||||
|
func Parse(r io.Reader) (email Email, err error) {
|
||||||
|
msg, err := mail.ReadMessage(r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
email, err = createEmailFromHeader(msg.Header)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType, params, err := parseContentType(msg.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch contentType {
|
||||||
|
case contentTypeMultipartMixed:
|
||||||
|
email.TextBody, email.HTMLBody, email.Attachments, email.EmbeddedFiles, err = parseMultipartMixed(msg.Body, params["boundary"])
|
||||||
|
case contentTypeMultipartAlternative:
|
||||||
|
email.TextBody, email.HTMLBody, email.EmbeddedFiles, err = parseMultipartAlternative(msg.Body, params["boundary"])
|
||||||
|
case contentTypeTextPlain:
|
||||||
|
message, _ := ioutil.ReadAll(msg.Body)
|
||||||
|
email.TextBody = strings.TrimSuffix(string(message[:]), "\n")
|
||||||
|
case contentTypeTextHtml:
|
||||||
|
message, _ := ioutil.ReadAll(msg.Body)
|
||||||
|
email.HTMLBody = strings.TrimSuffix(string(message[:]), "\n")
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("Unknown top level mime type: %s", contentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func createEmailFromHeader(header mail.Header) (email Email, err error) {
|
||||||
|
hp := headerParser{header: &header}
|
||||||
|
|
||||||
|
email.Subject = decodeMimeSentence(header.Get("Subject"))
|
||||||
|
email.From = hp.parseAddressList(header.Get("From"))
|
||||||
|
email.Sender = hp.parseAddress(header.Get("Sender"))
|
||||||
|
email.ReplyTo = hp.parseAddressList(header.Get("Reply-To"))
|
||||||
|
email.To = hp.parseAddressList(header.Get("To"))
|
||||||
|
email.Cc = hp.parseAddressList(header.Get("Cc"))
|
||||||
|
email.Bcc = hp.parseAddressList(header.Get("Bcc"))
|
||||||
|
email.Date = hp.parseTime(header.Get("Date"))
|
||||||
|
email.ResentFrom = hp.parseAddressList(header.Get("Resent-From"))
|
||||||
|
email.ResentSender = hp.parseAddress(header.Get("Resent-Sender"))
|
||||||
|
email.ResentTo = hp.parseAddressList(header.Get("Resent-To"))
|
||||||
|
email.ResentCc = hp.parseAddressList(header.Get("Resent-Cc"))
|
||||||
|
email.ResentBcc = hp.parseAddressList(header.Get("Resent-Bcc"))
|
||||||
|
email.ResentMessageID = hp.parseMessageId(header.Get("Resent-Message-ID"))
|
||||||
|
email.MessageID = hp.parseMessageId(header.Get("Message-ID"))
|
||||||
|
email.InReplyTo = hp.parseMessageIdList(header.Get("In-Reply-To"))
|
||||||
|
email.References = hp.parseMessageIdList(header.Get("References"))
|
||||||
|
email.ResentDate = hp.parseTime(header.Get("Resent-Date"))
|
||||||
|
|
||||||
|
if hp.err != nil {
|
||||||
|
err = hp.err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//decode whole header for easier access to extra fields
|
||||||
|
//todo: should we decode? aren't only standard fields mime encoded?
|
||||||
|
email.Header, err = decodeHeaderMime(header)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseContentType(contentTypeHeader string) (contentType string, params map[string]string, err error) {
|
||||||
|
if contentTypeHeader == "" {
|
||||||
|
contentType = contentTypeTextPlain
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return mime.ParseMediaType(contentTypeHeader)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMultipartRelated(msg io.Reader, boundary string) (textBody, htmlBody string, embeddedFiles []EmbeddedFile, err error) {
|
||||||
|
pmr := multipart.NewReader(msg, boundary)
|
||||||
|
for {
|
||||||
|
part, err := pmr.NextPart()
|
||||||
|
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType, params, err := mime.ParseMediaType(part.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch contentType {
|
||||||
|
case contentTypeTextPlain:
|
||||||
|
ppContent, err := ioutil.ReadAll(part)
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
textBody += strings.TrimSuffix(string(ppContent[:]), "\n")
|
||||||
|
case contentTypeTextHtml:
|
||||||
|
ppContent, err := ioutil.ReadAll(part)
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlBody += strings.TrimSuffix(string(ppContent[:]), "\n")
|
||||||
|
case contentTypeMultipartAlternative:
|
||||||
|
tb, hb, ef, err := parseMultipartAlternative(part, params["boundary"])
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlBody += hb
|
||||||
|
textBody += tb
|
||||||
|
embeddedFiles = append(embeddedFiles, ef...)
|
||||||
|
default:
|
||||||
|
if isEmbeddedFile(part) {
|
||||||
|
ef, err := decodeEmbeddedFile(part)
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
embeddedFiles = append(embeddedFiles, ef)
|
||||||
|
} else {
|
||||||
|
return textBody, htmlBody, embeddedFiles, fmt.Errorf("Can't process multipart/related inner mime type: %s", contentType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMultipartAlternative(msg io.Reader, boundary string) (textBody, htmlBody string, embeddedFiles []EmbeddedFile, err error) {
|
||||||
|
pmr := multipart.NewReader(msg, boundary)
|
||||||
|
for {
|
||||||
|
part, err := pmr.NextPart()
|
||||||
|
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType, params, err := mime.ParseMediaType(part.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch contentType {
|
||||||
|
case contentTypeTextPlain:
|
||||||
|
ppContent, err := ioutil.ReadAll(part)
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
textBody += strings.TrimSuffix(string(ppContent[:]), "\n")
|
||||||
|
case contentTypeTextHtml:
|
||||||
|
ppContent, err := ioutil.ReadAll(part)
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlBody += strings.TrimSuffix(string(ppContent[:]), "\n")
|
||||||
|
case contentTypeMultipartRelated:
|
||||||
|
tb, hb, ef, err := parseMultipartRelated(part, params["boundary"])
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlBody += hb
|
||||||
|
textBody += tb
|
||||||
|
embeddedFiles = append(embeddedFiles, ef...)
|
||||||
|
default:
|
||||||
|
if isEmbeddedFile(part) {
|
||||||
|
ef, err := decodeEmbeddedFile(part)
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
embeddedFiles = append(embeddedFiles, ef)
|
||||||
|
} else {
|
||||||
|
return textBody, htmlBody, embeddedFiles, fmt.Errorf("Can't process multipart/alternative inner mime type: %s", contentType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMultipartMixed(msg io.Reader, boundary string) (textBody, htmlBody string, attachments []Attachment, embeddedFiles []EmbeddedFile, err error) {
|
||||||
|
mr := multipart.NewReader(msg, boundary)
|
||||||
|
for {
|
||||||
|
part, err := mr.NextPart()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return textBody, htmlBody, attachments, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType, params, err := mime.ParseMediaType(part.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, attachments, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if contentType == contentTypeMultipartAlternative {
|
||||||
|
textBody, htmlBody, embeddedFiles, err = parseMultipartAlternative(part, params["boundary"])
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, attachments, embeddedFiles, err
|
||||||
|
}
|
||||||
|
} else if contentType == contentTypeMultipartRelated {
|
||||||
|
textBody, htmlBody, embeddedFiles, err = parseMultipartRelated(part, params["boundary"])
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, attachments, embeddedFiles, err
|
||||||
|
}
|
||||||
|
} else if isAttachment(part) {
|
||||||
|
at, err := decodeAttachment(part)
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, attachments, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
attachments = append(attachments, at)
|
||||||
|
} else {
|
||||||
|
return textBody, htmlBody, attachments, embeddedFiles, fmt.Errorf("Unknown multipart/mixed nested mime type: %s", contentType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return textBody, htmlBody, attachments, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeMimeSentence(s string) string {
|
||||||
|
result := []string{}
|
||||||
|
ss := strings.Split(s, " ")
|
||||||
|
|
||||||
|
for _, word := range ss {
|
||||||
|
dec := new(mime.WordDecoder)
|
||||||
|
w, err := dec.Decode(word)
|
||||||
|
if err != nil {
|
||||||
|
if len(result) == 0 {
|
||||||
|
w = word
|
||||||
|
} else {
|
||||||
|
w = " " + word
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(result, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeHeaderMime(header mail.Header) (mail.Header, error) {
|
||||||
|
parsedHeader := map[string][]string{}
|
||||||
|
|
||||||
|
for headerName, headerData := range header {
|
||||||
|
|
||||||
|
parsedHeaderData := []string{}
|
||||||
|
for _, headerValue := range headerData {
|
||||||
|
parsedHeaderData = append(parsedHeaderData, decodeMimeSentence(headerValue))
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedHeader[headerName] = parsedHeaderData
|
||||||
|
}
|
||||||
|
|
||||||
|
return mail.Header(parsedHeader), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodePartData(part *multipart.Part) (io.Reader, error) {
|
||||||
|
encoding := part.Header.Get("Content-Transfer-Encoding")
|
||||||
|
|
||||||
|
if strings.EqualFold(encoding, "base64") {
|
||||||
|
dr := base64.NewDecoder(base64.StdEncoding, part)
|
||||||
|
dd, err := ioutil.ReadAll(dr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes.NewReader(dd), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("Unknown encoding: %s", encoding)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isEmbeddedFile(part *multipart.Part) bool {
|
||||||
|
return part.Header.Get("Content-Transfer-Encoding") != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeEmbeddedFile(part *multipart.Part) (ef EmbeddedFile, err error) {
|
||||||
|
cid := decodeMimeSentence(part.Header.Get("Content-Id"))
|
||||||
|
decoded, err := decodePartData(part)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ef.CID = strings.Trim(cid, "<>")
|
||||||
|
ef.Data = decoded
|
||||||
|
ef.ContentType = part.Header.Get("Content-Type")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAttachment(part *multipart.Part) bool {
|
||||||
|
return part.FileName() != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeAttachment(part *multipart.Part) (at Attachment, err error) {
|
||||||
|
filename := decodeMimeSentence(part.FileName())
|
||||||
|
decoded, err := decodePartData(part)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
at.Filename = filename
|
||||||
|
at.Data = decoded
|
||||||
|
at.ContentType = strings.Split(part.Header.Get("Content-Type"), ";")[0]
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type headerParser struct {
|
||||||
|
header *mail.Header
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hp headerParser) parseAddress(s string) (ma *mail.Address) {
|
||||||
|
if hp.err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Trim(s, " \n") != "" {
|
||||||
|
ma, hp.err = mail.ParseAddress(s)
|
||||||
|
|
||||||
|
return ma
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hp headerParser) parseAddressList(s string) (ma []*mail.Address) {
|
||||||
|
if hp.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Trim(s, " \n") != "" {
|
||||||
|
ma, hp.err = mail.ParseAddressList(s)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hp headerParser) parseTime(s string) (t time.Time) {
|
||||||
|
if hp.err != nil || s == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t, hp.err = time.Parse(time.RFC1123Z, s)
|
||||||
|
if hp.err == nil {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
t, hp.err = time.Parse("Mon, 2 Jan 2006 15:04:05 -0700", s)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hp headerParser) parseMessageId(s string) string {
|
||||||
|
if hp.err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Trim(s, "<> ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hp headerParser) parseMessageIdList(s string) (result []string) {
|
||||||
|
if hp.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range strings.Split(s, " ") {
|
||||||
|
if strings.Trim(p, " \n") != "" {
|
||||||
|
result = append(result, hp.parseMessageId(p))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attachment with filename, content type and data (as a io.Reader)
|
||||||
|
type Attachment struct {
|
||||||
|
Filename string
|
||||||
|
ContentType string
|
||||||
|
Data io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmbeddedFile with content id, content type and data (as a io.Reader)
|
||||||
|
type EmbeddedFile struct {
|
||||||
|
CID string
|
||||||
|
ContentType string
|
||||||
|
Data io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email with fields for all the headers defined in RFC5322 with it's attachments and
|
||||||
|
type Email struct {
|
||||||
|
Header mail.Header
|
||||||
|
|
||||||
|
Subject string
|
||||||
|
Sender *mail.Address
|
||||||
|
From []*mail.Address
|
||||||
|
ReplyTo []*mail.Address
|
||||||
|
To []*mail.Address
|
||||||
|
Cc []*mail.Address
|
||||||
|
Bcc []*mail.Address
|
||||||
|
Date time.Time
|
||||||
|
MessageID string
|
||||||
|
InReplyTo []string
|
||||||
|
References []string
|
||||||
|
|
||||||
|
ResentFrom []*mail.Address
|
||||||
|
ResentSender *mail.Address
|
||||||
|
ResentTo []*mail.Address
|
||||||
|
ResentDate time.Time
|
||||||
|
ResentCc []*mail.Address
|
||||||
|
ResentBcc []*mail.Address
|
||||||
|
ResentMessageID string
|
||||||
|
|
||||||
|
HTMLBody string
|
||||||
|
TextBody string
|
||||||
|
|
||||||
|
Attachments []Attachment
|
||||||
|
EmbeddedFiles []EmbeddedFile
|
||||||
|
}
|
19
vendor/github.com/emersion/go-imap/.build.yml
generated
vendored
Normal file
19
vendor/github.com/emersion/go-imap/.build.yml
generated
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
image: alpine/edge
|
||||||
|
packages:
|
||||||
|
- go
|
||||||
|
# Required by codecov
|
||||||
|
- bash
|
||||||
|
- findutils
|
||||||
|
sources:
|
||||||
|
- https://github.com/emersion/go-imap
|
||||||
|
tasks:
|
||||||
|
- build: |
|
||||||
|
cd go-imap
|
||||||
|
go build -v ./...
|
||||||
|
- test: |
|
||||||
|
cd go-imap
|
||||||
|
go test -coverprofile=coverage.txt -covermode=atomic ./...
|
||||||
|
- upload-coverage: |
|
||||||
|
cd go-imap
|
||||||
|
export CODECOV_TOKEN=8c0f7014-fcfa-4ed9-8972-542eb5958fb3
|
||||||
|
curl -s https://codecov.io/bash | bash
|
28
vendor/github.com/emersion/go-imap/.gitignore
generated
vendored
Normal file
28
vendor/github.com/emersion/go-imap/.gitignore
generated
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
_obj
|
||||||
|
_test
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
*.[568vq]
|
||||||
|
[568vq].out
|
||||||
|
|
||||||
|
*.cgo1.go
|
||||||
|
*.cgo2.c
|
||||||
|
_cgo_defun.c
|
||||||
|
_cgo_gotypes.go
|
||||||
|
_cgo_export.*
|
||||||
|
|
||||||
|
_testmain.go
|
||||||
|
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
*.prof
|
||||||
|
|
||||||
|
/client.go
|
||||||
|
/server.go
|
||||||
|
coverage.txt
|
23
vendor/github.com/emersion/go-imap/LICENSE
generated
vendored
Normal file
23
vendor/github.com/emersion/go-imap/LICENSE
generated
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2013 The Go-IMAP Authors
|
||||||
|
Copyright (c) 2016 emersion
|
||||||
|
Copyright (c) 2016 Proton Technologies AG
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
168
vendor/github.com/emersion/go-imap/README.md
generated
vendored
Normal file
168
vendor/github.com/emersion/go-imap/README.md
generated
vendored
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
# go-imap
|
||||||
|
|
||||||
|
[![GoDoc](https://godoc.org/github.com/emersion/go-imap?status.svg)](https://godoc.org/github.com/emersion/go-imap)
|
||||||
|
[![builds.sr.ht status](https://builds.sr.ht/~emersion/go-imap.svg)](https://builds.sr.ht/~emersion/go-imap?)
|
||||||
|
[![Codecov](https://codecov.io/gh/emersion/go-imap/branch/master/graph/badge.svg)](https://codecov.io/gh/emersion/go-imap)
|
||||||
|
|
||||||
|
An [IMAP4rev1](https://tools.ietf.org/html/rfc3501) library written in Go. It
|
||||||
|
can be used to build a client and/or a server.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
go get github.com/emersion/go-imap/...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Client [![GoDoc](https://godoc.org/github.com/emersion/go-imap/client?status.svg)](https://godoc.org/github.com/emersion/go-imap/client)
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/client"
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.Println("Connecting to server...")
|
||||||
|
|
||||||
|
// Connect to server
|
||||||
|
c, err := client.DialTLS("mail.example.org:993", nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
log.Println("Connected")
|
||||||
|
|
||||||
|
// Don't forget to logout
|
||||||
|
defer c.Logout()
|
||||||
|
|
||||||
|
// Login
|
||||||
|
if err := c.Login("username", "password"); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
log.Println("Logged in")
|
||||||
|
|
||||||
|
// List mailboxes
|
||||||
|
mailboxes := make(chan *imap.MailboxInfo, 10)
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func () {
|
||||||
|
done <- c.List("", "*", mailboxes)
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Println("Mailboxes:")
|
||||||
|
for m := range mailboxes {
|
||||||
|
log.Println("* " + m.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := <-done; err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select INBOX
|
||||||
|
mbox, err := c.Select("INBOX", false)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
log.Println("Flags for INBOX:", mbox.Flags)
|
||||||
|
|
||||||
|
// Get the last 4 messages
|
||||||
|
from := uint32(1)
|
||||||
|
to := mbox.Messages
|
||||||
|
if mbox.Messages > 3 {
|
||||||
|
// We're using unsigned integers here, only substract if the result is > 0
|
||||||
|
from = mbox.Messages - 3
|
||||||
|
}
|
||||||
|
seqset := new(imap.SeqSet)
|
||||||
|
seqset.AddRange(from, to)
|
||||||
|
|
||||||
|
messages := make(chan *imap.Message, 10)
|
||||||
|
done = make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
done <- c.Fetch(seqset, []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 [![GoDoc](https://godoc.org/github.com/emersion/go-imap/server?status.svg)](https://godoc.org/github.com/emersion/go-imap/server)
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/server"
|
||||||
|
"github.com/emersion/go-imap/backend/memory"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Create a memory backend
|
||||||
|
be := memory.New()
|
||||||
|
|
||||||
|
// Create a new server
|
||||||
|
s := server.New(be)
|
||||||
|
s.Addr = ":1143"
|
||||||
|
// Since we will use this server for testing only, we can allow plain text
|
||||||
|
// authentication over unencrypted connections
|
||||||
|
s.AllowInsecureAuth = true
|
||||||
|
|
||||||
|
log.Println("Starting IMAP server at localhost:1143")
|
||||||
|
if err := s.ListenAndServe(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can now use `telnet localhost 1143` to manually connect to the server.
|
||||||
|
|
||||||
|
## Extending go-imap
|
||||||
|
|
||||||
|
### Extensions
|
||||||
|
|
||||||
|
Commands defined in IMAP extensions are available in other packages. See [the
|
||||||
|
wiki](https://github.com/emersion/go-imap/wiki/Using-extensions#using-client-extensions)
|
||||||
|
to learn how to use them.
|
||||||
|
|
||||||
|
* [APPENDLIMIT](https://github.com/emersion/go-imap-appendlimit)
|
||||||
|
* [COMPRESS](https://github.com/emersion/go-imap-compress)
|
||||||
|
* [ENABLE](https://github.com/emersion/go-imap-enable)
|
||||||
|
* [ID](https://github.com/ProtonMail/go-imap-id)
|
||||||
|
* [IDLE](https://github.com/emersion/go-imap-idle)
|
||||||
|
* [MOVE](https://github.com/emersion/go-imap-move)
|
||||||
|
* [QUOTA](https://github.com/emersion/go-imap-quota)
|
||||||
|
* [SORT and THREAD](https://github.com/emersion/go-imap-sortthread)
|
||||||
|
* [SPECIAL-USE](https://github.com/emersion/go-imap-specialuse)
|
||||||
|
* [UNSELECT](https://github.com/emersion/go-imap-unselect)
|
||||||
|
* [UIDPLUS](https://github.com/emersion/go-imap-uidplus)
|
||||||
|
|
||||||
|
### Server backends
|
||||||
|
|
||||||
|
* [Memory](https://github.com/emersion/go-imap/tree/master/backend/memory) (for testing)
|
||||||
|
* [Multi](https://github.com/emersion/go-imap-multi)
|
||||||
|
* [PGP](https://github.com/emersion/go-imap-pgp)
|
||||||
|
* [Proxy](https://github.com/emersion/go-imap-proxy)
|
||||||
|
|
||||||
|
### Related projects
|
||||||
|
|
||||||
|
* [go-message](https://github.com/emersion/go-message) - parsing and formatting MIME and mail messages
|
||||||
|
* [go-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
|
685
vendor/github.com/emersion/go-imap/client/client.go
generated
vendored
Normal file
685
vendor/github.com/emersion/go-imap/client/client.go
generated
vendored
Normal file
@ -0,0 +1,685 @@
|
|||||||
|
// 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"
|
||||||
|
"syscall"
|
||||||
|
"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 opErr, ok := err.(*net.OpError); ok {
|
||||||
|
if syscallErr, ok := opErr.Err.(*os.SyscallError); ok {
|
||||||
|
if syscallErr.Err == syscall.ECONNRESET {
|
||||||
|
return false, 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)
|
||||||
|
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
|
||||||
|
}
|
87
vendor/github.com/emersion/go-imap/client/cmd_any.go
generated
vendored
Normal file
87
vendor/github.com/emersion/go-imap/client/cmd_any.go
generated
vendored
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
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
|
||||||
|
}
|
254
vendor/github.com/emersion/go-imap/client/cmd_auth.go
generated
vendored
Normal file
254
vendor/github.com/emersion/go-imap/client/cmd_auth.go
generated
vendored
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
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 {
|
||||||
|
if err := c.ensureAuthenticated(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer close(ch)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if err := c.ensureAuthenticated(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer close(ch)
|
||||||
|
|
||||||
|
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.
|
||||||
|
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()
|
||||||
|
}
|
174
vendor/github.com/emersion/go-imap/client/cmd_noauth.go
generated
vendored
Normal file
174
vendor/github.com/emersion/go-imap/client/cmd_noauth.go
generated
vendored
Normal file
@ -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
|
||||||
|
}
|
260
vendor/github.com/emersion/go-imap/client/cmd_selected.go
generated
vendored
Normal file
260
vendor/github.com/emersion/go-imap/client/cmd_selected.go
generated
vendored
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
"github.com/emersion/go-imap/commands"
|
||||||
|
"github.com/emersion/go-imap/responses"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrNoMailboxSelected is returned if a command that requires a mailbox to be
|
||||||
|
// selected is called when there isn't.
|
||||||
|
var ErrNoMailboxSelected = errors.New("No mailbox selected")
|
||||||
|
|
||||||
|
// 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 c.State() != imap.SelectedState {
|
||||||
|
return ErrNoMailboxSelected
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := new(commands.Expunge)
|
||||||
|
|
||||||
|
var h responses.Handler
|
||||||
|
if ch != nil {
|
||||||
|
h = &responses.Expunge{SeqNums: ch}
|
||||||
|
defer close(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.
|
||||||
|
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}
|
||||||
|
|
||||||
|
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 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}
|
||||||
|
defer close(ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
24
vendor/github.com/emersion/go-imap/client/tag.go
generated
vendored
Normal file
24
vendor/github.com/emersion/go-imap/client/tag.go
generated
vendored
Normal file
@ -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
|
||||||
|
}
|
57
vendor/github.com/emersion/go-imap/command.go
generated
vendored
Normal file
57
vendor/github.com/emersion/go-imap/command.go
generated
vendored
Normal file
@ -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
|
||||||
|
}
|
93
vendor/github.com/emersion/go-imap/commands/append.go
generated
vendored
Normal file
93
vendor/github.com/emersion/go-imap/commands/append.go
generated
vendored
Normal file
@ -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
|
||||||
|
}
|
116
vendor/github.com/emersion/go-imap/commands/authenticate.go
generated
vendored
Normal file
116
vendor/github.com/emersion/go-imap/commands/authenticate.go
generated
vendored
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner.Scan()
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded = scanner.Text()
|
||||||
|
if encoded != "" {
|
||||||
|
response, err = base64.StdEncoding.DecodeString(encoded)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
vendor/github.com/emersion/go-imap/commands/capability.go
generated
vendored
Normal file
18
vendor/github.com/emersion/go-imap/commands/capability.go
generated
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Capability is a CAPABILITY command, as defined in RFC 3501 section 6.1.1.
|
||||||
|
type Capability struct{}
|
||||||
|
|
||||||
|
func (c *Capability) Command() *imap.Command {
|
||||||
|
return &imap.Command{
|
||||||
|
Name: "CAPABILITY",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Capability) Parse(fields []interface{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
18
vendor/github.com/emersion/go-imap/commands/check.go
generated
vendored
Normal file
18
vendor/github.com/emersion/go-imap/commands/check.go
generated
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check is a CHECK command, as defined in RFC 3501 section 6.4.1.
|
||||||
|
type Check struct{}
|
||||||
|
|
||||||
|
func (cmd *Check) Command() *imap.Command {
|
||||||
|
return &imap.Command{
|
||||||
|
Name: "CHECK",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Check) Parse(fields []interface{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
18
vendor/github.com/emersion/go-imap/commands/close.go
generated
vendored
Normal file
18
vendor/github.com/emersion/go-imap/commands/close.go
generated
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Close is a CLOSE command, as defined in RFC 3501 section 6.4.2.
|
||||||
|
type Close struct{}
|
||||||
|
|
||||||
|
func (cmd *Close) Command() *imap.Command {
|
||||||
|
return &imap.Command{
|
||||||
|
Name: "CLOSE",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Close) Parse(fields []interface{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
2
vendor/github.com/emersion/go-imap/commands/commands.go
generated
vendored
Normal file
2
vendor/github.com/emersion/go-imap/commands/commands.go
generated
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Package commands implements IMAP commands defined in RFC 3501.
|
||||||
|
package commands
|
47
vendor/github.com/emersion/go-imap/commands/copy.go
generated
vendored
Normal file
47
vendor/github.com/emersion/go-imap/commands/copy.go
generated
vendored
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
"github.com/emersion/go-imap/utf7"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Copy is a COPY command, as defined in RFC 3501 section 6.4.7.
|
||||||
|
type Copy struct {
|
||||||
|
SeqSet *imap.SeqSet
|
||||||
|
Mailbox string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Copy) Command() *imap.Command {
|
||||||
|
mailbox, _ := utf7.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
|
||||||
|
}
|
38
vendor/github.com/emersion/go-imap/commands/create.go
generated
vendored
Normal file
38
vendor/github.com/emersion/go-imap/commands/create.go
generated
vendored
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
"github.com/emersion/go-imap/utf7"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create is a CREATE command, as defined in RFC 3501 section 6.3.3.
|
||||||
|
type Create struct {
|
||||||
|
Mailbox string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Create) Command() *imap.Command {
|
||||||
|
mailbox, _ := utf7.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
|
||||||
|
}
|
38
vendor/github.com/emersion/go-imap/commands/delete.go
generated
vendored
Normal file
38
vendor/github.com/emersion/go-imap/commands/delete.go
generated
vendored
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
"github.com/emersion/go-imap/utf7"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Delete is a DELETE command, as defined in RFC 3501 section 6.3.3.
|
||||||
|
type Delete struct {
|
||||||
|
Mailbox string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Delete) Command() *imap.Command {
|
||||||
|
mailbox, _ := utf7.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
|
||||||
|
}
|
16
vendor/github.com/emersion/go-imap/commands/expunge.go
generated
vendored
Normal file
16
vendor/github.com/emersion/go-imap/commands/expunge.go
generated
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Expunge is an EXPUNGE command, as defined in RFC 3501 section 6.4.3.
|
||||||
|
type Expunge struct{}
|
||||||
|
|
||||||
|
func (cmd *Expunge) Command() *imap.Command {
|
||||||
|
return &imap.Command{Name: "EXPUNGE"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Expunge) Parse(fields []interface{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
55
vendor/github.com/emersion/go-imap/commands/fetch.go
generated
vendored
Normal file
55
vendor/github.com/emersion/go-imap/commands/fetch.go
generated
vendored
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Fetch is a FETCH command, as defined in RFC 3501 section 6.4.5.
|
||||||
|
type Fetch struct {
|
||||||
|
SeqSet *imap.SeqSet
|
||||||
|
Items []imap.FetchItem
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Fetch) Command() *imap.Command {
|
||||||
|
items := make([]interface{}, len(cmd.Items))
|
||||||
|
for i, item := range cmd.Items {
|
||||||
|
items[i] = imap.RawString(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &imap.Command{
|
||||||
|
Name: "FETCH",
|
||||||
|
Arguments: []interface{}{cmd.SeqSet, items},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Fetch) Parse(fields []interface{}) error {
|
||||||
|
if len(fields) < 2 {
|
||||||
|
return errors.New("No enough arguments")
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if seqset, ok := fields[0].(string); !ok {
|
||||||
|
return errors.New("Sequence set must be an atom")
|
||||||
|
} else if cmd.SeqSet, err = imap.ParseSeqSet(seqset); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch items := fields[1].(type) {
|
||||||
|
case string: // A macro or a single item
|
||||||
|
cmd.Items = imap.FetchItem(strings.ToUpper(items)).Expand()
|
||||||
|
case []interface{}: // A list of items
|
||||||
|
cmd.Items = make([]imap.FetchItem, 0, len(items))
|
||||||
|
for _, v := range items {
|
||||||
|
itemStr, _ := v.(string)
|
||||||
|
item := imap.FetchItem(strings.ToUpper(itemStr))
|
||||||
|
cmd.Items = append(cmd.Items, item.Expand()...)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return errors.New("Items must be either a string or a list")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
60
vendor/github.com/emersion/go-imap/commands/list.go
generated
vendored
Normal file
60
vendor/github.com/emersion/go-imap/commands/list.go
generated
vendored
Normal file
@ -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
|
||||||
|
}
|
36
vendor/github.com/emersion/go-imap/commands/login.go
generated
vendored
Normal file
36
vendor/github.com/emersion/go-imap/commands/login.go
generated
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Login is a LOGIN command, as defined in RFC 3501 section 6.2.2.
|
||||||
|
type Login struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Login) Command() *imap.Command {
|
||||||
|
return &imap.Command{
|
||||||
|
Name: "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
|
||||||
|
}
|
18
vendor/github.com/emersion/go-imap/commands/logout.go
generated
vendored
Normal file
18
vendor/github.com/emersion/go-imap/commands/logout.go
generated
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logout is a LOGOUT command, as defined in RFC 3501 section 6.1.3.
|
||||||
|
type Logout struct{}
|
||||||
|
|
||||||
|
func (c *Logout) Command() *imap.Command {
|
||||||
|
return &imap.Command{
|
||||||
|
Name: "LOGOUT",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Logout) Parse(fields []interface{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
18
vendor/github.com/emersion/go-imap/commands/noop.go
generated
vendored
Normal file
18
vendor/github.com/emersion/go-imap/commands/noop.go
generated
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Noop is a NOOP command, as defined in RFC 3501 section 6.1.2.
|
||||||
|
type Noop struct{}
|
||||||
|
|
||||||
|
func (c *Noop) Command() *imap.Command {
|
||||||
|
return &imap.Command{
|
||||||
|
Name: "NOOP",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Noop) Parse(fields []interface{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
51
vendor/github.com/emersion/go-imap/commands/rename.go
generated
vendored
Normal file
51
vendor/github.com/emersion/go-imap/commands/rename.go
generated
vendored
Normal file
@ -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
|
||||||
|
}
|
57
vendor/github.com/emersion/go-imap/commands/search.go
generated
vendored
Normal file
57
vendor/github.com/emersion/go-imap/commands/search.go
generated
vendored
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Search is a SEARCH command, as defined in RFC 3501 section 6.4.4.
|
||||||
|
type Search struct {
|
||||||
|
Charset string
|
||||||
|
Criteria *imap.SearchCriteria
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Search) Command() *imap.Command {
|
||||||
|
var args []interface{}
|
||||||
|
if cmd.Charset != "" {
|
||||||
|
args = append(args, imap.RawString("CHARSET"), 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)
|
||||||
|
}
|
45
vendor/github.com/emersion/go-imap/commands/select.go
generated
vendored
Normal file
45
vendor/github.com/emersion/go-imap/commands/select.go
generated
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
"github.com/emersion/go-imap/utf7"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Select is a SELECT command, as defined in RFC 3501 section 6.3.1. If ReadOnly
|
||||||
|
// is set to true, the EXAMINE command will be used instead.
|
||||||
|
type Select struct {
|
||||||
|
Mailbox string
|
||||||
|
ReadOnly bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Select) Command() *imap.Command {
|
||||||
|
name := "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
|
||||||
|
}
|
18
vendor/github.com/emersion/go-imap/commands/starttls.go
generated
vendored
Normal file
18
vendor/github.com/emersion/go-imap/commands/starttls.go
generated
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StartTLS is a STARTTLS command, as defined in RFC 3501 section 6.2.1.
|
||||||
|
type StartTLS struct{}
|
||||||
|
|
||||||
|
func (cmd *StartTLS) Command() *imap.Command {
|
||||||
|
return &imap.Command{
|
||||||
|
Name: "STARTTLS",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *StartTLS) Parse(fields []interface{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
58
vendor/github.com/emersion/go-imap/commands/status.go
generated
vendored
Normal file
58
vendor/github.com/emersion/go-imap/commands/status.go
generated
vendored
Normal file
@ -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
|
||||||
|
}
|
50
vendor/github.com/emersion/go-imap/commands/store.go
generated
vendored
Normal file
50
vendor/github.com/emersion/go-imap/commands/store.go
generated
vendored
Normal file
@ -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
|
||||||
|
}
|
63
vendor/github.com/emersion/go-imap/commands/subscribe.go
generated
vendored
Normal file
63
vendor/github.com/emersion/go-imap/commands/subscribe.go
generated
vendored
Normal file
@ -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
|
||||||
|
}
|
44
vendor/github.com/emersion/go-imap/commands/uid.go
generated
vendored
Normal file
44
vendor/github.com/emersion/go-imap/commands/uid.go
generated
vendored
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Uid is a UID command, as defined in RFC 3501 section 6.4.8. It wraps another
|
||||||
|
// command (e.g. wrapping a Fetch command will result in a UID FETCH).
|
||||||
|
type Uid struct {
|
||||||
|
Cmd imap.Commander
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Uid) Command() *imap.Command {
|
||||||
|
inner := cmd.Cmd.Command()
|
||||||
|
|
||||||
|
args := []interface{}{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
|
||||||
|
}
|
284
vendor/github.com/emersion/go-imap/conn.go
generated
vendored
Normal file
284
vendor/github.com/emersion/go-imap/conn.go
generated
vendored
Normal file
@ -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()
|
||||||
|
}
|
72
vendor/github.com/emersion/go-imap/date.go
generated
vendored
Normal file
72
vendor/github.com/emersion/go-imap/date.go
generated
vendored
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package imap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Date and time layouts.
|
||||||
|
// Dovecot adds a leading zero to dates:
|
||||||
|
// https://github.com/dovecot/core/blob/4fbd5c5e113078e72f29465ccc96d44955ceadc2/src/lib-imap/imap-date.c#L166
|
||||||
|
// Cyrus adds a leading space to dates:
|
||||||
|
// https://github.com/cyrusimap/cyrus-imapd/blob/1cb805a3bffbdf829df0964f3b802cdc917e76db/lib/times.c#L543
|
||||||
|
// GMail doesn't support leading spaces in dates used in SEARCH commands.
|
||||||
|
const (
|
||||||
|
// Defined in RFC 3501 as date-text on page 83.
|
||||||
|
DateLayout = "_2-Jan-2006"
|
||||||
|
// Defined in RFC 3501 as date-time on page 83.
|
||||||
|
DateTimeLayout = "_2-Jan-2006 15:04:05 -0700"
|
||||||
|
// Defined in RFC 5322 section 3.3, mentioned as env-date in RFC 3501 page 84.
|
||||||
|
envelopeDateTimeLayout = "Mon, 02 Jan 2006 15:04:05 -0700"
|
||||||
|
// Use as an example in RFC 3501 page 54.
|
||||||
|
searchDateLayout = "2-Jan-2006"
|
||||||
|
)
|
||||||
|
|
||||||
|
// time.Time with a specific layout.
|
||||||
|
type (
|
||||||
|
Date time.Time
|
||||||
|
DateTime time.Time
|
||||||
|
envelopeDateTime time.Time
|
||||||
|
searchDate time.Time
|
||||||
|
)
|
||||||
|
|
||||||
|
// Permutations of the layouts defined in RFC 5322, section 3.3.
|
||||||
|
var envelopeDateTimeLayouts = [...]string{
|
||||||
|
envelopeDateTimeLayout, // popular, try it first
|
||||||
|
"_2 Jan 2006 15:04:05 -0700",
|
||||||
|
"_2 Jan 2006 15:04:05 MST",
|
||||||
|
"_2 Jan 2006 15:04:05 -0700 (MST)",
|
||||||
|
"_2 Jan 2006 15:04 -0700",
|
||||||
|
"_2 Jan 2006 15:04 MST",
|
||||||
|
"_2 Jan 2006 15:04 -0700 (MST)",
|
||||||
|
"_2 Jan 06 15:04:05 -0700",
|
||||||
|
"_2 Jan 06 15:04:05 MST",
|
||||||
|
"_2 Jan 06 15:04:05 -0700 (MST)",
|
||||||
|
"_2 Jan 06 15:04 -0700",
|
||||||
|
"_2 Jan 06 15:04 MST",
|
||||||
|
"_2 Jan 06 15:04 -0700 (MST)",
|
||||||
|
"Mon, _2 Jan 2006 15:04:05 -0700",
|
||||||
|
"Mon, _2 Jan 2006 15:04:05 MST",
|
||||||
|
"Mon, _2 Jan 2006 15:04:05 -0700 (MST)",
|
||||||
|
"Mon, _2 Jan 2006 15:04 -0700",
|
||||||
|
"Mon, _2 Jan 2006 15:04 MST",
|
||||||
|
"Mon, _2 Jan 2006 15:04 -0700 (MST)",
|
||||||
|
"Mon, _2 Jan 06 15:04:05 -0700",
|
||||||
|
"Mon, _2 Jan 06 15:04:05 MST",
|
||||||
|
"Mon, _2 Jan 06 15:04:05 -0700 (MST)",
|
||||||
|
"Mon, _2 Jan 06 15:04 -0700",
|
||||||
|
"Mon, _2 Jan 06 15:04 MST",
|
||||||
|
"Mon, _2 Jan 06 15:04 -0700 (MST)",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try parsing the date based on the layouts defined in RFC 5322, section 3.3.
|
||||||
|
// Inspired by https://github.com/golang/go/blob/master/src/net/mail/message.go
|
||||||
|
func parseMessageDateTime(maybeDate string) (time.Time, error) {
|
||||||
|
for _, layout := range envelopeDateTimeLayouts {
|
||||||
|
parsed, err := time.Parse(layout, maybeDate)
|
||||||
|
if err == nil {
|
||||||
|
return parsed, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return time.Time{}, fmt.Errorf("date %s could not be parsed", maybeDate)
|
||||||
|
}
|
7
vendor/github.com/emersion/go-imap/go.mod
generated
vendored
Normal file
7
vendor/github.com/emersion/go-imap/go.mod
generated
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module github.com/emersion/go-imap
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/emersion/go-message v0.10.4-0.20190609165112-592ace5bc1ca
|
||||||
|
github.com/emersion/go-sasl v0.0.0-20190520160400-47d427600317
|
||||||
|
golang.org/x/text v0.3.2
|
||||||
|
)
|
18
vendor/github.com/emersion/go-imap/go.sum
generated
vendored
Normal file
18
vendor/github.com/emersion/go-imap/go.sum
generated
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/emersion/go-message v0.10.4-0.20190609165112-592ace5bc1ca h1:OYhqtJI4eOLvGtRIsUfP87VMJ1J/o6ks1tah9DlYkn4=
|
||||||
|
github.com/emersion/go-message v0.10.4-0.20190609165112-592ace5bc1ca/go.mod h1:3h+HsGTCFHmk4ngJ2IV/YPhdlaOcR6hcgqM3yca9v7c=
|
||||||
|
github.com/emersion/go-sasl v0.0.0-20190520160400-47d427600317 h1:tYZxAY8nu3JJQKios9f27Sbvbkfm4XHXT476gVtszu0=
|
||||||
|
github.com/emersion/go-sasl v0.0.0-20190520160400-47d427600317/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
|
||||||
|
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe h1:40SWqY0zE3qCi6ZrtTf5OUdNm5lDnGnjRSq9GgmeTrg=
|
||||||
|
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
||||||
|
github.com/martinlindhe/base36 v0.0.0-20190418230009-7c6542dfbb41 h1:CVsnY46BCLkX9XOhALJ/S7yb9ayc4eqjXSXO3tyB66A=
|
||||||
|
github.com/martinlindhe/base36 v0.0.0-20190418230009-7c6542dfbb41/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
106
vendor/github.com/emersion/go-imap/imap.go
generated
vendored
Normal file
106
vendor/github.com/emersion/go-imap/imap.go
generated
vendored
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
// 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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)
|
13
vendor/github.com/emersion/go-imap/literal.go
generated
vendored
Normal file
13
vendor/github.com/emersion/go-imap/literal.go
generated
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package imap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A literal, as defined in RFC 3501 section 4.3.
|
||||||
|
type Literal interface {
|
||||||
|
io.Reader
|
||||||
|
|
||||||
|
// Len returns the number of bytes of the literal.
|
||||||
|
Len() int
|
||||||
|
}
|
8
vendor/github.com/emersion/go-imap/logger.go
generated
vendored
Normal file
8
vendor/github.com/emersion/go-imap/logger.go
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package imap
|
||||||
|
|
||||||
|
// Logger is the behaviour used by server/client to
|
||||||
|
// report errors for accepting connections and unexpected behavior from handlers.
|
||||||
|
type Logger interface {
|
||||||
|
Printf(format string, v ...interface{})
|
||||||
|
Println(v ...interface{})
|
||||||
|
}
|
258
vendor/github.com/emersion/go-imap/mailbox.go
generated
vendored
Normal file
258
vendor/github.com/emersion/go-imap/mailbox.go
generated
vendored
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
package imap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/utf7"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The primary mailbox, as defined in RFC 3501 section 5.1.
|
||||||
|
const InboxName = "INBOX"
|
||||||
|
|
||||||
|
// CanonicalMailboxName returns the canonical form of a mailbox name. Mailbox names can be
|
||||||
|
// case-sensitive or case-insensitive depending on the backend implementation.
|
||||||
|
// The special INBOX mailbox is case-insensitive.
|
||||||
|
func CanonicalMailboxName(name string) string {
|
||||||
|
if strings.ToUpper(name) == InboxName {
|
||||||
|
return InboxName
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mailbox attributes definied in RFC 3501 section 7.2.2.
|
||||||
|
const (
|
||||||
|
// It is not possible for any child levels of hierarchy to exist under this\
|
||||||
|
// name; no child levels exist now and none can be created in the future.
|
||||||
|
NoInferiorsAttr = "\\Noinferiors"
|
||||||
|
// It is not possible to use this name as a selectable mailbox.
|
||||||
|
NoSelectAttr = "\\Noselect"
|
||||||
|
// The mailbox has been marked "interesting" by the server; the mailbox
|
||||||
|
// probably contains messages that have been added since the last time the
|
||||||
|
// mailbox was selected.
|
||||||
|
MarkedAttr = "\\Marked"
|
||||||
|
// The mailbox does not contain any additional messages since the last time
|
||||||
|
// the mailbox was selected.
|
||||||
|
UnmarkedAttr = "\\Unmarked"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
return errors.New("Mailbox delimiter must be a string")
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
// Thunderbird doesn't understand delimiters if not quoted
|
||||||
|
return []interface{}{attrs, info.Delimiter, 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 strings.HasPrefix(pattern, info.Delimiter) {
|
||||||
|
reference = ""
|
||||||
|
pattern = strings.TrimPrefix(pattern, info.Delimiter)
|
||||||
|
}
|
||||||
|
if reference != "" {
|
||||||
|
if !strings.HasSuffix(reference, info.Delimiter) {
|
||||||
|
reference += info.Delimiter
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(name, reference) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
name = strings.TrimPrefix(name, reference)
|
||||||
|
}
|
||||||
|
|
||||||
|
return info.match(name, pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
1084
vendor/github.com/emersion/go-imap/message.go
generated
vendored
Normal file
1084
vendor/github.com/emersion/go-imap/message.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
466
vendor/github.com/emersion/go-imap/read.go
generated
vendored
Normal file
466
vendor/github.com/emersion/go-imap/read.go
generated
vendored
Normal file
@ -0,0 +1,466 @@
|
|||||||
|
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 == "NIL" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return atom, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) ReadLiteral() (Literal, error) {
|
||||||
|
char, _, err := r.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if char != literalStart {
|
||||||
|
return nil, newParseError("literal string doesn't start with an open brace")
|
||||||
|
}
|
||||||
|
|
||||||
|
lstr, err := r.ReadString(byte(literalEnd))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
lstr = trimSuffix(lstr, literalEnd)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
181
vendor/github.com/emersion/go-imap/response.go
generated
vendored
Normal file
181
vendor/github.com/emersion/go-imap/response.go
generated
vendored
Normal file
@ -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
|
||||||
|
}
|
61
vendor/github.com/emersion/go-imap/responses/authenticate.go
generated
vendored
Normal file
61
vendor/github.com/emersion/go-imap/responses/authenticate.go
generated
vendored
Normal file
@ -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)
|
||||||
|
}
|
20
vendor/github.com/emersion/go-imap/responses/capability.go
generated
vendored
Normal file
20
vendor/github.com/emersion/go-imap/responses/capability.go
generated
vendored
Normal file
@ -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)
|
||||||
|
}
|
43
vendor/github.com/emersion/go-imap/responses/expunge.go
generated
vendored
Normal file
43
vendor/github.com/emersion/go-imap/responses/expunge.go
generated
vendored
Normal file
@ -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
|
||||||
|
}
|
47
vendor/github.com/emersion/go-imap/responses/fetch.go
generated
vendored
Normal file
47
vendor/github.com/emersion/go-imap/responses/fetch.go
generated
vendored
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Messages <- msg
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Fetch) WriteTo(w *imap.Writer) error {
|
||||||
|
for msg := range r.Messages {
|
||||||
|
resp := imap.NewUntaggedResp([]interface{}{msg.SeqNum, imap.RawString(fetchName), msg.Format()})
|
||||||
|
if err := resp.WriteTo(w); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
57
vendor/github.com/emersion/go-imap/responses/list.go
generated
vendored
Normal file
57
vendor/github.com/emersion/go-imap/responses/list.go
generated
vendored
Normal file
@ -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
|
||||||
|
}
|
35
vendor/github.com/emersion/go-imap/responses/responses.go
generated
vendored
Normal file
35
vendor/github.com/emersion/go-imap/responses/responses.go
generated
vendored
Normal file
@ -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
|
||||||
|
}
|
41
vendor/github.com/emersion/go-imap/responses/search.go
generated
vendored
Normal file
41
vendor/github.com/emersion/go-imap/responses/search.go
generated
vendored
Normal file
@ -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)
|
||||||
|
}
|
142
vendor/github.com/emersion/go-imap/responses/select.go
generated
vendored
Normal file
142
vendor/github.com/emersion/go-imap/responses/select.go
generated
vendored
Normal file
@ -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
|
||||||
|
}
|
53
vendor/github.com/emersion/go-imap/responses/status.go
generated
vendored
Normal file
53
vendor/github.com/emersion/go-imap/responses/status.go
generated
vendored
Normal file
@ -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)
|
||||||
|
}
|
366
vendor/github.com/emersion/go-imap/search.go
generated
vendored
Normal file
366
vendor/github.com/emersion/go-imap/search.go
generated
vendored
Normal file
@ -0,0 +1,366 @@
|
|||||||
|
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"), 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"), 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())
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields
|
||||||
|
}
|
289
vendor/github.com/emersion/go-imap/seqset.go
generated
vendored
Normal file
289
vendor/github.com/emersion/go-imap/seqset.go
generated
vendored
Normal file
@ -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)
|
||||||
|
}
|
120
vendor/github.com/emersion/go-imap/status.go
generated
vendored
Normal file
120
vendor/github.com/emersion/go-imap/status.go
generated
vendored
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
package imap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A status response type.
|
||||||
|
type StatusRespType string
|
||||||
|
|
||||||
|
// Status response types defined in RFC 3501 section 7.1.
|
||||||
|
const (
|
||||||
|
// The OK response indicates an information message from the server. When
|
||||||
|
// tagged, it indicates successful completion of the associated command.
|
||||||
|
// The untagged form indicates an information-only message.
|
||||||
|
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()
|
||||||
|
}
|
148
vendor/github.com/emersion/go-imap/utf7/decoder.go
generated
vendored
Normal file
148
vendor/github.com/emersion/go-imap/utf7/decoder.go
generated
vendored
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
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) {
|
||||||
|
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]
|
||||||
|
}
|
91
vendor/github.com/emersion/go-imap/utf7/encoder.go
generated
vendored
Normal file
91
vendor/github.com/emersion/go-imap/utf7/encoder.go
generated
vendored
Normal file
@ -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
|
||||||
|
}
|
34
vendor/github.com/emersion/go-imap/utf7/utf7.go
generated
vendored
Normal file
34
vendor/github.com/emersion/go-imap/utf7/utf7.go
generated
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// 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{}
|
239
vendor/github.com/emersion/go-imap/write.go
generated
vendored
Normal file
239
vendor/github.com/emersion/go-imap/write.go
generated
vendored
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
package imap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"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))
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if n != literalLen {
|
||||||
|
return fmt.Errorf("imap: size of Literal is not equal to Len() (%d != %d)", n, 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}
|
||||||
|
}
|
19
vendor/github.com/emersion/go-message/.build.yml
generated
vendored
Normal file
19
vendor/github.com/emersion/go-message/.build.yml
generated
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
image: alpine/edge
|
||||||
|
packages:
|
||||||
|
- go
|
||||||
|
# Required by codecov
|
||||||
|
- bash
|
||||||
|
- findutils
|
||||||
|
sources:
|
||||||
|
- https://github.com/emersion/go-message
|
||||||
|
tasks:
|
||||||
|
- build: |
|
||||||
|
cd go-message
|
||||||
|
go build -v ./...
|
||||||
|
- test: |
|
||||||
|
cd go-message
|
||||||
|
go test -coverprofile=coverage.txt -covermode=atomic ./...
|
||||||
|
- upload-coverage: |
|
||||||
|
cd go-message
|
||||||
|
export CODECOV_TOKEN=aa72bd72-88cd-4bc7-aaa8-a3206d058935
|
||||||
|
curl -s https://codecov.io/bash | bash
|
24
vendor/github.com/emersion/go-message/.gitignore
generated
vendored
Normal file
24
vendor/github.com/emersion/go-message/.gitignore
generated
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
_obj
|
||||||
|
_test
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
*.[568vq]
|
||||||
|
[568vq].out
|
||||||
|
|
||||||
|
*.cgo1.go
|
||||||
|
*.cgo2.c
|
||||||
|
_cgo_defun.c
|
||||||
|
_cgo_gotypes.go
|
||||||
|
_cgo_export.*
|
||||||
|
|
||||||
|
_testmain.go
|
||||||
|
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
*.prof
|
21
vendor/github.com/emersion/go-message/LICENSE
generated
vendored
Normal file
21
vendor/github.com/emersion/go-message/LICENSE
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
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.
|
32
vendor/github.com/emersion/go-message/README.md
generated
vendored
Normal file
32
vendor/github.com/emersion/go-message/README.md
generated
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# go-message
|
||||||
|
|
||||||
|
[![GoDoc](https://godoc.org/github.com/emersion/go-message?status.svg)](https://godoc.org/github.com/emersion/go-message)
|
||||||
|
[![builds.sr.ht status](https://builds.sr.ht/~emersion/go-message.svg)](https://builds.sr.ht/~emersion/go-message?)
|
||||||
|
[![codecov](https://codecov.io/gh/emersion/go-message/branch/master/graph/badge.svg)](https://codecov.io/gh/emersion/go-message)
|
||||||
|
[![Unstable](https://img.shields.io/badge/stability-unstable-yellow.svg)](https://github.com/emersion/stability-badges#unstable)
|
||||||
|
|
||||||
|
A Go library for the Internet Message Format. It implements:
|
||||||
|
|
||||||
|
* [RFC 5322]: Internet Message Format
|
||||||
|
* [RFC 2045], [RFC 2046] and [RFC 2047]: Multipurpose Internet Mail Extensions
|
||||||
|
* [RFC 2183]: Content-Disposition Header Field
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* Streaming API
|
||||||
|
* Automatic encoding and charset handling
|
||||||
|
* A [`mail`](https://godoc.org/github.com/emersion/go-message/mail) subpackage
|
||||||
|
to read and write mail messages
|
||||||
|
* DKIM-friendly
|
||||||
|
* A [`textproto`](https://godoc.org/github.com/emersion/go-message/textproto)
|
||||||
|
subpackage that just implements the wire format
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
[RFC 5322]: https://tools.ietf.org/html/rfc5322
|
||||||
|
[RFC 2045]: https://tools.ietf.org/html/rfc2045
|
||||||
|
[RFC 2046]: https://tools.ietf.org/html/rfc2046
|
||||||
|
[RFC 2047]: https://tools.ietf.org/html/rfc2047
|
||||||
|
[RFC 2183]: https://tools.ietf.org/html/rfc2183
|
57
vendor/github.com/emersion/go-message/charset.go
generated
vendored
Normal file
57
vendor/github.com/emersion/go-message/charset.go
generated
vendored
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type unknownCharsetError struct {
|
||||||
|
error
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsUnknownCharset returns a boolean indicating whether the error is known to
|
||||||
|
// report that the charset advertised by the entity is unknown.
|
||||||
|
func IsUnknownCharset(err error) bool {
|
||||||
|
_, ok := err.(unknownCharsetError)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// Importing github.com/emersion/go-message/charset will set CharsetReader to
|
||||||
|
// a function that handles most common charsets. Alternatively, CharsetReader
|
||||||
|
// can be set to e.g. golang.org/x/net/html/charset.NewReaderLabel.
|
||||||
|
var CharsetReader func(charset string, input io.Reader) (io.Reader, error)
|
||||||
|
|
||||||
|
// charsetReader calls CharsetReader if non-nil.
|
||||||
|
func charsetReader(charset string, input io.Reader) (io.Reader, error) {
|
||||||
|
charset = strings.ToLower(charset)
|
||||||
|
// "ascii" is not in the spec but is common
|
||||||
|
if charset == "utf-8" || charset == "us-ascii" || charset == "ascii" {
|
||||||
|
return input, nil
|
||||||
|
}
|
||||||
|
if CharsetReader != nil {
|
||||||
|
return CharsetReader(charset, input)
|
||||||
|
}
|
||||||
|
return input, fmt.Errorf("message: unhandled charset %q", charset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeHeader decodes an internationalized header field. If it fails, it
|
||||||
|
// returns the input string and the error.
|
||||||
|
func decodeHeader(s string) (string, error) {
|
||||||
|
wordDecoder := mime.WordDecoder{CharsetReader: charsetReader}
|
||||||
|
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)
|
||||||
|
}
|
60
vendor/github.com/emersion/go-message/encoding.go
generated
vendored
Normal file
60
vendor/github.com/emersion/go-message/encoding.go
generated
vendored
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/quotedprintable"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/emersion/go-textwrapper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type unknownEncodingError struct {
|
||||||
|
error
|
||||||
|
}
|
||||||
|
|
||||||
|
func isUnknownEncoding(err error) bool {
|
||||||
|
_, ok := err.(unknownEncodingError)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodingReader(enc string, r io.Reader) (io.Reader, error) {
|
||||||
|
var dec io.Reader
|
||||||
|
switch strings.ToLower(enc) {
|
||||||
|
case "quoted-printable":
|
||||||
|
dec = quotedprintable.NewReader(r)
|
||||||
|
case "base64":
|
||||||
|
dec = base64.NewDecoder(base64.StdEncoding, r)
|
||||||
|
case "7bit", "8bit", "binary", "":
|
||||||
|
dec = r
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unhandled encoding %q", enc)
|
||||||
|
}
|
||||||
|
return dec, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type nopCloser struct {
|
||||||
|
io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nopCloser) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodingWriter(enc string, w io.Writer) (io.WriteCloser, error) {
|
||||||
|
var wc io.WriteCloser
|
||||||
|
switch strings.ToLower(enc) {
|
||||||
|
case "quoted-printable":
|
||||||
|
wc = quotedprintable.NewWriter(w)
|
||||||
|
case "base64":
|
||||||
|
wc = base64.NewEncoder(base64.StdEncoding, textwrapper.NewRFC822(w))
|
||||||
|
case "7bit", "8bit":
|
||||||
|
wc = nopCloser{textwrapper.New(w, "\r\n", 1000)}
|
||||||
|
case "binary", "":
|
||||||
|
wc = nopCloser{w}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unhandled encoding %q", enc)
|
||||||
|
}
|
||||||
|
return wc, nil
|
||||||
|
}
|
118
vendor/github.com/emersion/go-message/entity.go
generated
vendored
Normal file
118
vendor/github.com/emersion/go-message/entity.go
generated
vendored
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/emersion/go-message/textproto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// An Entity is either a whole message or a one of the parts in the body of a
|
||||||
|
// multipart entity.
|
||||||
|
type Entity struct {
|
||||||
|
Header Header // The entity's header.
|
||||||
|
Body io.Reader // The decoded entity's body.
|
||||||
|
|
||||||
|
mediaType string
|
||||||
|
mediaParams map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// New makes a new message with the provided header and body. The entity's
|
||||||
|
// transfer encoding and charset are automatically decoded to UTF-8.
|
||||||
|
//
|
||||||
|
// If the message uses an unknown transfer encoding or charset, New returns an
|
||||||
|
// error that verifies IsUnknownCharset, but also returns an Entity that can
|
||||||
|
// be read.
|
||||||
|
func New(header Header, body io.Reader) (*Entity, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
enc := header.Get("Content-Transfer-Encoding")
|
||||||
|
if decoded, encErr := encodingReader(enc, body); encErr != nil {
|
||||||
|
err = unknownEncodingError{encErr}
|
||||||
|
} else {
|
||||||
|
body = decoded
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaType, mediaParams, _ := header.ContentType()
|
||||||
|
if ch, ok := mediaParams["charset"]; ok {
|
||||||
|
if converted, charsetErr := charsetReader(ch, body); charsetErr != nil {
|
||||||
|
err = unknownCharsetError{charsetErr}
|
||||||
|
} else {
|
||||||
|
body = converted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Entity{
|
||||||
|
Header: header,
|
||||||
|
Body: body,
|
||||||
|
mediaType: mediaType,
|
||||||
|
mediaParams: mediaParams,
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMultipart makes a new multipart message with the provided header and
|
||||||
|
// parts. The Content-Type header must begin with "multipart/".
|
||||||
|
//
|
||||||
|
// If the message uses an unknown transfer encoding, NewMultipart returns an
|
||||||
|
// error that verifies IsUnknownCharset, but also returns an Entity that can
|
||||||
|
// be read.
|
||||||
|
func NewMultipart(header Header, parts []*Entity) (*Entity, error) {
|
||||||
|
r := &multipartBody{
|
||||||
|
header: header,
|
||||||
|
parts: parts,
|
||||||
|
}
|
||||||
|
|
||||||
|
return New(header, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read reads a message from r. The message's encoding and charset are
|
||||||
|
// automatically decoded to raw UTF-8. Note that this function only reads the
|
||||||
|
// message header.
|
||||||
|
//
|
||||||
|
// If the message uses an unknown transfer encoding or charset, Read returns an
|
||||||
|
// error that verifies IsUnknownCharset, but also returns an Entity that can
|
||||||
|
// be read.
|
||||||
|
func Read(r io.Reader) (*Entity, error) {
|
||||||
|
br := bufio.NewReader(r)
|
||||||
|
h, err := textproto.ReadHeader(br)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return New(Header{h}, br)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultipartReader returns a MultipartReader that reads parts from this entity's
|
||||||
|
// body. If this entity is not multipart, it returns nil.
|
||||||
|
func (e *Entity) MultipartReader() MultipartReader {
|
||||||
|
if !strings.HasPrefix(e.mediaType, "multipart/") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if mb, ok := e.Body.(*multipartBody); ok {
|
||||||
|
return mb
|
||||||
|
}
|
||||||
|
return &multipartReader{textproto.NewMultipartReader(e.Body, e.mediaParams["boundary"])}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeBodyTo writes this entity's body to w (without the header).
|
||||||
|
func (e *Entity) writeBodyTo(w *Writer) error {
|
||||||
|
var err error
|
||||||
|
if mb, ok := e.Body.(*multipartBody); ok {
|
||||||
|
err = mb.writeBodyTo(w)
|
||||||
|
} else {
|
||||||
|
_, err = io.Copy(w, e.Body)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteTo writes this entity's header and body to w.
|
||||||
|
func (e *Entity) WriteTo(w io.Writer) error {
|
||||||
|
ew, err := CreateWriter(w, e.Header)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer ew.Close()
|
||||||
|
|
||||||
|
return e.writeBodyTo(ew)
|
||||||
|
}
|
9
vendor/github.com/emersion/go-message/go.mod
generated
vendored
Normal file
9
vendor/github.com/emersion/go-message/go.mod
generated
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
module github.com/emersion/go-message
|
||||||
|
|
||||||
|
go 1.12
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe
|
||||||
|
github.com/martinlindhe/base36 v0.0.0-20190418230009-7c6542dfbb41
|
||||||
|
golang.org/x/text v0.3.2
|
||||||
|
)
|
101
vendor/github.com/emersion/go-message/header.go
generated
vendored
Normal file
101
vendor/github.com/emersion/go-message/header.go
generated
vendored
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mime"
|
||||||
|
|
||||||
|
"github.com/emersion/go-message/textproto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseHeaderWithParams(s string) (f string, params map[string]string, err error) {
|
||||||
|
f, params, err = mime.ParseMediaType(s)
|
||||||
|
if err != nil {
|
||||||
|
return s, nil, err
|
||||||
|
}
|
||||||
|
for k, v := range params {
|
||||||
|
params[k], _ = decodeHeader(v)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatHeaderWithParams(f string, params map[string]string) string {
|
||||||
|
encParams := make(map[string]string)
|
||||||
|
for k, v := range params {
|
||||||
|
encParams[k] = encodeHeader(v)
|
||||||
|
}
|
||||||
|
return mime.FormatMediaType(f, encParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeaderFields iterates over header fields.
|
||||||
|
type HeaderFields interface {
|
||||||
|
textproto.HeaderFields
|
||||||
|
|
||||||
|
// Text parses the value of the current field as plaintext. The field
|
||||||
|
// charset is decoded to UTF-8.
|
||||||
|
Text() (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type headerFields struct {
|
||||||
|
textproto.HeaderFields
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hf *headerFields) Text() (string, error) {
|
||||||
|
return decodeHeader(hf.Value())
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Header represents the key-value pairs in a message header.
|
||||||
|
type Header struct {
|
||||||
|
textproto.Header
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContentType parses the Content-Type header field.
|
||||||
|
//
|
||||||
|
// If no Content-Type is specified, it returns "text/plain".
|
||||||
|
func (h *Header) ContentType() (t string, params map[string]string, err error) {
|
||||||
|
v := h.Get("Content-Type")
|
||||||
|
if v == "" {
|
||||||
|
return "text/plain", nil, nil
|
||||||
|
}
|
||||||
|
return parseHeaderWithParams(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetContentType formats the Content-Type header field.
|
||||||
|
func (h *Header) SetContentType(t string, params map[string]string) {
|
||||||
|
h.Set("Content-Type", formatHeaderWithParams(t, params))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContentDisposition parses the Content-Disposition header field, as defined in
|
||||||
|
// RFC 2183.
|
||||||
|
func (h *Header) ContentDisposition() (disp string, params map[string]string, err error) {
|
||||||
|
return parseHeaderWithParams(h.Get("Content-Disposition"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetContentDisposition formats the Content-Disposition header field, as
|
||||||
|
// defined in RFC 2183.
|
||||||
|
func (h *Header) SetContentDisposition(disp string, params map[string]string) {
|
||||||
|
h.Set("Content-Disposition", formatHeaderWithParams(disp, params))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text parses a plaintext header field. The field charset is automatically
|
||||||
|
// decoded to UTF-8.
|
||||||
|
func (h *Header) Text(k string) (string, error) {
|
||||||
|
return decodeHeader(h.Get(k))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetText sets a plaintext header field.
|
||||||
|
func (h *Header) SetText(k, v string) {
|
||||||
|
h.Set(k, encodeHeader(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields iterates over all the header fields.
|
||||||
|
//
|
||||||
|
// The header may not be mutated while iterating, except using HeaderFields.Del.
|
||||||
|
func (h *Header) Fields() HeaderFields {
|
||||||
|
return &headerFields{h.Header.Fields()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FieldsByKey iterates over all fields having the specified key.
|
||||||
|
//
|
||||||
|
// The header may not be mutated while iterating, except using HeaderFields.Del.
|
||||||
|
func (h *Header) FieldsByKey(k string) HeaderFields {
|
||||||
|
return &headerFields{h.Header.FieldsByKey(k)}
|
||||||
|
}
|
37
vendor/github.com/emersion/go-message/mail/address.go
generated
vendored
Normal file
37
vendor/github.com/emersion/go-message/mail/address.go
generated
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/mail"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Address represents a single mail address.
|
||||||
|
type Address mail.Address
|
||||||
|
|
||||||
|
// String formats the address as a valid RFC 5322 address. If the address's name
|
||||||
|
// contains non-ASCII characters the name will be rendered according to
|
||||||
|
// RFC 2047.
|
||||||
|
func (a *Address) String() string {
|
||||||
|
return ((*mail.Address)(a)).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAddressList(s string) ([]*Address, error) {
|
||||||
|
list, err := mail.ParseAddressList(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
addrs := make([]*Address, len(list))
|
||||||
|
for i, a := range list {
|
||||||
|
addrs[i] = (*Address)(a)
|
||||||
|
}
|
||||||
|
return addrs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatAddressList(l []*Address) string {
|
||||||
|
formatted := make([]string, len(l))
|
||||||
|
for i, a := range l {
|
||||||
|
formatted[i] = a.String()
|
||||||
|
}
|
||||||
|
return strings.Join(formatted, ", ")
|
||||||
|
}
|
30
vendor/github.com/emersion/go-message/mail/attachment.go
generated
vendored
Normal file
30
vendor/github.com/emersion/go-message/mail/attachment.go
generated
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emersion/go-message"
|
||||||
|
)
|
||||||
|
|
||||||
|
// An AttachmentHeader represents an attachment's header.
|
||||||
|
type AttachmentHeader struct {
|
||||||
|
message.Header
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filename parses the attachment's filename.
|
||||||
|
func (h *AttachmentHeader) Filename() (string, error) {
|
||||||
|
_, params, err := h.ContentDisposition()
|
||||||
|
|
||||||
|
filename, ok := params["filename"]
|
||||||
|
if !ok {
|
||||||
|
// Using "name" in Content-Type is discouraged
|
||||||
|
_, params, err = h.ContentType()
|
||||||
|
filename = params["name"]
|
||||||
|
}
|
||||||
|
|
||||||
|
return filename, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFilename formats the attachment's filename.
|
||||||
|
func (h *AttachmentHeader) SetFilename(filename string) {
|
||||||
|
dispParams := map[string]string{"filename": filename}
|
||||||
|
h.SetContentDisposition("attachment", dispParams)
|
||||||
|
}
|
51
vendor/github.com/emersion/go-message/mail/header.go
generated
vendored
Normal file
51
vendor/github.com/emersion/go-message/mail/header.go
generated
vendored
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/mail"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/emersion/go-message"
|
||||||
|
)
|
||||||
|
|
||||||
|
const dateLayout = "Mon, 02 Jan 2006 15:04:05 -0700"
|
||||||
|
|
||||||
|
// A Header is a mail header.
|
||||||
|
type Header struct {
|
||||||
|
message.Header
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddressList parses the named header field as a list of addresses. If the
|
||||||
|
// header is missing, it returns nil.
|
||||||
|
func (h *Header) AddressList(key string) ([]*Address, error) {
|
||||||
|
v := h.Get(key)
|
||||||
|
if v == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return parseAddressList(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAddressList formats the named header to the provided list of addresses.
|
||||||
|
func (h *Header) SetAddressList(key string, addrs []*Address) {
|
||||||
|
h.Set(key, formatAddressList(addrs))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date parses the Date header field.
|
||||||
|
func (h *Header) Date() (time.Time, error) {
|
||||||
|
return mail.ParseDate(h.Get("Date"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDate formats the Date header field.
|
||||||
|
func (h *Header) SetDate(t time.Time) {
|
||||||
|
h.Set("Date", t.Format(dateLayout))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subject parses the Subject header field. If there is an error, the raw field
|
||||||
|
// value is returned alongside the error.
|
||||||
|
func (h *Header) Subject() (string, error) {
|
||||||
|
return h.Text("Subject")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSubject formats the Subject header field.
|
||||||
|
func (h *Header) SetSubject(s string) {
|
||||||
|
h.SetText("Subject", s)
|
||||||
|
}
|
10
vendor/github.com/emersion/go-message/mail/inline.go
generated
vendored
Normal file
10
vendor/github.com/emersion/go-message/mail/inline.go
generated
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emersion/go-message"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A InlineHeader represents a message text header.
|
||||||
|
type InlineHeader struct {
|
||||||
|
message.Header
|
||||||
|
}
|
40
vendor/github.com/emersion/go-message/mail/mail.go
generated
vendored
Normal file
40
vendor/github.com/emersion/go-message/mail/mail.go
generated
vendored
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// Package mail implements reading and writing mail messages.
|
||||||
|
//
|
||||||
|
// This package assumes that a mail message contains one or more text parts and
|
||||||
|
// zero or more attachment parts. Each text part represents a different version
|
||||||
|
// of the message content (e.g. a different type, a different language and so
|
||||||
|
// on).
|
||||||
|
//
|
||||||
|
// RFC 5322 defines the Internet Message Format.
|
||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"crypto/rand"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/martinlindhe/base36"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Generates an RFC 2822-compliant Message-Id based on the informational draft
|
||||||
|
// "Recommendations for generating Message IDs", for lack of a better
|
||||||
|
// authoritative source.
|
||||||
|
func GenerateMessageID() string {
|
||||||
|
var (
|
||||||
|
now bytes.Buffer
|
||||||
|
nonce []byte = make([]byte, 8)
|
||||||
|
)
|
||||||
|
binary.Write(&now, binary.BigEndian, time.Now().UnixNano())
|
||||||
|
rand.Read(nonce)
|
||||||
|
hostname, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
hostname = "localhost"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("<%s.%s@%s>",
|
||||||
|
base36.EncodeBytes(now.Bytes()),
|
||||||
|
base36.EncodeBytes(nonce),
|
||||||
|
hostname)
|
||||||
|
}
|
130
vendor/github.com/emersion/go-message/mail/reader.go
generated
vendored
Normal file
130
vendor/github.com/emersion/go-message/mail/reader.go
generated
vendored
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"container/list"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/emersion/go-message"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A PartHeader is a mail part header. It contains convenience functions to get
|
||||||
|
// and set header fields.
|
||||||
|
type PartHeader interface {
|
||||||
|
// Add adds the key, value pair to the header.
|
||||||
|
Add(key, value string)
|
||||||
|
// Del deletes the values associated with key.
|
||||||
|
Del(key string)
|
||||||
|
// Get gets the first value associated with the given key. If there are no
|
||||||
|
// values associated with the key, Get returns "".
|
||||||
|
Get(key string) string
|
||||||
|
// Set sets the header entries associated with key to the single element
|
||||||
|
// value. It replaces any existing values associated with key.
|
||||||
|
Set(key, value string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Part is either a mail text or an attachment. Header is either a InlineHeader
|
||||||
|
// or an AttachmentHeader.
|
||||||
|
type Part struct {
|
||||||
|
Header PartHeader
|
||||||
|
Body io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Reader reads a mail message.
|
||||||
|
type Reader struct {
|
||||||
|
Header Header
|
||||||
|
|
||||||
|
e *message.Entity
|
||||||
|
readers *list.List
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReader creates a new mail reader.
|
||||||
|
func NewReader(e *message.Entity) *Reader {
|
||||||
|
mr := e.MultipartReader()
|
||||||
|
if mr == nil {
|
||||||
|
// Artificially create a multipart entity
|
||||||
|
// With this header, no error will be returned by message.NewMultipart
|
||||||
|
var h message.Header
|
||||||
|
h.Set("Content-Type", "multipart/mixed")
|
||||||
|
me, _ := message.NewMultipart(h, []*message.Entity{e})
|
||||||
|
mr = me.MultipartReader()
|
||||||
|
}
|
||||||
|
|
||||||
|
l := list.New()
|
||||||
|
l.PushBack(mr)
|
||||||
|
|
||||||
|
return &Reader{Header{e.Header}, e, l}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateReader reads a mail header from r and returns a new mail reader.
|
||||||
|
//
|
||||||
|
// If the message uses an unknown transfer encoding or charset, CreateReader
|
||||||
|
// returns an error that verifies message.IsUnknownCharset, but also returns a
|
||||||
|
// Reader that can be used.
|
||||||
|
func CreateReader(r io.Reader) (*Reader, error) {
|
||||||
|
e, err := message.Read(r)
|
||||||
|
if err != nil && !message.IsUnknownCharset(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewReader(e), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextPart returns the next mail part. If there is no more part, io.EOF is
|
||||||
|
// returned as error.
|
||||||
|
//
|
||||||
|
// The returned Part.Body must be read completely before the next call to
|
||||||
|
// NextPart, otherwise it will be discarded.
|
||||||
|
//
|
||||||
|
// If the part uses an unknown transfer encoding or charset, NextPart returns an
|
||||||
|
// error that verifies message.IsUnknownCharset, but also returns a Part that
|
||||||
|
// can be used.
|
||||||
|
func (r *Reader) NextPart() (*Part, error) {
|
||||||
|
for r.readers.Len() > 0 {
|
||||||
|
e := r.readers.Back()
|
||||||
|
mr := e.Value.(message.MultipartReader)
|
||||||
|
|
||||||
|
p, err := mr.NextPart()
|
||||||
|
if err == io.EOF {
|
||||||
|
// This whole multipart entity has been read, continue with the next one
|
||||||
|
r.readers.Remove(e)
|
||||||
|
continue
|
||||||
|
} else if err != nil && !message.IsUnknownCharset(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if pmr := p.MultipartReader(); pmr != nil {
|
||||||
|
// This is a multipart part, read it
|
||||||
|
r.readers.PushBack(pmr)
|
||||||
|
} else {
|
||||||
|
// This is a non-multipart part, return a mail part
|
||||||
|
mp := &Part{Body: p.Body}
|
||||||
|
t, _, _ := p.Header.ContentType()
|
||||||
|
disp, _, _ := p.Header.ContentDisposition()
|
||||||
|
if disp == "inline" || (disp != "attachment" && strings.HasPrefix(t, "text/")) {
|
||||||
|
mp.Header = &InlineHeader{p.Header}
|
||||||
|
} else {
|
||||||
|
mp.Header = &AttachmentHeader{p.Header}
|
||||||
|
}
|
||||||
|
return mp, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close finishes the reader.
|
||||||
|
func (r *Reader) Close() error {
|
||||||
|
for r.readers.Len() > 0 {
|
||||||
|
e := r.readers.Back()
|
||||||
|
mr := e.Value.(message.MultipartReader)
|
||||||
|
|
||||||
|
if err := mr.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.readers.Remove(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
112
vendor/github.com/emersion/go-message/mail/writer.go
generated
vendored
Normal file
112
vendor/github.com/emersion/go-message/mail/writer.go
generated
vendored
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/emersion/go-message"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initInlineContentTransferEncoding(h *message.Header) {
|
||||||
|
if !h.Has("Content-Transfer-Encoding") {
|
||||||
|
t, _, _ := h.ContentType()
|
||||||
|
if strings.HasPrefix(t, "text/") {
|
||||||
|
h.Set("Content-Transfer-Encoding", "quoted-printable")
|
||||||
|
} else {
|
||||||
|
h.Set("Content-Transfer-Encoding", "base64")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initInlineHeader(h *InlineHeader) {
|
||||||
|
h.Set("Content-Disposition", "inline")
|
||||||
|
initInlineContentTransferEncoding(&h.Header)
|
||||||
|
}
|
||||||
|
|
||||||
|
func initAttachmentHeader(h *AttachmentHeader) {
|
||||||
|
disp, _, _ := h.ContentDisposition()
|
||||||
|
if disp != "attachment" {
|
||||||
|
h.Set("Content-Disposition", "attachment")
|
||||||
|
}
|
||||||
|
if !h.Has("Content-Transfer-Encoding") {
|
||||||
|
h.Set("Content-Transfer-Encoding", "base64")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Writer writes a mail message. A mail message contains one or more text
|
||||||
|
// parts and zero or more attachments.
|
||||||
|
type Writer struct {
|
||||||
|
mw *message.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateWriter writes a mail header to w and creates a new Writer.
|
||||||
|
func CreateWriter(w io.Writer, header Header) (*Writer, error) {
|
||||||
|
header.Set("Content-Type", "multipart/mixed")
|
||||||
|
|
||||||
|
mw, err := message.CreateWriter(w, header.Header)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Writer{mw}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSingleInlineWriter writes a mail header to w. The mail will contain a
|
||||||
|
// single inline part. The body of the part should be written to the returned
|
||||||
|
// io.WriteCloser. Only one single inline part should be written, use
|
||||||
|
// CreateWriter if you want multiple parts.
|
||||||
|
func CreateSingleInlineWriter(w io.Writer, header Header) (io.WriteCloser, error) {
|
||||||
|
initInlineContentTransferEncoding(&header.Header)
|
||||||
|
return message.CreateWriter(w, header.Header)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateInline creates a InlineWriter. One or more parts representing the same
|
||||||
|
// text in different formats can be written to a InlineWriter.
|
||||||
|
func (w *Writer) CreateInline() (*InlineWriter, error) {
|
||||||
|
var h message.Header
|
||||||
|
h.Set("Content-Type", "multipart/alternative")
|
||||||
|
|
||||||
|
mw, err := w.mw.CreatePart(h)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &InlineWriter{mw}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSingleInline creates a new single text part with the provided header.
|
||||||
|
// The body of the part should be written to the returned io.WriteCloser. Only
|
||||||
|
// one single text part should be written, use CreateInline if you want multiple
|
||||||
|
// text parts.
|
||||||
|
func (w *Writer) CreateSingleInline(h InlineHeader) (io.WriteCloser, error) {
|
||||||
|
initInlineHeader(&h)
|
||||||
|
return w.mw.CreatePart(h.Header)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateAttachment creates a new attachment with the provided header. The body
|
||||||
|
// of the part should be written to the returned io.WriteCloser.
|
||||||
|
func (w *Writer) CreateAttachment(h AttachmentHeader) (io.WriteCloser, error) {
|
||||||
|
initAttachmentHeader(&h)
|
||||||
|
return w.mw.CreatePart(h.Header)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close finishes the Writer.
|
||||||
|
func (w *Writer) Close() error {
|
||||||
|
return w.mw.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// InlineWriter writes a mail message's text.
|
||||||
|
type InlineWriter struct {
|
||||||
|
mw *message.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatePart creates a new text part with the provided header. The body of the
|
||||||
|
// part should be written to the returned io.WriteCloser.
|
||||||
|
func (w *InlineWriter) CreatePart(h InlineHeader) (io.WriteCloser, error) {
|
||||||
|
initInlineHeader(&h)
|
||||||
|
return w.mw.CreatePart(h.Header)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close finishes the InlineWriter.
|
||||||
|
func (w *InlineWriter) Close() error {
|
||||||
|
return w.mw.Close()
|
||||||
|
}
|
12
vendor/github.com/emersion/go-message/message.go
generated
vendored
Normal file
12
vendor/github.com/emersion/go-message/message.go
generated
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// Package message implements reading and writing multipurpose messages.
|
||||||
|
//
|
||||||
|
// RFC 2045, RFC 2046 and RFC 2047 defines MIME, and RFC 2183 defines the
|
||||||
|
// Content-Disposition header field.
|
||||||
|
//
|
||||||
|
// Add this import to your package if you want to handle most common charsets
|
||||||
|
// by default:
|
||||||
|
//
|
||||||
|
// import (
|
||||||
|
// _ "github.com/emersion/go-message/charset"
|
||||||
|
// )
|
||||||
|
package message
|
116
vendor/github.com/emersion/go-message/multipart.go
generated
vendored
Normal file
116
vendor/github.com/emersion/go-message/multipart.go
generated
vendored
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/emersion/go-message/textproto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MultipartReader is an iterator over parts in a MIME multipart body.
|
||||||
|
type MultipartReader interface {
|
||||||
|
io.Closer
|
||||||
|
|
||||||
|
// NextPart returns the next part in the multipart or an error. When there are
|
||||||
|
// no more parts, the error io.EOF is returned.
|
||||||
|
//
|
||||||
|
// Entity.Body must be read completely before the next call to NextPart,
|
||||||
|
// otherwise it will be discarded.
|
||||||
|
NextPart() (*Entity, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type multipartReader struct {
|
||||||
|
r *textproto.MultipartReader
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextPart implements MultipartReader.
|
||||||
|
func (r *multipartReader) NextPart() (*Entity, error) {
|
||||||
|
p, err := r.r.NextPart()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return New(Header{p.Header}, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements io.Closer.
|
||||||
|
func (r *multipartReader) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type multipartBody struct {
|
||||||
|
header Header
|
||||||
|
parts []*Entity
|
||||||
|
|
||||||
|
r *io.PipeReader
|
||||||
|
w *Writer
|
||||||
|
|
||||||
|
i int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read implements io.Reader.
|
||||||
|
func (m *multipartBody) Read(p []byte) (n int, err error) {
|
||||||
|
if m.r == nil {
|
||||||
|
r, w := io.Pipe()
|
||||||
|
m.r = r
|
||||||
|
|
||||||
|
var err error
|
||||||
|
m.w, err = createWriter(w, &m.header)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent calls to NextPart to succeed
|
||||||
|
m.i = len(m.parts)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := m.writeBodyTo(m.w); err != nil {
|
||||||
|
w.CloseWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.w.Close(); err != nil {
|
||||||
|
w.CloseWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Close()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.r.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements io.Closer.
|
||||||
|
func (m *multipartBody) Close() error {
|
||||||
|
if m.r != nil {
|
||||||
|
m.r.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextPart implements MultipartReader.
|
||||||
|
func (m *multipartBody) NextPart() (*Entity, error) {
|
||||||
|
if m.i >= len(m.parts) {
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
part := m.parts[m.i]
|
||||||
|
m.i++
|
||||||
|
return part, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *multipartBody) writeBodyTo(w *Writer) error {
|
||||||
|
for _, p := range m.parts {
|
||||||
|
pw, err := w.CreatePart(p.Header)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.writeBodyTo(pw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := pw.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
526
vendor/github.com/emersion/go-message/textproto/header.go
generated
vendored
Normal file
526
vendor/github.com/emersion/go-message/textproto/header.go
generated
vendored
Normal file
@ -0,0 +1,526 @@
|
|||||||
|
package textproto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/textproto"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type headerField struct {
|
||||||
|
b []byte // Raw header field, including whitespace
|
||||||
|
k string
|
||||||
|
v string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHeaderField(k, v string, b []byte) *headerField {
|
||||||
|
return &headerField{k: textproto.CanonicalMIMEHeaderKey(k), v: v, b: b}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Header represents the key-value pairs in a message header.
|
||||||
|
//
|
||||||
|
// The header representation is idempotent: if the header can be read and
|
||||||
|
// written, the result will be exactly the same as the original (including
|
||||||
|
// whitespace). This is required for e.g. DKIM.
|
||||||
|
//
|
||||||
|
// Mutating the header is restricted: the only two allowed operations are
|
||||||
|
// inserting a new header field at the top and deleting a header field. This is
|
||||||
|
// again necessary for DKIM.
|
||||||
|
type Header struct {
|
||||||
|
// Fields are in reverse order so that inserting a new field at the top is
|
||||||
|
// cheap.
|
||||||
|
l []*headerField
|
||||||
|
m map[string][]*headerField
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeHeaderMap(fs []*headerField) map[string][]*headerField {
|
||||||
|
if len(fs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m := make(map[string][]*headerField)
|
||||||
|
for i, f := range fs {
|
||||||
|
m[f.k] = append(m[f.k], fs[i])
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHeader(fs []*headerField) Header {
|
||||||
|
// Reverse order
|
||||||
|
for i := len(fs)/2 - 1; i >= 0; i-- {
|
||||||
|
opp := len(fs) - 1 - i
|
||||||
|
fs[i], fs[opp] = fs[opp], fs[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate map
|
||||||
|
m := makeHeaderMap(fs)
|
||||||
|
|
||||||
|
return Header{l: fs, m: m}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add adds the key, value pair to the header. It prepends to any existing
|
||||||
|
// fields associated with key.
|
||||||
|
func (h *Header) Add(k, v string) {
|
||||||
|
k = textproto.CanonicalMIMEHeaderKey(k)
|
||||||
|
|
||||||
|
if h.m == nil {
|
||||||
|
h.m = make(map[string][]*headerField)
|
||||||
|
}
|
||||||
|
|
||||||
|
f := newHeaderField(k, v, nil)
|
||||||
|
h.l = append(h.l, f)
|
||||||
|
h.m[k] = append(h.m[k], f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get gets the first value associated with the given key. If there are no
|
||||||
|
// values associated with the key, Get returns "".
|
||||||
|
func (h *Header) Get(k string) string {
|
||||||
|
fields := h.m[textproto.CanonicalMIMEHeaderKey(k)]
|
||||||
|
if len(fields) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fields[len(fields)-1].v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sets the header fields associated with key to the single field value.
|
||||||
|
// It replaces any existing values associated with key.
|
||||||
|
func (h *Header) Set(k, v string) {
|
||||||
|
h.Del(k)
|
||||||
|
h.Add(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Del deletes the values associated with key.
|
||||||
|
func (h *Header) Del(k string) {
|
||||||
|
k = textproto.CanonicalMIMEHeaderKey(k)
|
||||||
|
|
||||||
|
delete(h.m, k)
|
||||||
|
|
||||||
|
// Delete existing keys
|
||||||
|
for i := len(h.l) - 1; i >= 0; i-- {
|
||||||
|
if h.l[i].k == k {
|
||||||
|
h.l = append(h.l[:i], h.l[i+1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has checks whether the header has a field with the specified key.
|
||||||
|
func (h *Header) Has(k string) bool {
|
||||||
|
_, ok := h.m[textproto.CanonicalMIMEHeaderKey(k)]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy creates an independent copy of the header.
|
||||||
|
func (h *Header) Copy() Header {
|
||||||
|
l := make([]*headerField, len(h.l))
|
||||||
|
copy(l, h.l)
|
||||||
|
m := makeHeaderMap(l)
|
||||||
|
return Header{l: l, m: m}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeaderFields iterates over header fields. Its cursor starts before the first
|
||||||
|
// field of the header. Use Next to advance from field to field.
|
||||||
|
type HeaderFields interface {
|
||||||
|
// Next advances to the next header field. It returns true on success, or
|
||||||
|
// false if there is no next field.
|
||||||
|
Next() (more bool)
|
||||||
|
// Key returns the key of the current field.
|
||||||
|
Key() string
|
||||||
|
// Value returns the value of the current field.
|
||||||
|
Value() string
|
||||||
|
// Del deletes the current field.
|
||||||
|
Del()
|
||||||
|
}
|
||||||
|
|
||||||
|
type headerFields struct {
|
||||||
|
h *Header
|
||||||
|
cur int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *headerFields) Next() bool {
|
||||||
|
fs.cur++
|
||||||
|
return fs.cur < len(fs.h.l)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *headerFields) index() int {
|
||||||
|
if fs.cur < 0 {
|
||||||
|
panic("message: HeaderFields method called before Next")
|
||||||
|
}
|
||||||
|
if fs.cur >= len(fs.h.l) {
|
||||||
|
panic("message: HeaderFields method called after Next returned false")
|
||||||
|
}
|
||||||
|
return len(fs.h.l)-fs.cur-1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *headerFields) field() *headerField {
|
||||||
|
return fs.h.l[fs.index()]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *headerFields) Key() string {
|
||||||
|
return fs.field().k
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *headerFields) Value() string {
|
||||||
|
return fs.field().v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *headerFields) Del() {
|
||||||
|
f := fs.field()
|
||||||
|
|
||||||
|
ok := false
|
||||||
|
for i, ff := range fs.h.m[f.k] {
|
||||||
|
if ff == f {
|
||||||
|
ok = true
|
||||||
|
fs.h.m[f.k] = append(fs.h.m[f.k][:i], fs.h.m[f.k][i+1:]...)
|
||||||
|
if len(fs.h.m[f.k]) == 0 {
|
||||||
|
delete(fs.h.m, f.k)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
panic("message: field not found in Header.m")
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.h.l = append(fs.h.l[:fs.index()], fs.h.l[fs.index()+1:]...)
|
||||||
|
fs.cur--
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields iterates over all the header fields.
|
||||||
|
//
|
||||||
|
// The header may not be mutated while iterating, except using HeaderFields.Del.
|
||||||
|
func (h *Header) Fields() HeaderFields {
|
||||||
|
return &headerFields{h, -1}
|
||||||
|
}
|
||||||
|
|
||||||
|
type headerFieldsByKey struct {
|
||||||
|
h *Header
|
||||||
|
k string
|
||||||
|
cur int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *headerFieldsByKey) Next() bool {
|
||||||
|
fs.cur++
|
||||||
|
return fs.cur < len(fs.h.m[fs.k])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *headerFieldsByKey) index() int {
|
||||||
|
if fs.cur < 0 {
|
||||||
|
panic("message: headerfields.key or value called before next")
|
||||||
|
}
|
||||||
|
if fs.cur >= len(fs.h.m[fs.k]) {
|
||||||
|
panic("message: headerfields.key or value called after next returned false")
|
||||||
|
}
|
||||||
|
return len(fs.h.m[fs.k])-fs.cur-1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *headerFieldsByKey) field() *headerField {
|
||||||
|
return fs.h.m[fs.k][fs.index()]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *headerFieldsByKey) Key() string {
|
||||||
|
return fs.field().k
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *headerFieldsByKey) Value() string {
|
||||||
|
return fs.field().v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *headerFieldsByKey) Del() {
|
||||||
|
f := fs.field()
|
||||||
|
|
||||||
|
ok := false
|
||||||
|
for i := range fs.h.l {
|
||||||
|
if f == fs.h.l[i] {
|
||||||
|
ok = true
|
||||||
|
fs.h.l = append(fs.h.l[:i], fs.h.l[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
panic("message: field not found in Header.l")
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.h.m[fs.k] = append(fs.h.m[fs.k][:fs.index()], fs.h.m[fs.k][fs.index()+1:]...)
|
||||||
|
if len(fs.h.m[fs.k]) == 0 {
|
||||||
|
delete(fs.h.m, fs.k)
|
||||||
|
}
|
||||||
|
fs.cur--
|
||||||
|
}
|
||||||
|
|
||||||
|
// FieldsByKey iterates over all fields having the specified key.
|
||||||
|
//
|
||||||
|
// The header may not be mutated while iterating, except using HeaderFields.Del.
|
||||||
|
func (h *Header) FieldsByKey(k string) HeaderFields {
|
||||||
|
return &headerFieldsByKey{h, textproto.CanonicalMIMEHeaderKey(k), -1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readLineSlice(r *bufio.Reader, line []byte) ([]byte, error) {
|
||||||
|
for {
|
||||||
|
l, more, err := r.ReadLine()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
line = append(line, l...)
|
||||||
|
if !more {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return line, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSpace(c byte) bool {
|
||||||
|
return c == ' ' || c == '\t'
|
||||||
|
}
|
||||||
|
|
||||||
|
// trim returns s with leading and trailing spaces and tabs removed.
|
||||||
|
// It does not assume Unicode or UTF-8.
|
||||||
|
func trim(s []byte) []byte {
|
||||||
|
i := 0
|
||||||
|
for i < len(s) && isSpace(s[i]) {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
n := len(s)
|
||||||
|
for n > i && isSpace(s[n-1]) {
|
||||||
|
n--
|
||||||
|
}
|
||||||
|
return s[i:n]
|
||||||
|
}
|
||||||
|
|
||||||
|
// skipSpace skips R over all spaces and returns the number of bytes skipped.
|
||||||
|
func skipSpace(r *bufio.Reader) int {
|
||||||
|
n := 0
|
||||||
|
for {
|
||||||
|
c, err := r.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
// bufio will keep err until next read.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !isSpace(c) {
|
||||||
|
r.UnreadByte()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasContinuationLine(r *bufio.Reader) bool {
|
||||||
|
c, err := r.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return false // bufio will keep err until next read.
|
||||||
|
}
|
||||||
|
r.UnreadByte()
|
||||||
|
return isSpace(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readContinuedLineSlice(r *bufio.Reader) ([]byte, error) {
|
||||||
|
// Read the first line.
|
||||||
|
line, err := readLineSlice(r, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(line) == 0 { // blank line - no continuation
|
||||||
|
return line, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
line = append(line, '\r', '\n')
|
||||||
|
|
||||||
|
// Read continuation lines.
|
||||||
|
for hasContinuationLine(r) {
|
||||||
|
line, err = readLineSlice(r, line)
|
||||||
|
if err != nil {
|
||||||
|
break // bufio will keep err until next read.
|
||||||
|
}
|
||||||
|
|
||||||
|
line = append(line, '\r', '\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
return line, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeContinued(b *strings.Builder, l []byte) {
|
||||||
|
// Strip trailing \r, if any
|
||||||
|
if len(l) > 0 && l[len(l)-1] == '\r' {
|
||||||
|
l = l[:len(l)-1]
|
||||||
|
}
|
||||||
|
l = trim(l)
|
||||||
|
if len(l) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if b.Len() > 0 {
|
||||||
|
b.WriteByte(' ')
|
||||||
|
}
|
||||||
|
b.Write(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip newlines and spaces around newlines.
|
||||||
|
func trimAroundNewlines(v []byte) string {
|
||||||
|
var b strings.Builder
|
||||||
|
for {
|
||||||
|
i := bytes.IndexByte(v, '\n')
|
||||||
|
if i < 0 {
|
||||||
|
writeContinued(&b, v)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
writeContinued(&b, v[:i])
|
||||||
|
v = v[i+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadHeader reads a MIME header from r. The header is a sequence of possibly
|
||||||
|
// continued Key: Value lines ending in a blank line.
|
||||||
|
func ReadHeader(r *bufio.Reader) (Header, error) {
|
||||||
|
var fs []*headerField
|
||||||
|
|
||||||
|
// The first line cannot start with a leading space.
|
||||||
|
if buf, err := r.Peek(1); err == nil && isSpace(buf[0]) {
|
||||||
|
line, err := readLineSlice(r, nil)
|
||||||
|
if err != nil {
|
||||||
|
return newHeader(fs), err
|
||||||
|
}
|
||||||
|
|
||||||
|
return newHeader(fs), fmt.Errorf("message: malformed MIME header initial line: %v", string(line))
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
kv, err := readContinuedLineSlice(r)
|
||||||
|
if len(kv) == 0 {
|
||||||
|
return newHeader(fs), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key ends at first colon; should not have trailing spaces but they
|
||||||
|
// appear in the wild, violating specs, so we remove them if present.
|
||||||
|
i := bytes.IndexByte(kv, ':')
|
||||||
|
if i < 0 {
|
||||||
|
return newHeader(fs), fmt.Errorf("message: malformed MIME header line: %v", string(kv))
|
||||||
|
}
|
||||||
|
|
||||||
|
key := textproto.CanonicalMIMEHeaderKey(string(trim(kv[:i])))
|
||||||
|
|
||||||
|
// As per RFC 7230 field-name is a token, tokens consist of one or more
|
||||||
|
// chars. We could return a an error here, but better to be liberal in
|
||||||
|
// what we accept, so if we get an empty key, skip it.
|
||||||
|
if key == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
i++ // skip colon
|
||||||
|
v := kv[i:]
|
||||||
|
|
||||||
|
value := trimAroundNewlines(v)
|
||||||
|
fs = append(fs, newHeaderField(key, value, kv))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return newHeader(fs), err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxHeaderLen = 76
|
||||||
|
|
||||||
|
// Regexp that detects Quoted Printable (QP) characters
|
||||||
|
var qpReg = regexp.MustCompile("(=[0-9A-Z]{2,2})+")
|
||||||
|
|
||||||
|
// formatHeaderField formats a header field, ensuring each line is no longer
|
||||||
|
// than 76 characters. It tries to fold lines at whitespace characters if
|
||||||
|
// possible. If the header contains a word longer than this limit, it will be
|
||||||
|
// split.
|
||||||
|
func formatHeaderField(k, v string) string {
|
||||||
|
s := k + ": "
|
||||||
|
|
||||||
|
if v == "" {
|
||||||
|
return s + "\r\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
first := true
|
||||||
|
for len(v) > 0 {
|
||||||
|
maxlen := maxHeaderLen
|
||||||
|
if first {
|
||||||
|
maxlen -= len(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll need to fold before i
|
||||||
|
foldBefore := maxlen + 1
|
||||||
|
foldAt := len(v)
|
||||||
|
|
||||||
|
var folding string
|
||||||
|
if foldBefore > len(v) {
|
||||||
|
// We reached the end of the string
|
||||||
|
if v[len(v)-1] != '\n' {
|
||||||
|
// If there isn't already a trailing CRLF, insert one
|
||||||
|
folding = "\r\n"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Find the last QP character before limit
|
||||||
|
foldAtQP := qpReg.FindAllStringIndex(v[:foldBefore], -1)
|
||||||
|
// Find the closest whitespace before i
|
||||||
|
foldAtEOL := strings.LastIndexAny(v[:foldBefore], " \t\n")
|
||||||
|
|
||||||
|
// Fold at the latest whitespace by default
|
||||||
|
foldAt = foldAtEOL
|
||||||
|
|
||||||
|
// if there are QP characters in the string
|
||||||
|
if len(foldAtQP) > 0 {
|
||||||
|
// Get the start index of the last QP character
|
||||||
|
foldAtQPLastIndex := foldAtQP[len(foldAtQP)-1][0]
|
||||||
|
if foldAtQPLastIndex > foldAt {
|
||||||
|
// Fold at the latest QP character if there are no whitespaces after it and before line hard limit
|
||||||
|
foldAt = foldAtQPLastIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if foldAt == 0 {
|
||||||
|
// The whitespace we found was the previous folding WSP
|
||||||
|
foldAt = foldBefore - 1
|
||||||
|
} else if foldAt < 0 {
|
||||||
|
// We didn't find any whitespace, we have to insert one
|
||||||
|
foldAt = foldBefore - 2
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v[foldAt] {
|
||||||
|
case ' ', '\t':
|
||||||
|
if v[foldAt-1] != '\n' {
|
||||||
|
folding = "\r\n" // The next char will be a WSP, don't need to insert one
|
||||||
|
}
|
||||||
|
case '\n':
|
||||||
|
folding = "" // There is already a CRLF, nothing to do
|
||||||
|
default:
|
||||||
|
folding = "\r\n " // Another char, we need to insert CRLF + WSP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s += v[:foldAt] + folding
|
||||||
|
v = v[foldAt:]
|
||||||
|
first = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteHeader writes a MIME header to w.
|
||||||
|
func WriteHeader(w io.Writer, h Header) error {
|
||||||
|
// TODO: wrap lines when necessary
|
||||||
|
|
||||||
|
for i := len(h.l) - 1; i >= 0; i-- {
|
||||||
|
f := h.l[i]
|
||||||
|
|
||||||
|
if f.b == nil {
|
||||||
|
f.b = []byte(formatHeaderField(f.k, f.v))
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := w.Write(f.b); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := w.Write([]byte{'\r', '\n'})
|
||||||
|
return err
|
||||||
|
}
|
488
vendor/github.com/emersion/go-message/textproto/multipart.go
generated
vendored
Normal file
488
vendor/github.com/emersion/go-message/textproto/multipart.go
generated
vendored
Normal file
@ -0,0 +1,488 @@
|
|||||||
|
// 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 textproto
|
||||||
|
|
||||||
|
// Multipart is defined in RFC 2046.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"mime"
|
||||||
|
)
|
||||||
|
|
||||||
|
var emptyParams = make(map[string]string)
|
||||||
|
|
||||||
|
// This constant needs to be at least 76 for this package to work correctly.
|
||||||
|
// This is because \r\n--separator_of_len_70- would fill the buffer and it
|
||||||
|
// wouldn't be safe to consume a single byte from it.
|
||||||
|
const peekBufferSize = 4096
|
||||||
|
|
||||||
|
// A Part represents a single part in a multipart body.
|
||||||
|
type Part struct {
|
||||||
|
Header Header
|
||||||
|
|
||||||
|
mr *MultipartReader
|
||||||
|
|
||||||
|
disposition string
|
||||||
|
dispositionParams map[string]string
|
||||||
|
|
||||||
|
// r is either a reader directly reading from mr
|
||||||
|
r io.Reader
|
||||||
|
|
||||||
|
n int // known data bytes waiting in mr.bufReader
|
||||||
|
total int64 // total data bytes read already
|
||||||
|
err error // error to return when n == 0
|
||||||
|
readErr error // read error observed from mr.bufReader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Part) parseContentDisposition() {
|
||||||
|
v := p.Header.Get("Content-Disposition")
|
||||||
|
var err error
|
||||||
|
p.disposition, p.dispositionParams, err = mime.ParseMediaType(v)
|
||||||
|
if err != nil {
|
||||||
|
p.dispositionParams = emptyParams
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMultipartReader creates a new multipart reader reading from r using the
|
||||||
|
// given MIME boundary.
|
||||||
|
//
|
||||||
|
// The boundary is usually obtained from the "boundary" parameter of
|
||||||
|
// the message's "Content-Type" header. Use mime.ParseMediaType to
|
||||||
|
// parse such headers.
|
||||||
|
func NewMultipartReader(r io.Reader, boundary string) *MultipartReader {
|
||||||
|
b := []byte("\r\n--" + boundary + "--")
|
||||||
|
return &MultipartReader{
|
||||||
|
bufReader: bufio.NewReaderSize(&stickyErrorReader{r: r}, peekBufferSize),
|
||||||
|
nl: b[:2],
|
||||||
|
nlDashBoundary: b[:len(b)-2],
|
||||||
|
dashBoundaryDash: b[2:],
|
||||||
|
dashBoundary: b[2 : len(b)-2],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stickyErrorReader is an io.Reader which never calls Read on its
|
||||||
|
// underlying Reader once an error has been seen. (the io.Reader
|
||||||
|
// interface's contract promises nothing about the return values of
|
||||||
|
// Read calls after an error, yet this package does do multiple Reads
|
||||||
|
// after error)
|
||||||
|
type stickyErrorReader struct {
|
||||||
|
r io.Reader
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *stickyErrorReader) Read(p []byte) (n int, _ error) {
|
||||||
|
if r.err != nil {
|
||||||
|
return 0, r.err
|
||||||
|
}
|
||||||
|
n, r.err = r.r.Read(p)
|
||||||
|
return n, r.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPart(mr *MultipartReader) (*Part, error) {
|
||||||
|
bp := &Part{mr: mr}
|
||||||
|
if err := bp.populateHeaders(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bp.r = partReader{bp}
|
||||||
|
return bp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bp *Part) populateHeaders() error {
|
||||||
|
header, err := ReadHeader(bp.mr.bufReader)
|
||||||
|
if err == nil {
|
||||||
|
bp.Header = header
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read reads the body of a part, after its headers and before the
|
||||||
|
// next part (if any) begins.
|
||||||
|
func (p *Part) Read(d []byte) (n int, err error) {
|
||||||
|
return p.r.Read(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
// partReader implements io.Reader by reading raw bytes directly from the
|
||||||
|
// wrapped *Part, without doing any Transfer-Encoding decoding.
|
||||||
|
type partReader struct {
|
||||||
|
p *Part
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pr partReader) Read(d []byte) (int, error) {
|
||||||
|
p := pr.p
|
||||||
|
br := p.mr.bufReader
|
||||||
|
|
||||||
|
// Read into buffer until we identify some data to return,
|
||||||
|
// or we find a reason to stop (boundary or read error).
|
||||||
|
for p.n == 0 && p.err == nil {
|
||||||
|
peek, _ := br.Peek(br.Buffered())
|
||||||
|
p.n, p.err = scanUntilBoundary(peek, p.mr.dashBoundary, p.mr.nlDashBoundary, p.total, p.readErr)
|
||||||
|
if p.n == 0 && p.err == nil {
|
||||||
|
// Force buffered I/O to read more into buffer.
|
||||||
|
_, p.readErr = br.Peek(len(peek) + 1)
|
||||||
|
if p.readErr == io.EOF {
|
||||||
|
p.readErr = io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read out from "data to return" part of buffer.
|
||||||
|
if p.n == 0 {
|
||||||
|
return 0, p.err
|
||||||
|
}
|
||||||
|
n := len(d)
|
||||||
|
if n > p.n {
|
||||||
|
n = p.n
|
||||||
|
}
|
||||||
|
n, _ = br.Read(d[:n])
|
||||||
|
p.total += int64(n)
|
||||||
|
p.n -= n
|
||||||
|
if p.n == 0 {
|
||||||
|
return n, p.err
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanUntilBoundary scans buf to identify how much of it can be safely
|
||||||
|
// returned as part of the Part body.
|
||||||
|
// dashBoundary is "--boundary".
|
||||||
|
// nlDashBoundary is "\r\n--boundary" or "\n--boundary", depending on what mode we are in.
|
||||||
|
// The comments below (and the name) assume "\n--boundary", but either is accepted.
|
||||||
|
// total is the number of bytes read out so far. If total == 0, then a leading "--boundary" is recognized.
|
||||||
|
// readErr is the read error, if any, that followed reading the bytes in buf.
|
||||||
|
// scanUntilBoundary returns the number of data bytes from buf that can be
|
||||||
|
// returned as part of the Part body and also the error to return (if any)
|
||||||
|
// once those data bytes are done.
|
||||||
|
func scanUntilBoundary(buf, dashBoundary, nlDashBoundary []byte, total int64, readErr error) (int, error) {
|
||||||
|
if total == 0 {
|
||||||
|
// At beginning of body, allow dashBoundary.
|
||||||
|
if bytes.HasPrefix(buf, dashBoundary) {
|
||||||
|
switch matchAfterPrefix(buf, dashBoundary, readErr) {
|
||||||
|
case -1:
|
||||||
|
return len(dashBoundary), nil
|
||||||
|
case 0:
|
||||||
|
return 0, nil
|
||||||
|
case +1:
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bytes.HasPrefix(dashBoundary, buf) {
|
||||||
|
return 0, readErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search for "\n--boundary".
|
||||||
|
if i := bytes.Index(buf, nlDashBoundary); i >= 0 {
|
||||||
|
switch matchAfterPrefix(buf[i:], nlDashBoundary, readErr) {
|
||||||
|
case -1:
|
||||||
|
return i + len(nlDashBoundary), nil
|
||||||
|
case 0:
|
||||||
|
return i, nil
|
||||||
|
case +1:
|
||||||
|
return i, io.EOF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bytes.HasPrefix(nlDashBoundary, buf) {
|
||||||
|
return 0, readErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, anything up to the final \n is not part of the boundary
|
||||||
|
// and so must be part of the body.
|
||||||
|
// Also if the section from the final \n onward is not a prefix of the boundary,
|
||||||
|
// it too must be part of the body.
|
||||||
|
i := bytes.LastIndexByte(buf, nlDashBoundary[0])
|
||||||
|
if i >= 0 && bytes.HasPrefix(nlDashBoundary, buf[i:]) {
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
return len(buf), readErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchAfterPrefix checks whether buf should be considered to match the boundary.
|
||||||
|
// The prefix is "--boundary" or "\r\n--boundary" or "\n--boundary",
|
||||||
|
// and the caller has verified already that bytes.HasPrefix(buf, prefix) is true.
|
||||||
|
//
|
||||||
|
// matchAfterPrefix returns +1 if the buffer does match the boundary,
|
||||||
|
// meaning the prefix is followed by a dash, space, tab, cr, nl, or end of input.
|
||||||
|
// It returns -1 if the buffer definitely does NOT match the boundary,
|
||||||
|
// meaning the prefix is followed by some other character.
|
||||||
|
// For example, "--foobar" does not match "--foo".
|
||||||
|
// It returns 0 more input needs to be read to make the decision,
|
||||||
|
// meaning that len(buf) == len(prefix) and readErr == nil.
|
||||||
|
func matchAfterPrefix(buf, prefix []byte, readErr error) int {
|
||||||
|
if len(buf) == len(prefix) {
|
||||||
|
if readErr != nil {
|
||||||
|
return +1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
c := buf[len(prefix)]
|
||||||
|
if c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '-' {
|
||||||
|
return +1
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Part) Close() error {
|
||||||
|
io.Copy(ioutil.Discard, p)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultipartReader is an iterator over parts in a MIME multipart body.
|
||||||
|
// MultipartReader's underlying parser consumes its input as needed. Seeking
|
||||||
|
// isn't supported.
|
||||||
|
type MultipartReader struct {
|
||||||
|
bufReader *bufio.Reader
|
||||||
|
|
||||||
|
currentPart *Part
|
||||||
|
partsRead int
|
||||||
|
|
||||||
|
nl []byte // "\r\n" or "\n" (set after seeing first boundary line)
|
||||||
|
nlDashBoundary []byte // nl + "--boundary"
|
||||||
|
dashBoundaryDash []byte // "--boundary--"
|
||||||
|
dashBoundary []byte // "--boundary"
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextPart returns the next part in the multipart or an error.
|
||||||
|
// When there are no more parts, the error io.EOF is returned.
|
||||||
|
func (r *MultipartReader) NextPart() (*Part, error) {
|
||||||
|
if r.currentPart != nil {
|
||||||
|
r.currentPart.Close()
|
||||||
|
}
|
||||||
|
if string(r.dashBoundary) == "--" {
|
||||||
|
return nil, fmt.Errorf("multipart: boundary is empty")
|
||||||
|
}
|
||||||
|
expectNewPart := false
|
||||||
|
for {
|
||||||
|
line, err := r.bufReader.ReadSlice('\n')
|
||||||
|
|
||||||
|
if err == io.EOF && r.isFinalBoundary(line) {
|
||||||
|
// If the buffer ends in "--boundary--" without the
|
||||||
|
// trailing "\r\n", ReadSlice will return an error
|
||||||
|
// (since it's missing the '\n'), but this is a valid
|
||||||
|
// multipart EOF so we need to return io.EOF instead of
|
||||||
|
// a fmt-wrapped one.
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("multipart: NextPart: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.isBoundaryDelimiterLine(line) {
|
||||||
|
r.partsRead++
|
||||||
|
bp, err := newPart(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r.currentPart = bp
|
||||||
|
return bp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.isFinalBoundary(line) {
|
||||||
|
// Expected EOF
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
if expectNewPart {
|
||||||
|
return nil, fmt.Errorf("multipart: expecting a new Part; got line %q", string(line))
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.partsRead == 0 {
|
||||||
|
// skip line
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume the "\n" or "\r\n" separator between the
|
||||||
|
// body of the previous part and the boundary line we
|
||||||
|
// now expect will follow. (either a new part or the
|
||||||
|
// end boundary)
|
||||||
|
if bytes.Equal(line, r.nl) {
|
||||||
|
expectNewPart = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("multipart: unexpected line in Next(): %q", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isFinalBoundary reports whether line is the final boundary line
|
||||||
|
// indicating that all parts are over.
|
||||||
|
// It matches `^--boundary--[ \t]*(\r\n)?$`
|
||||||
|
func (mr *MultipartReader) isFinalBoundary(line []byte) bool {
|
||||||
|
if !bytes.HasPrefix(line, mr.dashBoundaryDash) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
rest := line[len(mr.dashBoundaryDash):]
|
||||||
|
rest = skipLWSPChar(rest)
|
||||||
|
return len(rest) == 0 || bytes.Equal(rest, mr.nl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mr *MultipartReader) isBoundaryDelimiterLine(line []byte) (ret bool) {
|
||||||
|
// https://tools.ietf.org/html/rfc2046#section-5.1
|
||||||
|
// The boundary delimiter line is then defined as a line
|
||||||
|
// consisting entirely of two hyphen characters ("-",
|
||||||
|
// decimal value 45) followed by the boundary parameter
|
||||||
|
// value from the Content-Type header field, optional linear
|
||||||
|
// whitespace, and a terminating CRLF.
|
||||||
|
if !bytes.HasPrefix(line, mr.dashBoundary) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
rest := line[len(mr.dashBoundary):]
|
||||||
|
rest = skipLWSPChar(rest)
|
||||||
|
|
||||||
|
// On the first part, see our lines are ending in \n instead of \r\n
|
||||||
|
// and switch into that mode if so. This is a violation of the spec,
|
||||||
|
// but occurs in practice.
|
||||||
|
if mr.partsRead == 0 && len(rest) == 1 && rest[0] == '\n' {
|
||||||
|
mr.nl = mr.nl[1:]
|
||||||
|
mr.nlDashBoundary = mr.nlDashBoundary[1:]
|
||||||
|
}
|
||||||
|
return bytes.Equal(rest, mr.nl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// skipLWSPChar returns b with leading spaces and tabs removed.
|
||||||
|
// RFC 822 defines:
|
||||||
|
// LWSP-char = SPACE / HTAB
|
||||||
|
func skipLWSPChar(b []byte) []byte {
|
||||||
|
for len(b) > 0 && (b[0] == ' ' || b[0] == '\t') {
|
||||||
|
b = b[1:]
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// A MultipartWriter generates multipart messages.
|
||||||
|
type MultipartWriter struct {
|
||||||
|
w io.Writer
|
||||||
|
boundary string
|
||||||
|
lastpart *part
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMultipartWriter returns a new multipart Writer with a random boundary,
|
||||||
|
// writing to w.
|
||||||
|
func NewMultipartWriter(w io.Writer) *MultipartWriter {
|
||||||
|
return &MultipartWriter{
|
||||||
|
w: w,
|
||||||
|
boundary: randomBoundary(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Boundary returns the Writer's boundary.
|
||||||
|
func (w *MultipartWriter) Boundary() string {
|
||||||
|
return w.boundary
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBoundary overrides the Writer's default randomly-generated
|
||||||
|
// boundary separator with an explicit value.
|
||||||
|
//
|
||||||
|
// SetBoundary must be called before any parts are created, may only
|
||||||
|
// contain certain ASCII characters, and must be non-empty and
|
||||||
|
// at most 70 bytes long.
|
||||||
|
func (w *MultipartWriter) SetBoundary(boundary string) error {
|
||||||
|
if w.lastpart != nil {
|
||||||
|
return errors.New("mime: SetBoundary called after write")
|
||||||
|
}
|
||||||
|
// rfc2046#section-5.1.1
|
||||||
|
if len(boundary) < 1 || len(boundary) > 70 {
|
||||||
|
return errors.New("mime: invalid boundary length")
|
||||||
|
}
|
||||||
|
end := len(boundary) - 1
|
||||||
|
for i, b := range boundary {
|
||||||
|
if 'A' <= b && b <= 'Z' || 'a' <= b && b <= 'z' || '0' <= b && b <= '9' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch b {
|
||||||
|
case '\'', '(', ')', '+', '_', ',', '-', '.', '/', ':', '=', '?':
|
||||||
|
continue
|
||||||
|
case ' ':
|
||||||
|
if i != end {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.New("mime: invalid boundary character")
|
||||||
|
}
|
||||||
|
w.boundary = boundary
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomBoundary() string {
|
||||||
|
var buf [30]byte
|
||||||
|
_, err := io.ReadFull(rand.Reader, buf[:])
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%x", buf[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatePart creates a new multipart section with the provided
|
||||||
|
// header. The body of the part should be written to the returned
|
||||||
|
// Writer. After calling CreatePart, any previous part may no longer
|
||||||
|
// be written to.
|
||||||
|
func (w *MultipartWriter) CreatePart(header Header) (io.Writer, error) {
|
||||||
|
if w.lastpart != nil {
|
||||||
|
if err := w.lastpart.close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var b bytes.Buffer
|
||||||
|
if w.lastpart != nil {
|
||||||
|
fmt.Fprintf(&b, "\r\n--%s\r\n", w.boundary)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(&b, "--%s\r\n", w.boundary)
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteHeader(&b, header)
|
||||||
|
|
||||||
|
_, err := io.Copy(w.w, &b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p := &part{
|
||||||
|
mw: w,
|
||||||
|
}
|
||||||
|
w.lastpart = p
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close finishes the multipart message and writes the trailing
|
||||||
|
// boundary end line to the output.
|
||||||
|
func (w *MultipartWriter) Close() error {
|
||||||
|
if w.lastpart != nil {
|
||||||
|
if err := w.lastpart.close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
w.lastpart = nil
|
||||||
|
}
|
||||||
|
_, err := fmt.Fprintf(w.w, "\r\n--%s--\r\n", w.boundary)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type part struct {
|
||||||
|
mw *MultipartWriter
|
||||||
|
closed bool
|
||||||
|
we error // last error that occurred writing
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *part) close() error {
|
||||||
|
p.closed = true
|
||||||
|
return p.we
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *part) Write(d []byte) (n int, err error) {
|
||||||
|
if p.closed {
|
||||||
|
return 0, errors.New("multipart: can't write to finished part")
|
||||||
|
}
|
||||||
|
n, err = p.mw.w.Write(d)
|
||||||
|
if err != nil {
|
||||||
|
p.we = err
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user