Initialize module and dependencies
This commit is contained in:
28
go.mod
Normal file
28
go.mod
Normal file
@@ -0,0 +1,28 @@
|
||||
module raven
|
||||
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/emersion/go-imap/v2 v2.0.0-beta.7
|
||||
github.com/emersion/go-message v0.18.2
|
||||
github.com/sashabaranov/go-openai v1.41.2
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect
|
||||
golang.org/x/vuln v1.1.4 // indirect
|
||||
)
|
||||
|
||||
tool (
|
||||
golang.org/x/tools/cmd/deadcode
|
||||
golang.org/x/tools/cmd/goimports
|
||||
golang.org/x/vuln/cmd/govulncheck
|
||||
)
|
||||
67
go.sum
Normal file
67
go.sum
Normal file
@@ -0,0 +1,67 @@
|
||||
github.com/emersion/go-imap/v2 v2.0.0-beta.7 h1:lNznYWa5uhMrngnSYEklzCeye4DBq9TEJ+pr0K593+8=
|
||||
github.com/emersion/go-imap/v2 v2.0.0-beta.7/go.mod h1:BZTFHsS1hmgBkFlHqbxGLXk2hnRqTItUgwjSSCsYNAk=
|
||||
github.com/emersion/go-message v0.18.2 h1:rl55SQdjd9oJcIoQNhubD2Acs1E6IzlZISRTK7x/Lpg=
|
||||
github.com/emersion/go-message v0.18.2/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA=
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786 h1:rcv+Ippz6RAtvaGgKxc+8FQIpxHgsF+HBzPyYL2cyVU=
|
||||
github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/sashabaranov/go-openai v1.41.2 h1:vfPRBZNMpnqu8ELsclWcAvF19lDNgh1t6TVfFFOPiSM=
|
||||
github.com/sashabaranov/go-openai v1.41.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc h1:bH6xUXay0AIFMElXG2rQ4uiE+7ncwtiOdPfYK1NK2XA=
|
||||
golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/tools/go/expect v0.1.0-deprecated h1:jY2C5HGYR5lqex3gEniOQL0r7Dq5+VGVgY1nudX5lXY=
|
||||
golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
|
||||
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
|
||||
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=
|
||||
golang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I=
|
||||
golang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
19
vendor/github.com/emersion/go-imap/v2/.build.yml
generated
vendored
Normal file
19
vendor/github.com/emersion/go-imap/v2/.build.yml
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
image: alpine/latest
|
||||
packages:
|
||||
- dovecot
|
||||
- go
|
||||
sources:
|
||||
- https://github.com/emersion/go-imap#v2
|
||||
tasks:
|
||||
- build: |
|
||||
cd go-imap
|
||||
go build -race -v ./...
|
||||
- test: |
|
||||
cd go-imap
|
||||
go test -race ./...
|
||||
- test-dovecot: |
|
||||
cd go-imap
|
||||
GOIMAP_TEST_DOVECOT=1 go test -race ./imapclient
|
||||
- gofmt: |
|
||||
cd go-imap
|
||||
test -z $(gofmt -l .)
|
||||
23
vendor/github.com/emersion/go-imap/v2/LICENSE
generated
vendored
Normal file
23
vendor/github.com/emersion/go-imap/v2/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 The Go-IMAP Authors
|
||||
Copyright (c) 2016 Proton Technologies AG
|
||||
Copyright (c) 2023 Simon Ser
|
||||
|
||||
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.
|
||||
29
vendor/github.com/emersion/go-imap/v2/README.md
generated
vendored
Normal file
29
vendor/github.com/emersion/go-imap/v2/README.md
generated
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# go-imap
|
||||
|
||||
[](https://pkg.go.dev/github.com/emersion/go-imap/v2)
|
||||
|
||||
An [IMAP4rev2] library for Go.
|
||||
|
||||
> **Note**
|
||||
> This is the README for go-imap v2. This new major version is still in
|
||||
> development. For go-imap v1, see the [v1 branch].
|
||||
|
||||
## Usage
|
||||
|
||||
To add go-imap to your project, run:
|
||||
|
||||
go get github.com/emersion/go-imap/v2
|
||||
|
||||
Documentation and examples for the module are available here:
|
||||
|
||||
- [Client docs]
|
||||
- [Server docs]
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
[IMAP4rev2]: https://www.rfc-editor.org/rfc/rfc9051.html
|
||||
[v1 branch]: https://github.com/emersion/go-imap/tree/v1
|
||||
[Client docs]: https://pkg.go.dev/github.com/emersion/go-imap/v2/imapclient
|
||||
[Server docs]: https://pkg.go.dev/github.com/emersion/go-imap/v2/imapserver
|
||||
104
vendor/github.com/emersion/go-imap/v2/acl.go
generated
vendored
Normal file
104
vendor/github.com/emersion/go-imap/v2/acl.go
generated
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
package imap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IMAP4 ACL extension (RFC 2086)
|
||||
|
||||
// Right describes a set of operations controlled by the IMAP ACL extension.
|
||||
type Right byte
|
||||
|
||||
const (
|
||||
// Standard rights
|
||||
RightLookup = Right('l') // mailbox is visible to LIST/LSUB commands
|
||||
RightRead = Right('r') // SELECT the mailbox, perform CHECK, FETCH, PARTIAL, SEARCH, COPY from mailbox
|
||||
RightSeen = Right('s') // keep seen/unseen information across sessions (STORE SEEN flag)
|
||||
RightWrite = Right('w') // STORE flags other than SEEN and DELETED
|
||||
RightInsert = Right('i') // perform APPEND, COPY into mailbox
|
||||
RightPost = Right('p') // send mail to submission address for mailbox, not enforced by IMAP4 itself
|
||||
RightCreate = Right('c') // CREATE new sub-mailboxes in any implementation-defined hierarchy
|
||||
RightDelete = Right('d') // STORE DELETED flag, perform EXPUNGE
|
||||
RightAdminister = Right('a') // perform SETACL
|
||||
)
|
||||
|
||||
// RightSetAll contains all standard rights.
|
||||
var RightSetAll = RightSet("lrswipcda")
|
||||
|
||||
// RightsIdentifier is an ACL identifier.
|
||||
type RightsIdentifier string
|
||||
|
||||
// RightsIdentifierAnyone is the universal identity (matches everyone).
|
||||
const RightsIdentifierAnyone = RightsIdentifier("anyone")
|
||||
|
||||
// NewRightsIdentifierUsername returns a rights identifier referring to a
|
||||
// username, checking for reserved values.
|
||||
func NewRightsIdentifierUsername(username string) (RightsIdentifier, error) {
|
||||
if username == string(RightsIdentifierAnyone) || strings.HasPrefix(username, "-") {
|
||||
return "", fmt.Errorf("imap: reserved rights identifier")
|
||||
}
|
||||
return RightsIdentifier(username), nil
|
||||
}
|
||||
|
||||
// RightModification indicates how to mutate a right set.
|
||||
type RightModification byte
|
||||
|
||||
const (
|
||||
RightModificationReplace = RightModification(0)
|
||||
RightModificationAdd = RightModification('+')
|
||||
RightModificationRemove = RightModification('-')
|
||||
)
|
||||
|
||||
// A RightSet is a set of rights.
|
||||
type RightSet []Right
|
||||
|
||||
// String returns a string representation of the right set.
|
||||
func (r RightSet) String() string {
|
||||
return string(r)
|
||||
}
|
||||
|
||||
// Add returns a new right set containing rights from both sets.
|
||||
func (r RightSet) Add(rights RightSet) RightSet {
|
||||
newRights := make(RightSet, len(r), len(r)+len(rights))
|
||||
copy(newRights, r)
|
||||
|
||||
for _, right := range rights {
|
||||
if !strings.ContainsRune(string(r), rune(right)) {
|
||||
newRights = append(newRights, right)
|
||||
}
|
||||
}
|
||||
|
||||
return newRights
|
||||
}
|
||||
|
||||
// Remove returns a new right set containing all rights in r except these in
|
||||
// the provided set.
|
||||
func (r RightSet) Remove(rights RightSet) RightSet {
|
||||
newRights := make(RightSet, 0, len(r))
|
||||
|
||||
for _, right := range r {
|
||||
if !strings.ContainsRune(string(rights), rune(right)) {
|
||||
newRights = append(newRights, right)
|
||||
}
|
||||
}
|
||||
|
||||
return newRights
|
||||
}
|
||||
|
||||
// Equal returns true if both right sets contain exactly the same rights.
|
||||
func (rs1 RightSet) Equal(rs2 RightSet) bool {
|
||||
for _, r := range rs1 {
|
||||
if !strings.ContainsRune(string(rs2), rune(r)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range rs2 {
|
||||
if !strings.ContainsRune(string(rs1), rune(r)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
18
vendor/github.com/emersion/go-imap/v2/append.go
generated
vendored
Normal file
18
vendor/github.com/emersion/go-imap/v2/append.go
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
package imap
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// AppendOptions contains options for the APPEND command.
|
||||
type AppendOptions struct {
|
||||
Flags []Flag
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
// AppendData is the data returned by an APPEND command.
|
||||
type AppendData struct {
|
||||
// requires UIDPLUS or IMAP4rev2
|
||||
UID UID
|
||||
UIDValidity uint32
|
||||
}
|
||||
212
vendor/github.com/emersion/go-imap/v2/capability.go
generated
vendored
Normal file
212
vendor/github.com/emersion/go-imap/v2/capability.go
generated
vendored
Normal file
@@ -0,0 +1,212 @@
|
||||
package imap
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Cap represents an IMAP capability.
|
||||
type Cap string
|
||||
|
||||
// Registered capabilities.
|
||||
//
|
||||
// See: https://www.iana.org/assignments/imap-capabilities/
|
||||
const (
|
||||
CapIMAP4rev1 Cap = "IMAP4rev1" // RFC 3501
|
||||
CapIMAP4rev2 Cap = "IMAP4rev2" // RFC 9051
|
||||
|
||||
CapStartTLS Cap = "STARTTLS"
|
||||
CapLoginDisabled Cap = "LOGINDISABLED"
|
||||
|
||||
// Folded in IMAP4rev2
|
||||
CapNamespace Cap = "NAMESPACE" // RFC 2342
|
||||
CapUnselect Cap = "UNSELECT" // RFC 3691
|
||||
CapUIDPlus Cap = "UIDPLUS" // RFC 4315
|
||||
CapESearch Cap = "ESEARCH" // RFC 4731
|
||||
CapSearchRes Cap = "SEARCHRES" // RFC 5182
|
||||
CapEnable Cap = "ENABLE" // RFC 5161
|
||||
CapIdle Cap = "IDLE" // RFC 2177
|
||||
CapSASLIR Cap = "SASL-IR" // RFC 4959
|
||||
CapListExtended Cap = "LIST-EXTENDED" // RFC 5258
|
||||
CapListStatus Cap = "LIST-STATUS" // RFC 5819
|
||||
CapMove Cap = "MOVE" // RFC 6851
|
||||
CapLiteralMinus Cap = "LITERAL-" // RFC 7888
|
||||
CapStatusSize Cap = "STATUS=SIZE" // RFC 8438
|
||||
CapChildren Cap = "CHILDREN" // RFC 3348
|
||||
|
||||
CapACL Cap = "ACL" // RFC 4314
|
||||
CapAppendLimit Cap = "APPENDLIMIT" // RFC 7889
|
||||
CapBinary Cap = "BINARY" // RFC 3516
|
||||
CapCatenate Cap = "CATENATE" // RFC 4469
|
||||
CapCondStore Cap = "CONDSTORE" // RFC 7162
|
||||
CapConvert Cap = "CONVERT" // RFC 5259
|
||||
CapCreateSpecialUse Cap = "CREATE-SPECIAL-USE" // RFC 6154
|
||||
CapESort Cap = "ESORT" // RFC 5267
|
||||
CapFilters Cap = "FILTERS" // RFC 5466
|
||||
CapID Cap = "ID" // RFC 2971
|
||||
CapLanguage Cap = "LANGUAGE" // RFC 5255
|
||||
CapListMyRights Cap = "LIST-MYRIGHTS" // RFC 8440
|
||||
CapLiteralPlus Cap = "LITERAL+" // RFC 7888
|
||||
CapLoginReferrals Cap = "LOGIN-REFERRALS" // RFC 2221
|
||||
CapMailboxReferrals Cap = "MAILBOX-REFERRALS" // RFC 2193
|
||||
CapMetadata Cap = "METADATA" // RFC 5464
|
||||
CapMetadataServer Cap = "METADATA-SERVER" // RFC 5464
|
||||
CapMultiAppend Cap = "MULTIAPPEND" // RFC 3502
|
||||
CapMultiSearch Cap = "MULTISEARCH" // RFC 7377
|
||||
CapNotify Cap = "NOTIFY" // RFC 5465
|
||||
CapObjectID Cap = "OBJECTID" // RFC 8474
|
||||
CapPreview Cap = "PREVIEW" // RFC 8970
|
||||
CapQResync Cap = "QRESYNC" // RFC 7162
|
||||
CapQuota Cap = "QUOTA" // RFC 9208
|
||||
CapQuotaSet Cap = "QUOTASET" // RFC 9208
|
||||
CapReplace Cap = "REPLACE" // RFC 8508
|
||||
CapSaveDate Cap = "SAVEDATE" // RFC 8514
|
||||
CapSearchFuzzy Cap = "SEARCH=FUZZY" // RFC 6203
|
||||
CapSort Cap = "SORT" // RFC 5256
|
||||
CapSortDisplay Cap = "SORT=DISPLAY" // RFC 5957
|
||||
CapSpecialUse Cap = "SPECIAL-USE" // RFC 6154
|
||||
CapUnauthenticate Cap = "UNAUTHENTICATE" // RFC 8437
|
||||
CapURLPartial Cap = "URL-PARTIAL" // RFC 5550
|
||||
CapURLAuth Cap = "URLAUTH" // RFC 4467
|
||||
CapUTF8Accept Cap = "UTF8=ACCEPT" // RFC 6855
|
||||
CapUTF8Only Cap = "UTF8=ONLY" // RFC 6855
|
||||
CapWithin Cap = "WITHIN" // RFC 5032
|
||||
CapUIDOnly Cap = "UIDONLY" // RFC 9586
|
||||
CapListMetadata Cap = "LIST-METADATA" // RFC 9590
|
||||
CapInProgress Cap = "INPROGRESS" // RFC 9585
|
||||
)
|
||||
|
||||
var imap4rev2Caps = CapSet{
|
||||
CapNamespace: {},
|
||||
CapUnselect: {},
|
||||
CapUIDPlus: {},
|
||||
CapESearch: {},
|
||||
CapSearchRes: {},
|
||||
CapEnable: {},
|
||||
CapIdle: {},
|
||||
CapSASLIR: {},
|
||||
CapListExtended: {},
|
||||
CapListStatus: {},
|
||||
CapMove: {},
|
||||
CapLiteralMinus: {},
|
||||
CapStatusSize: {},
|
||||
CapChildren: {},
|
||||
}
|
||||
|
||||
// AuthCap returns the capability name for an SASL authentication mechanism.
|
||||
func AuthCap(mechanism string) Cap {
|
||||
return Cap("AUTH=" + mechanism)
|
||||
}
|
||||
|
||||
// CapSet is a set of capabilities.
|
||||
type CapSet map[Cap]struct{}
|
||||
|
||||
func (set CapSet) has(c Cap) bool {
|
||||
_, ok := set[c]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (set CapSet) Copy() CapSet {
|
||||
newSet := make(CapSet, len(set))
|
||||
for c := range set {
|
||||
newSet[c] = struct{}{}
|
||||
}
|
||||
return newSet
|
||||
}
|
||||
|
||||
// Has checks whether a capability is supported.
|
||||
//
|
||||
// Some capabilities are implied by others, as such Has may return true even if
|
||||
// the capability is not in the map.
|
||||
func (set CapSet) Has(c Cap) bool {
|
||||
if set.has(c) {
|
||||
return true
|
||||
}
|
||||
|
||||
if set.has(CapIMAP4rev2) && imap4rev2Caps.has(c) {
|
||||
return true
|
||||
}
|
||||
|
||||
if c == CapLiteralMinus && set.has(CapLiteralPlus) {
|
||||
return true
|
||||
}
|
||||
if c == CapCondStore && set.has(CapQResync) {
|
||||
return true
|
||||
}
|
||||
if c == CapUTF8Accept && set.has(CapUTF8Only) {
|
||||
return true
|
||||
}
|
||||
if c == CapAppendLimit {
|
||||
_, ok := set.AppendLimit()
|
||||
return ok
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// AuthMechanisms returns the list of supported SASL mechanisms for
|
||||
// authentication.
|
||||
func (set CapSet) AuthMechanisms() []string {
|
||||
var l []string
|
||||
for c := range set {
|
||||
if !strings.HasPrefix(string(c), "AUTH=") {
|
||||
continue
|
||||
}
|
||||
mech := strings.TrimPrefix(string(c), "AUTH=")
|
||||
l = append(l, mech)
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// AppendLimit checks the APPENDLIMIT capability.
|
||||
//
|
||||
// If the server supports APPENDLIMIT, ok is true. If the server doesn't have
|
||||
// the same upload limit for all mailboxes, limit is nil and per-mailbox
|
||||
// limits must be queried via STATUS.
|
||||
func (set CapSet) AppendLimit() (limit *uint32, ok bool) {
|
||||
if set.has(CapAppendLimit) {
|
||||
return nil, true
|
||||
}
|
||||
|
||||
for c := range set {
|
||||
if !strings.HasPrefix(string(c), "APPENDLIMIT=") {
|
||||
continue
|
||||
}
|
||||
|
||||
limitStr := strings.TrimPrefix(string(c), "APPENDLIMIT=")
|
||||
limit64, err := strconv.ParseUint(limitStr, 10, 32)
|
||||
if err == nil && limit64 > 0 {
|
||||
limit32 := uint32(limit64)
|
||||
return &limit32, true
|
||||
}
|
||||
}
|
||||
|
||||
limit32 := ^uint32(0)
|
||||
return &limit32, false
|
||||
}
|
||||
|
||||
// QuotaResourceTypes returns the list of supported QUOTA resource types.
|
||||
func (set CapSet) QuotaResourceTypes() []QuotaResourceType {
|
||||
var l []QuotaResourceType
|
||||
for c := range set {
|
||||
if !strings.HasPrefix(string(c), "QUOTA=RES-") {
|
||||
continue
|
||||
}
|
||||
t := strings.TrimPrefix(string(c), "QUOTA=RES-")
|
||||
l = append(l, QuotaResourceType(t))
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// ThreadAlgorithms returns the list of supported threading algorithms.
|
||||
func (set CapSet) ThreadAlgorithms() []ThreadAlgorithm {
|
||||
var l []ThreadAlgorithm
|
||||
for c := range set {
|
||||
if !strings.HasPrefix(string(c), "THREAD=") {
|
||||
continue
|
||||
}
|
||||
alg := strings.TrimPrefix(string(c), "THREAD=")
|
||||
l = append(l, ThreadAlgorithm(alg))
|
||||
}
|
||||
return l
|
||||
}
|
||||
9
vendor/github.com/emersion/go-imap/v2/copy.go
generated
vendored
Normal file
9
vendor/github.com/emersion/go-imap/v2/copy.go
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
package imap
|
||||
|
||||
// CopyData is the data returned by a COPY command.
|
||||
type CopyData struct {
|
||||
// requires UIDPLUS or IMAP4rev2
|
||||
UIDValidity uint32
|
||||
SourceUIDs UIDSet
|
||||
DestUIDs UIDSet
|
||||
}
|
||||
6
vendor/github.com/emersion/go-imap/v2/create.go
generated
vendored
Normal file
6
vendor/github.com/emersion/go-imap/v2/create.go
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
package imap
|
||||
|
||||
// CreateOptions contains options for the CREATE command.
|
||||
type CreateOptions struct {
|
||||
SpecialUse []MailboxAttr // requires CREATE-SPECIAL-USE
|
||||
}
|
||||
284
vendor/github.com/emersion/go-imap/v2/fetch.go
generated
vendored
Normal file
284
vendor/github.com/emersion/go-imap/v2/fetch.go
generated
vendored
Normal file
@@ -0,0 +1,284 @@
|
||||
package imap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// FetchOptions contains options for the FETCH command.
|
||||
type FetchOptions struct {
|
||||
// Fields to fetch
|
||||
BodyStructure *FetchItemBodyStructure
|
||||
Envelope bool
|
||||
Flags bool
|
||||
InternalDate bool
|
||||
RFC822Size bool
|
||||
UID bool
|
||||
BodySection []*FetchItemBodySection
|
||||
BinarySection []*FetchItemBinarySection // requires IMAP4rev2 or BINARY
|
||||
BinarySectionSize []*FetchItemBinarySectionSize // requires IMAP4rev2 or BINARY
|
||||
ModSeq bool // requires CONDSTORE
|
||||
|
||||
ChangedSince uint64 // requires CONDSTORE
|
||||
}
|
||||
|
||||
// FetchItemBodyStructure contains FETCH options for the body structure.
|
||||
type FetchItemBodyStructure struct {
|
||||
Extended bool
|
||||
}
|
||||
|
||||
// PartSpecifier describes whether to fetch a part's header, body, or both.
|
||||
type PartSpecifier string
|
||||
|
||||
const (
|
||||
PartSpecifierNone PartSpecifier = ""
|
||||
PartSpecifierHeader PartSpecifier = "HEADER"
|
||||
PartSpecifierMIME PartSpecifier = "MIME"
|
||||
PartSpecifierText PartSpecifier = "TEXT"
|
||||
)
|
||||
|
||||
// SectionPartial describes a byte range when fetching a message's payload.
|
||||
type SectionPartial struct {
|
||||
Offset, Size int64
|
||||
}
|
||||
|
||||
// FetchItemBodySection is a FETCH BODY[] data item.
|
||||
//
|
||||
// To fetch the whole body of a message, use the zero FetchItemBodySection:
|
||||
//
|
||||
// imap.FetchItemBodySection{}
|
||||
//
|
||||
// To fetch only a specific part, use the Part field:
|
||||
//
|
||||
// imap.FetchItemBodySection{Part: []int{1, 2, 3}}
|
||||
//
|
||||
// To fetch only the header of the message, use the Specifier field:
|
||||
//
|
||||
// imap.FetchItemBodySection{Specifier: imap.PartSpecifierHeader}
|
||||
type FetchItemBodySection struct {
|
||||
Specifier PartSpecifier
|
||||
Part []int
|
||||
HeaderFields []string
|
||||
HeaderFieldsNot []string
|
||||
Partial *SectionPartial
|
||||
Peek bool
|
||||
}
|
||||
|
||||
// FetchItemBinarySection is a FETCH BINARY[] data item.
|
||||
type FetchItemBinarySection struct {
|
||||
Part []int
|
||||
Partial *SectionPartial
|
||||
Peek bool
|
||||
}
|
||||
|
||||
// FetchItemBinarySectionSize is a FETCH BINARY.SIZE[] data item.
|
||||
type FetchItemBinarySectionSize struct {
|
||||
Part []int
|
||||
}
|
||||
|
||||
// Envelope is the envelope structure of a message.
|
||||
//
|
||||
// The subject and addresses are UTF-8 (ie, not in their encoded form). The
|
||||
// In-Reply-To and Message-ID values contain message identifiers without angle
|
||||
// brackets.
|
||||
type Envelope struct {
|
||||
Date time.Time
|
||||
Subject string
|
||||
From []Address
|
||||
Sender []Address
|
||||
ReplyTo []Address
|
||||
To []Address
|
||||
Cc []Address
|
||||
Bcc []Address
|
||||
InReplyTo []string
|
||||
MessageID string
|
||||
}
|
||||
|
||||
// Address represents a sender or recipient of a message.
|
||||
type Address struct {
|
||||
Name string
|
||||
Mailbox string
|
||||
Host string
|
||||
}
|
||||
|
||||
// Addr returns the e-mail address in the form "foo@example.org".
|
||||
//
|
||||
// If the address is a start or end of group, the empty string is returned.
|
||||
func (addr *Address) Addr() string {
|
||||
if addr.Mailbox == "" || addr.Host == "" {
|
||||
return ""
|
||||
}
|
||||
return addr.Mailbox + "@" + addr.Host
|
||||
}
|
||||
|
||||
// IsGroupStart returns true if this address is a start of group marker.
|
||||
//
|
||||
// In that case, Mailbox contains the group name phrase.
|
||||
func (addr *Address) IsGroupStart() bool {
|
||||
return addr.Host == "" && addr.Mailbox != ""
|
||||
}
|
||||
|
||||
// IsGroupEnd returns true if this address is a end of group marker.
|
||||
func (addr *Address) IsGroupEnd() bool {
|
||||
return addr.Host == "" && addr.Mailbox == ""
|
||||
}
|
||||
|
||||
// BodyStructure describes the body structure of a message.
|
||||
//
|
||||
// A BodyStructure value is either a *BodyStructureSinglePart or a
|
||||
// *BodyStructureMultiPart.
|
||||
type BodyStructure interface {
|
||||
// MediaType returns the MIME type of this body structure, e.g. "text/plain".
|
||||
MediaType() string
|
||||
// Walk walks the body structure tree, calling f for each part in the tree,
|
||||
// including bs itself. The parts are visited in DFS pre-order.
|
||||
Walk(f BodyStructureWalkFunc)
|
||||
// Disposition returns the body structure disposition, if available.
|
||||
Disposition() *BodyStructureDisposition
|
||||
|
||||
bodyStructure()
|
||||
}
|
||||
|
||||
var (
|
||||
_ BodyStructure = (*BodyStructureSinglePart)(nil)
|
||||
_ BodyStructure = (*BodyStructureMultiPart)(nil)
|
||||
)
|
||||
|
||||
// BodyStructureSinglePart is a body structure with a single part.
|
||||
type BodyStructureSinglePart struct {
|
||||
Type, Subtype string
|
||||
Params map[string]string
|
||||
ID string
|
||||
Description string
|
||||
Encoding string
|
||||
Size uint32
|
||||
|
||||
MessageRFC822 *BodyStructureMessageRFC822 // only for "message/rfc822"
|
||||
Text *BodyStructureText // only for "text/*"
|
||||
Extended *BodyStructureSinglePartExt
|
||||
}
|
||||
|
||||
func (bs *BodyStructureSinglePart) MediaType() string {
|
||||
return strings.ToLower(bs.Type) + "/" + strings.ToLower(bs.Subtype)
|
||||
}
|
||||
|
||||
func (bs *BodyStructureSinglePart) Walk(f BodyStructureWalkFunc) {
|
||||
f([]int{1}, bs)
|
||||
}
|
||||
|
||||
func (bs *BodyStructureSinglePart) Disposition() *BodyStructureDisposition {
|
||||
if bs.Extended == nil {
|
||||
return nil
|
||||
}
|
||||
return bs.Extended.Disposition
|
||||
}
|
||||
|
||||
// Filename decodes the body structure's filename, if any.
|
||||
func (bs *BodyStructureSinglePart) Filename() string {
|
||||
var filename string
|
||||
if bs.Extended != nil && bs.Extended.Disposition != nil {
|
||||
filename = bs.Extended.Disposition.Params["filename"]
|
||||
}
|
||||
if filename == "" {
|
||||
// Note: using "name" in Content-Type is discouraged
|
||||
filename = bs.Params["name"]
|
||||
}
|
||||
return filename
|
||||
}
|
||||
|
||||
func (*BodyStructureSinglePart) bodyStructure() {}
|
||||
|
||||
// BodyStructureMessageRFC822 contains metadata specific to RFC 822 parts for
|
||||
// BodyStructureSinglePart.
|
||||
type BodyStructureMessageRFC822 struct {
|
||||
Envelope *Envelope
|
||||
BodyStructure BodyStructure
|
||||
NumLines int64
|
||||
}
|
||||
|
||||
// BodyStructureText contains metadata specific to text parts for
|
||||
// BodyStructureSinglePart.
|
||||
type BodyStructureText struct {
|
||||
NumLines int64
|
||||
}
|
||||
|
||||
// BodyStructureSinglePartExt contains extended body structure data for
|
||||
// BodyStructureSinglePart.
|
||||
type BodyStructureSinglePartExt struct {
|
||||
Disposition *BodyStructureDisposition
|
||||
Language []string
|
||||
Location string
|
||||
}
|
||||
|
||||
// BodyStructureMultiPart is a body structure with multiple parts.
|
||||
type BodyStructureMultiPart struct {
|
||||
Children []BodyStructure
|
||||
Subtype string
|
||||
|
||||
Extended *BodyStructureMultiPartExt
|
||||
}
|
||||
|
||||
func (bs *BodyStructureMultiPart) MediaType() string {
|
||||
return "multipart/" + strings.ToLower(bs.Subtype)
|
||||
}
|
||||
|
||||
func (bs *BodyStructureMultiPart) Walk(f BodyStructureWalkFunc) {
|
||||
bs.walk(f, nil)
|
||||
}
|
||||
|
||||
func (bs *BodyStructureMultiPart) walk(f BodyStructureWalkFunc, path []int) {
|
||||
if !f(path, bs) {
|
||||
return
|
||||
}
|
||||
|
||||
pathBuf := make([]int, len(path))
|
||||
copy(pathBuf, path)
|
||||
for i, part := range bs.Children {
|
||||
num := i + 1
|
||||
partPath := append(pathBuf, num)
|
||||
|
||||
switch part := part.(type) {
|
||||
case *BodyStructureSinglePart:
|
||||
f(partPath, part)
|
||||
case *BodyStructureMultiPart:
|
||||
part.walk(f, partPath)
|
||||
default:
|
||||
panic(fmt.Errorf("unsupported body structure type %T", part))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (bs *BodyStructureMultiPart) Disposition() *BodyStructureDisposition {
|
||||
if bs.Extended == nil {
|
||||
return nil
|
||||
}
|
||||
return bs.Extended.Disposition
|
||||
}
|
||||
|
||||
func (*BodyStructureMultiPart) bodyStructure() {}
|
||||
|
||||
// BodyStructureMultiPartExt contains extended body structure data for
|
||||
// BodyStructureMultiPart.
|
||||
type BodyStructureMultiPartExt struct {
|
||||
Params map[string]string
|
||||
Disposition *BodyStructureDisposition
|
||||
Language []string
|
||||
Location string
|
||||
}
|
||||
|
||||
// BodyStructureDisposition describes the content disposition of a part
|
||||
// (specified in the Content-Disposition header field).
|
||||
type BodyStructureDisposition struct {
|
||||
Value string
|
||||
Params map[string]string
|
||||
}
|
||||
|
||||
// BodyStructureWalkFunc is a function called for each body structure visited
|
||||
// by BodyStructure.Walk.
|
||||
//
|
||||
// The path argument contains the IMAP part path.
|
||||
//
|
||||
// The function should return true to visit all of the part's children or false
|
||||
// to skip them.
|
||||
type BodyStructureWalkFunc func(path []int, part BodyStructure) (walkChildren bool)
|
||||
15
vendor/github.com/emersion/go-imap/v2/id.go
generated
vendored
Normal file
15
vendor/github.com/emersion/go-imap/v2/id.go
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
package imap
|
||||
|
||||
type IDData struct {
|
||||
Name string
|
||||
Version string
|
||||
OS string
|
||||
OSVersion string
|
||||
Vendor string
|
||||
SupportURL string
|
||||
Address string
|
||||
Date string
|
||||
Command string
|
||||
Arguments string
|
||||
Environment string
|
||||
}
|
||||
105
vendor/github.com/emersion/go-imap/v2/imap.go
generated
vendored
Normal file
105
vendor/github.com/emersion/go-imap/v2/imap.go
generated
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
// Package imap implements IMAP4rev2.
|
||||
//
|
||||
// IMAP4rev2 is defined in RFC 9051.
|
||||
//
|
||||
// This package contains types and functions common to both the client and
|
||||
// server. See the imapclient and imapserver sub-packages.
|
||||
package imap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// ConnState describes the connection state.
|
||||
//
|
||||
// See RFC 9051 section 3.
|
||||
type ConnState int
|
||||
|
||||
const (
|
||||
ConnStateNone ConnState = iota
|
||||
ConnStateNotAuthenticated
|
||||
ConnStateAuthenticated
|
||||
ConnStateSelected
|
||||
ConnStateLogout
|
||||
)
|
||||
|
||||
// String implements fmt.Stringer.
|
||||
func (state ConnState) String() string {
|
||||
switch state {
|
||||
case ConnStateNone:
|
||||
return "none"
|
||||
case ConnStateNotAuthenticated:
|
||||
return "not authenticated"
|
||||
case ConnStateAuthenticated:
|
||||
return "authenticated"
|
||||
case ConnStateSelected:
|
||||
return "selected"
|
||||
case ConnStateLogout:
|
||||
return "logout"
|
||||
default:
|
||||
panic(fmt.Errorf("imap: unknown connection state %v", int(state)))
|
||||
}
|
||||
}
|
||||
|
||||
// MailboxAttr is a mailbox attribute.
|
||||
//
|
||||
// Mailbox attributes are defined in RFC 9051 section 7.3.1.
|
||||
type MailboxAttr string
|
||||
|
||||
const (
|
||||
// Base attributes
|
||||
MailboxAttrNonExistent MailboxAttr = "\\NonExistent"
|
||||
MailboxAttrNoInferiors MailboxAttr = "\\Noinferiors"
|
||||
MailboxAttrNoSelect MailboxAttr = "\\Noselect"
|
||||
MailboxAttrHasChildren MailboxAttr = "\\HasChildren"
|
||||
MailboxAttrHasNoChildren MailboxAttr = "\\HasNoChildren"
|
||||
MailboxAttrMarked MailboxAttr = "\\Marked"
|
||||
MailboxAttrUnmarked MailboxAttr = "\\Unmarked"
|
||||
MailboxAttrSubscribed MailboxAttr = "\\Subscribed"
|
||||
MailboxAttrRemote MailboxAttr = "\\Remote"
|
||||
|
||||
// Role (aka. "special-use") attributes
|
||||
MailboxAttrAll MailboxAttr = "\\All"
|
||||
MailboxAttrArchive MailboxAttr = "\\Archive"
|
||||
MailboxAttrDrafts MailboxAttr = "\\Drafts"
|
||||
MailboxAttrFlagged MailboxAttr = "\\Flagged"
|
||||
MailboxAttrJunk MailboxAttr = "\\Junk"
|
||||
MailboxAttrSent MailboxAttr = "\\Sent"
|
||||
MailboxAttrTrash MailboxAttr = "\\Trash"
|
||||
MailboxAttrImportant MailboxAttr = "\\Important" // RFC 8457
|
||||
)
|
||||
|
||||
// Flag is a message flag.
|
||||
//
|
||||
// Message flags are defined in RFC 9051 section 2.3.2.
|
||||
type Flag string
|
||||
|
||||
const (
|
||||
// System flags
|
||||
FlagSeen Flag = "\\Seen"
|
||||
FlagAnswered Flag = "\\Answered"
|
||||
FlagFlagged Flag = "\\Flagged"
|
||||
FlagDeleted Flag = "\\Deleted"
|
||||
FlagDraft Flag = "\\Draft"
|
||||
|
||||
// Widely used flags
|
||||
FlagForwarded Flag = "$Forwarded"
|
||||
FlagMDNSent Flag = "$MDNSent" // Message Disposition Notification sent
|
||||
FlagJunk Flag = "$Junk"
|
||||
FlagNotJunk Flag = "$NotJunk"
|
||||
FlagPhishing Flag = "$Phishing"
|
||||
FlagImportant Flag = "$Important" // RFC 8457
|
||||
|
||||
// Permanent flags
|
||||
FlagWildcard Flag = "\\*"
|
||||
)
|
||||
|
||||
// LiteralReader is a reader for IMAP literals.
|
||||
type LiteralReader interface {
|
||||
io.Reader
|
||||
Size() int64
|
||||
}
|
||||
|
||||
// UID is a message unique identifier.
|
||||
type UID uint32
|
||||
138
vendor/github.com/emersion/go-imap/v2/imapclient/acl.go
generated
vendored
Normal file
138
vendor/github.com/emersion/go-imap/v2/imapclient/acl.go
generated
vendored
Normal file
@@ -0,0 +1,138 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
// MyRights sends a MYRIGHTS command.
|
||||
//
|
||||
// This command requires support for the ACL extension.
|
||||
func (c *Client) MyRights(mailbox string) *MyRightsCommand {
|
||||
cmd := &MyRightsCommand{}
|
||||
enc := c.beginCommand("MYRIGHTS", cmd)
|
||||
enc.SP().Mailbox(mailbox)
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
// SetACL sends a SETACL command.
|
||||
//
|
||||
// This command requires support for the ACL extension.
|
||||
func (c *Client) SetACL(mailbox string, ri imap.RightsIdentifier, rm imap.RightModification, rs imap.RightSet) *SetACLCommand {
|
||||
cmd := &SetACLCommand{}
|
||||
enc := c.beginCommand("SETACL", cmd)
|
||||
enc.SP().Mailbox(mailbox).SP().String(string(ri)).SP()
|
||||
enc.String(internal.FormatRights(rm, rs))
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
// SetACLCommand is a SETACL command.
|
||||
type SetACLCommand struct {
|
||||
commandBase
|
||||
}
|
||||
|
||||
func (cmd *SetACLCommand) Wait() error {
|
||||
return cmd.wait()
|
||||
}
|
||||
|
||||
// GetACL sends a GETACL command.
|
||||
//
|
||||
// This command requires support for the ACL extension.
|
||||
func (c *Client) GetACL(mailbox string) *GetACLCommand {
|
||||
cmd := &GetACLCommand{}
|
||||
enc := c.beginCommand("GETACL", cmd)
|
||||
enc.SP().Mailbox(mailbox)
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
// GetACLCommand is a GETACL command.
|
||||
type GetACLCommand struct {
|
||||
commandBase
|
||||
data GetACLData
|
||||
}
|
||||
|
||||
func (cmd *GetACLCommand) Wait() (*GetACLData, error) {
|
||||
return &cmd.data, cmd.wait()
|
||||
}
|
||||
|
||||
func (c *Client) handleMyRights() error {
|
||||
data, err := readMyRights(c.dec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("in myrights-response: %v", err)
|
||||
}
|
||||
if cmd := findPendingCmdByType[*MyRightsCommand](c); cmd != nil {
|
||||
cmd.data = *data
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) handleGetACL() error {
|
||||
data, err := readGetACL(c.dec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("in getacl-response: %v", err)
|
||||
}
|
||||
if cmd := findPendingCmdByType[*GetACLCommand](c); cmd != nil {
|
||||
cmd.data = *data
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MyRightsCommand is a MYRIGHTS command.
|
||||
type MyRightsCommand struct {
|
||||
commandBase
|
||||
data MyRightsData
|
||||
}
|
||||
|
||||
func (cmd *MyRightsCommand) Wait() (*MyRightsData, error) {
|
||||
return &cmd.data, cmd.wait()
|
||||
}
|
||||
|
||||
// MyRightsData is the data returned by the MYRIGHTS command.
|
||||
type MyRightsData struct {
|
||||
Mailbox string
|
||||
Rights imap.RightSet
|
||||
}
|
||||
|
||||
func readMyRights(dec *imapwire.Decoder) (*MyRightsData, error) {
|
||||
var (
|
||||
rights string
|
||||
data MyRightsData
|
||||
)
|
||||
if !dec.ExpectMailbox(&data.Mailbox) || !dec.ExpectSP() || !dec.ExpectAString(&rights) {
|
||||
return nil, dec.Err()
|
||||
}
|
||||
|
||||
data.Rights = imap.RightSet(rights)
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
// GetACLData is the data returned by the GETACL command.
|
||||
type GetACLData struct {
|
||||
Mailbox string
|
||||
Rights map[imap.RightsIdentifier]imap.RightSet
|
||||
}
|
||||
|
||||
func readGetACL(dec *imapwire.Decoder) (*GetACLData, error) {
|
||||
data := &GetACLData{Rights: make(map[imap.RightsIdentifier]imap.RightSet)}
|
||||
|
||||
if !dec.ExpectMailbox(&data.Mailbox) {
|
||||
return nil, dec.Err()
|
||||
}
|
||||
|
||||
for dec.SP() {
|
||||
var rsStr, riStr string
|
||||
if !dec.ExpectAString(&riStr) || !dec.ExpectSP() || !dec.ExpectAString(&rsStr) {
|
||||
return nil, dec.Err()
|
||||
}
|
||||
|
||||
data.Rights[imap.RightsIdentifier(riStr)] = imap.RightSet(rsStr)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
58
vendor/github.com/emersion/go-imap/v2/imapclient/append.go
generated
vendored
Normal file
58
vendor/github.com/emersion/go-imap/v2/imapclient/append.go
generated
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal"
|
||||
)
|
||||
|
||||
// Append sends an APPEND command.
|
||||
//
|
||||
// The caller must call AppendCommand.Close.
|
||||
//
|
||||
// The options are optional.
|
||||
func (c *Client) Append(mailbox string, size int64, options *imap.AppendOptions) *AppendCommand {
|
||||
cmd := &AppendCommand{}
|
||||
cmd.enc = c.beginCommand("APPEND", cmd)
|
||||
cmd.enc.SP().Mailbox(mailbox).SP()
|
||||
if options != nil && len(options.Flags) > 0 {
|
||||
cmd.enc.List(len(options.Flags), func(i int) {
|
||||
cmd.enc.Flag(options.Flags[i])
|
||||
}).SP()
|
||||
}
|
||||
if options != nil && !options.Time.IsZero() {
|
||||
cmd.enc.String(options.Time.Format(internal.DateTimeLayout)).SP()
|
||||
}
|
||||
// TODO: literal8 for BINARY
|
||||
// TODO: UTF8 data ext for UTF8=ACCEPT, with literal8
|
||||
cmd.wc = cmd.enc.Literal(size)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// AppendCommand is an APPEND command.
|
||||
//
|
||||
// Callers must write the message contents, then call Close.
|
||||
type AppendCommand struct {
|
||||
commandBase
|
||||
enc *commandEncoder
|
||||
wc io.WriteCloser
|
||||
data imap.AppendData
|
||||
}
|
||||
|
||||
func (cmd *AppendCommand) Write(b []byte) (int, error) {
|
||||
return cmd.wc.Write(b)
|
||||
}
|
||||
|
||||
func (cmd *AppendCommand) Close() error {
|
||||
err := cmd.wc.Close()
|
||||
if cmd.enc != nil {
|
||||
cmd.enc.end()
|
||||
cmd.enc = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (cmd *AppendCommand) Wait() (*imap.AppendData, error) {
|
||||
return &cmd.data, cmd.wait()
|
||||
}
|
||||
100
vendor/github.com/emersion/go-imap/v2/imapclient/authenticate.go
generated
vendored
Normal file
100
vendor/github.com/emersion/go-imap/v2/imapclient/authenticate.go
generated
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/emersion/go-sasl"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal"
|
||||
)
|
||||
|
||||
// Authenticate sends an AUTHENTICATE command.
|
||||
//
|
||||
// Unlike other commands, this method blocks until the SASL exchange completes.
|
||||
func (c *Client) Authenticate(saslClient sasl.Client) error {
|
||||
mech, initialResp, err := saslClient.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// c.Caps may send a CAPABILITY command, so check it before c.beginCommand
|
||||
var hasSASLIR bool
|
||||
if initialResp != nil {
|
||||
hasSASLIR = c.Caps().Has(imap.CapSASLIR)
|
||||
}
|
||||
|
||||
cmd := &authenticateCommand{}
|
||||
contReq := c.registerContReq(cmd)
|
||||
enc := c.beginCommand("AUTHENTICATE", cmd)
|
||||
enc.SP().Atom(mech)
|
||||
if initialResp != nil && hasSASLIR {
|
||||
enc.SP().Atom(internal.EncodeSASL(initialResp))
|
||||
initialResp = nil
|
||||
}
|
||||
enc.flush()
|
||||
defer enc.end()
|
||||
|
||||
for {
|
||||
challengeStr, err := contReq.Wait()
|
||||
if err != nil {
|
||||
return cmd.wait()
|
||||
}
|
||||
|
||||
if challengeStr == "" {
|
||||
if initialResp == nil {
|
||||
return fmt.Errorf("imapclient: server requested SASL initial response, but we don't have one")
|
||||
}
|
||||
|
||||
contReq = c.registerContReq(cmd)
|
||||
if err := c.writeSASLResp(initialResp); err != nil {
|
||||
return err
|
||||
}
|
||||
initialResp = nil
|
||||
continue
|
||||
}
|
||||
|
||||
challenge, err := internal.DecodeSASL(challengeStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := saslClient.Next(challenge)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contReq = c.registerContReq(cmd)
|
||||
if err := c.writeSASLResp(resp); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type authenticateCommand struct {
|
||||
commandBase
|
||||
}
|
||||
|
||||
func (c *Client) writeSASLResp(resp []byte) error {
|
||||
respStr := internal.EncodeSASL(resp)
|
||||
if _, err := c.bw.WriteString(respStr + "\r\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.bw.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unauthenticate sends an UNAUTHENTICATE command.
|
||||
//
|
||||
// This command requires support for the UNAUTHENTICATE extension.
|
||||
func (c *Client) Unauthenticate() *Command {
|
||||
cmd := &unauthenticateCommand{}
|
||||
c.beginCommand("UNAUTHENTICATE", cmd).end()
|
||||
return &cmd.Command
|
||||
}
|
||||
|
||||
type unauthenticateCommand struct {
|
||||
Command
|
||||
}
|
||||
56
vendor/github.com/emersion/go-imap/v2/imapclient/capability.go
generated
vendored
Normal file
56
vendor/github.com/emersion/go-imap/v2/imapclient/capability.go
generated
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
// Capability sends a CAPABILITY command.
|
||||
func (c *Client) Capability() *CapabilityCommand {
|
||||
cmd := &CapabilityCommand{}
|
||||
c.beginCommand("CAPABILITY", cmd).end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c *Client) handleCapability() error {
|
||||
caps, err := readCapabilities(c.dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.setCaps(caps)
|
||||
if cmd := findPendingCmdByType[*CapabilityCommand](c); cmd != nil {
|
||||
cmd.caps = caps
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CapabilityCommand is a CAPABILITY command.
|
||||
type CapabilityCommand struct {
|
||||
commandBase
|
||||
caps imap.CapSet
|
||||
}
|
||||
|
||||
func (cmd *CapabilityCommand) Wait() (imap.CapSet, error) {
|
||||
err := cmd.wait()
|
||||
return cmd.caps, err
|
||||
}
|
||||
|
||||
func readCapabilities(dec *imapwire.Decoder) (imap.CapSet, error) {
|
||||
caps := make(imap.CapSet)
|
||||
for dec.SP() {
|
||||
// Some IMAP servers send multiple SP between caps:
|
||||
// https://github.com/emersion/go-imap/pull/652
|
||||
for dec.SP() {
|
||||
}
|
||||
|
||||
cap, err := internal.ExpectCap(dec)
|
||||
if err != nil {
|
||||
return caps, fmt.Errorf("in capability-data: %w", err)
|
||||
}
|
||||
caps[cap] = struct{}{}
|
||||
}
|
||||
return caps, nil
|
||||
}
|
||||
1234
vendor/github.com/emersion/go-imap/v2/imapclient/client.go
generated
vendored
Normal file
1234
vendor/github.com/emersion/go-imap/v2/imapclient/client.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
37
vendor/github.com/emersion/go-imap/v2/imapclient/copy.go
generated
vendored
Normal file
37
vendor/github.com/emersion/go-imap/v2/imapclient/copy.go
generated
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
// Copy sends a COPY command.
|
||||
func (c *Client) Copy(numSet imap.NumSet, mailbox string) *CopyCommand {
|
||||
cmd := &CopyCommand{}
|
||||
enc := c.beginCommand(uidCmdName("COPY", imapwire.NumSetKind(numSet)), cmd)
|
||||
enc.SP().NumSet(numSet).SP().Mailbox(mailbox)
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
// CopyCommand is a COPY command.
|
||||
type CopyCommand struct {
|
||||
commandBase
|
||||
data imap.CopyData
|
||||
}
|
||||
|
||||
func (cmd *CopyCommand) Wait() (*imap.CopyData, error) {
|
||||
return &cmd.data, cmd.wait()
|
||||
}
|
||||
|
||||
func readRespCodeCopyUID(dec *imapwire.Decoder) (uidValidity uint32, srcUIDs, dstUIDs imap.UIDSet, err error) {
|
||||
if !dec.ExpectNumber(&uidValidity) || !dec.ExpectSP() || !dec.ExpectUIDSet(&srcUIDs) || !dec.ExpectSP() || !dec.ExpectUIDSet(&dstUIDs) {
|
||||
return 0, nil, nil, dec.Err()
|
||||
}
|
||||
if srcUIDs.Dynamic() || dstUIDs.Dynamic() {
|
||||
return 0, nil, nil, fmt.Errorf("imapclient: server returned dynamic number set in COPYUID response")
|
||||
}
|
||||
return uidValidity, srcUIDs, dstUIDs, nil
|
||||
}
|
||||
21
vendor/github.com/emersion/go-imap/v2/imapclient/create.go
generated
vendored
Normal file
21
vendor/github.com/emersion/go-imap/v2/imapclient/create.go
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-imap/v2"
|
||||
)
|
||||
|
||||
// Create sends a CREATE command.
|
||||
//
|
||||
// A nil options pointer is equivalent to a zero options value.
|
||||
func (c *Client) Create(mailbox string, options *imap.CreateOptions) *Command {
|
||||
cmd := &Command{}
|
||||
enc := c.beginCommand("CREATE", cmd)
|
||||
enc.SP().Mailbox(mailbox)
|
||||
if options != nil && len(options.SpecialUse) > 0 {
|
||||
enc.SP().Special('(').Atom("USE").SP().List(len(options.SpecialUse), func(i int) {
|
||||
enc.MailboxAttr(options.SpecialUse[i])
|
||||
}).Special(')')
|
||||
}
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
69
vendor/github.com/emersion/go-imap/v2/imapclient/enable.go
generated
vendored
Normal file
69
vendor/github.com/emersion/go-imap/v2/imapclient/enable.go
generated
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
)
|
||||
|
||||
// Enable sends an ENABLE command.
|
||||
//
|
||||
// This command requires support for IMAP4rev2 or the ENABLE extension.
|
||||
func (c *Client) Enable(caps ...imap.Cap) *EnableCommand {
|
||||
// Enabling an extension may change the IMAP syntax, so only allow the
|
||||
// extensions we support here
|
||||
for _, name := range caps {
|
||||
switch name {
|
||||
case imap.CapIMAP4rev2, imap.CapUTF8Accept, imap.CapMetadata, imap.CapMetadataServer:
|
||||
// ok
|
||||
default:
|
||||
done := make(chan error)
|
||||
close(done)
|
||||
err := fmt.Errorf("imapclient: cannot enable %q: not supported", name)
|
||||
return &EnableCommand{commandBase: commandBase{done: done, err: err}}
|
||||
}
|
||||
}
|
||||
|
||||
cmd := &EnableCommand{}
|
||||
enc := c.beginCommand("ENABLE", cmd)
|
||||
for _, c := range caps {
|
||||
enc.SP().Atom(string(c))
|
||||
}
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c *Client) handleEnabled() error {
|
||||
caps, err := readCapabilities(c.dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.mutex.Lock()
|
||||
for name := range caps {
|
||||
c.enabled[name] = struct{}{}
|
||||
}
|
||||
c.mutex.Unlock()
|
||||
|
||||
if cmd := findPendingCmdByType[*EnableCommand](c); cmd != nil {
|
||||
cmd.data.Caps = caps
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnableCommand is an ENABLE command.
|
||||
type EnableCommand struct {
|
||||
commandBase
|
||||
data EnableData
|
||||
}
|
||||
|
||||
func (cmd *EnableCommand) Wait() (*EnableData, error) {
|
||||
return &cmd.data, cmd.wait()
|
||||
}
|
||||
|
||||
// EnableData is the data returned by the ENABLE command.
|
||||
type EnableData struct {
|
||||
// Capabilities that were successfully enabled
|
||||
Caps imap.CapSet
|
||||
}
|
||||
84
vendor/github.com/emersion/go-imap/v2/imapclient/expunge.go
generated
vendored
Normal file
84
vendor/github.com/emersion/go-imap/v2/imapclient/expunge.go
generated
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-imap/v2"
|
||||
)
|
||||
|
||||
// Expunge sends an EXPUNGE command.
|
||||
func (c *Client) Expunge() *ExpungeCommand {
|
||||
cmd := &ExpungeCommand{seqNums: make(chan uint32, 128)}
|
||||
c.beginCommand("EXPUNGE", cmd).end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
// UIDExpunge sends a UID EXPUNGE command.
|
||||
//
|
||||
// This command requires support for IMAP4rev2 or the UIDPLUS extension.
|
||||
func (c *Client) UIDExpunge(uids imap.UIDSet) *ExpungeCommand {
|
||||
cmd := &ExpungeCommand{seqNums: make(chan uint32, 128)}
|
||||
enc := c.beginCommand("UID EXPUNGE", cmd)
|
||||
enc.SP().NumSet(uids)
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c *Client) handleExpunge(seqNum uint32) error {
|
||||
c.mutex.Lock()
|
||||
if c.state == imap.ConnStateSelected && c.mailbox.NumMessages > 0 {
|
||||
c.mailbox = c.mailbox.copy()
|
||||
c.mailbox.NumMessages--
|
||||
}
|
||||
c.mutex.Unlock()
|
||||
|
||||
cmd := findPendingCmdByType[*ExpungeCommand](c)
|
||||
if cmd != nil {
|
||||
cmd.seqNums <- seqNum
|
||||
} else if handler := c.options.unilateralDataHandler().Expunge; handler != nil {
|
||||
handler(seqNum)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExpungeCommand is an EXPUNGE command.
|
||||
//
|
||||
// The caller must fully consume the ExpungeCommand. A simple way to do so is
|
||||
// to defer a call to FetchCommand.Close.
|
||||
type ExpungeCommand struct {
|
||||
commandBase
|
||||
seqNums chan uint32
|
||||
}
|
||||
|
||||
// Next advances to the next expunged message sequence number.
|
||||
//
|
||||
// On success, the message sequence number is returned. On error or if there
|
||||
// are no more messages, 0 is returned. To check the error value, use Close.
|
||||
func (cmd *ExpungeCommand) Next() uint32 {
|
||||
return <-cmd.seqNums
|
||||
}
|
||||
|
||||
// Close releases the command.
|
||||
//
|
||||
// Calling Close unblocks the IMAP client decoder and lets it read the next
|
||||
// responses. Next will always return nil after Close.
|
||||
func (cmd *ExpungeCommand) Close() error {
|
||||
for cmd.Next() != 0 {
|
||||
// ignore
|
||||
}
|
||||
return cmd.wait()
|
||||
}
|
||||
|
||||
// Collect accumulates expunged sequence numbers into a list.
|
||||
//
|
||||
// This is equivalent to calling Next repeatedly and then Close.
|
||||
func (cmd *ExpungeCommand) Collect() ([]uint32, error) {
|
||||
var l []uint32
|
||||
for {
|
||||
seqNum := cmd.Next()
|
||||
if seqNum == 0 {
|
||||
break
|
||||
}
|
||||
l = append(l, seqNum)
|
||||
}
|
||||
return l, cmd.Close()
|
||||
}
|
||||
1326
vendor/github.com/emersion/go-imap/v2/imapclient/fetch.go
generated
vendored
Normal file
1326
vendor/github.com/emersion/go-imap/v2/imapclient/fetch.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
163
vendor/github.com/emersion/go-imap/v2/imapclient/id.go
generated
vendored
Normal file
163
vendor/github.com/emersion/go-imap/v2/imapclient/id.go
generated
vendored
Normal file
@@ -0,0 +1,163 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
// ID sends an ID command.
|
||||
//
|
||||
// The ID command is introduced in RFC 2971. It requires support for the ID
|
||||
// extension.
|
||||
//
|
||||
// An example ID command:
|
||||
//
|
||||
// ID ("name" "go-imap" "version" "1.0" "os" "Linux" "os-version" "7.9.4" "vendor" "Yahoo")
|
||||
func (c *Client) ID(idData *imap.IDData) *IDCommand {
|
||||
cmd := &IDCommand{}
|
||||
enc := c.beginCommand("ID", cmd)
|
||||
|
||||
if idData == nil {
|
||||
enc.SP().NIL()
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
enc.SP().Special('(')
|
||||
isFirstKey := true
|
||||
if idData.Name != "" {
|
||||
addIDKeyValue(enc, &isFirstKey, "name", idData.Name)
|
||||
}
|
||||
if idData.Version != "" {
|
||||
addIDKeyValue(enc, &isFirstKey, "version", idData.Version)
|
||||
}
|
||||
if idData.OS != "" {
|
||||
addIDKeyValue(enc, &isFirstKey, "os", idData.OS)
|
||||
}
|
||||
if idData.OSVersion != "" {
|
||||
addIDKeyValue(enc, &isFirstKey, "os-version", idData.OSVersion)
|
||||
}
|
||||
if idData.Vendor != "" {
|
||||
addIDKeyValue(enc, &isFirstKey, "vendor", idData.Vendor)
|
||||
}
|
||||
if idData.SupportURL != "" {
|
||||
addIDKeyValue(enc, &isFirstKey, "support-url", idData.SupportURL)
|
||||
}
|
||||
if idData.Address != "" {
|
||||
addIDKeyValue(enc, &isFirstKey, "address", idData.Address)
|
||||
}
|
||||
if idData.Date != "" {
|
||||
addIDKeyValue(enc, &isFirstKey, "date", idData.Date)
|
||||
}
|
||||
if idData.Command != "" {
|
||||
addIDKeyValue(enc, &isFirstKey, "command", idData.Command)
|
||||
}
|
||||
if idData.Arguments != "" {
|
||||
addIDKeyValue(enc, &isFirstKey, "arguments", idData.Arguments)
|
||||
}
|
||||
if idData.Environment != "" {
|
||||
addIDKeyValue(enc, &isFirstKey, "environment", idData.Environment)
|
||||
}
|
||||
|
||||
enc.Special(')')
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
func addIDKeyValue(enc *commandEncoder, isFirstKey *bool, key, value string) {
|
||||
if isFirstKey == nil {
|
||||
panic("isFirstKey cannot be nil")
|
||||
} else if !*isFirstKey {
|
||||
enc.SP().Quoted(key).SP().Quoted(value)
|
||||
} else {
|
||||
enc.Quoted(key).SP().Quoted(value)
|
||||
}
|
||||
*isFirstKey = false
|
||||
}
|
||||
|
||||
func (c *Client) handleID() error {
|
||||
data, err := c.readID(c.dec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("in id: %v", err)
|
||||
}
|
||||
|
||||
if cmd := findPendingCmdByType[*IDCommand](c); cmd != nil {
|
||||
cmd.data = *data
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) readID(dec *imapwire.Decoder) (*imap.IDData, error) {
|
||||
var data = imap.IDData{}
|
||||
|
||||
if !dec.ExpectSP() {
|
||||
return nil, dec.Err()
|
||||
}
|
||||
|
||||
if dec.ExpectNIL() {
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
currKey := ""
|
||||
err := dec.ExpectList(func() error {
|
||||
var keyOrValue string
|
||||
if !dec.String(&keyOrValue) {
|
||||
return fmt.Errorf("in id key-val list: %v", dec.Err())
|
||||
}
|
||||
|
||||
if currKey == "" {
|
||||
currKey = keyOrValue
|
||||
return nil
|
||||
}
|
||||
|
||||
switch currKey {
|
||||
case "name":
|
||||
data.Name = keyOrValue
|
||||
case "version":
|
||||
data.Version = keyOrValue
|
||||
case "os":
|
||||
data.OS = keyOrValue
|
||||
case "os-version":
|
||||
data.OSVersion = keyOrValue
|
||||
case "vendor":
|
||||
data.Vendor = keyOrValue
|
||||
case "support-url":
|
||||
data.SupportURL = keyOrValue
|
||||
case "address":
|
||||
data.Address = keyOrValue
|
||||
case "date":
|
||||
data.Date = keyOrValue
|
||||
case "command":
|
||||
data.Command = keyOrValue
|
||||
case "arguments":
|
||||
data.Arguments = keyOrValue
|
||||
case "environment":
|
||||
data.Environment = keyOrValue
|
||||
default:
|
||||
// Ignore unknown key
|
||||
// Yahoo server sends "host" and "remote-host" keys
|
||||
// which are not defined in RFC 2971
|
||||
}
|
||||
currKey = ""
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
type IDCommand struct {
|
||||
commandBase
|
||||
data imap.IDData
|
||||
}
|
||||
|
||||
func (r *IDCommand) Wait() (*imap.IDData, error) {
|
||||
return &r.data, r.wait()
|
||||
}
|
||||
157
vendor/github.com/emersion/go-imap/v2/imapclient/idle.go
generated
vendored
Normal file
157
vendor/github.com/emersion/go-imap/v2/imapclient/idle.go
generated
vendored
Normal file
@@ -0,0 +1,157 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const idleRestartInterval = 28 * time.Minute
|
||||
|
||||
// Idle sends an IDLE command.
|
||||
//
|
||||
// Unlike other commands, this method blocks until the server acknowledges it.
|
||||
// On success, the IDLE command is running and other commands cannot be sent.
|
||||
// The caller must invoke IdleCommand.Close to stop IDLE and unblock the
|
||||
// client.
|
||||
//
|
||||
// This command requires support for IMAP4rev2 or the IDLE extension. The IDLE
|
||||
// command is restarted automatically to avoid getting disconnected due to
|
||||
// inactivity timeouts.
|
||||
func (c *Client) Idle() (*IdleCommand, error) {
|
||||
child, err := c.idle()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmd := &IdleCommand{
|
||||
stop: make(chan struct{}),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
go cmd.run(c, child)
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
// IdleCommand is an IDLE command.
|
||||
//
|
||||
// Initially, the IDLE command is running. The server may send unilateral
|
||||
// data. The client cannot send any command while IDLE is running.
|
||||
//
|
||||
// Close must be called to stop the IDLE command.
|
||||
type IdleCommand struct {
|
||||
stopped atomic.Bool
|
||||
stop chan struct{}
|
||||
done chan struct{}
|
||||
|
||||
err error
|
||||
lastChild *idleCommand
|
||||
}
|
||||
|
||||
func (cmd *IdleCommand) run(c *Client, child *idleCommand) {
|
||||
defer close(cmd.done)
|
||||
|
||||
timer := time.NewTimer(idleRestartInterval)
|
||||
defer timer.Stop()
|
||||
|
||||
defer func() {
|
||||
if child != nil {
|
||||
if err := child.Close(); err != nil && cmd.err == nil {
|
||||
cmd.err = err
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
timer.Reset(idleRestartInterval)
|
||||
|
||||
if cmd.err = child.Close(); cmd.err != nil {
|
||||
return
|
||||
}
|
||||
if child, cmd.err = c.idle(); cmd.err != nil {
|
||||
return
|
||||
}
|
||||
case <-c.decCh:
|
||||
cmd.lastChild = child
|
||||
return
|
||||
case <-cmd.stop:
|
||||
cmd.lastChild = child
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close stops the IDLE command.
|
||||
//
|
||||
// This method blocks until the command to stop IDLE is written, but doesn't
|
||||
// wait for the server to respond. Callers can use Wait for this purpose.
|
||||
func (cmd *IdleCommand) Close() error {
|
||||
if cmd.stopped.Swap(true) {
|
||||
return fmt.Errorf("imapclient: IDLE already closed")
|
||||
}
|
||||
close(cmd.stop)
|
||||
<-cmd.done
|
||||
return cmd.err
|
||||
}
|
||||
|
||||
// Wait blocks until the IDLE command has completed.
|
||||
func (cmd *IdleCommand) Wait() error {
|
||||
<-cmd.done
|
||||
if cmd.err != nil {
|
||||
return cmd.err
|
||||
}
|
||||
return cmd.lastChild.Wait()
|
||||
}
|
||||
|
||||
func (c *Client) idle() (*idleCommand, error) {
|
||||
cmd := &idleCommand{}
|
||||
contReq := c.registerContReq(cmd)
|
||||
cmd.enc = c.beginCommand("IDLE", cmd)
|
||||
cmd.enc.flush()
|
||||
|
||||
_, err := contReq.Wait()
|
||||
if err != nil {
|
||||
cmd.enc.end()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
// idleCommand represents a singular IDLE command, without the restart logic.
|
||||
type idleCommand struct {
|
||||
commandBase
|
||||
enc *commandEncoder
|
||||
}
|
||||
|
||||
// Close stops the IDLE command.
|
||||
//
|
||||
// This method blocks until the command to stop IDLE is written, but doesn't
|
||||
// wait for the server to respond. Callers can use Wait for this purpose.
|
||||
func (cmd *idleCommand) Close() error {
|
||||
if cmd.err != nil {
|
||||
return cmd.err
|
||||
}
|
||||
if cmd.enc == nil {
|
||||
return fmt.Errorf("imapclient: IDLE command closed twice")
|
||||
}
|
||||
cmd.enc.client.setWriteTimeout(cmdWriteTimeout)
|
||||
_, err := cmd.enc.client.bw.WriteString("DONE\r\n")
|
||||
if err == nil {
|
||||
err = cmd.enc.client.bw.Flush()
|
||||
}
|
||||
cmd.enc.end()
|
||||
cmd.enc = nil
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait blocks until the IDLE command has completed.
|
||||
//
|
||||
// Wait can only be called after Close.
|
||||
func (cmd *idleCommand) Wait() error {
|
||||
if cmd.enc != nil {
|
||||
panic("imapclient: idleCommand.Close must be called before Wait")
|
||||
}
|
||||
return cmd.wait()
|
||||
}
|
||||
259
vendor/github.com/emersion/go-imap/v2/imapclient/list.go
generated
vendored
Normal file
259
vendor/github.com/emersion/go-imap/v2/imapclient/list.go
generated
vendored
Normal file
@@ -0,0 +1,259 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
func getSelectOpts(options *imap.ListOptions) []string {
|
||||
if options == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var l []string
|
||||
if options.SelectSubscribed {
|
||||
l = append(l, "SUBSCRIBED")
|
||||
}
|
||||
if options.SelectRemote {
|
||||
l = append(l, "REMOTE")
|
||||
}
|
||||
if options.SelectRecursiveMatch {
|
||||
l = append(l, "RECURSIVEMATCH")
|
||||
}
|
||||
if options.SelectSpecialUse {
|
||||
l = append(l, "SPECIAL-USE")
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
func getReturnOpts(options *imap.ListOptions) []string {
|
||||
if options == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var l []string
|
||||
if options.ReturnSubscribed {
|
||||
l = append(l, "SUBSCRIBED")
|
||||
}
|
||||
if options.ReturnChildren {
|
||||
l = append(l, "CHILDREN")
|
||||
}
|
||||
if options.ReturnStatus != nil {
|
||||
l = append(l, "STATUS")
|
||||
}
|
||||
if options.ReturnSpecialUse {
|
||||
l = append(l, "SPECIAL-USE")
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// List sends a LIST command.
|
||||
//
|
||||
// The caller must fully consume the ListCommand. A simple way to do so is to
|
||||
// defer a call to ListCommand.Close.
|
||||
//
|
||||
// A nil options pointer is equivalent to a zero options value.
|
||||
//
|
||||
// A non-zero options value requires support for IMAP4rev2 or the LIST-EXTENDED
|
||||
// extension.
|
||||
func (c *Client) List(ref, pattern string, options *imap.ListOptions) *ListCommand {
|
||||
cmd := &ListCommand{
|
||||
mailboxes: make(chan *imap.ListData, 64),
|
||||
returnStatus: options != nil && options.ReturnStatus != nil,
|
||||
}
|
||||
enc := c.beginCommand("LIST", cmd)
|
||||
if selectOpts := getSelectOpts(options); len(selectOpts) > 0 {
|
||||
enc.SP().List(len(selectOpts), func(i int) {
|
||||
enc.Atom(selectOpts[i])
|
||||
})
|
||||
}
|
||||
enc.SP().Mailbox(ref).SP().Mailbox(pattern)
|
||||
if returnOpts := getReturnOpts(options); len(returnOpts) > 0 {
|
||||
enc.SP().Atom("RETURN").SP().List(len(returnOpts), func(i int) {
|
||||
opt := returnOpts[i]
|
||||
enc.Atom(opt)
|
||||
if opt == "STATUS" {
|
||||
returnStatus := statusItems(options.ReturnStatus)
|
||||
enc.SP().List(len(returnStatus), func(j int) {
|
||||
enc.Atom(returnStatus[j])
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c *Client) handleList() error {
|
||||
data, err := readList(c.dec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("in LIST: %v", err)
|
||||
}
|
||||
|
||||
cmd := c.findPendingCmdFunc(func(cmd command) bool {
|
||||
switch cmd := cmd.(type) {
|
||||
case *ListCommand:
|
||||
return true // TODO: match pattern, check if already handled
|
||||
case *SelectCommand:
|
||||
return cmd.mailbox == data.Mailbox && cmd.data.List == nil
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})
|
||||
switch cmd := cmd.(type) {
|
||||
case *ListCommand:
|
||||
if cmd.returnStatus {
|
||||
if cmd.pendingData != nil {
|
||||
cmd.mailboxes <- cmd.pendingData
|
||||
}
|
||||
cmd.pendingData = data
|
||||
} else {
|
||||
cmd.mailboxes <- data
|
||||
}
|
||||
case *SelectCommand:
|
||||
cmd.data.List = data
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListCommand is a LIST command.
|
||||
type ListCommand struct {
|
||||
commandBase
|
||||
mailboxes chan *imap.ListData
|
||||
|
||||
returnStatus bool
|
||||
pendingData *imap.ListData
|
||||
}
|
||||
|
||||
// Next advances to the next mailbox.
|
||||
//
|
||||
// On success, the mailbox LIST data is returned. On error or if there are no
|
||||
// more mailboxes, nil is returned.
|
||||
func (cmd *ListCommand) Next() *imap.ListData {
|
||||
return <-cmd.mailboxes
|
||||
}
|
||||
|
||||
// Close releases the command.
|
||||
//
|
||||
// Calling Close unblocks the IMAP client decoder and lets it read the next
|
||||
// responses. Next will always return nil after Close.
|
||||
func (cmd *ListCommand) Close() error {
|
||||
for cmd.Next() != nil {
|
||||
// ignore
|
||||
}
|
||||
return cmd.wait()
|
||||
}
|
||||
|
||||
// Collect accumulates mailboxes into a list.
|
||||
//
|
||||
// This is equivalent to calling Next repeatedly and then Close.
|
||||
func (cmd *ListCommand) Collect() ([]*imap.ListData, error) {
|
||||
var l []*imap.ListData
|
||||
for {
|
||||
data := cmd.Next()
|
||||
if data == nil {
|
||||
break
|
||||
}
|
||||
l = append(l, data)
|
||||
}
|
||||
return l, cmd.Close()
|
||||
}
|
||||
|
||||
func readList(dec *imapwire.Decoder) (*imap.ListData, error) {
|
||||
var data imap.ListData
|
||||
|
||||
var err error
|
||||
data.Attrs, err = internal.ExpectMailboxAttrList(dec)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("in mbx-list-flags: %w", err)
|
||||
}
|
||||
|
||||
if !dec.ExpectSP() {
|
||||
return nil, dec.Err()
|
||||
}
|
||||
|
||||
data.Delim, err = readDelim(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !dec.ExpectSP() || !dec.ExpectMailbox(&data.Mailbox) {
|
||||
return nil, dec.Err()
|
||||
}
|
||||
|
||||
if dec.SP() {
|
||||
err := dec.ExpectList(func() error {
|
||||
var tag string
|
||||
if !dec.ExpectAString(&tag) || !dec.ExpectSP() {
|
||||
return dec.Err()
|
||||
}
|
||||
var err error
|
||||
switch strings.ToUpper(tag) {
|
||||
case "CHILDINFO":
|
||||
data.ChildInfo, err = readChildInfoExtendedItem(dec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("in childinfo-extended-item: %v", err)
|
||||
}
|
||||
case "OLDNAME":
|
||||
data.OldName, err = readOldNameExtendedItem(dec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("in oldname-extended-item: %v", err)
|
||||
}
|
||||
default:
|
||||
if !dec.DiscardValue() {
|
||||
return fmt.Errorf("in tagged-ext-val: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("in mbox-list-extended: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
func readChildInfoExtendedItem(dec *imapwire.Decoder) (*imap.ListDataChildInfo, error) {
|
||||
var childInfo imap.ListDataChildInfo
|
||||
err := dec.ExpectList(func() error {
|
||||
var opt string
|
||||
if !dec.ExpectAString(&opt) {
|
||||
return dec.Err()
|
||||
}
|
||||
if strings.ToUpper(opt) == "SUBSCRIBED" {
|
||||
childInfo.Subscribed = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return &childInfo, err
|
||||
}
|
||||
|
||||
func readOldNameExtendedItem(dec *imapwire.Decoder) (string, error) {
|
||||
var name string
|
||||
if !dec.ExpectSpecial('(') || !dec.ExpectMailbox(&name) || !dec.ExpectSpecial(')') {
|
||||
return "", dec.Err()
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
||||
func readDelim(dec *imapwire.Decoder) (rune, error) {
|
||||
var delimStr string
|
||||
if dec.Quoted(&delimStr) {
|
||||
delim, size := utf8.DecodeRuneInString(delimStr)
|
||||
if delim == utf8.RuneError || size != len(delimStr) {
|
||||
return 0, fmt.Errorf("mailbox delimiter must be a single rune")
|
||||
}
|
||||
return delim, nil
|
||||
} else if !dec.ExpectNIL() {
|
||||
return 0, dec.Err()
|
||||
} else {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
205
vendor/github.com/emersion/go-imap/v2/imapclient/metadata.go
generated
vendored
Normal file
205
vendor/github.com/emersion/go-imap/v2/imapclient/metadata.go
generated
vendored
Normal file
@@ -0,0 +1,205 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
type GetMetadataDepth int
|
||||
|
||||
const (
|
||||
GetMetadataDepthZero GetMetadataDepth = 0
|
||||
GetMetadataDepthOne GetMetadataDepth = 1
|
||||
GetMetadataDepthInfinity GetMetadataDepth = -1
|
||||
)
|
||||
|
||||
func (depth GetMetadataDepth) String() string {
|
||||
switch depth {
|
||||
case GetMetadataDepthZero:
|
||||
return "0"
|
||||
case GetMetadataDepthOne:
|
||||
return "1"
|
||||
case GetMetadataDepthInfinity:
|
||||
return "infinity"
|
||||
default:
|
||||
panic(fmt.Errorf("imapclient: unknown GETMETADATA depth %d", depth))
|
||||
}
|
||||
}
|
||||
|
||||
// GetMetadataOptions contains options for the GETMETADATA command.
|
||||
type GetMetadataOptions struct {
|
||||
MaxSize *uint32
|
||||
Depth GetMetadataDepth
|
||||
}
|
||||
|
||||
func (options *GetMetadataOptions) names() []string {
|
||||
if options == nil {
|
||||
return nil
|
||||
}
|
||||
var l []string
|
||||
if options.MaxSize != nil {
|
||||
l = append(l, "MAXSIZE")
|
||||
}
|
||||
if options.Depth != GetMetadataDepthZero {
|
||||
l = append(l, "DEPTH")
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// GetMetadata sends a GETMETADATA command.
|
||||
//
|
||||
// This command requires support for the METADATA or METADATA-SERVER extension.
|
||||
func (c *Client) GetMetadata(mailbox string, entries []string, options *GetMetadataOptions) *GetMetadataCommand {
|
||||
cmd := &GetMetadataCommand{mailbox: mailbox}
|
||||
enc := c.beginCommand("GETMETADATA", cmd)
|
||||
enc.SP().Mailbox(mailbox)
|
||||
if opts := options.names(); len(opts) > 0 {
|
||||
enc.SP().List(len(opts), func(i int) {
|
||||
opt := opts[i]
|
||||
enc.Atom(opt).SP()
|
||||
switch opt {
|
||||
case "MAXSIZE":
|
||||
enc.Number(*options.MaxSize)
|
||||
case "DEPTH":
|
||||
enc.Atom(options.Depth.String())
|
||||
default:
|
||||
panic(fmt.Errorf("imapclient: unknown GETMETADATA option %q", opt))
|
||||
}
|
||||
})
|
||||
}
|
||||
enc.SP().List(len(entries), func(i int) {
|
||||
enc.String(entries[i])
|
||||
})
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
// SetMetadata sends a SETMETADATA command.
|
||||
//
|
||||
// To remove an entry, set it to nil.
|
||||
//
|
||||
// This command requires support for the METADATA or METADATA-SERVER extension.
|
||||
func (c *Client) SetMetadata(mailbox string, entries map[string]*[]byte) *Command {
|
||||
cmd := &Command{}
|
||||
enc := c.beginCommand("SETMETADATA", cmd)
|
||||
enc.SP().Mailbox(mailbox).SP().Special('(')
|
||||
i := 0
|
||||
for k, v := range entries {
|
||||
if i > 0 {
|
||||
enc.SP()
|
||||
}
|
||||
enc.String(k).SP()
|
||||
if v == nil {
|
||||
enc.NIL()
|
||||
} else {
|
||||
enc.String(string(*v)) // TODO: use literals if required
|
||||
}
|
||||
i++
|
||||
}
|
||||
enc.Special(')')
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c *Client) handleMetadata() error {
|
||||
data, err := readMetadataResp(c.dec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("in metadata-resp: %v", err)
|
||||
}
|
||||
|
||||
cmd := c.findPendingCmdFunc(func(anyCmd command) bool {
|
||||
cmd, ok := anyCmd.(*GetMetadataCommand)
|
||||
return ok && cmd.mailbox == data.Mailbox
|
||||
})
|
||||
if cmd != nil && len(data.EntryValues) > 0 {
|
||||
cmd := cmd.(*GetMetadataCommand)
|
||||
cmd.data.Mailbox = data.Mailbox
|
||||
if cmd.data.Entries == nil {
|
||||
cmd.data.Entries = make(map[string]*[]byte)
|
||||
}
|
||||
// The server might send multiple METADATA responses for a single
|
||||
// METADATA command
|
||||
for k, v := range data.EntryValues {
|
||||
cmd.data.Entries[k] = v
|
||||
}
|
||||
} else if handler := c.options.unilateralDataHandler().Metadata; handler != nil && len(data.EntryList) > 0 {
|
||||
handler(data.Mailbox, data.EntryList)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMetadataCommand is a GETMETADATA command.
|
||||
type GetMetadataCommand struct {
|
||||
commandBase
|
||||
mailbox string
|
||||
data GetMetadataData
|
||||
}
|
||||
|
||||
func (cmd *GetMetadataCommand) Wait() (*GetMetadataData, error) {
|
||||
return &cmd.data, cmd.wait()
|
||||
}
|
||||
|
||||
// GetMetadataData is the data returned by the GETMETADATA command.
|
||||
type GetMetadataData struct {
|
||||
Mailbox string
|
||||
Entries map[string]*[]byte
|
||||
}
|
||||
|
||||
type metadataResp struct {
|
||||
Mailbox string
|
||||
EntryList []string
|
||||
EntryValues map[string]*[]byte
|
||||
}
|
||||
|
||||
func readMetadataResp(dec *imapwire.Decoder) (*metadataResp, error) {
|
||||
var data metadataResp
|
||||
|
||||
if !dec.ExpectMailbox(&data.Mailbox) || !dec.ExpectSP() {
|
||||
return nil, dec.Err()
|
||||
}
|
||||
|
||||
isList, err := dec.List(func() error {
|
||||
var name string
|
||||
if !dec.ExpectAString(&name) || !dec.ExpectSP() {
|
||||
return dec.Err()
|
||||
}
|
||||
|
||||
// TODO: decode as []byte
|
||||
var (
|
||||
value *[]byte
|
||||
s string
|
||||
)
|
||||
if dec.String(&s) || dec.Literal(&s) {
|
||||
b := []byte(s)
|
||||
value = &b
|
||||
} else if !dec.ExpectNIL() {
|
||||
return dec.Err()
|
||||
}
|
||||
|
||||
if data.EntryValues == nil {
|
||||
data.EntryValues = make(map[string]*[]byte)
|
||||
}
|
||||
data.EntryValues[name] = value
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !isList {
|
||||
var name string
|
||||
if !dec.ExpectAString(&name) {
|
||||
return nil, dec.Err()
|
||||
}
|
||||
data.EntryList = append(data.EntryList, name)
|
||||
|
||||
for dec.SP() {
|
||||
if !dec.ExpectAString(&name) {
|
||||
return nil, dec.Err()
|
||||
}
|
||||
data.EntryList = append(data.EntryList, name)
|
||||
}
|
||||
}
|
||||
|
||||
return &data, nil
|
||||
}
|
||||
74
vendor/github.com/emersion/go-imap/v2/imapclient/move.go
generated
vendored
Normal file
74
vendor/github.com/emersion/go-imap/v2/imapclient/move.go
generated
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
// Move sends a MOVE command.
|
||||
//
|
||||
// If the server doesn't support IMAP4rev2 nor the MOVE extension, a fallback
|
||||
// with COPY + STORE + EXPUNGE commands is used.
|
||||
func (c *Client) Move(numSet imap.NumSet, mailbox string) *MoveCommand {
|
||||
// If the server doesn't support MOVE, fallback to [UID] COPY,
|
||||
// [UID] STORE +FLAGS.SILENT \Deleted and [UID] EXPUNGE
|
||||
cmdName := "MOVE"
|
||||
if !c.Caps().Has(imap.CapMove) {
|
||||
cmdName = "COPY"
|
||||
}
|
||||
|
||||
cmd := &MoveCommand{}
|
||||
enc := c.beginCommand(uidCmdName(cmdName, imapwire.NumSetKind(numSet)), cmd)
|
||||
enc.SP().NumSet(numSet).SP().Mailbox(mailbox)
|
||||
enc.end()
|
||||
|
||||
if cmdName == "COPY" {
|
||||
cmd.store = c.Store(numSet, &imap.StoreFlags{
|
||||
Op: imap.StoreFlagsAdd,
|
||||
Silent: true,
|
||||
Flags: []imap.Flag{imap.FlagDeleted},
|
||||
}, nil)
|
||||
if uidSet, ok := numSet.(imap.UIDSet); ok && c.Caps().Has(imap.CapUIDPlus) {
|
||||
cmd.expunge = c.UIDExpunge(uidSet)
|
||||
} else {
|
||||
cmd.expunge = c.Expunge()
|
||||
}
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// MoveCommand is a MOVE command.
|
||||
type MoveCommand struct {
|
||||
commandBase
|
||||
data MoveData
|
||||
|
||||
// Fallback
|
||||
store *FetchCommand
|
||||
expunge *ExpungeCommand
|
||||
}
|
||||
|
||||
func (cmd *MoveCommand) Wait() (*MoveData, error) {
|
||||
if err := cmd.wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cmd.store != nil {
|
||||
if err := cmd.store.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if cmd.expunge != nil {
|
||||
if err := cmd.expunge.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &cmd.data, nil
|
||||
}
|
||||
|
||||
// MoveData contains the data returned by a MOVE command.
|
||||
type MoveData struct {
|
||||
// requires UIDPLUS or IMAP4rev2
|
||||
UIDValidity uint32
|
||||
SourceUIDs imap.NumSet
|
||||
DestUIDs imap.NumSet
|
||||
}
|
||||
110
vendor/github.com/emersion/go-imap/v2/imapclient/namespace.go
generated
vendored
Normal file
110
vendor/github.com/emersion/go-imap/v2/imapclient/namespace.go
generated
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
// Namespace sends a NAMESPACE command.
|
||||
//
|
||||
// This command requires support for IMAP4rev2 or the NAMESPACE extension.
|
||||
func (c *Client) Namespace() *NamespaceCommand {
|
||||
cmd := &NamespaceCommand{}
|
||||
c.beginCommand("NAMESPACE", cmd).end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c *Client) handleNamespace() error {
|
||||
data, err := readNamespaceResponse(c.dec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("in namespace-response: %v", err)
|
||||
}
|
||||
if cmd := findPendingCmdByType[*NamespaceCommand](c); cmd != nil {
|
||||
cmd.data = *data
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NamespaceCommand is a NAMESPACE command.
|
||||
type NamespaceCommand struct {
|
||||
commandBase
|
||||
data imap.NamespaceData
|
||||
}
|
||||
|
||||
func (cmd *NamespaceCommand) Wait() (*imap.NamespaceData, error) {
|
||||
return &cmd.data, cmd.wait()
|
||||
}
|
||||
|
||||
func readNamespaceResponse(dec *imapwire.Decoder) (*imap.NamespaceData, error) {
|
||||
var (
|
||||
data imap.NamespaceData
|
||||
err error
|
||||
)
|
||||
|
||||
data.Personal, err = readNamespace(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !dec.ExpectSP() {
|
||||
return nil, dec.Err()
|
||||
}
|
||||
|
||||
data.Other, err = readNamespace(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !dec.ExpectSP() {
|
||||
return nil, dec.Err()
|
||||
}
|
||||
|
||||
data.Shared, err = readNamespace(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
func readNamespace(dec *imapwire.Decoder) ([]imap.NamespaceDescriptor, error) {
|
||||
var l []imap.NamespaceDescriptor
|
||||
err := dec.ExpectNList(func() error {
|
||||
descr, err := readNamespaceDescr(dec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("in namespace-descr: %v", err)
|
||||
}
|
||||
l = append(l, *descr)
|
||||
return nil
|
||||
})
|
||||
return l, err
|
||||
}
|
||||
|
||||
func readNamespaceDescr(dec *imapwire.Decoder) (*imap.NamespaceDescriptor, error) {
|
||||
var descr imap.NamespaceDescriptor
|
||||
|
||||
if !dec.ExpectSpecial('(') || !dec.ExpectString(&descr.Prefix) || !dec.ExpectSP() {
|
||||
return nil, dec.Err()
|
||||
}
|
||||
|
||||
var err error
|
||||
descr.Delim, err = readDelim(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Skip namespace-response-extensions
|
||||
for dec.SP() {
|
||||
if !dec.DiscardValue() {
|
||||
return nil, dec.Err()
|
||||
}
|
||||
}
|
||||
|
||||
if !dec.ExpectSpecial(')') {
|
||||
return nil, dec.Err()
|
||||
}
|
||||
|
||||
return &descr, nil
|
||||
}
|
||||
176
vendor/github.com/emersion/go-imap/v2/imapclient/quota.go
generated
vendored
Normal file
176
vendor/github.com/emersion/go-imap/v2/imapclient/quota.go
generated
vendored
Normal file
@@ -0,0 +1,176 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
// GetQuota sends a GETQUOTA command.
|
||||
//
|
||||
// This command requires support for the QUOTA extension.
|
||||
func (c *Client) GetQuota(root string) *GetQuotaCommand {
|
||||
cmd := &GetQuotaCommand{root: root}
|
||||
enc := c.beginCommand("GETQUOTA", cmd)
|
||||
enc.SP().String(root)
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
// GetQuotaRoot sends a GETQUOTAROOT command.
|
||||
//
|
||||
// This command requires support for the QUOTA extension.
|
||||
func (c *Client) GetQuotaRoot(mailbox string) *GetQuotaRootCommand {
|
||||
cmd := &GetQuotaRootCommand{mailbox: mailbox}
|
||||
enc := c.beginCommand("GETQUOTAROOT", cmd)
|
||||
enc.SP().Mailbox(mailbox)
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
// SetQuota sends a SETQUOTA command.
|
||||
//
|
||||
// This command requires support for the SETQUOTA extension.
|
||||
func (c *Client) SetQuota(root string, limits map[imap.QuotaResourceType]int64) *Command {
|
||||
// TODO: consider returning the QUOTA response data?
|
||||
cmd := &Command{}
|
||||
enc := c.beginCommand("SETQUOTA", cmd)
|
||||
enc.SP().String(root).SP().Special('(')
|
||||
i := 0
|
||||
for typ, limit := range limits {
|
||||
if i > 0 {
|
||||
enc.SP()
|
||||
}
|
||||
enc.Atom(string(typ)).SP().Number64(limit)
|
||||
i++
|
||||
}
|
||||
enc.Special(')')
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c *Client) handleQuota() error {
|
||||
data, err := readQuotaResponse(c.dec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("in quota-response: %v", err)
|
||||
}
|
||||
|
||||
cmd := c.findPendingCmdFunc(func(cmd command) bool {
|
||||
switch cmd := cmd.(type) {
|
||||
case *GetQuotaCommand:
|
||||
return cmd.root == data.Root
|
||||
case *GetQuotaRootCommand:
|
||||
for _, root := range cmd.roots {
|
||||
if root == data.Root {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})
|
||||
switch cmd := cmd.(type) {
|
||||
case *GetQuotaCommand:
|
||||
cmd.data = data
|
||||
case *GetQuotaRootCommand:
|
||||
cmd.data = append(cmd.data, *data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) handleQuotaRoot() error {
|
||||
mailbox, roots, err := readQuotaRoot(c.dec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("in quotaroot-response: %v", err)
|
||||
}
|
||||
|
||||
cmd := c.findPendingCmdFunc(func(anyCmd command) bool {
|
||||
cmd, ok := anyCmd.(*GetQuotaRootCommand)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return cmd.mailbox == mailbox
|
||||
})
|
||||
if cmd != nil {
|
||||
cmd := cmd.(*GetQuotaRootCommand)
|
||||
cmd.roots = roots
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetQuotaCommand is a GETQUOTA command.
|
||||
type GetQuotaCommand struct {
|
||||
commandBase
|
||||
root string
|
||||
data *QuotaData
|
||||
}
|
||||
|
||||
func (cmd *GetQuotaCommand) Wait() (*QuotaData, error) {
|
||||
if err := cmd.wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cmd.data, nil
|
||||
}
|
||||
|
||||
// GetQuotaRootCommand is a GETQUOTAROOT command.
|
||||
type GetQuotaRootCommand struct {
|
||||
commandBase
|
||||
mailbox string
|
||||
roots []string
|
||||
data []QuotaData
|
||||
}
|
||||
|
||||
func (cmd *GetQuotaRootCommand) Wait() ([]QuotaData, error) {
|
||||
if err := cmd.wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cmd.data, nil
|
||||
}
|
||||
|
||||
// QuotaData is the data returned by a QUOTA response.
|
||||
type QuotaData struct {
|
||||
Root string
|
||||
Resources map[imap.QuotaResourceType]QuotaResourceData
|
||||
}
|
||||
|
||||
// QuotaResourceData contains the usage and limit for a quota resource.
|
||||
type QuotaResourceData struct {
|
||||
Usage int64
|
||||
Limit int64
|
||||
}
|
||||
|
||||
func readQuotaResponse(dec *imapwire.Decoder) (*QuotaData, error) {
|
||||
var data QuotaData
|
||||
if !dec.ExpectAString(&data.Root) || !dec.ExpectSP() {
|
||||
return nil, dec.Err()
|
||||
}
|
||||
data.Resources = make(map[imap.QuotaResourceType]QuotaResourceData)
|
||||
err := dec.ExpectList(func() error {
|
||||
var (
|
||||
name string
|
||||
resData QuotaResourceData
|
||||
)
|
||||
if !dec.ExpectAtom(&name) || !dec.ExpectSP() || !dec.ExpectNumber64(&resData.Usage) || !dec.ExpectSP() || !dec.ExpectNumber64(&resData.Limit) {
|
||||
return fmt.Errorf("in quota-resource: %v", dec.Err())
|
||||
}
|
||||
data.Resources[imap.QuotaResourceType(name)] = resData
|
||||
return nil
|
||||
})
|
||||
return &data, err
|
||||
}
|
||||
|
||||
func readQuotaRoot(dec *imapwire.Decoder) (mailbox string, roots []string, err error) {
|
||||
if !dec.ExpectMailbox(&mailbox) {
|
||||
return "", nil, dec.Err()
|
||||
}
|
||||
for dec.SP() {
|
||||
var root string
|
||||
if !dec.ExpectAString(&root) {
|
||||
return "", nil, dec.Err()
|
||||
}
|
||||
roots = append(roots, root)
|
||||
}
|
||||
return mailbox, roots, nil
|
||||
}
|
||||
401
vendor/github.com/emersion/go-imap/v2/imapclient/search.go
generated
vendored
Normal file
401
vendor/github.com/emersion/go-imap/v2/imapclient/search.go
generated
vendored
Normal file
@@ -0,0 +1,401 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
func returnSearchOptions(options *imap.SearchOptions) []string {
|
||||
if options == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
m := map[string]bool{
|
||||
"MIN": options.ReturnMin,
|
||||
"MAX": options.ReturnMax,
|
||||
"ALL": options.ReturnAll,
|
||||
"COUNT": options.ReturnCount,
|
||||
}
|
||||
|
||||
var l []string
|
||||
for k, ret := range m {
|
||||
if ret {
|
||||
l = append(l, k)
|
||||
}
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
func (c *Client) search(numKind imapwire.NumKind, criteria *imap.SearchCriteria, options *imap.SearchOptions) *SearchCommand {
|
||||
// The IMAP4rev2 SEARCH charset defaults to UTF-8. When UTF8=ACCEPT is
|
||||
// enabled, specifying any CHARSET is invalid. For IMAP4rev1 the default is
|
||||
// undefined and only US-ASCII support is required. What's more, some
|
||||
// servers completely reject the CHARSET keyword. So, let's check if we
|
||||
// actually have UTF-8 strings in the search criteria before using that.
|
||||
// TODO: there might be a benefit in specifying CHARSET UTF-8 for IMAP4rev1
|
||||
// servers even if we only send ASCII characters: the server then must
|
||||
// decode encoded headers and Content-Transfer-Encoding before matching the
|
||||
// criteria.
|
||||
var charset string
|
||||
if !c.Caps().Has(imap.CapIMAP4rev2) && !c.enabled.Has(imap.CapUTF8Accept) && !searchCriteriaIsASCII(criteria) {
|
||||
charset = "UTF-8"
|
||||
}
|
||||
|
||||
var all imap.NumSet
|
||||
switch numKind {
|
||||
case imapwire.NumKindSeq:
|
||||
all = imap.SeqSet(nil)
|
||||
case imapwire.NumKindUID:
|
||||
all = imap.UIDSet(nil)
|
||||
}
|
||||
|
||||
cmd := &SearchCommand{}
|
||||
cmd.data.All = all
|
||||
enc := c.beginCommand(uidCmdName("SEARCH", numKind), cmd)
|
||||
if returnOpts := returnSearchOptions(options); len(returnOpts) > 0 {
|
||||
enc.SP().Atom("RETURN").SP().List(len(returnOpts), func(i int) {
|
||||
enc.Atom(returnOpts[i])
|
||||
})
|
||||
}
|
||||
enc.SP()
|
||||
if charset != "" {
|
||||
enc.Atom("CHARSET").SP().Atom(charset).SP()
|
||||
}
|
||||
writeSearchKey(enc.Encoder, criteria)
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Search sends a SEARCH command.
|
||||
func (c *Client) Search(criteria *imap.SearchCriteria, options *imap.SearchOptions) *SearchCommand {
|
||||
return c.search(imapwire.NumKindSeq, criteria, options)
|
||||
}
|
||||
|
||||
// UIDSearch sends a UID SEARCH command.
|
||||
func (c *Client) UIDSearch(criteria *imap.SearchCriteria, options *imap.SearchOptions) *SearchCommand {
|
||||
return c.search(imapwire.NumKindUID, criteria, options)
|
||||
}
|
||||
|
||||
func (c *Client) handleSearch() error {
|
||||
cmd := findPendingCmdByType[*SearchCommand](c)
|
||||
for c.dec.SP() {
|
||||
if c.dec.Special('(') {
|
||||
var name string
|
||||
if !c.dec.ExpectAtom(&name) || !c.dec.ExpectSP() {
|
||||
return c.dec.Err()
|
||||
} else if strings.ToUpper(name) != "MODSEQ" {
|
||||
return fmt.Errorf("in search-sort-mod-seq: expected %q, got %q", "MODSEQ", name)
|
||||
}
|
||||
var modSeq uint64
|
||||
if !c.dec.ExpectModSeq(&modSeq) || !c.dec.ExpectSpecial(')') {
|
||||
return c.dec.Err()
|
||||
}
|
||||
if cmd != nil {
|
||||
cmd.data.ModSeq = modSeq
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
var num uint32
|
||||
if !c.dec.ExpectNumber(&num) {
|
||||
return c.dec.Err()
|
||||
}
|
||||
if cmd != nil {
|
||||
switch all := cmd.data.All.(type) {
|
||||
case imap.SeqSet:
|
||||
all.AddNum(num)
|
||||
cmd.data.All = all
|
||||
case imap.UIDSet:
|
||||
all.AddNum(imap.UID(num))
|
||||
cmd.data.All = all
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) handleESearch() error {
|
||||
if !c.dec.ExpectSP() {
|
||||
return c.dec.Err()
|
||||
}
|
||||
tag, data, err := readESearchResponse(c.dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := c.findPendingCmdFunc(func(anyCmd command) bool {
|
||||
cmd, ok := anyCmd.(*SearchCommand)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if tag != "" {
|
||||
return cmd.tag == tag
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
})
|
||||
if cmd != nil {
|
||||
cmd := cmd.(*SearchCommand)
|
||||
cmd.data = *data
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SearchCommand is a SEARCH command.
|
||||
type SearchCommand struct {
|
||||
commandBase
|
||||
data imap.SearchData
|
||||
}
|
||||
|
||||
func (cmd *SearchCommand) Wait() (*imap.SearchData, error) {
|
||||
return &cmd.data, cmd.wait()
|
||||
}
|
||||
|
||||
func writeSearchKey(enc *imapwire.Encoder, criteria *imap.SearchCriteria) {
|
||||
firstItem := true
|
||||
encodeItem := func() *imapwire.Encoder {
|
||||
if !firstItem {
|
||||
enc.SP()
|
||||
}
|
||||
firstItem = false
|
||||
return enc
|
||||
}
|
||||
|
||||
for _, seqSet := range criteria.SeqNum {
|
||||
encodeItem().NumSet(seqSet)
|
||||
}
|
||||
for _, uidSet := range criteria.UID {
|
||||
encodeItem().Atom("UID").SP().NumSet(uidSet)
|
||||
}
|
||||
|
||||
if !criteria.Since.IsZero() && !criteria.Before.IsZero() && criteria.Before.Sub(criteria.Since) == 24*time.Hour {
|
||||
encodeItem().Atom("ON").SP().String(criteria.Since.Format(internal.DateLayout))
|
||||
} else {
|
||||
if !criteria.Since.IsZero() {
|
||||
encodeItem().Atom("SINCE").SP().String(criteria.Since.Format(internal.DateLayout))
|
||||
}
|
||||
if !criteria.Before.IsZero() {
|
||||
encodeItem().Atom("BEFORE").SP().String(criteria.Before.Format(internal.DateLayout))
|
||||
}
|
||||
}
|
||||
if !criteria.SentSince.IsZero() && !criteria.SentBefore.IsZero() && criteria.SentBefore.Sub(criteria.SentSince) == 24*time.Hour {
|
||||
encodeItem().Atom("SENTON").SP().String(criteria.SentSince.Format(internal.DateLayout))
|
||||
} else {
|
||||
if !criteria.SentSince.IsZero() {
|
||||
encodeItem().Atom("SENTSINCE").SP().String(criteria.SentSince.Format(internal.DateLayout))
|
||||
}
|
||||
if !criteria.SentBefore.IsZero() {
|
||||
encodeItem().Atom("SENTBEFORE").SP().String(criteria.SentBefore.Format(internal.DateLayout))
|
||||
}
|
||||
}
|
||||
|
||||
for _, kv := range criteria.Header {
|
||||
switch k := strings.ToUpper(kv.Key); k {
|
||||
case "BCC", "CC", "FROM", "SUBJECT", "TO":
|
||||
encodeItem().Atom(k)
|
||||
default:
|
||||
encodeItem().Atom("HEADER").SP().String(kv.Key)
|
||||
}
|
||||
enc.SP().String(kv.Value)
|
||||
}
|
||||
|
||||
for _, s := range criteria.Body {
|
||||
encodeItem().Atom("BODY").SP().String(s)
|
||||
}
|
||||
for _, s := range criteria.Text {
|
||||
encodeItem().Atom("TEXT").SP().String(s)
|
||||
}
|
||||
|
||||
for _, flag := range criteria.Flag {
|
||||
if k := flagSearchKey(flag); k != "" {
|
||||
encodeItem().Atom(k)
|
||||
} else {
|
||||
encodeItem().Atom("KEYWORD").SP().Flag(flag)
|
||||
}
|
||||
}
|
||||
for _, flag := range criteria.NotFlag {
|
||||
if k := flagSearchKey(flag); k != "" {
|
||||
encodeItem().Atom("UN" + k)
|
||||
} else {
|
||||
encodeItem().Atom("UNKEYWORD").SP().Flag(flag)
|
||||
}
|
||||
}
|
||||
|
||||
if criteria.Larger > 0 {
|
||||
encodeItem().Atom("LARGER").SP().Number64(criteria.Larger)
|
||||
}
|
||||
if criteria.Smaller > 0 {
|
||||
encodeItem().Atom("SMALLER").SP().Number64(criteria.Smaller)
|
||||
}
|
||||
|
||||
if modSeq := criteria.ModSeq; modSeq != nil {
|
||||
encodeItem().Atom("MODSEQ")
|
||||
if modSeq.MetadataName != "" && modSeq.MetadataType != "" {
|
||||
enc.SP().Quoted(modSeq.MetadataName).SP().Atom(string(modSeq.MetadataType))
|
||||
}
|
||||
enc.SP()
|
||||
if modSeq.ModSeq != 0 {
|
||||
enc.ModSeq(modSeq.ModSeq)
|
||||
} else {
|
||||
enc.Atom("0")
|
||||
}
|
||||
}
|
||||
|
||||
for _, not := range criteria.Not {
|
||||
encodeItem().Atom("NOT").SP()
|
||||
enc.Special('(')
|
||||
writeSearchKey(enc, ¬)
|
||||
enc.Special(')')
|
||||
}
|
||||
for _, or := range criteria.Or {
|
||||
encodeItem().Atom("OR").SP()
|
||||
enc.Special('(')
|
||||
writeSearchKey(enc, &or[0])
|
||||
enc.Special(')')
|
||||
enc.SP()
|
||||
enc.Special('(')
|
||||
writeSearchKey(enc, &or[1])
|
||||
enc.Special(')')
|
||||
}
|
||||
|
||||
if firstItem {
|
||||
enc.Atom("ALL")
|
||||
}
|
||||
}
|
||||
|
||||
func flagSearchKey(flag imap.Flag) string {
|
||||
switch flag {
|
||||
case imap.FlagAnswered, imap.FlagDeleted, imap.FlagDraft, imap.FlagFlagged, imap.FlagSeen:
|
||||
return strings.ToUpper(strings.TrimPrefix(string(flag), "\\"))
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func readESearchResponse(dec *imapwire.Decoder) (tag string, data *imap.SearchData, err error) {
|
||||
data = &imap.SearchData{}
|
||||
if dec.Special('(') { // search-correlator
|
||||
var correlator string
|
||||
if !dec.ExpectAtom(&correlator) || !dec.ExpectSP() || !dec.ExpectAString(&tag) || !dec.ExpectSpecial(')') {
|
||||
return "", nil, dec.Err()
|
||||
}
|
||||
if correlator != "TAG" {
|
||||
return "", nil, fmt.Errorf("in search-correlator: name must be TAG, but got %q", correlator)
|
||||
}
|
||||
}
|
||||
|
||||
var name string
|
||||
if !dec.SP() {
|
||||
return tag, data, nil
|
||||
} else if !dec.ExpectAtom(&name) {
|
||||
return "", nil, dec.Err()
|
||||
}
|
||||
isUID := name == "UID"
|
||||
|
||||
if isUID {
|
||||
if !dec.SP() {
|
||||
return tag, data, nil
|
||||
} else if !dec.ExpectAtom(&name) {
|
||||
return "", nil, dec.Err()
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
if !dec.ExpectSP() {
|
||||
return "", nil, dec.Err()
|
||||
}
|
||||
|
||||
switch strings.ToUpper(name) {
|
||||
case "MIN":
|
||||
var num uint32
|
||||
if !dec.ExpectNumber(&num) {
|
||||
return "", nil, dec.Err()
|
||||
}
|
||||
data.Min = num
|
||||
case "MAX":
|
||||
var num uint32
|
||||
if !dec.ExpectNumber(&num) {
|
||||
return "", nil, dec.Err()
|
||||
}
|
||||
data.Max = num
|
||||
case "ALL":
|
||||
numKind := imapwire.NumKindSeq
|
||||
if isUID {
|
||||
numKind = imapwire.NumKindUID
|
||||
}
|
||||
if !dec.ExpectNumSet(numKind, &data.All) {
|
||||
return "", nil, dec.Err()
|
||||
}
|
||||
if data.All.Dynamic() {
|
||||
return "", nil, fmt.Errorf("imapclient: server returned a dynamic ALL number set in SEARCH response")
|
||||
}
|
||||
case "COUNT":
|
||||
var num uint32
|
||||
if !dec.ExpectNumber(&num) {
|
||||
return "", nil, dec.Err()
|
||||
}
|
||||
data.Count = num
|
||||
case "MODSEQ":
|
||||
var modSeq uint64
|
||||
if !dec.ExpectModSeq(&modSeq) {
|
||||
return "", nil, dec.Err()
|
||||
}
|
||||
data.ModSeq = modSeq
|
||||
default:
|
||||
if !dec.DiscardValue() {
|
||||
return "", nil, dec.Err()
|
||||
}
|
||||
}
|
||||
|
||||
if !dec.SP() {
|
||||
break
|
||||
} else if !dec.ExpectAtom(&name) {
|
||||
return "", nil, dec.Err()
|
||||
}
|
||||
}
|
||||
|
||||
return tag, data, nil
|
||||
}
|
||||
|
||||
func searchCriteriaIsASCII(criteria *imap.SearchCriteria) bool {
|
||||
for _, kv := range criteria.Header {
|
||||
if !isASCII(kv.Key) || !isASCII(kv.Value) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, s := range criteria.Body {
|
||||
if !isASCII(s) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, s := range criteria.Text {
|
||||
if !isASCII(s) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, not := range criteria.Not {
|
||||
if !searchCriteriaIsASCII(¬) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, or := range criteria.Or {
|
||||
if !searchCriteriaIsASCII(&or[0]) || !searchCriteriaIsASCII(&or[1]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isASCII(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] > unicode.MaxASCII {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
100
vendor/github.com/emersion/go-imap/v2/imapclient/select.go
generated
vendored
Normal file
100
vendor/github.com/emersion/go-imap/v2/imapclient/select.go
generated
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal"
|
||||
)
|
||||
|
||||
// Select sends a SELECT or EXAMINE command.
|
||||
//
|
||||
// A nil options pointer is equivalent to a zero options value.
|
||||
func (c *Client) Select(mailbox string, options *imap.SelectOptions) *SelectCommand {
|
||||
cmdName := "SELECT"
|
||||
if options != nil && options.ReadOnly {
|
||||
cmdName = "EXAMINE"
|
||||
}
|
||||
|
||||
cmd := &SelectCommand{mailbox: mailbox}
|
||||
enc := c.beginCommand(cmdName, cmd)
|
||||
enc.SP().Mailbox(mailbox)
|
||||
if options != nil && options.CondStore {
|
||||
enc.SP().Special('(').Atom("CONDSTORE").Special(')')
|
||||
}
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Unselect sends an UNSELECT command.
|
||||
//
|
||||
// This command requires support for IMAP4rev2 or the UNSELECT extension.
|
||||
func (c *Client) Unselect() *Command {
|
||||
cmd := &unselectCommand{}
|
||||
c.beginCommand("UNSELECT", cmd).end()
|
||||
return &cmd.Command
|
||||
}
|
||||
|
||||
// UnselectAndExpunge sends a CLOSE command.
|
||||
//
|
||||
// CLOSE implicitly performs a silent EXPUNGE command.
|
||||
func (c *Client) UnselectAndExpunge() *Command {
|
||||
cmd := &unselectCommand{}
|
||||
c.beginCommand("CLOSE", cmd).end()
|
||||
return &cmd.Command
|
||||
}
|
||||
|
||||
func (c *Client) handleFlags() error {
|
||||
flags, err := internal.ExpectFlagList(c.dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.mutex.Lock()
|
||||
if c.state == imap.ConnStateSelected {
|
||||
c.mailbox = c.mailbox.copy()
|
||||
c.mailbox.PermanentFlags = flags
|
||||
}
|
||||
c.mutex.Unlock()
|
||||
|
||||
cmd := findPendingCmdByType[*SelectCommand](c)
|
||||
if cmd != nil {
|
||||
cmd.data.Flags = flags
|
||||
} else if handler := c.options.unilateralDataHandler().Mailbox; handler != nil {
|
||||
handler(&UnilateralDataMailbox{Flags: flags})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) handleExists(num uint32) error {
|
||||
cmd := findPendingCmdByType[*SelectCommand](c)
|
||||
if cmd != nil {
|
||||
cmd.data.NumMessages = num
|
||||
} else {
|
||||
c.mutex.Lock()
|
||||
if c.state == imap.ConnStateSelected {
|
||||
c.mailbox = c.mailbox.copy()
|
||||
c.mailbox.NumMessages = num
|
||||
}
|
||||
c.mutex.Unlock()
|
||||
|
||||
if handler := c.options.unilateralDataHandler().Mailbox; handler != nil {
|
||||
handler(&UnilateralDataMailbox{NumMessages: &num})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SelectCommand is a SELECT command.
|
||||
type SelectCommand struct {
|
||||
commandBase
|
||||
mailbox string
|
||||
data imap.SelectData
|
||||
}
|
||||
|
||||
func (cmd *SelectCommand) Wait() (*imap.SelectData, error) {
|
||||
return &cmd.data, cmd.wait()
|
||||
}
|
||||
|
||||
type unselectCommand struct {
|
||||
Command
|
||||
}
|
||||
84
vendor/github.com/emersion/go-imap/v2/imapclient/sort.go
generated
vendored
Normal file
84
vendor/github.com/emersion/go-imap/v2/imapclient/sort.go
generated
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
type SortKey string
|
||||
|
||||
const (
|
||||
SortKeyArrival SortKey = "ARRIVAL"
|
||||
SortKeyCc SortKey = "CC"
|
||||
SortKeyDate SortKey = "DATE"
|
||||
SortKeyFrom SortKey = "FROM"
|
||||
SortKeySize SortKey = "SIZE"
|
||||
SortKeySubject SortKey = "SUBJECT"
|
||||
SortKeyTo SortKey = "TO"
|
||||
)
|
||||
|
||||
type SortCriterion struct {
|
||||
Key SortKey
|
||||
Reverse bool
|
||||
}
|
||||
|
||||
// SortOptions contains options for the SORT command.
|
||||
type SortOptions struct {
|
||||
SearchCriteria *imap.SearchCriteria
|
||||
SortCriteria []SortCriterion
|
||||
}
|
||||
|
||||
func (c *Client) sort(numKind imapwire.NumKind, options *SortOptions) *SortCommand {
|
||||
cmd := &SortCommand{}
|
||||
enc := c.beginCommand(uidCmdName("SORT", numKind), cmd)
|
||||
enc.SP().List(len(options.SortCriteria), func(i int) {
|
||||
criterion := options.SortCriteria[i]
|
||||
if criterion.Reverse {
|
||||
enc.Atom("REVERSE").SP()
|
||||
}
|
||||
enc.Atom(string(criterion.Key))
|
||||
})
|
||||
enc.SP().Atom("UTF-8").SP()
|
||||
writeSearchKey(enc.Encoder, options.SearchCriteria)
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c *Client) handleSort() error {
|
||||
cmd := findPendingCmdByType[*SortCommand](c)
|
||||
for c.dec.SP() {
|
||||
var num uint32
|
||||
if !c.dec.ExpectNumber(&num) {
|
||||
return c.dec.Err()
|
||||
}
|
||||
if cmd != nil {
|
||||
cmd.nums = append(cmd.nums, num)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sort sends a SORT command.
|
||||
//
|
||||
// This command requires support for the SORT extension.
|
||||
func (c *Client) Sort(options *SortOptions) *SortCommand {
|
||||
return c.sort(imapwire.NumKindSeq, options)
|
||||
}
|
||||
|
||||
// UIDSort sends a UID SORT command.
|
||||
//
|
||||
// See Sort.
|
||||
func (c *Client) UIDSort(options *SortOptions) *SortCommand {
|
||||
return c.sort(imapwire.NumKindUID, options)
|
||||
}
|
||||
|
||||
// SortCommand is a SORT command.
|
||||
type SortCommand struct {
|
||||
commandBase
|
||||
nums []uint32
|
||||
}
|
||||
|
||||
func (cmd *SortCommand) Wait() ([]uint32, error) {
|
||||
err := cmd.wait()
|
||||
return cmd.nums, err
|
||||
}
|
||||
83
vendor/github.com/emersion/go-imap/v2/imapclient/starttls.go
generated
vendored
Normal file
83
vendor/github.com/emersion/go-imap/v2/imapclient/starttls.go
generated
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
)
|
||||
|
||||
// startTLS sends a STARTTLS command.
|
||||
//
|
||||
// Unlike other commands, this method blocks until the command completes.
|
||||
func (c *Client) startTLS(config *tls.Config) error {
|
||||
upgradeDone := make(chan struct{})
|
||||
cmd := &startTLSCommand{
|
||||
tlsConfig: config,
|
||||
upgradeDone: upgradeDone,
|
||||
}
|
||||
enc := c.beginCommand("STARTTLS", cmd)
|
||||
enc.flush()
|
||||
defer enc.end()
|
||||
|
||||
// Once a client issues a STARTTLS command, it MUST NOT issue further
|
||||
// commands until a server response is seen and the TLS negotiation is
|
||||
// complete
|
||||
|
||||
if err := cmd.wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The decoder goroutine will invoke Client.upgradeStartTLS
|
||||
<-upgradeDone
|
||||
|
||||
return cmd.tlsConn.Handshake()
|
||||
}
|
||||
|
||||
// upgradeStartTLS finishes the STARTTLS upgrade after the server has sent an
|
||||
// OK response. It runs in the decoder goroutine.
|
||||
func (c *Client) upgradeStartTLS(startTLS *startTLSCommand) {
|
||||
defer close(startTLS.upgradeDone)
|
||||
|
||||
// Drain buffered data from our bufio.Reader
|
||||
var buf bytes.Buffer
|
||||
if _, err := io.CopyN(&buf, c.br, int64(c.br.Buffered())); err != nil {
|
||||
panic(err) // unreachable
|
||||
}
|
||||
|
||||
var cleartextConn net.Conn
|
||||
if buf.Len() > 0 {
|
||||
r := io.MultiReader(&buf, c.conn)
|
||||
cleartextConn = startTLSConn{c.conn, r}
|
||||
} else {
|
||||
cleartextConn = c.conn
|
||||
}
|
||||
|
||||
tlsConn := tls.Client(cleartextConn, startTLS.tlsConfig)
|
||||
rw := c.options.wrapReadWriter(tlsConn)
|
||||
|
||||
c.br.Reset(rw)
|
||||
// Unfortunately we can't re-use the bufio.Writer here, it races with
|
||||
// Client.StartTLS
|
||||
c.bw = bufio.NewWriter(rw)
|
||||
|
||||
startTLS.tlsConn = tlsConn
|
||||
}
|
||||
|
||||
type startTLSCommand struct {
|
||||
commandBase
|
||||
tlsConfig *tls.Config
|
||||
|
||||
upgradeDone chan<- struct{}
|
||||
tlsConn *tls.Conn
|
||||
}
|
||||
|
||||
type startTLSConn struct {
|
||||
net.Conn
|
||||
r io.Reader
|
||||
}
|
||||
|
||||
func (conn startTLSConn) Read(b []byte) (int, error) {
|
||||
return conn.r.Read(b)
|
||||
}
|
||||
164
vendor/github.com/emersion/go-imap/v2/imapclient/status.go
generated
vendored
Normal file
164
vendor/github.com/emersion/go-imap/v2/imapclient/status.go
generated
vendored
Normal file
@@ -0,0 +1,164 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
func statusItems(options *imap.StatusOptions) []string {
|
||||
m := map[string]bool{
|
||||
"MESSAGES": options.NumMessages,
|
||||
"UIDNEXT": options.UIDNext,
|
||||
"UIDVALIDITY": options.UIDValidity,
|
||||
"UNSEEN": options.NumUnseen,
|
||||
"DELETED": options.NumDeleted,
|
||||
"SIZE": options.Size,
|
||||
"APPENDLIMIT": options.AppendLimit,
|
||||
"DELETED-STORAGE": options.DeletedStorage,
|
||||
"HIGHESTMODSEQ": options.HighestModSeq,
|
||||
}
|
||||
|
||||
var l []string
|
||||
for k, req := range m {
|
||||
if req {
|
||||
l = append(l, k)
|
||||
}
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// Status sends a STATUS command.
|
||||
//
|
||||
// A nil options pointer is equivalent to a zero options value.
|
||||
func (c *Client) Status(mailbox string, options *imap.StatusOptions) *StatusCommand {
|
||||
if options == nil {
|
||||
options = new(imap.StatusOptions)
|
||||
}
|
||||
if options.NumRecent {
|
||||
panic("StatusOptions.NumRecent is not supported in imapclient")
|
||||
}
|
||||
|
||||
cmd := &StatusCommand{mailbox: mailbox}
|
||||
enc := c.beginCommand("STATUS", cmd)
|
||||
enc.SP().Mailbox(mailbox).SP()
|
||||
items := statusItems(options)
|
||||
enc.List(len(items), func(i int) {
|
||||
enc.Atom(items[i])
|
||||
})
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c *Client) handleStatus() error {
|
||||
data, err := readStatus(c.dec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("in status: %v", err)
|
||||
}
|
||||
|
||||
cmd := c.findPendingCmdFunc(func(cmd command) bool {
|
||||
switch cmd := cmd.(type) {
|
||||
case *StatusCommand:
|
||||
return cmd.mailbox == data.Mailbox
|
||||
case *ListCommand:
|
||||
return cmd.returnStatus && cmd.pendingData != nil && cmd.pendingData.Mailbox == data.Mailbox
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})
|
||||
switch cmd := cmd.(type) {
|
||||
case *StatusCommand:
|
||||
cmd.data = *data
|
||||
case *ListCommand:
|
||||
cmd.pendingData.Status = data
|
||||
cmd.mailboxes <- cmd.pendingData
|
||||
cmd.pendingData = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StatusCommand is a STATUS command.
|
||||
type StatusCommand struct {
|
||||
commandBase
|
||||
mailbox string
|
||||
data imap.StatusData
|
||||
}
|
||||
|
||||
func (cmd *StatusCommand) Wait() (*imap.StatusData, error) {
|
||||
return &cmd.data, cmd.wait()
|
||||
}
|
||||
|
||||
func readStatus(dec *imapwire.Decoder) (*imap.StatusData, error) {
|
||||
var data imap.StatusData
|
||||
|
||||
if !dec.ExpectMailbox(&data.Mailbox) || !dec.ExpectSP() {
|
||||
return nil, dec.Err()
|
||||
}
|
||||
|
||||
err := dec.ExpectList(func() error {
|
||||
if err := readStatusAttVal(dec, &data); err != nil {
|
||||
return fmt.Errorf("in status-att-val: %v", dec.Err())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return &data, err
|
||||
}
|
||||
|
||||
func readStatusAttVal(dec *imapwire.Decoder, data *imap.StatusData) error {
|
||||
var name string
|
||||
if !dec.ExpectAtom(&name) || !dec.ExpectSP() {
|
||||
return dec.Err()
|
||||
}
|
||||
|
||||
var ok bool
|
||||
switch strings.ToUpper(name) {
|
||||
case "MESSAGES":
|
||||
var num uint32
|
||||
ok = dec.ExpectNumber(&num)
|
||||
data.NumMessages = &num
|
||||
case "UIDNEXT":
|
||||
var uidNext imap.UID
|
||||
ok = dec.ExpectUID(&uidNext)
|
||||
data.UIDNext = uidNext
|
||||
case "UIDVALIDITY":
|
||||
ok = dec.ExpectNumber(&data.UIDValidity)
|
||||
case "UNSEEN":
|
||||
var num uint32
|
||||
ok = dec.ExpectNumber(&num)
|
||||
data.NumUnseen = &num
|
||||
case "DELETED":
|
||||
var num uint32
|
||||
ok = dec.ExpectNumber(&num)
|
||||
data.NumDeleted = &num
|
||||
case "SIZE":
|
||||
var size int64
|
||||
ok = dec.ExpectNumber64(&size)
|
||||
data.Size = &size
|
||||
case "APPENDLIMIT":
|
||||
var num uint32
|
||||
if dec.Number(&num) {
|
||||
ok = true
|
||||
} else {
|
||||
ok = dec.ExpectNIL()
|
||||
num = ^uint32(0)
|
||||
}
|
||||
data.AppendLimit = &num
|
||||
case "DELETED-STORAGE":
|
||||
var storage int64
|
||||
ok = dec.ExpectNumber64(&storage)
|
||||
data.DeletedStorage = &storage
|
||||
case "HIGHESTMODSEQ":
|
||||
ok = dec.ExpectModSeq(&data.HighestModSeq)
|
||||
default:
|
||||
if !dec.DiscardValue() {
|
||||
return dec.Err()
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return dec.Err()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
44
vendor/github.com/emersion/go-imap/v2/imapclient/store.go
generated
vendored
Normal file
44
vendor/github.com/emersion/go-imap/v2/imapclient/store.go
generated
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
// Store sends a STORE command.
|
||||
//
|
||||
// Unless StoreFlags.Silent is set, the server will return the updated values.
|
||||
//
|
||||
// A nil options pointer is equivalent to a zero options value.
|
||||
func (c *Client) Store(numSet imap.NumSet, store *imap.StoreFlags, options *imap.StoreOptions) *FetchCommand {
|
||||
cmd := &FetchCommand{
|
||||
numSet: numSet,
|
||||
msgs: make(chan *FetchMessageData, 128),
|
||||
}
|
||||
enc := c.beginCommand(uidCmdName("STORE", imapwire.NumSetKind(numSet)), cmd)
|
||||
enc.SP().NumSet(numSet).SP()
|
||||
if options != nil && options.UnchangedSince != 0 {
|
||||
enc.Special('(').Atom("UNCHANGEDSINCE").SP().ModSeq(options.UnchangedSince).Special(')').SP()
|
||||
}
|
||||
switch store.Op {
|
||||
case imap.StoreFlagsSet:
|
||||
// nothing to do
|
||||
case imap.StoreFlagsAdd:
|
||||
enc.Special('+')
|
||||
case imap.StoreFlagsDel:
|
||||
enc.Special('-')
|
||||
default:
|
||||
panic(fmt.Errorf("imapclient: unknown store flags op: %v", store.Op))
|
||||
}
|
||||
enc.Atom("FLAGS")
|
||||
if store.Silent {
|
||||
enc.Atom(".SILENT")
|
||||
}
|
||||
enc.SP().List(len(store.Flags), func(i int) {
|
||||
enc.Flag(store.Flags[i])
|
||||
})
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
85
vendor/github.com/emersion/go-imap/v2/imapclient/thread.go
generated
vendored
Normal file
85
vendor/github.com/emersion/go-imap/v2/imapclient/thread.go
generated
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
// ThreadOptions contains options for the THREAD command.
|
||||
type ThreadOptions struct {
|
||||
Algorithm imap.ThreadAlgorithm
|
||||
SearchCriteria *imap.SearchCriteria
|
||||
}
|
||||
|
||||
func (c *Client) thread(numKind imapwire.NumKind, options *ThreadOptions) *ThreadCommand {
|
||||
cmd := &ThreadCommand{}
|
||||
enc := c.beginCommand(uidCmdName("THREAD", numKind), cmd)
|
||||
enc.SP().Atom(string(options.Algorithm)).SP().Atom("UTF-8").SP()
|
||||
writeSearchKey(enc.Encoder, options.SearchCriteria)
|
||||
enc.end()
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Thread sends a THREAD command.
|
||||
//
|
||||
// This command requires support for the THREAD extension.
|
||||
func (c *Client) Thread(options *ThreadOptions) *ThreadCommand {
|
||||
return c.thread(imapwire.NumKindSeq, options)
|
||||
}
|
||||
|
||||
// UIDThread sends a UID THREAD command.
|
||||
//
|
||||
// See Thread.
|
||||
func (c *Client) UIDThread(options *ThreadOptions) *ThreadCommand {
|
||||
return c.thread(imapwire.NumKindUID, options)
|
||||
}
|
||||
|
||||
func (c *Client) handleThread() error {
|
||||
cmd := findPendingCmdByType[*ThreadCommand](c)
|
||||
for c.dec.SP() {
|
||||
data, err := readThreadList(c.dec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("in thread-list: %v", err)
|
||||
}
|
||||
if cmd != nil {
|
||||
cmd.data = append(cmd.data, *data)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ThreadCommand is a THREAD command.
|
||||
type ThreadCommand struct {
|
||||
commandBase
|
||||
data []ThreadData
|
||||
}
|
||||
|
||||
func (cmd *ThreadCommand) Wait() ([]ThreadData, error) {
|
||||
err := cmd.wait()
|
||||
return cmd.data, err
|
||||
}
|
||||
|
||||
type ThreadData struct {
|
||||
Chain []uint32
|
||||
SubThreads []ThreadData
|
||||
}
|
||||
|
||||
func readThreadList(dec *imapwire.Decoder) (*ThreadData, error) {
|
||||
var data ThreadData
|
||||
err := dec.ExpectList(func() error {
|
||||
var num uint32
|
||||
if len(data.SubThreads) == 0 && dec.Number(&num) {
|
||||
data.Chain = append(data.Chain, num)
|
||||
} else {
|
||||
sub, err := readThreadList(dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data.SubThreads = append(data.SubThreads, *sub)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return &data, err
|
||||
}
|
||||
13
vendor/github.com/emersion/go-imap/v2/internal/acl.go
generated
vendored
Normal file
13
vendor/github.com/emersion/go-imap/v2/internal/acl.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-imap/v2"
|
||||
)
|
||||
|
||||
func FormatRights(rm imap.RightModification, rs imap.RightSet) string {
|
||||
s := ""
|
||||
if rm != imap.RightModificationReplace {
|
||||
s = string(rm)
|
||||
}
|
||||
return s + string(rs)
|
||||
}
|
||||
306
vendor/github.com/emersion/go-imap/v2/internal/imapnum/numset.go
generated
vendored
Normal file
306
vendor/github.com/emersion/go-imap/v2/internal/imapnum/numset.go
generated
vendored
Normal file
@@ -0,0 +1,306 @@
|
||||
package imapnum
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Range 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 Range struct {
|
||||
Start, Stop uint32
|
||||
}
|
||||
|
||||
// Contains returns true if the seq-number q is contained in range value s.
|
||||
// The dynamic value "*" contains only other "*" values, the dynamic range "n:*"
|
||||
// contains "*" and all numbers >= n.
|
||||
func (s Range) 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 Range) Less(q uint32) bool {
|
||||
return (s.Stop < q || q == 0) && s.Stop != 0
|
||||
}
|
||||
|
||||
// Merge combines range 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 Range) Merge(t Range) (union Range, ok bool) {
|
||||
union = s
|
||||
if s == t {
|
||||
return s, true
|
||||
}
|
||||
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 Range{s.Start, t.Stop}, true // s intersects or touches t
|
||||
}
|
||||
return union, false
|
||||
}
|
||||
// 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 union, false
|
||||
}
|
||||
|
||||
// String returns range value s as a seq-number or seq-range string.
|
||||
func (s Range) 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))
|
||||
}
|
||||
|
||||
func (s Range) append(nums []uint32) (out []uint32, ok bool) {
|
||||
if s.Start == 0 || s.Stop == 0 {
|
||||
return nil, false
|
||||
}
|
||||
for n := s.Start; n <= s.Stop; n++ {
|
||||
nums = append(nums, n)
|
||||
}
|
||||
return nums, true
|
||||
}
|
||||
|
||||
// Set 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 Set []Range
|
||||
|
||||
// AddNum inserts new numbers into the set. The value 0 represents "*".
|
||||
func (s *Set) AddNum(q ...uint32) {
|
||||
for _, v := range q {
|
||||
s.insert(Range{v, v})
|
||||
}
|
||||
}
|
||||
|
||||
// AddRange inserts a new range into the set.
|
||||
func (s *Set) AddRange(start, stop uint32) {
|
||||
if (stop < start && stop != 0) || start == 0 {
|
||||
s.insert(Range{stop, start})
|
||||
} else {
|
||||
s.insert(Range{start, stop})
|
||||
}
|
||||
}
|
||||
|
||||
// AddSet inserts all values from t into s.
|
||||
func (s *Set) AddSet(t Set) {
|
||||
for _, v := range t {
|
||||
s.insert(v)
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamic returns true if the set contains "*" or "n:*" values.
|
||||
func (s Set) Dynamic() bool {
|
||||
return len(s) > 0 && s[len(s)-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 Set) Contains(q uint32) bool {
|
||||
if _, ok := s.search(q); ok {
|
||||
return q != 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Nums returns a slice of all numbers contained in the set.
|
||||
func (s Set) Nums() (nums []uint32, ok bool) {
|
||||
for _, v := range s {
|
||||
nums, ok = v.append(nums)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
return nums, true
|
||||
}
|
||||
|
||||
// String returns a sorted representation of all contained number values.
|
||||
func (s Set) String() string {
|
||||
if len(s) == 0 {
|
||||
return ""
|
||||
}
|
||||
b := make([]byte, 0, 64)
|
||||
for _, v := range s {
|
||||
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 range value v to the set.
|
||||
func (ptr *Set) insert(v Range) {
|
||||
s := *ptr
|
||||
defer func() {
|
||||
*ptr = s
|
||||
}()
|
||||
|
||||
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[i-1], merged = s[i-1].Merge(v)
|
||||
}
|
||||
if i == len(s) {
|
||||
// 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[i], merged = s[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[i], continue trying to merge until the end
|
||||
for j := i + 1; j < len(s); j++ {
|
||||
if s[i], merged = s[i].Merge(s[j]); !merged {
|
||||
if j > i+1 {
|
||||
// cut out all entries between i and j that were merged
|
||||
s = append(s[:i+1], s[j:]...)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
// everything after s[i] was merged
|
||||
s = s[:i+1]
|
||||
}
|
||||
|
||||
// insertAt inserts a new range value v at index i, resizing s.Set as needed.
|
||||
func (ptr *Set) insertAt(i int, v Range) {
|
||||
s := *ptr
|
||||
defer func() {
|
||||
*ptr = s
|
||||
}()
|
||||
|
||||
if n := len(s); i == n {
|
||||
// insert at the end
|
||||
s = append(s, v)
|
||||
return
|
||||
} else if n < cap(s) {
|
||||
// enough space, shift everything at and after i to the right
|
||||
s = s[:n+1]
|
||||
copy(s[i+1:], s[i:])
|
||||
} else {
|
||||
// allocate new slice and copy everything, n is at least 1
|
||||
set := make([]Range, n+1, n*2)
|
||||
copy(set, s[:i])
|
||||
copy(set[i+1:], s[i:])
|
||||
s = set
|
||||
}
|
||||
s[i] = v
|
||||
}
|
||||
|
||||
// search attempts to find the index of the range 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 Set) search(q uint32) (i int, ok bool) {
|
||||
min, max := 0, len(s)-1
|
||||
for min < max {
|
||||
if mid := (min + max) >> 1; s[mid].Less(q) {
|
||||
min = mid + 1
|
||||
} else {
|
||||
max = mid
|
||||
}
|
||||
}
|
||||
if max < 0 || s[min].Less(q) {
|
||||
return len(s), false // q is the new largest value
|
||||
}
|
||||
return min, s[min].Contains(q)
|
||||
}
|
||||
|
||||
// errBadNumSet is used to report problems with the format of a number set
|
||||
// value.
|
||||
type errBadNumSet string
|
||||
|
||||
func (err errBadNumSet) Error() string {
|
||||
return fmt.Sprintf("imap: bad number set value %q", string(err))
|
||||
}
|
||||
|
||||
// parseNum parses a single seq-number value (non-zero uint32 or "*").
|
||||
func parseNum(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, errBadNumSet(v)
|
||||
}
|
||||
|
||||
// parseNumRange 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 parseNumRange(v string) (Range, error) {
|
||||
var (
|
||||
r Range
|
||||
err error
|
||||
)
|
||||
if sep := strings.IndexRune(v, ':'); sep < 0 {
|
||||
r.Start, err = parseNum(v)
|
||||
r.Stop = r.Start
|
||||
return r, err
|
||||
} else if r.Start, err = parseNum(v[:sep]); err == nil {
|
||||
if r.Stop, err = parseNum(v[sep+1:]); err == nil {
|
||||
if (r.Stop < r.Start && r.Stop != 0) || r.Start == 0 {
|
||||
r.Start, r.Stop = r.Stop, r.Start
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
}
|
||||
return r, errBadNumSet(v)
|
||||
}
|
||||
|
||||
// ParseSet returns a new Set after parsing the set string.
|
||||
func ParseSet(set string) (Set, error) {
|
||||
var s Set
|
||||
for _, sv := range strings.Split(set, ",") {
|
||||
r, err := parseNumRange(sv)
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
s.AddRange(r.Start, r.Stop)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
654
vendor/github.com/emersion/go-imap/v2/internal/imapwire/decoder.go
generated
vendored
Normal file
654
vendor/github.com/emersion/go-imap/v2/internal/imapwire/decoder.go
generated
vendored
Normal file
@@ -0,0 +1,654 @@
|
||||
package imapwire
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapnum"
|
||||
"github.com/emersion/go-imap/v2/internal/utf7"
|
||||
)
|
||||
|
||||
// This limits the max list nesting depth to prevent stack overflow.
|
||||
const maxListDepth = 1000
|
||||
|
||||
// IsAtomChar returns true if ch is an ATOM-CHAR.
|
||||
func IsAtomChar(ch byte) bool {
|
||||
switch ch {
|
||||
case '(', ')', '{', ' ', '%', '*', '"', '\\', ']':
|
||||
return false
|
||||
default:
|
||||
return !unicode.IsControl(rune(ch))
|
||||
}
|
||||
}
|
||||
|
||||
// Is non-empty char
|
||||
func isAStringChar(ch byte) bool {
|
||||
return IsAtomChar(ch) || ch == ']'
|
||||
}
|
||||
|
||||
// DecoderExpectError is an error due to the Decoder.Expect family of methods.
|
||||
type DecoderExpectError struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
func (err *DecoderExpectError) Error() string {
|
||||
return fmt.Sprintf("imapwire: %v", err.Message)
|
||||
}
|
||||
|
||||
// A Decoder reads IMAP data.
|
||||
//
|
||||
// There are multiple families of methods:
|
||||
//
|
||||
// - Methods directly named after IMAP grammar elements attempt to decode
|
||||
// said element, and return false if it's another element.
|
||||
// - "Expect" methods do the same, but set the decoder error (see Err) on
|
||||
// failure.
|
||||
type Decoder struct {
|
||||
// CheckBufferedLiteralFunc is called when a literal is about to be decoded
|
||||
// and needs to be fully buffered in memory.
|
||||
CheckBufferedLiteralFunc func(size int64, nonSync bool) error
|
||||
// MaxSize defines a maximum number of bytes to be read from the input.
|
||||
// Literals are ignored.
|
||||
MaxSize int64
|
||||
|
||||
r *bufio.Reader
|
||||
side ConnSide
|
||||
err error
|
||||
literal bool
|
||||
crlf bool
|
||||
listDepth int
|
||||
readBytes int64
|
||||
}
|
||||
|
||||
// NewDecoder creates a new decoder.
|
||||
func NewDecoder(r *bufio.Reader, side ConnSide) *Decoder {
|
||||
return &Decoder{r: r, side: side}
|
||||
}
|
||||
|
||||
func (dec *Decoder) mustUnreadByte() {
|
||||
if err := dec.r.UnreadByte(); err != nil {
|
||||
panic(fmt.Errorf("imapwire: failed to unread byte: %v", err))
|
||||
}
|
||||
dec.readBytes--
|
||||
}
|
||||
|
||||
// Err returns the decoder error, if any.
|
||||
func (dec *Decoder) Err() error {
|
||||
return dec.err
|
||||
}
|
||||
|
||||
func (dec *Decoder) returnErr(err error) bool {
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
if dec.err == nil {
|
||||
dec.err = err
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (dec *Decoder) readByte() (byte, bool) {
|
||||
if dec.MaxSize > 0 && dec.readBytes > dec.MaxSize {
|
||||
return 0, dec.returnErr(fmt.Errorf("imapwire: max size exceeded"))
|
||||
}
|
||||
dec.crlf = false
|
||||
if dec.literal {
|
||||
return 0, dec.returnErr(fmt.Errorf("imapwire: cannot decode while a literal is open"))
|
||||
}
|
||||
b, err := dec.r.ReadByte()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
return b, dec.returnErr(err)
|
||||
}
|
||||
dec.readBytes++
|
||||
return b, true
|
||||
}
|
||||
|
||||
func (dec *Decoder) acceptByte(want byte) bool {
|
||||
got, ok := dec.readByte()
|
||||
if !ok {
|
||||
return false
|
||||
} else if got != want {
|
||||
dec.mustUnreadByte()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// EOF returns true if end-of-file is reached.
|
||||
func (dec *Decoder) EOF() bool {
|
||||
_, err := dec.r.ReadByte()
|
||||
if err == io.EOF {
|
||||
return true
|
||||
} else if err != nil {
|
||||
return dec.returnErr(err)
|
||||
}
|
||||
dec.mustUnreadByte()
|
||||
return false
|
||||
}
|
||||
|
||||
// Expect sets the decoder error if ok is false.
|
||||
func (dec *Decoder) Expect(ok bool, name string) bool {
|
||||
if !ok {
|
||||
msg := fmt.Sprintf("expected %v", name)
|
||||
if dec.r.Buffered() > 0 {
|
||||
b, _ := dec.r.Peek(1)
|
||||
msg += fmt.Sprintf(", got %q", b)
|
||||
}
|
||||
return dec.returnErr(&DecoderExpectError{Message: msg})
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (dec *Decoder) SP() bool {
|
||||
if dec.acceptByte(' ') {
|
||||
// https://github.com/emersion/go-imap/issues/571
|
||||
b, ok := dec.readByte()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
dec.mustUnreadByte()
|
||||
return b != '\r' && b != '\n'
|
||||
}
|
||||
|
||||
// Special case: SP is optional if the next field is a parenthesized list
|
||||
b, ok := dec.readByte()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
dec.mustUnreadByte()
|
||||
return b == '('
|
||||
}
|
||||
|
||||
func (dec *Decoder) ExpectSP() bool {
|
||||
return dec.Expect(dec.SP(), "SP")
|
||||
}
|
||||
|
||||
func (dec *Decoder) CRLF() bool {
|
||||
dec.acceptByte(' ') // https://github.com/emersion/go-imap/issues/540
|
||||
dec.acceptByte('\r') // be liberal in what we receive and accept lone LF
|
||||
if !dec.acceptByte('\n') {
|
||||
return false
|
||||
}
|
||||
dec.crlf = true
|
||||
return true
|
||||
}
|
||||
|
||||
func (dec *Decoder) ExpectCRLF() bool {
|
||||
return dec.Expect(dec.CRLF(), "CRLF")
|
||||
}
|
||||
|
||||
func (dec *Decoder) Func(ptr *string, valid func(ch byte) bool) bool {
|
||||
var sb strings.Builder
|
||||
for {
|
||||
b, ok := dec.readByte()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if !valid(b) {
|
||||
dec.mustUnreadByte()
|
||||
break
|
||||
}
|
||||
|
||||
sb.WriteByte(b)
|
||||
}
|
||||
if sb.Len() == 0 {
|
||||
return false
|
||||
}
|
||||
*ptr = sb.String()
|
||||
return true
|
||||
}
|
||||
|
||||
func (dec *Decoder) Atom(ptr *string) bool {
|
||||
return dec.Func(ptr, IsAtomChar)
|
||||
}
|
||||
|
||||
func (dec *Decoder) ExpectAtom(ptr *string) bool {
|
||||
return dec.Expect(dec.Atom(ptr), "atom")
|
||||
}
|
||||
|
||||
func (dec *Decoder) ExpectNIL() bool {
|
||||
var s string
|
||||
return dec.ExpectAtom(&s) && dec.Expect(s == "NIL", "NIL")
|
||||
}
|
||||
|
||||
func (dec *Decoder) Special(b byte) bool {
|
||||
return dec.acceptByte(b)
|
||||
}
|
||||
|
||||
func (dec *Decoder) ExpectSpecial(b byte) bool {
|
||||
return dec.Expect(dec.Special(b), fmt.Sprintf("'%v'", string(b)))
|
||||
}
|
||||
|
||||
func (dec *Decoder) Text(ptr *string) bool {
|
||||
var sb strings.Builder
|
||||
for {
|
||||
b, ok := dec.readByte()
|
||||
if !ok {
|
||||
return false
|
||||
} else if b == '\r' || b == '\n' {
|
||||
dec.mustUnreadByte()
|
||||
break
|
||||
}
|
||||
sb.WriteByte(b)
|
||||
}
|
||||
if sb.Len() == 0 {
|
||||
return false
|
||||
}
|
||||
*ptr = sb.String()
|
||||
return true
|
||||
}
|
||||
|
||||
func (dec *Decoder) ExpectText(ptr *string) bool {
|
||||
return dec.Expect(dec.Text(ptr), "text")
|
||||
}
|
||||
|
||||
func (dec *Decoder) DiscardUntilByte(untilCh byte) {
|
||||
for {
|
||||
ch, ok := dec.readByte()
|
||||
if !ok {
|
||||
return
|
||||
} else if ch == untilCh {
|
||||
dec.mustUnreadByte()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (dec *Decoder) DiscardLine() {
|
||||
if dec.crlf {
|
||||
return
|
||||
}
|
||||
var text string
|
||||
dec.Text(&text)
|
||||
dec.CRLF()
|
||||
}
|
||||
|
||||
func (dec *Decoder) DiscardValue() bool {
|
||||
var s string
|
||||
if dec.String(&s) {
|
||||
return true
|
||||
}
|
||||
|
||||
isList, err := dec.List(func() error {
|
||||
if !dec.DiscardValue() {
|
||||
return dec.Err()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return false
|
||||
} else if isList {
|
||||
return true
|
||||
}
|
||||
|
||||
if dec.Atom(&s) {
|
||||
return true
|
||||
}
|
||||
|
||||
dec.Expect(false, "value")
|
||||
return false
|
||||
}
|
||||
|
||||
func (dec *Decoder) numberStr() (s string, ok bool) {
|
||||
var sb strings.Builder
|
||||
for {
|
||||
ch, ok := dec.readByte()
|
||||
if !ok {
|
||||
return "", false
|
||||
} else if ch < '0' || ch > '9' {
|
||||
dec.mustUnreadByte()
|
||||
break
|
||||
}
|
||||
sb.WriteByte(ch)
|
||||
}
|
||||
if sb.Len() == 0 {
|
||||
return "", false
|
||||
}
|
||||
return sb.String(), true
|
||||
}
|
||||
|
||||
func (dec *Decoder) Number(ptr *uint32) bool {
|
||||
s, ok := dec.numberStr()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
v64, err := strconv.ParseUint(s, 10, 32)
|
||||
if err != nil {
|
||||
return false // can happen on overflow
|
||||
}
|
||||
*ptr = uint32(v64)
|
||||
return true
|
||||
}
|
||||
|
||||
func (dec *Decoder) ExpectNumber(ptr *uint32) bool {
|
||||
return dec.Expect(dec.Number(ptr), "number")
|
||||
}
|
||||
|
||||
func (dec *Decoder) ExpectBodyFldOctets(ptr *uint32) bool {
|
||||
// Workaround: some servers incorrectly return "-1" for the body structure
|
||||
// size. See:
|
||||
// https://github.com/emersion/go-imap/issues/534
|
||||
if dec.acceptByte('-') {
|
||||
*ptr = 0
|
||||
return dec.Expect(dec.acceptByte('1'), "-1 (body-fld-octets workaround)")
|
||||
}
|
||||
return dec.ExpectNumber(ptr)
|
||||
}
|
||||
|
||||
func (dec *Decoder) Number64(ptr *int64) bool {
|
||||
s, ok := dec.numberStr()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
v, err := strconv.ParseInt(s, 10, 64)
|
||||
if err != nil {
|
||||
return false // can happen on overflow
|
||||
}
|
||||
*ptr = v
|
||||
return true
|
||||
}
|
||||
|
||||
func (dec *Decoder) ExpectNumber64(ptr *int64) bool {
|
||||
return dec.Expect(dec.Number64(ptr), "number64")
|
||||
}
|
||||
|
||||
func (dec *Decoder) ModSeq(ptr *uint64) bool {
|
||||
s, ok := dec.numberStr()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
v, err := strconv.ParseUint(s, 10, 64)
|
||||
if err != nil {
|
||||
return false // can happen on overflow
|
||||
}
|
||||
*ptr = v
|
||||
return true
|
||||
}
|
||||
|
||||
func (dec *Decoder) ExpectModSeq(ptr *uint64) bool {
|
||||
return dec.Expect(dec.ModSeq(ptr), "mod-sequence-value")
|
||||
}
|
||||
|
||||
func (dec *Decoder) Quoted(ptr *string) bool {
|
||||
if !dec.Special('"') {
|
||||
return false
|
||||
}
|
||||
var sb strings.Builder
|
||||
for {
|
||||
ch, ok := dec.readByte()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if ch == '"' {
|
||||
break
|
||||
}
|
||||
|
||||
if ch == '\\' {
|
||||
ch, ok = dec.readByte()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteByte(ch)
|
||||
}
|
||||
*ptr = sb.String()
|
||||
return true
|
||||
}
|
||||
|
||||
func (dec *Decoder) ExpectAString(ptr *string) bool {
|
||||
if dec.Quoted(ptr) {
|
||||
return true
|
||||
}
|
||||
if dec.Literal(ptr) {
|
||||
return true
|
||||
}
|
||||
// We cannot do dec.Atom(ptr) here because sometimes mailbox names are unquoted,
|
||||
// and they can contain special characters like `]`.
|
||||
return dec.Expect(dec.Func(ptr, isAStringChar), "ASTRING-CHAR")
|
||||
}
|
||||
|
||||
func (dec *Decoder) String(ptr *string) bool {
|
||||
return dec.Quoted(ptr) || dec.Literal(ptr)
|
||||
}
|
||||
|
||||
func (dec *Decoder) ExpectString(ptr *string) bool {
|
||||
return dec.Expect(dec.String(ptr), "string")
|
||||
}
|
||||
|
||||
func (dec *Decoder) ExpectNString(ptr *string) bool {
|
||||
var s string
|
||||
if dec.Atom(&s) {
|
||||
if !dec.Expect(s == "NIL", "nstring") {
|
||||
return false
|
||||
}
|
||||
*ptr = ""
|
||||
return true
|
||||
}
|
||||
return dec.ExpectString(ptr)
|
||||
}
|
||||
|
||||
func (dec *Decoder) ExpectNStringReader() (lit *LiteralReader, nonSync, ok bool) {
|
||||
var s string
|
||||
if dec.Atom(&s) {
|
||||
if !dec.Expect(s == "NIL", "nstring") {
|
||||
return nil, false, false
|
||||
}
|
||||
return nil, true, true
|
||||
}
|
||||
// TODO: read quoted string as a string instead of buffering
|
||||
if dec.Quoted(&s) {
|
||||
return newLiteralReaderFromString(s), true, true
|
||||
}
|
||||
if lit, nonSync, ok = dec.LiteralReader(); ok {
|
||||
return lit, nonSync, true
|
||||
} else {
|
||||
return nil, false, dec.Expect(false, "nstring")
|
||||
}
|
||||
}
|
||||
|
||||
func (dec *Decoder) List(f func() error) (isList bool, err error) {
|
||||
if !dec.Special('(') {
|
||||
return false, nil
|
||||
}
|
||||
if dec.Special(')') {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
dec.listDepth++
|
||||
defer func() {
|
||||
dec.listDepth--
|
||||
}()
|
||||
|
||||
if dec.listDepth >= maxListDepth {
|
||||
return false, fmt.Errorf("imapwire: exceeded max depth")
|
||||
}
|
||||
|
||||
for {
|
||||
if err := f(); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
if dec.Special(')') {
|
||||
return true, nil
|
||||
} else if !dec.ExpectSP() {
|
||||
return true, dec.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (dec *Decoder) ExpectList(f func() error) error {
|
||||
isList, err := dec.List(f)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !dec.Expect(isList, "(") {
|
||||
return dec.Err()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dec *Decoder) ExpectNList(f func() error) error {
|
||||
var s string
|
||||
if dec.Atom(&s) {
|
||||
if !dec.Expect(s == "NIL", "NIL") {
|
||||
return dec.Err()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return dec.ExpectList(f)
|
||||
}
|
||||
|
||||
func (dec *Decoder) ExpectMailbox(ptr *string) bool {
|
||||
var name string
|
||||
if !dec.ExpectAString(&name) {
|
||||
return false
|
||||
}
|
||||
if strings.EqualFold(name, "INBOX") {
|
||||
*ptr = "INBOX"
|
||||
return true
|
||||
}
|
||||
name, err := utf7.Decode(name)
|
||||
if err == nil {
|
||||
*ptr = name
|
||||
}
|
||||
return dec.returnErr(err)
|
||||
}
|
||||
|
||||
func (dec *Decoder) ExpectUID(ptr *imap.UID) bool {
|
||||
var num uint32
|
||||
if !dec.ExpectNumber(&num) {
|
||||
return false
|
||||
}
|
||||
*ptr = imap.UID(num)
|
||||
return true
|
||||
}
|
||||
|
||||
func (dec *Decoder) ExpectNumSet(kind NumKind, ptr *imap.NumSet) bool {
|
||||
if dec.Special('$') {
|
||||
*ptr = imap.SearchRes()
|
||||
return true
|
||||
}
|
||||
|
||||
var s string
|
||||
if !dec.Expect(dec.Func(&s, isNumSetChar), "sequence-set") {
|
||||
return false
|
||||
}
|
||||
numSet, err := imapnum.ParseSet(s)
|
||||
if err != nil {
|
||||
return dec.returnErr(err)
|
||||
}
|
||||
|
||||
switch kind {
|
||||
case NumKindSeq:
|
||||
*ptr = seqSetFromNumSet(numSet)
|
||||
case NumKindUID:
|
||||
*ptr = uidSetFromNumSet(numSet)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (dec *Decoder) ExpectUIDSet(ptr *imap.UIDSet) bool {
|
||||
var numSet imap.NumSet
|
||||
ok := dec.ExpectNumSet(NumKindUID, &numSet)
|
||||
if ok {
|
||||
*ptr = numSet.(imap.UIDSet)
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
func isNumSetChar(ch byte) bool {
|
||||
return ch == '*' || IsAtomChar(ch)
|
||||
}
|
||||
|
||||
func (dec *Decoder) Literal(ptr *string) bool {
|
||||
lit, nonSync, ok := dec.LiteralReader()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if dec.CheckBufferedLiteralFunc != nil {
|
||||
if err := dec.CheckBufferedLiteralFunc(lit.Size(), nonSync); err != nil {
|
||||
lit.cancel()
|
||||
return false
|
||||
}
|
||||
}
|
||||
var sb strings.Builder
|
||||
_, err := io.Copy(&sb, lit)
|
||||
if err == nil {
|
||||
*ptr = sb.String()
|
||||
}
|
||||
return dec.returnErr(err)
|
||||
}
|
||||
|
||||
func (dec *Decoder) LiteralReader() (lit *LiteralReader, nonSync, ok bool) {
|
||||
if !dec.Special('{') {
|
||||
return nil, false, false
|
||||
}
|
||||
var size int64
|
||||
if !dec.ExpectNumber64(&size) {
|
||||
return nil, false, false
|
||||
}
|
||||
if dec.side == ConnSideServer {
|
||||
nonSync = dec.acceptByte('+')
|
||||
}
|
||||
if !dec.ExpectSpecial('}') || !dec.ExpectCRLF() {
|
||||
return nil, false, false
|
||||
}
|
||||
dec.literal = true
|
||||
lit = &LiteralReader{
|
||||
dec: dec,
|
||||
size: size,
|
||||
r: io.LimitReader(dec.r, size),
|
||||
}
|
||||
return lit, nonSync, true
|
||||
}
|
||||
|
||||
func (dec *Decoder) ExpectLiteralReader() (lit *LiteralReader, nonSync bool, err error) {
|
||||
lit, nonSync, ok := dec.LiteralReader()
|
||||
if !dec.Expect(ok, "literal") {
|
||||
return nil, false, dec.Err()
|
||||
}
|
||||
return lit, nonSync, nil
|
||||
}
|
||||
|
||||
type LiteralReader struct {
|
||||
dec *Decoder
|
||||
size int64
|
||||
r io.Reader
|
||||
}
|
||||
|
||||
func newLiteralReaderFromString(s string) *LiteralReader {
|
||||
return &LiteralReader{
|
||||
size: int64(len(s)),
|
||||
r: strings.NewReader(s),
|
||||
}
|
||||
}
|
||||
|
||||
func (lit *LiteralReader) Size() int64 {
|
||||
return lit.size
|
||||
}
|
||||
|
||||
func (lit *LiteralReader) Read(b []byte) (int, error) {
|
||||
n, err := lit.r.Read(b)
|
||||
if err == io.EOF {
|
||||
lit.cancel()
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (lit *LiteralReader) cancel() {
|
||||
if lit.dec == nil {
|
||||
return
|
||||
}
|
||||
lit.dec.literal = false
|
||||
lit.dec = nil
|
||||
}
|
||||
341
vendor/github.com/emersion/go-imap/v2/internal/imapwire/encoder.go
generated
vendored
Normal file
341
vendor/github.com/emersion/go-imap/v2/internal/imapwire/encoder.go
generated
vendored
Normal file
@@ -0,0 +1,341 @@
|
||||
package imapwire
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/utf7"
|
||||
)
|
||||
|
||||
// An Encoder writes IMAP data.
|
||||
//
|
||||
// Most methods don't return an error, instead they defer error handling until
|
||||
// CRLF is called. These methods return the Encoder so that calls can be
|
||||
// chained.
|
||||
type Encoder struct {
|
||||
// QuotedUTF8 allows raw UTF-8 in quoted strings. This requires IMAP4rev2
|
||||
// to be available, or UTF8=ACCEPT to be enabled.
|
||||
QuotedUTF8 bool
|
||||
// LiteralMinus enables non-synchronizing literals for short payloads.
|
||||
// This requires IMAP4rev2 or LITERAL-. This is only meaningful for
|
||||
// clients.
|
||||
LiteralMinus bool
|
||||
// LiteralPlus enables non-synchronizing literals for all payloads. This
|
||||
// requires LITERAL+. This is only meaningful for clients.
|
||||
LiteralPlus bool
|
||||
// NewContinuationRequest creates a new continuation request. This is only
|
||||
// meaningful for clients.
|
||||
NewContinuationRequest func() *ContinuationRequest
|
||||
|
||||
w *bufio.Writer
|
||||
side ConnSide
|
||||
err error
|
||||
literal bool
|
||||
}
|
||||
|
||||
// NewEncoder creates a new encoder.
|
||||
func NewEncoder(w *bufio.Writer, side ConnSide) *Encoder {
|
||||
return &Encoder{w: w, side: side}
|
||||
}
|
||||
|
||||
func (enc *Encoder) setErr(err error) {
|
||||
if enc.err == nil {
|
||||
enc.err = err
|
||||
}
|
||||
}
|
||||
|
||||
func (enc *Encoder) writeString(s string) *Encoder {
|
||||
if enc.err != nil {
|
||||
return enc
|
||||
}
|
||||
if enc.literal {
|
||||
enc.err = fmt.Errorf("imapwire: cannot encode while a literal is open")
|
||||
return enc
|
||||
}
|
||||
if _, err := enc.w.WriteString(s); err != nil {
|
||||
enc.err = err
|
||||
}
|
||||
return enc
|
||||
}
|
||||
|
||||
// CRLF writes a "\r\n" sequence and flushes the buffered writer.
|
||||
func (enc *Encoder) CRLF() error {
|
||||
enc.writeString("\r\n")
|
||||
if enc.err != nil {
|
||||
return enc.err
|
||||
}
|
||||
return enc.w.Flush()
|
||||
}
|
||||
|
||||
func (enc *Encoder) Atom(s string) *Encoder {
|
||||
return enc.writeString(s)
|
||||
}
|
||||
|
||||
func (enc *Encoder) SP() *Encoder {
|
||||
return enc.writeString(" ")
|
||||
}
|
||||
|
||||
func (enc *Encoder) Special(ch byte) *Encoder {
|
||||
return enc.writeString(string(ch))
|
||||
}
|
||||
|
||||
func (enc *Encoder) Quoted(s string) *Encoder {
|
||||
var sb strings.Builder
|
||||
sb.Grow(2 + len(s))
|
||||
sb.WriteByte('"')
|
||||
for i := 0; i < len(s); i++ {
|
||||
ch := s[i]
|
||||
if ch == '"' || ch == '\\' {
|
||||
sb.WriteByte('\\')
|
||||
}
|
||||
sb.WriteByte(ch)
|
||||
}
|
||||
sb.WriteByte('"')
|
||||
return enc.writeString(sb.String())
|
||||
}
|
||||
|
||||
func (enc *Encoder) String(s string) *Encoder {
|
||||
if !enc.validQuoted(s) {
|
||||
enc.stringLiteral(s)
|
||||
return enc
|
||||
}
|
||||
return enc.Quoted(s)
|
||||
}
|
||||
|
||||
func (enc *Encoder) validQuoted(s string) bool {
|
||||
if len(s) > 4096 {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := 0; i < len(s); i++ {
|
||||
ch := s[i]
|
||||
|
||||
// NUL, CR and LF are never valid
|
||||
switch ch {
|
||||
case 0, '\r', '\n':
|
||||
return false
|
||||
}
|
||||
|
||||
if !enc.QuotedUTF8 && ch > unicode.MaxASCII {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (enc *Encoder) stringLiteral(s string) {
|
||||
var sync *ContinuationRequest
|
||||
if enc.side == ConnSideClient && (!enc.LiteralMinus || len(s) > 4096) && !enc.LiteralPlus {
|
||||
if enc.NewContinuationRequest != nil {
|
||||
sync = enc.NewContinuationRequest()
|
||||
}
|
||||
if sync == nil {
|
||||
enc.setErr(fmt.Errorf("imapwire: cannot send synchronizing literal"))
|
||||
return
|
||||
}
|
||||
}
|
||||
wc := enc.Literal(int64(len(s)), sync)
|
||||
_, writeErr := io.WriteString(wc, s)
|
||||
closeErr := wc.Close()
|
||||
if writeErr != nil {
|
||||
enc.setErr(writeErr)
|
||||
} else if closeErr != nil {
|
||||
enc.setErr(closeErr)
|
||||
}
|
||||
}
|
||||
|
||||
func (enc *Encoder) Mailbox(name string) *Encoder {
|
||||
if strings.EqualFold(name, "INBOX") {
|
||||
return enc.Atom("INBOX")
|
||||
} else {
|
||||
if enc.QuotedUTF8 {
|
||||
name = utf7.Escape(name)
|
||||
} else {
|
||||
name = utf7.Encode(name)
|
||||
}
|
||||
return enc.String(name)
|
||||
}
|
||||
}
|
||||
|
||||
func (enc *Encoder) NumSet(numSet imap.NumSet) *Encoder {
|
||||
s := numSet.String()
|
||||
if s == "" {
|
||||
enc.setErr(fmt.Errorf("imapwire: cannot encode empty sequence set"))
|
||||
return enc
|
||||
}
|
||||
return enc.writeString(s)
|
||||
}
|
||||
|
||||
func (enc *Encoder) Flag(flag imap.Flag) *Encoder {
|
||||
if flag != "\\*" && !isValidFlag(string(flag)) {
|
||||
enc.setErr(fmt.Errorf("imapwire: invalid flag %q", flag))
|
||||
return enc
|
||||
}
|
||||
return enc.writeString(string(flag))
|
||||
}
|
||||
|
||||
func (enc *Encoder) MailboxAttr(attr imap.MailboxAttr) *Encoder {
|
||||
if !strings.HasPrefix(string(attr), "\\") || !isValidFlag(string(attr)) {
|
||||
enc.setErr(fmt.Errorf("imapwire: invalid mailbox attribute %q", attr))
|
||||
return enc
|
||||
}
|
||||
return enc.writeString(string(attr))
|
||||
}
|
||||
|
||||
// isValidFlag checks whether the provided string satisfies
|
||||
// flag-keyword / flag-extension.
|
||||
func isValidFlag(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
ch := s[i]
|
||||
if ch == '\\' {
|
||||
if i != 0 {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if !IsAtomChar(ch) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return len(s) > 0
|
||||
}
|
||||
|
||||
func (enc *Encoder) Number(v uint32) *Encoder {
|
||||
return enc.writeString(strconv.FormatUint(uint64(v), 10))
|
||||
}
|
||||
|
||||
func (enc *Encoder) Number64(v int64) *Encoder {
|
||||
// TODO: disallow negative values
|
||||
return enc.writeString(strconv.FormatInt(v, 10))
|
||||
}
|
||||
|
||||
func (enc *Encoder) ModSeq(v uint64) *Encoder {
|
||||
// TODO: disallow zero values
|
||||
return enc.writeString(strconv.FormatUint(v, 10))
|
||||
}
|
||||
|
||||
// List writes a parenthesized list.
|
||||
func (enc *Encoder) List(n int, f func(i int)) *Encoder {
|
||||
enc.Special('(')
|
||||
for i := 0; i < n; i++ {
|
||||
if i > 0 {
|
||||
enc.SP()
|
||||
}
|
||||
f(i)
|
||||
}
|
||||
enc.Special(')')
|
||||
return enc
|
||||
}
|
||||
|
||||
func (enc *Encoder) BeginList() *ListEncoder {
|
||||
enc.Special('(')
|
||||
return &ListEncoder{enc: enc}
|
||||
}
|
||||
|
||||
func (enc *Encoder) NIL() *Encoder {
|
||||
return enc.Atom("NIL")
|
||||
}
|
||||
|
||||
func (enc *Encoder) Text(s string) *Encoder {
|
||||
return enc.writeString(s)
|
||||
}
|
||||
|
||||
func (enc *Encoder) UID(uid imap.UID) *Encoder {
|
||||
return enc.Number(uint32(uid))
|
||||
}
|
||||
|
||||
// Literal writes a literal.
|
||||
//
|
||||
// The caller must write exactly size bytes to the returned writer.
|
||||
//
|
||||
// If sync is non-nil, the literal is synchronizing: the encoder will wait for
|
||||
// nil to be sent to the channel before writing the literal data. If an error
|
||||
// is sent to the channel, the literal will be cancelled.
|
||||
func (enc *Encoder) Literal(size int64, sync *ContinuationRequest) io.WriteCloser {
|
||||
if sync != nil && enc.side == ConnSideServer {
|
||||
panic("imapwire: sync must be nil on a server-side Encoder.Literal")
|
||||
}
|
||||
|
||||
// TODO: literal8
|
||||
enc.writeString("{")
|
||||
enc.Number64(size)
|
||||
if sync == nil && enc.side == ConnSideClient {
|
||||
enc.writeString("+")
|
||||
}
|
||||
enc.writeString("}")
|
||||
|
||||
if sync == nil {
|
||||
enc.writeString("\r\n")
|
||||
} else {
|
||||
if err := enc.CRLF(); err != nil {
|
||||
return errorWriter{err}
|
||||
}
|
||||
if _, err := sync.Wait(); err != nil {
|
||||
enc.setErr(err)
|
||||
return errorWriter{err}
|
||||
}
|
||||
}
|
||||
|
||||
enc.literal = true
|
||||
return &literalWriter{
|
||||
enc: enc,
|
||||
n: size,
|
||||
}
|
||||
}
|
||||
|
||||
type errorWriter struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (ew errorWriter) Write(b []byte) (int, error) {
|
||||
return 0, ew.err
|
||||
}
|
||||
|
||||
func (ew errorWriter) Close() error {
|
||||
return ew.err
|
||||
}
|
||||
|
||||
type literalWriter struct {
|
||||
enc *Encoder
|
||||
n int64
|
||||
}
|
||||
|
||||
func (lw *literalWriter) Write(b []byte) (int, error) {
|
||||
if lw.n-int64(len(b)) < 0 {
|
||||
return 0, fmt.Errorf("wrote too many bytes in literal")
|
||||
}
|
||||
n, err := lw.enc.w.Write(b)
|
||||
lw.n -= int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (lw *literalWriter) Close() error {
|
||||
lw.enc.literal = false
|
||||
if lw.n != 0 {
|
||||
return fmt.Errorf("wrote too few bytes in literal (%v remaining)", lw.n)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ListEncoder struct {
|
||||
enc *Encoder
|
||||
n int
|
||||
}
|
||||
|
||||
func (le *ListEncoder) Item() *Encoder {
|
||||
if le.n > 0 {
|
||||
le.enc.SP()
|
||||
}
|
||||
le.n++
|
||||
return le.enc
|
||||
}
|
||||
|
||||
func (le *ListEncoder) End() {
|
||||
le.enc.Special(')')
|
||||
le.enc = nil
|
||||
}
|
||||
47
vendor/github.com/emersion/go-imap/v2/internal/imapwire/imapwire.go
generated
vendored
Normal file
47
vendor/github.com/emersion/go-imap/v2/internal/imapwire/imapwire.go
generated
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
// Package imapwire implements the IMAP wire protocol.
|
||||
//
|
||||
// The IMAP wire protocol is defined in RFC 9051 section 4.
|
||||
package imapwire
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ConnSide describes the side of a connection: client or server.
|
||||
type ConnSide int
|
||||
|
||||
const (
|
||||
ConnSideClient ConnSide = 1 + iota
|
||||
ConnSideServer
|
||||
)
|
||||
|
||||
// ContinuationRequest is a continuation request.
|
||||
//
|
||||
// The sender must call either Done or Cancel. The receiver must call Wait.
|
||||
type ContinuationRequest struct {
|
||||
done chan struct{}
|
||||
err error
|
||||
text string
|
||||
}
|
||||
|
||||
func NewContinuationRequest() *ContinuationRequest {
|
||||
return &ContinuationRequest{done: make(chan struct{})}
|
||||
}
|
||||
|
||||
func (cont *ContinuationRequest) Cancel(err error) {
|
||||
if err == nil {
|
||||
err = fmt.Errorf("imapwire: continuation request cancelled")
|
||||
}
|
||||
cont.err = err
|
||||
close(cont.done)
|
||||
}
|
||||
|
||||
func (cont *ContinuationRequest) Done(text string) {
|
||||
cont.text = text
|
||||
close(cont.done)
|
||||
}
|
||||
|
||||
func (cont *ContinuationRequest) Wait() (string, error) {
|
||||
<-cont.done
|
||||
return cont.text, cont.err
|
||||
}
|
||||
39
vendor/github.com/emersion/go-imap/v2/internal/imapwire/num.go
generated
vendored
Normal file
39
vendor/github.com/emersion/go-imap/v2/internal/imapwire/num.go
generated
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
package imapwire
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapnum"
|
||||
)
|
||||
|
||||
type NumKind int
|
||||
|
||||
const (
|
||||
NumKindSeq NumKind = iota + 1
|
||||
NumKindUID
|
||||
)
|
||||
|
||||
func seqSetFromNumSet(s imapnum.Set) imap.SeqSet {
|
||||
return *(*imap.SeqSet)(unsafe.Pointer(&s))
|
||||
}
|
||||
|
||||
func uidSetFromNumSet(s imapnum.Set) imap.UIDSet {
|
||||
return *(*imap.UIDSet)(unsafe.Pointer(&s))
|
||||
}
|
||||
|
||||
func NumSetKind(numSet imap.NumSet) NumKind {
|
||||
switch numSet.(type) {
|
||||
case imap.SeqSet:
|
||||
return NumKindSeq
|
||||
case imap.UIDSet:
|
||||
return NumKindUID
|
||||
default:
|
||||
panic("imap: invalid NumSet type")
|
||||
}
|
||||
}
|
||||
|
||||
func ParseSeqSet(s string) (imap.SeqSet, error) {
|
||||
numSet, err := imapnum.ParseSet(s)
|
||||
return seqSetFromNumSet(numSet), err
|
||||
}
|
||||
188
vendor/github.com/emersion/go-imap/v2/internal/internal.go
generated
vendored
Normal file
188
vendor/github.com/emersion/go-imap/v2/internal/internal.go
generated
vendored
Normal file
@@ -0,0 +1,188 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
const (
|
||||
DateTimeLayout = "_2-Jan-2006 15:04:05 -0700"
|
||||
DateLayout = "2-Jan-2006"
|
||||
)
|
||||
|
||||
const FlagRecent imap.Flag = "\\Recent" // removed in IMAP4rev2
|
||||
|
||||
func DecodeDateTime(dec *imapwire.Decoder) (time.Time, error) {
|
||||
var s string
|
||||
if !dec.Quoted(&s) {
|
||||
return time.Time{}, nil
|
||||
}
|
||||
t, err := time.Parse(DateTimeLayout, s)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("in date-time: %v", err) // TODO: use imapwire.DecodeExpectError?
|
||||
}
|
||||
return t, err
|
||||
}
|
||||
|
||||
func ExpectDateTime(dec *imapwire.Decoder) (time.Time, error) {
|
||||
t, err := DecodeDateTime(dec)
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
if !dec.Expect(!t.IsZero(), "date-time") {
|
||||
return t, dec.Err()
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func ExpectDate(dec *imapwire.Decoder) (time.Time, error) {
|
||||
var s string
|
||||
if !dec.ExpectAString(&s) {
|
||||
return time.Time{}, dec.Err()
|
||||
}
|
||||
t, err := time.Parse(DateLayout, s)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("in date: %v", err) // use imapwire.DecodeExpectError?
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func ExpectFlagList(dec *imapwire.Decoder) ([]imap.Flag, error) {
|
||||
var flags []imap.Flag
|
||||
err := dec.ExpectList(func() error {
|
||||
// Some servers start the list with a space, so we need to skip it
|
||||
// https://github.com/emersion/go-imap/pull/633
|
||||
dec.SP()
|
||||
|
||||
flag, err := ExpectFlag(dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
flags = append(flags, flag)
|
||||
return nil
|
||||
})
|
||||
return flags, err
|
||||
}
|
||||
|
||||
func ExpectCap(dec *imapwire.Decoder) (imap.Cap, error) {
|
||||
var name string
|
||||
if !dec.ExpectAtom(&name) {
|
||||
return "", dec.Err()
|
||||
}
|
||||
return canonicalCap(name), nil
|
||||
}
|
||||
|
||||
func ExpectFlag(dec *imapwire.Decoder) (imap.Flag, error) {
|
||||
isSystem := dec.Special('\\')
|
||||
if isSystem && dec.Special('*') {
|
||||
return imap.FlagWildcard, nil // flag-perm
|
||||
}
|
||||
var name string
|
||||
if !dec.ExpectAtom(&name) {
|
||||
return "", fmt.Errorf("in flag: %w", dec.Err())
|
||||
}
|
||||
if isSystem {
|
||||
name = "\\" + name
|
||||
}
|
||||
return canonicalFlag(name), nil
|
||||
}
|
||||
|
||||
func ExpectMailboxAttrList(dec *imapwire.Decoder) ([]imap.MailboxAttr, error) {
|
||||
var attrs []imap.MailboxAttr
|
||||
err := dec.ExpectList(func() error {
|
||||
attr, err := ExpectMailboxAttr(dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
attrs = append(attrs, attr)
|
||||
return nil
|
||||
})
|
||||
return attrs, err
|
||||
}
|
||||
|
||||
func ExpectMailboxAttr(dec *imapwire.Decoder) (imap.MailboxAttr, error) {
|
||||
flag, err := ExpectFlag(dec)
|
||||
return canonicalMailboxAttr(string(flag)), err
|
||||
}
|
||||
|
||||
var (
|
||||
canonOnce sync.Once
|
||||
canonFlag map[string]imap.Flag
|
||||
canonMailboxAttr map[string]imap.MailboxAttr
|
||||
)
|
||||
|
||||
func canonInit() {
|
||||
flags := []imap.Flag{
|
||||
imap.FlagSeen,
|
||||
imap.FlagAnswered,
|
||||
imap.FlagFlagged,
|
||||
imap.FlagDeleted,
|
||||
imap.FlagDraft,
|
||||
imap.FlagForwarded,
|
||||
imap.FlagMDNSent,
|
||||
imap.FlagJunk,
|
||||
imap.FlagNotJunk,
|
||||
imap.FlagPhishing,
|
||||
imap.FlagImportant,
|
||||
}
|
||||
mailboxAttrs := []imap.MailboxAttr{
|
||||
imap.MailboxAttrNonExistent,
|
||||
imap.MailboxAttrNoInferiors,
|
||||
imap.MailboxAttrNoSelect,
|
||||
imap.MailboxAttrHasChildren,
|
||||
imap.MailboxAttrHasNoChildren,
|
||||
imap.MailboxAttrMarked,
|
||||
imap.MailboxAttrUnmarked,
|
||||
imap.MailboxAttrSubscribed,
|
||||
imap.MailboxAttrRemote,
|
||||
imap.MailboxAttrAll,
|
||||
imap.MailboxAttrArchive,
|
||||
imap.MailboxAttrDrafts,
|
||||
imap.MailboxAttrFlagged,
|
||||
imap.MailboxAttrJunk,
|
||||
imap.MailboxAttrSent,
|
||||
imap.MailboxAttrTrash,
|
||||
imap.MailboxAttrImportant,
|
||||
}
|
||||
|
||||
canonFlag = make(map[string]imap.Flag)
|
||||
for _, flag := range flags {
|
||||
canonFlag[strings.ToLower(string(flag))] = flag
|
||||
}
|
||||
|
||||
canonMailboxAttr = make(map[string]imap.MailboxAttr)
|
||||
for _, attr := range mailboxAttrs {
|
||||
canonMailboxAttr[strings.ToLower(string(attr))] = attr
|
||||
}
|
||||
}
|
||||
|
||||
func canonicalFlag(s string) imap.Flag {
|
||||
canonOnce.Do(canonInit)
|
||||
if flag, ok := canonFlag[strings.ToLower(s)]; ok {
|
||||
return flag
|
||||
}
|
||||
return imap.Flag(s)
|
||||
}
|
||||
|
||||
func canonicalMailboxAttr(s string) imap.MailboxAttr {
|
||||
canonOnce.Do(canonInit)
|
||||
if attr, ok := canonMailboxAttr[strings.ToLower(s)]; ok {
|
||||
return attr
|
||||
}
|
||||
return imap.MailboxAttr(s)
|
||||
}
|
||||
|
||||
func canonicalCap(s string) imap.Cap {
|
||||
// Only two caps are not fully uppercase
|
||||
for _, cap := range []imap.Cap{imap.CapIMAP4rev1, imap.CapIMAP4rev2} {
|
||||
if strings.EqualFold(s, string(cap)) {
|
||||
return cap
|
||||
}
|
||||
}
|
||||
return imap.Cap(strings.ToUpper(s))
|
||||
}
|
||||
23
vendor/github.com/emersion/go-imap/v2/internal/sasl.go
generated
vendored
Normal file
23
vendor/github.com/emersion/go-imap/v2/internal/sasl.go
generated
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
)
|
||||
|
||||
func EncodeSASL(b []byte) string {
|
||||
if len(b) == 0 {
|
||||
return "="
|
||||
} else {
|
||||
return base64.StdEncoding.EncodeToString(b)
|
||||
}
|
||||
}
|
||||
|
||||
func DecodeSASL(s string) ([]byte, error) {
|
||||
if s == "=" {
|
||||
// go-sasl treats nil as no challenge/response, so return a non-nil
|
||||
// empty byte slice
|
||||
return []byte{}, nil
|
||||
} else {
|
||||
return base64.StdEncoding.DecodeString(s)
|
||||
}
|
||||
}
|
||||
118
vendor/github.com/emersion/go-imap/v2/internal/utf7/decoder.go
generated
vendored
Normal file
118
vendor/github.com/emersion/go-imap/v2/internal/utf7/decoder.go
generated
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
package utf7
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// ErrInvalidUTF7 means that a decoder encountered invalid UTF-7.
|
||||
var ErrInvalidUTF7 = errors.New("utf7: invalid UTF-7")
|
||||
|
||||
// Decode decodes a string encoded with modified UTF-7.
|
||||
//
|
||||
// Note, raw UTF-8 is accepted.
|
||||
func Decode(src string) (string, error) {
|
||||
if !utf8.ValidString(src) {
|
||||
return "", errors.New("invalid UTF-8")
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.Grow(len(src))
|
||||
|
||||
ascii := true
|
||||
for i := 0; i < len(src); i++ {
|
||||
ch := src[i]
|
||||
|
||||
if ch < min || (ch > max && ch < utf8.RuneSelf) {
|
||||
// Illegal code point in ASCII mode. Note, UTF-8 codepoints are
|
||||
// always allowed.
|
||||
return "", ErrInvalidUTF7
|
||||
}
|
||||
|
||||
if ch != '&' {
|
||||
sb.WriteByte(ch)
|
||||
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
|
||||
return "", ErrInvalidUTF7
|
||||
}
|
||||
}
|
||||
|
||||
if i == len(src) { // Implicit shift ("&...")
|
||||
return "", ErrInvalidUTF7
|
||||
}
|
||||
|
||||
if i == start { // Escape sequence "&-"
|
||||
sb.WriteByte('&')
|
||||
ascii = true
|
||||
} else { // Control or non-ASCII code points in base64
|
||||
if !ascii { // Null shift ("&...-&...-")
|
||||
return "", ErrInvalidUTF7
|
||||
}
|
||||
|
||||
b := decode([]byte(src[start:i]))
|
||||
if len(b) == 0 { // Bad encoding
|
||||
return "", ErrInvalidUTF7
|
||||
}
|
||||
sb.Write(b)
|
||||
|
||||
ascii = false
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
// 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 == utf8.RuneError {
|
||||
return nil
|
||||
}
|
||||
} else if min <= r && r <= max {
|
||||
return nil
|
||||
}
|
||||
j += utf8.EncodeRune(s[j:], r)
|
||||
}
|
||||
return s[:j]
|
||||
}
|
||||
88
vendor/github.com/emersion/go-imap/v2/internal/utf7/encoder.go
generated
vendored
Normal file
88
vendor/github.com/emersion/go-imap/v2/internal/utf7/encoder.go
generated
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
package utf7
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Encode encodes a string with modified UTF-7.
|
||||
func Encode(src string) string {
|
||||
var sb strings.Builder
|
||||
sb.Grow(len(src))
|
||||
|
||||
for i := 0; i < len(src); {
|
||||
ch := src[i]
|
||||
|
||||
if min <= ch && ch <= max {
|
||||
sb.WriteByte(ch)
|
||||
if ch == '&' {
|
||||
sb.WriteByte('-')
|
||||
}
|
||||
|
||||
i++
|
||||
} else {
|
||||
start := i
|
||||
|
||||
// Find the next printable ASCII code point
|
||||
i++
|
||||
for i < len(src) && (src[i] < min || src[i] > max) {
|
||||
i++
|
||||
}
|
||||
|
||||
sb.Write(encode([]byte(src[start:i])))
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// 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 != utf8.RuneError {
|
||||
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
|
||||
}
|
||||
|
||||
// Escape passes through raw UTF-8 as-is and escapes the special UTF-7 marker
|
||||
// (the ampersand character).
|
||||
func Escape(src string) string {
|
||||
var sb strings.Builder
|
||||
sb.Grow(len(src))
|
||||
|
||||
for _, ch := range src {
|
||||
sb.WriteRune(ch)
|
||||
if ch == '&' {
|
||||
sb.WriteByte('-')
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
13
vendor/github.com/emersion/go-imap/v2/internal/utf7/utf7.go
generated
vendored
Normal file
13
vendor/github.com/emersion/go-imap/v2/internal/utf7/utf7.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// Package utf7 implements modified UTF-7 encoding defined in RFC 3501 section 5.1.3
|
||||
package utf7
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
)
|
||||
|
||||
const (
|
||||
min = 0x20 // Minimum self-representing UTF-7 value
|
||||
max = 0x7E // Maximum self-representing UTF-7 value
|
||||
)
|
||||
|
||||
var b64Enc = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,")
|
||||
30
vendor/github.com/emersion/go-imap/v2/list.go
generated
vendored
Normal file
30
vendor/github.com/emersion/go-imap/v2/list.go
generated
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
package imap
|
||||
|
||||
// ListOptions contains options for the LIST command.
|
||||
type ListOptions struct {
|
||||
SelectSubscribed bool
|
||||
SelectRemote bool
|
||||
SelectRecursiveMatch bool // requires SelectSubscribed to be set
|
||||
SelectSpecialUse bool // requires SPECIAL-USE
|
||||
|
||||
ReturnSubscribed bool
|
||||
ReturnChildren bool
|
||||
ReturnStatus *StatusOptions // requires IMAP4rev2 or LIST-STATUS
|
||||
ReturnSpecialUse bool // requires SPECIAL-USE
|
||||
}
|
||||
|
||||
// ListData is the mailbox data returned by a LIST command.
|
||||
type ListData struct {
|
||||
Attrs []MailboxAttr
|
||||
Delim rune
|
||||
Mailbox string
|
||||
|
||||
// Extended data
|
||||
ChildInfo *ListDataChildInfo
|
||||
OldName string
|
||||
Status *StatusData
|
||||
}
|
||||
|
||||
type ListDataChildInfo struct {
|
||||
Subscribed bool
|
||||
}
|
||||
14
vendor/github.com/emersion/go-imap/v2/namespace.go
generated
vendored
Normal file
14
vendor/github.com/emersion/go-imap/v2/namespace.go
generated
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
package imap
|
||||
|
||||
// NamespaceData is the data returned by the NAMESPACE command.
|
||||
type NamespaceData struct {
|
||||
Personal []NamespaceDescriptor
|
||||
Other []NamespaceDescriptor
|
||||
Shared []NamespaceDescriptor
|
||||
}
|
||||
|
||||
// NamespaceDescriptor describes a namespace.
|
||||
type NamespaceDescriptor struct {
|
||||
Prefix string
|
||||
Delim rune
|
||||
}
|
||||
149
vendor/github.com/emersion/go-imap/v2/numset.go
generated
vendored
Normal file
149
vendor/github.com/emersion/go-imap/v2/numset.go
generated
vendored
Normal file
@@ -0,0 +1,149 @@
|
||||
package imap
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/emersion/go-imap/v2/internal/imapnum"
|
||||
)
|
||||
|
||||
// NumSet is a set of numbers identifying messages. NumSet is either a SeqSet
|
||||
// or a UIDSet.
|
||||
type NumSet interface {
|
||||
// String returns the IMAP representation of the message number set.
|
||||
String() string
|
||||
// Dynamic returns true if the set contains "*" or "n:*" ranges or if the
|
||||
// set represents the special SEARCHRES marker.
|
||||
Dynamic() bool
|
||||
|
||||
numSet() imapnum.Set
|
||||
}
|
||||
|
||||
var (
|
||||
_ NumSet = SeqSet(nil)
|
||||
_ NumSet = UIDSet(nil)
|
||||
)
|
||||
|
||||
// SeqSet is a set of message sequence numbers.
|
||||
type SeqSet []SeqRange
|
||||
|
||||
// SeqSetNum returns a new SeqSet containing the specified sequence numbers.
|
||||
func SeqSetNum(nums ...uint32) SeqSet {
|
||||
var s SeqSet
|
||||
s.AddNum(nums...)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *SeqSet) numSetPtr() *imapnum.Set {
|
||||
return (*imapnum.Set)(unsafe.Pointer(s))
|
||||
}
|
||||
|
||||
func (s SeqSet) numSet() imapnum.Set {
|
||||
return *s.numSetPtr()
|
||||
}
|
||||
|
||||
func (s SeqSet) String() string {
|
||||
return s.numSet().String()
|
||||
}
|
||||
|
||||
func (s SeqSet) Dynamic() bool {
|
||||
return s.numSet().Dynamic()
|
||||
}
|
||||
|
||||
// Contains returns true if the non-zero sequence number num is contained in
|
||||
// the set.
|
||||
func (s *SeqSet) Contains(num uint32) bool {
|
||||
return s.numSet().Contains(num)
|
||||
}
|
||||
|
||||
// Nums returns a slice of all sequence numbers contained in the set.
|
||||
func (s *SeqSet) Nums() ([]uint32, bool) {
|
||||
return s.numSet().Nums()
|
||||
}
|
||||
|
||||
// AddNum inserts new sequence numbers into the set. The value 0 represents "*".
|
||||
func (s *SeqSet) AddNum(nums ...uint32) {
|
||||
s.numSetPtr().AddNum(nums...)
|
||||
}
|
||||
|
||||
// AddRange inserts a new range into the set.
|
||||
func (s *SeqSet) AddRange(start, stop uint32) {
|
||||
s.numSetPtr().AddRange(start, stop)
|
||||
}
|
||||
|
||||
// AddSet inserts all sequence numbers from other into s.
|
||||
func (s *SeqSet) AddSet(other SeqSet) {
|
||||
s.numSetPtr().AddSet(other.numSet())
|
||||
}
|
||||
|
||||
// SeqRange is a range of message sequence numbers.
|
||||
type SeqRange struct {
|
||||
Start, Stop uint32
|
||||
}
|
||||
|
||||
// UIDSet is a set of message UIDs.
|
||||
type UIDSet []UIDRange
|
||||
|
||||
// UIDSetNum returns a new UIDSet containing the specified UIDs.
|
||||
func UIDSetNum(uids ...UID) UIDSet {
|
||||
var s UIDSet
|
||||
s.AddNum(uids...)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *UIDSet) numSetPtr() *imapnum.Set {
|
||||
return (*imapnum.Set)(unsafe.Pointer(s))
|
||||
}
|
||||
|
||||
func (s UIDSet) numSet() imapnum.Set {
|
||||
return *s.numSetPtr()
|
||||
}
|
||||
|
||||
func (s UIDSet) String() string {
|
||||
if IsSearchRes(s) {
|
||||
return "$"
|
||||
}
|
||||
return s.numSet().String()
|
||||
}
|
||||
|
||||
func (s UIDSet) Dynamic() bool {
|
||||
return s.numSet().Dynamic() || IsSearchRes(s)
|
||||
}
|
||||
|
||||
// Contains returns true if the non-zero UID uid is contained in the set.
|
||||
func (s UIDSet) Contains(uid UID) bool {
|
||||
return s.numSet().Contains(uint32(uid))
|
||||
}
|
||||
|
||||
// Nums returns a slice of all UIDs contained in the set.
|
||||
func (s UIDSet) Nums() ([]UID, bool) {
|
||||
nums, ok := s.numSet().Nums()
|
||||
return uidListFromNumList(nums), ok
|
||||
}
|
||||
|
||||
// AddNum inserts new UIDs into the set. The value 0 represents "*".
|
||||
func (s *UIDSet) AddNum(uids ...UID) {
|
||||
s.numSetPtr().AddNum(numListFromUIDList(uids)...)
|
||||
}
|
||||
|
||||
// AddRange inserts a new range into the set.
|
||||
func (s *UIDSet) AddRange(start, stop UID) {
|
||||
s.numSetPtr().AddRange(uint32(start), uint32(stop))
|
||||
}
|
||||
|
||||
// AddSet inserts all UIDs from other into s.
|
||||
func (s *UIDSet) AddSet(other UIDSet) {
|
||||
s.numSetPtr().AddSet(other.numSet())
|
||||
}
|
||||
|
||||
// UIDRange is a range of message UIDs.
|
||||
type UIDRange struct {
|
||||
Start, Stop UID
|
||||
}
|
||||
|
||||
func numListFromUIDList(uids []UID) []uint32 {
|
||||
return *(*[]uint32)(unsafe.Pointer(&uids))
|
||||
}
|
||||
|
||||
func uidListFromNumList(nums []uint32) []UID {
|
||||
return *(*[]UID)(unsafe.Pointer(&nums))
|
||||
}
|
||||
13
vendor/github.com/emersion/go-imap/v2/quota.go
generated
vendored
Normal file
13
vendor/github.com/emersion/go-imap/v2/quota.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
package imap
|
||||
|
||||
// QuotaResourceType is a QUOTA resource type.
|
||||
//
|
||||
// See RFC 9208 section 5.
|
||||
type QuotaResourceType string
|
||||
|
||||
const (
|
||||
QuotaResourceStorage QuotaResourceType = "STORAGE"
|
||||
QuotaResourceMessage QuotaResourceType = "MESSAGE"
|
||||
QuotaResourceMailbox QuotaResourceType = "MAILBOX"
|
||||
QuotaResourceAnnotationStorage QuotaResourceType = "ANNOTATION-STORAGE"
|
||||
)
|
||||
4
vendor/github.com/emersion/go-imap/v2/rename.go
generated
vendored
Normal file
4
vendor/github.com/emersion/go-imap/v2/rename.go
generated
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
package imap
|
||||
|
||||
// RenameOptions contains options for the RENAME command.
|
||||
type RenameOptions struct{}
|
||||
81
vendor/github.com/emersion/go-imap/v2/response.go
generated
vendored
Normal file
81
vendor/github.com/emersion/go-imap/v2/response.go
generated
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
package imap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// StatusResponseType is a generic status response type.
|
||||
type StatusResponseType string
|
||||
|
||||
const (
|
||||
StatusResponseTypeOK StatusResponseType = "OK"
|
||||
StatusResponseTypeNo StatusResponseType = "NO"
|
||||
StatusResponseTypeBad StatusResponseType = "BAD"
|
||||
StatusResponseTypePreAuth StatusResponseType = "PREAUTH"
|
||||
StatusResponseTypeBye StatusResponseType = "BYE"
|
||||
)
|
||||
|
||||
// ResponseCode is a response code.
|
||||
type ResponseCode string
|
||||
|
||||
const (
|
||||
ResponseCodeAlert ResponseCode = "ALERT"
|
||||
ResponseCodeAlreadyExists ResponseCode = "ALREADYEXISTS"
|
||||
ResponseCodeAuthenticationFailed ResponseCode = "AUTHENTICATIONFAILED"
|
||||
ResponseCodeAuthorizationFailed ResponseCode = "AUTHORIZATIONFAILED"
|
||||
ResponseCodeBadCharset ResponseCode = "BADCHARSET"
|
||||
ResponseCodeCannot ResponseCode = "CANNOT"
|
||||
ResponseCodeClientBug ResponseCode = "CLIENTBUG"
|
||||
ResponseCodeContactAdmin ResponseCode = "CONTACTADMIN"
|
||||
ResponseCodeCorruption ResponseCode = "CORRUPTION"
|
||||
ResponseCodeExpired ResponseCode = "EXPIRED"
|
||||
ResponseCodeHasChildren ResponseCode = "HASCHILDREN"
|
||||
ResponseCodeInUse ResponseCode = "INUSE"
|
||||
ResponseCodeLimit ResponseCode = "LIMIT"
|
||||
ResponseCodeNonExistent ResponseCode = "NONEXISTENT"
|
||||
ResponseCodeNoPerm ResponseCode = "NOPERM"
|
||||
ResponseCodeOverQuota ResponseCode = "OVERQUOTA"
|
||||
ResponseCodeParse ResponseCode = "PARSE"
|
||||
ResponseCodePrivacyRequired ResponseCode = "PRIVACYREQUIRED"
|
||||
ResponseCodeServerBug ResponseCode = "SERVERBUG"
|
||||
ResponseCodeTryCreate ResponseCode = "TRYCREATE"
|
||||
ResponseCodeUnavailable ResponseCode = "UNAVAILABLE"
|
||||
ResponseCodeUnknownCTE ResponseCode = "UNKNOWN-CTE"
|
||||
|
||||
// METADATA
|
||||
ResponseCodeTooMany ResponseCode = "TOOMANY"
|
||||
ResponseCodeNoPrivate ResponseCode = "NOPRIVATE"
|
||||
|
||||
// APPENDLIMIT
|
||||
ResponseCodeTooBig ResponseCode = "TOOBIG"
|
||||
)
|
||||
|
||||
// StatusResponse is a generic status response.
|
||||
//
|
||||
// See RFC 9051 section 7.1.
|
||||
type StatusResponse struct {
|
||||
Type StatusResponseType
|
||||
Code ResponseCode
|
||||
Text string
|
||||
}
|
||||
|
||||
// Error is an IMAP error caused by a status response.
|
||||
type Error StatusResponse
|
||||
|
||||
var _ error = (*Error)(nil)
|
||||
|
||||
// Error implements the error interface.
|
||||
func (err *Error) Error() string {
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "imap: %v", err.Type)
|
||||
if err.Code != "" {
|
||||
fmt.Fprintf(&sb, " [%v]", err.Code)
|
||||
}
|
||||
text := err.Text
|
||||
if text == "" {
|
||||
text = "<unknown>"
|
||||
}
|
||||
fmt.Fprintf(&sb, " %v", text)
|
||||
return sb.String()
|
||||
}
|
||||
201
vendor/github.com/emersion/go-imap/v2/search.go
generated
vendored
Normal file
201
vendor/github.com/emersion/go-imap/v2/search.go
generated
vendored
Normal file
@@ -0,0 +1,201 @@
|
||||
package imap
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SearchOptions contains options for the SEARCH command.
|
||||
type SearchOptions struct {
|
||||
// Requires IMAP4rev2 or ESEARCH
|
||||
ReturnMin bool
|
||||
ReturnMax bool
|
||||
ReturnAll bool
|
||||
ReturnCount bool
|
||||
// Requires IMAP4rev2 or SEARCHRES
|
||||
ReturnSave bool
|
||||
}
|
||||
|
||||
// SearchCriteria is a criteria for the SEARCH command.
|
||||
//
|
||||
// When multiple fields are populated, the result is the intersection ("and"
|
||||
// function) of all messages that match the fields.
|
||||
//
|
||||
// And, Not and Or can be used to combine multiple criteria together. For
|
||||
// instance, the following criteria matches messages not containing "hello":
|
||||
//
|
||||
// SearchCriteria{Not: []SearchCriteria{{
|
||||
// Body: []string{"hello"},
|
||||
// }}}
|
||||
//
|
||||
// The following criteria matches messages containing either "hello" or
|
||||
// "world":
|
||||
//
|
||||
// SearchCriteria{Or: [][2]SearchCriteria{{
|
||||
// {Body: []string{"hello"}},
|
||||
// {Body: []string{"world"}},
|
||||
// }}}
|
||||
type SearchCriteria struct {
|
||||
SeqNum []SeqSet
|
||||
UID []UIDSet
|
||||
|
||||
// Only the date is used, the time and timezone are ignored
|
||||
Since time.Time
|
||||
Before time.Time
|
||||
SentSince time.Time
|
||||
SentBefore time.Time
|
||||
|
||||
Header []SearchCriteriaHeaderField
|
||||
Body []string
|
||||
Text []string
|
||||
|
||||
Flag []Flag
|
||||
NotFlag []Flag
|
||||
|
||||
Larger int64
|
||||
Smaller int64
|
||||
|
||||
Not []SearchCriteria
|
||||
Or [][2]SearchCriteria
|
||||
|
||||
ModSeq *SearchCriteriaModSeq // requires CONDSTORE
|
||||
}
|
||||
|
||||
// And intersects two search criteria.
|
||||
func (criteria *SearchCriteria) And(other *SearchCriteria) {
|
||||
criteria.SeqNum = append(criteria.SeqNum, other.SeqNum...)
|
||||
criteria.UID = append(criteria.UID, other.UID...)
|
||||
|
||||
criteria.Since = intersectSince(criteria.Since, other.Since)
|
||||
criteria.Before = intersectBefore(criteria.Before, other.Before)
|
||||
criteria.SentSince = intersectSince(criteria.SentSince, other.SentSince)
|
||||
criteria.SentBefore = intersectBefore(criteria.SentBefore, other.SentBefore)
|
||||
|
||||
criteria.Header = append(criteria.Header, other.Header...)
|
||||
criteria.Body = append(criteria.Body, other.Body...)
|
||||
criteria.Text = append(criteria.Text, other.Text...)
|
||||
|
||||
criteria.Flag = append(criteria.Flag, other.Flag...)
|
||||
criteria.NotFlag = append(criteria.NotFlag, other.NotFlag...)
|
||||
|
||||
if criteria.Larger == 0 || other.Larger > criteria.Larger {
|
||||
criteria.Larger = other.Larger
|
||||
}
|
||||
if criteria.Smaller == 0 || other.Smaller < criteria.Smaller {
|
||||
criteria.Smaller = other.Smaller
|
||||
}
|
||||
|
||||
criteria.Not = append(criteria.Not, other.Not...)
|
||||
criteria.Or = append(criteria.Or, other.Or...)
|
||||
}
|
||||
|
||||
func intersectSince(t1, t2 time.Time) time.Time {
|
||||
switch {
|
||||
case t1.IsZero():
|
||||
return t2
|
||||
case t2.IsZero():
|
||||
return t1
|
||||
case t1.After(t2):
|
||||
return t1
|
||||
default:
|
||||
return t2
|
||||
}
|
||||
}
|
||||
|
||||
func intersectBefore(t1, t2 time.Time) time.Time {
|
||||
switch {
|
||||
case t1.IsZero():
|
||||
return t2
|
||||
case t2.IsZero():
|
||||
return t1
|
||||
case t1.Before(t2):
|
||||
return t1
|
||||
default:
|
||||
return t2
|
||||
}
|
||||
}
|
||||
|
||||
type SearchCriteriaHeaderField struct {
|
||||
Key, Value string
|
||||
}
|
||||
|
||||
type SearchCriteriaModSeq struct {
|
||||
ModSeq uint64
|
||||
MetadataName string
|
||||
MetadataType SearchCriteriaMetadataType
|
||||
}
|
||||
|
||||
type SearchCriteriaMetadataType string
|
||||
|
||||
const (
|
||||
SearchCriteriaMetadataAll SearchCriteriaMetadataType = "all"
|
||||
SearchCriteriaMetadataPrivate SearchCriteriaMetadataType = "priv"
|
||||
SearchCriteriaMetadataShared SearchCriteriaMetadataType = "shared"
|
||||
)
|
||||
|
||||
// SearchData is the data returned by a SEARCH command.
|
||||
type SearchData struct {
|
||||
All NumSet
|
||||
|
||||
// requires IMAP4rev2 or ESEARCH
|
||||
Min uint32
|
||||
Max uint32
|
||||
Count uint32
|
||||
|
||||
// requires CONDSTORE
|
||||
ModSeq uint64
|
||||
}
|
||||
|
||||
// AllSeqNums returns All as a slice of sequence numbers.
|
||||
func (data *SearchData) AllSeqNums() []uint32 {
|
||||
seqSet, ok := data.All.(SeqSet)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Note: a dynamic sequence set would be a server bug
|
||||
nums, ok := seqSet.Nums()
|
||||
if !ok {
|
||||
panic("imap: SearchData.All is a dynamic number set")
|
||||
}
|
||||
return nums
|
||||
}
|
||||
|
||||
// AllUIDs returns All as a slice of UIDs.
|
||||
func (data *SearchData) AllUIDs() []UID {
|
||||
uidSet, ok := data.All.(UIDSet)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Note: a dynamic sequence set would be a server bug
|
||||
uids, ok := uidSet.Nums()
|
||||
if !ok {
|
||||
panic("imap: SearchData.All is a dynamic number set")
|
||||
}
|
||||
return uids
|
||||
}
|
||||
|
||||
// searchRes is a special empty UIDSet which can be used as a marker. It has
|
||||
// a non-zero cap so that its data pointer is non-nil and can be compared.
|
||||
//
|
||||
// It's a UIDSet rather than a SeqSet so that it can be passed to the
|
||||
// UID EXPUNGE command.
|
||||
var (
|
||||
searchRes = make(UIDSet, 0, 1)
|
||||
searchResAddr = reflect.ValueOf(searchRes).Pointer()
|
||||
)
|
||||
|
||||
// SearchRes returns a special marker which can be used instead of a UIDSet to
|
||||
// reference the last SEARCH result. On the wire, it's encoded as '$'.
|
||||
//
|
||||
// It requires IMAP4rev2 or the SEARCHRES extension.
|
||||
func SearchRes() UIDSet {
|
||||
return searchRes
|
||||
}
|
||||
|
||||
// IsSearchRes checks whether a sequence set is a reference to the last SEARCH
|
||||
// result. See SearchRes.
|
||||
func IsSearchRes(numSet NumSet) bool {
|
||||
return reflect.ValueOf(numSet).Pointer() == searchResAddr
|
||||
}
|
||||
31
vendor/github.com/emersion/go-imap/v2/select.go
generated
vendored
Normal file
31
vendor/github.com/emersion/go-imap/v2/select.go
generated
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
package imap
|
||||
|
||||
// SelectOptions contains options for the SELECT or EXAMINE command.
|
||||
type SelectOptions struct {
|
||||
ReadOnly bool
|
||||
CondStore bool // requires CONDSTORE
|
||||
}
|
||||
|
||||
// SelectData is the data returned by a SELECT command.
|
||||
//
|
||||
// In the old RFC 2060, PermanentFlags, UIDNext and UIDValidity are optional.
|
||||
type SelectData struct {
|
||||
// Flags defined for this mailbox
|
||||
Flags []Flag
|
||||
// Flags that the client can change permanently
|
||||
PermanentFlags []Flag
|
||||
// Number of messages in this mailbox (aka. "EXISTS")
|
||||
NumMessages uint32
|
||||
// Sequence number of the first unseen message. Obsolete, IMAP4rev1 only.
|
||||
// Server-only, not supported in imapclient.
|
||||
FirstUnseenSeqNum uint32
|
||||
// Number of recent messages in this mailbox. Obsolete, IMAP4rev1 only.
|
||||
// Server-only, not supported in imapclient.
|
||||
NumRecent uint32
|
||||
UIDNext UID
|
||||
UIDValidity uint32
|
||||
|
||||
List *ListData // requires IMAP4rev2
|
||||
|
||||
HighestModSeq uint64 // requires CONDSTORE
|
||||
}
|
||||
35
vendor/github.com/emersion/go-imap/v2/status.go
generated
vendored
Normal file
35
vendor/github.com/emersion/go-imap/v2/status.go
generated
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
package imap
|
||||
|
||||
// StatusOptions contains options for the STATUS command.
|
||||
type StatusOptions struct {
|
||||
NumMessages bool
|
||||
NumRecent bool // Obsolete, IMAP4rev1 only. Server-only, not supported in imapclient.
|
||||
UIDNext bool
|
||||
UIDValidity bool
|
||||
NumUnseen bool
|
||||
NumDeleted bool // requires IMAP4rev2 or QUOTA
|
||||
Size bool // requires IMAP4rev2 or STATUS=SIZE
|
||||
|
||||
AppendLimit bool // requires APPENDLIMIT
|
||||
DeletedStorage bool // requires QUOTA=RES-STORAGE
|
||||
HighestModSeq bool // requires CONDSTORE
|
||||
}
|
||||
|
||||
// StatusData is the data returned by a STATUS command.
|
||||
//
|
||||
// The mailbox name is always populated. The remaining fields are optional.
|
||||
type StatusData struct {
|
||||
Mailbox string
|
||||
|
||||
NumMessages *uint32
|
||||
NumRecent *uint32 // Obsolete, IMAP4rev1 only. Server-only, not supported in imapclient.
|
||||
UIDNext UID
|
||||
UIDValidity uint32
|
||||
NumUnseen *uint32
|
||||
NumDeleted *uint32
|
||||
Size *int64
|
||||
|
||||
AppendLimit *uint32
|
||||
DeletedStorage *int64
|
||||
HighestModSeq uint64
|
||||
}
|
||||
22
vendor/github.com/emersion/go-imap/v2/store.go
generated
vendored
Normal file
22
vendor/github.com/emersion/go-imap/v2/store.go
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
package imap
|
||||
|
||||
// StoreOptions contains options for the STORE command.
|
||||
type StoreOptions struct {
|
||||
UnchangedSince uint64 // requires CONDSTORE
|
||||
}
|
||||
|
||||
// StoreFlagsOp is a flag operation: set, add or delete.
|
||||
type StoreFlagsOp int
|
||||
|
||||
const (
|
||||
StoreFlagsSet StoreFlagsOp = iota
|
||||
StoreFlagsAdd
|
||||
StoreFlagsDel
|
||||
)
|
||||
|
||||
// StoreFlags alters message flags.
|
||||
type StoreFlags struct {
|
||||
Op StoreFlagsOp
|
||||
Silent bool
|
||||
Flags []Flag
|
||||
}
|
||||
9
vendor/github.com/emersion/go-imap/v2/thread.go
generated
vendored
Normal file
9
vendor/github.com/emersion/go-imap/v2/thread.go
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
package imap
|
||||
|
||||
// ThreadAlgorithm is a threading algorithm.
|
||||
type ThreadAlgorithm string
|
||||
|
||||
const (
|
||||
ThreadOrderedSubject ThreadAlgorithm = "ORDEREDSUBJECT"
|
||||
ThreadReferences ThreadAlgorithm = "REFERENCES"
|
||||
)
|
||||
20
vendor/github.com/emersion/go-message/.build.yml
generated
vendored
Normal file
20
vendor/github.com/emersion/go-message/.build.yml
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
image: alpine/latest
|
||||
packages:
|
||||
- go
|
||||
sources:
|
||||
- https://github.com/emersion/go-message
|
||||
artifacts:
|
||||
- coverage.html
|
||||
tasks:
|
||||
- build: |
|
||||
cd go-message
|
||||
go build -v ./...
|
||||
- test: |
|
||||
cd go-message
|
||||
go test -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
- coverage: |
|
||||
cd go-message
|
||||
go tool cover -html=coverage.txt -o ~/coverage.html
|
||||
- gofmt: |
|
||||
cd go-message
|
||||
test -z $(gofmt -l .)
|
||||
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.
|
||||
30
vendor/github.com/emersion/go-message/README.md
generated
vendored
Normal file
30
vendor/github.com/emersion/go-message/README.md
generated
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
# go-message
|
||||
|
||||
[](https://pkg.go.dev/github.com/emersion/go-message)
|
||||
|
||||
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 (to decode all charsets, add
|
||||
`import _ "github.com/emersion/go-message/charset"` to your application)
|
||||
* A [`mail`](https://godocs.io/github.com/emersion/go-message/mail) subpackage
|
||||
to read and write mail messages
|
||||
* DKIM-friendly
|
||||
* A [`textproto`](https://godocs.io/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
|
||||
66
vendor/github.com/emersion/go-message/charset.go
generated
vendored
Normal file
66
vendor/github.com/emersion/go-message/charset.go
generated
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
package message
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type UnknownCharsetError struct {
|
||||
e error
|
||||
}
|
||||
|
||||
func (u UnknownCharsetError) Unwrap() error { return u.e }
|
||||
|
||||
func (u UnknownCharsetError) Error() string {
|
||||
return "unknown charset: " + u.e.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 {
|
||||
return errors.As(err, new(UnknownCharsetError))
|
||||
}
|
||||
|
||||
// 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)
|
||||
if charset == "utf-8" || charset == "us-ascii" {
|
||||
return input, nil
|
||||
}
|
||||
if CharsetReader != nil {
|
||||
r, err := CharsetReader(charset, input)
|
||||
if err != nil {
|
||||
return r, UnknownCharsetError{err}
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
return input, UnknownCharsetError{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)
|
||||
}
|
||||
64
vendor/github.com/emersion/go-message/charset/charset.go
generated
vendored
Normal file
64
vendor/github.com/emersion/go-message/charset/charset.go
generated
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
// Package charset provides functions to decode and encode charsets.
|
||||
//
|
||||
// It imports all supported charsets, which adds about 1MiB to binaries size.
|
||||
// Importing the package automatically sets message.CharsetReader.
|
||||
package charset
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-message"
|
||||
"golang.org/x/text/encoding"
|
||||
"golang.org/x/text/encoding/charmap"
|
||||
"golang.org/x/text/encoding/htmlindex"
|
||||
"golang.org/x/text/encoding/ianaindex"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
)
|
||||
|
||||
// Quirks table for charsets not handled by ianaindex
|
||||
//
|
||||
// A nil entry disables the charset.
|
||||
//
|
||||
// For aliases, see
|
||||
// https://www.iana.org/assignments/character-sets/character-sets.xhtml
|
||||
var charsets = map[string]encoding.Encoding{
|
||||
"ansi_x3.110-1983": charmap.ISO8859_1, // see RFC 1345 page 62, mostly superset of ISO 8859-1
|
||||
"x-utf_8j": unicode.UTF8, // alias for UTF-8, see https://icu4c-demos.unicode.org/icu-bin/convexp?s=ALL
|
||||
}
|
||||
|
||||
func init() {
|
||||
message.CharsetReader = Reader
|
||||
}
|
||||
|
||||
// Reader returns an io.Reader that converts the provided charset to UTF-8.
|
||||
func Reader(charset string, input io.Reader) (io.Reader, error) {
|
||||
var err error
|
||||
enc, ok := charsets[strings.ToLower(charset)]
|
||||
if ok && enc == nil {
|
||||
return nil, fmt.Errorf("charset %q: charset is disabled", charset)
|
||||
} else if !ok {
|
||||
enc, err = ianaindex.MIME.Encoding(charset)
|
||||
}
|
||||
if enc == nil {
|
||||
enc, err = ianaindex.MIME.Encoding("cs" + charset)
|
||||
}
|
||||
if enc == nil {
|
||||
enc, err = htmlindex.Get(charset)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("charset %q: %v", charset, err)
|
||||
}
|
||||
// See https://github.com/golang/go/issues/19421
|
||||
if enc == nil {
|
||||
return nil, fmt.Errorf("charset %q: unsupported charset", charset)
|
||||
}
|
||||
return enc.NewDecoder().Reader(input), nil
|
||||
}
|
||||
|
||||
// RegisterEncoding registers an encoding. This is intended to be called from
|
||||
// the init function in packages that want to support additional charsets.
|
||||
func RegisterEncoding(name string, enc encoding.Encoding) {
|
||||
charsets[name] = enc
|
||||
}
|
||||
155
vendor/github.com/emersion/go-message/encoding.go
generated
vendored
Normal file
155
vendor/github.com/emersion/go-message/encoding.go
generated
vendored
Normal file
@@ -0,0 +1,155 @@
|
||||
package message
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/quotedprintable"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type UnknownEncodingError struct {
|
||||
e error
|
||||
}
|
||||
|
||||
func (u UnknownEncodingError) Unwrap() error { return u.e }
|
||||
|
||||
func (u UnknownEncodingError) Error() string {
|
||||
return "encoding error: " + u.e.Error()
|
||||
}
|
||||
|
||||
// IsUnknownEncoding returns a boolean indicating whether the error is known to
|
||||
// report that the encoding advertised by the entity is unknown.
|
||||
func IsUnknownEncoding(err error) bool {
|
||||
return errors.As(err, new(UnknownEncodingError))
|
||||
}
|
||||
|
||||
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":
|
||||
wrapped := &whitespaceReplacingReader{wrapped: r}
|
||||
dec = base64.NewDecoder(base64.StdEncoding, wrapped)
|
||||
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, &lineWrapper{w: w, maxLineLen: 76})
|
||||
case "7bit", "8bit":
|
||||
wc = nopCloser{&lineWrapper{w: w, maxLineLen: 998}}
|
||||
case "binary", "":
|
||||
wc = nopCloser{w}
|
||||
default:
|
||||
return nil, fmt.Errorf("unhandled encoding %q", enc)
|
||||
}
|
||||
return wc, nil
|
||||
}
|
||||
|
||||
// whitespaceReplacingReader replaces space and tab characters with a LF so
|
||||
// base64 bodies with a continuation indent can be decoded by the base64 decoder
|
||||
// even though it is against the spec.
|
||||
type whitespaceReplacingReader struct {
|
||||
wrapped io.Reader
|
||||
}
|
||||
|
||||
func (r *whitespaceReplacingReader) Read(p []byte) (int, error) {
|
||||
n, err := r.wrapped.Read(p)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
if p[i] == ' ' || p[i] == '\t' {
|
||||
p[i] = '\n'
|
||||
}
|
||||
}
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
type lineWrapper struct {
|
||||
w io.Writer
|
||||
maxLineLen int
|
||||
|
||||
curLineLen int
|
||||
cr bool
|
||||
}
|
||||
|
||||
func (w *lineWrapper) Write(b []byte) (int, error) {
|
||||
var written int
|
||||
for len(b) > 0 {
|
||||
var l []byte
|
||||
l, b = cutLine(b, w.maxLineLen-w.curLineLen)
|
||||
|
||||
lf := bytes.HasSuffix(l, []byte("\n"))
|
||||
l = bytes.TrimSuffix(l, []byte("\n"))
|
||||
|
||||
n, err := w.w.Write(l)
|
||||
if err != nil {
|
||||
return written, err
|
||||
}
|
||||
written += n
|
||||
|
||||
cr := bytes.HasSuffix(l, []byte("\r"))
|
||||
if len(l) == 0 {
|
||||
cr = w.cr
|
||||
}
|
||||
|
||||
if !lf && len(b) == 0 {
|
||||
w.curLineLen += len(l)
|
||||
w.cr = cr
|
||||
break
|
||||
}
|
||||
w.curLineLen = 0
|
||||
|
||||
ending := []byte("\r\n")
|
||||
if cr {
|
||||
ending = []byte("\n")
|
||||
}
|
||||
_, err = w.w.Write(ending)
|
||||
if err != nil {
|
||||
return written, err
|
||||
}
|
||||
// If the written `\n` was part of the input bytes slice, then account for it.
|
||||
if lf {
|
||||
written++
|
||||
}
|
||||
w.cr = false
|
||||
}
|
||||
|
||||
return written, nil
|
||||
}
|
||||
|
||||
func cutLine(b []byte, max int) ([]byte, []byte) {
|
||||
for i := 0; i < len(b); i++ {
|
||||
if b[i] == '\r' && i == max {
|
||||
continue
|
||||
}
|
||||
if b[i] == '\n' {
|
||||
return b[:i+1], b[i+1:]
|
||||
}
|
||||
if i >= max {
|
||||
return b[:i], b[i:]
|
||||
}
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
264
vendor/github.com/emersion/go-message/entity.go
generated
vendored
Normal file
264
vendor/github.com/emersion/go-message/entity.go
generated
vendored
Normal file
@@ -0,0 +1,264 @@
|
||||
package message
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"math"
|
||||
"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
|
||||
|
||||
mediaType, mediaParams, _ := header.ContentType()
|
||||
|
||||
// QUIRK: RFC 2045 section 6.4 specifies that multipart messages can't have
|
||||
// a Content-Transfer-Encoding other than "7bit", "8bit" or "binary".
|
||||
// However some messages in the wild are non-conformant and have it set to
|
||||
// e.g. "quoted-printable". So we just ignore it for multipart.
|
||||
// See https://github.com/emersion/go-message/issues/48
|
||||
if !strings.HasPrefix(mediaType, "multipart/") {
|
||||
enc := header.Get("Content-Transfer-Encoding")
|
||||
if decoded, encErr := encodingReader(enc, body); encErr != nil {
|
||||
err = UnknownEncodingError{encErr}
|
||||
} else {
|
||||
body = decoded
|
||||
}
|
||||
}
|
||||
|
||||
// RFC 2046 section 4.1.2: charset only applies to text/*
|
||||
if strings.HasPrefix(mediaType, "text/") {
|
||||
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)
|
||||
}
|
||||
|
||||
const defaultMaxHeaderBytes = 1 << 20 // 1 MB
|
||||
|
||||
var errHeaderTooBig = errors.New("message: header exceeds maximum size")
|
||||
|
||||
// limitedReader is the same as io.LimitedReader, but returns a custom error.
|
||||
type limitedReader struct {
|
||||
R io.Reader
|
||||
N int64
|
||||
}
|
||||
|
||||
func (lr *limitedReader) Read(p []byte) (int, error) {
|
||||
if lr.N <= 0 {
|
||||
return 0, errHeaderTooBig
|
||||
}
|
||||
if int64(len(p)) > lr.N {
|
||||
p = p[0:lr.N]
|
||||
}
|
||||
n, err := lr.R.Read(p)
|
||||
lr.N -= int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// ReadOptions are options for ReadWithOptions.
|
||||
type ReadOptions struct {
|
||||
// MaxHeaderBytes limits the maximum permissible size of a message header
|
||||
// block. If exceeded, an error will be returned.
|
||||
//
|
||||
// Set to -1 for no limit, set to 0 for the default value (1MB).
|
||||
MaxHeaderBytes int64
|
||||
}
|
||||
|
||||
// withDefaults returns a sanitised version of the options with defaults/special
|
||||
// values accounted for.
|
||||
func (o *ReadOptions) withDefaults() *ReadOptions {
|
||||
var out ReadOptions
|
||||
if o != nil {
|
||||
out = *o
|
||||
}
|
||||
if out.MaxHeaderBytes == 0 {
|
||||
out.MaxHeaderBytes = defaultMaxHeaderBytes
|
||||
} else if out.MaxHeaderBytes < 0 {
|
||||
out.MaxHeaderBytes = math.MaxInt64
|
||||
}
|
||||
return &out
|
||||
}
|
||||
|
||||
// ReadWithOptions see Read, but allows overriding some parameters with
|
||||
// ReadOptions.
|
||||
//
|
||||
// If the message uses an unknown transfer encoding or charset, ReadWithOptions
|
||||
// returns an error that verifies IsUnknownCharset or IsUnknownEncoding, but
|
||||
// also returns an Entity that can be read.
|
||||
func ReadWithOptions(r io.Reader, opts *ReadOptions) (*Entity, error) {
|
||||
opts = opts.withDefaults()
|
||||
|
||||
lr := &limitedReader{R: r, N: opts.MaxHeaderBytes}
|
||||
br := bufio.NewReader(lr)
|
||||
|
||||
h, err := textproto.ReadHeader(br)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lr.N = math.MaxInt64
|
||||
|
||||
return New(Header{h}, br)
|
||||
}
|
||||
|
||||
// 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 or IsUnknownEncoding, but also returns
|
||||
// an Entity that can be read.
|
||||
func Read(r io.Reader) (*Entity, error) {
|
||||
return ReadWithOptions(r, nil)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
if err := e.writeBodyTo(ew); err != nil {
|
||||
ew.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
return ew.Close()
|
||||
}
|
||||
|
||||
// WalkFunc is the type of the function called for each part visited by Walk.
|
||||
//
|
||||
// The path argument is a list of multipart indices leading to the part. The
|
||||
// root part has a nil path.
|
||||
//
|
||||
// If there was an encoding error walking to a part, the incoming error will
|
||||
// describe the problem and the function can decide how to handle that error.
|
||||
//
|
||||
// Unlike IMAP part paths, indices start from 0 (instead of 1) and a
|
||||
// non-multipart message has a nil path (instead of {1}).
|
||||
//
|
||||
// If an error is returned, processing stops.
|
||||
type WalkFunc func(path []int, entity *Entity, err error) error
|
||||
|
||||
// Walk walks the entity's multipart tree, calling walkFunc for each part in
|
||||
// the tree, including the root entity.
|
||||
//
|
||||
// Walk consumes the entity.
|
||||
func (e *Entity) Walk(walkFunc WalkFunc) error {
|
||||
var multipartReaders []MultipartReader
|
||||
var path []int
|
||||
part := e
|
||||
for {
|
||||
var err error
|
||||
if part == nil {
|
||||
if len(multipartReaders) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// Get the next part from the last multipart reader
|
||||
mr := multipartReaders[len(multipartReaders)-1]
|
||||
part, err = mr.NextPart()
|
||||
if err == io.EOF {
|
||||
multipartReaders = multipartReaders[:len(multipartReaders)-1]
|
||||
path = path[:len(path)-1]
|
||||
continue
|
||||
} else if IsUnknownEncoding(err) || IsUnknownCharset(err) {
|
||||
// Forward the error to walkFunc
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path[len(path)-1]++
|
||||
}
|
||||
|
||||
// Copy the path since we'll mutate it on the next iteration
|
||||
var pathCopy []int
|
||||
if len(path) > 0 {
|
||||
pathCopy = make([]int, len(path))
|
||||
copy(pathCopy, path)
|
||||
}
|
||||
|
||||
if err := walkFunc(pathCopy, part, err); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if mr := part.MultipartReader(); mr != nil {
|
||||
multipartReaders = append(multipartReaders, mr)
|
||||
path = append(path, -1)
|
||||
}
|
||||
|
||||
part = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
118
vendor/github.com/emersion/go-message/header.go
generated
vendored
Normal file
118
vendor/github.com/emersion/go-message/header.go
generated
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
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. If the header field's charset is unknown,
|
||||
// the raw field value is returned and the error verifies IsUnknownCharset.
|
||||
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
|
||||
}
|
||||
|
||||
// HeaderFromMap creates a header from a map of header fields.
|
||||
//
|
||||
// This function is provided for interoperability with the standard library.
|
||||
// If possible, ReadHeader should be used instead to avoid loosing information.
|
||||
// The map representation looses the ordering of the fields, the capitalization
|
||||
// of the header keys, and the whitespace of the original header.
|
||||
func HeaderFromMap(m map[string][]string) Header {
|
||||
return Header{textproto.HeaderFromMap(m)}
|
||||
}
|
||||
|
||||
// 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. If the header field's charset is unknown, the raw field
|
||||
// value is returned and the error verifies IsUnknownCharset.
|
||||
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))
|
||||
}
|
||||
|
||||
// Copy creates a stand-alone copy of the header.
|
||||
func (h *Header) Copy() Header {
|
||||
return Header{h.Header.Copy()}
|
||||
}
|
||||
|
||||
// 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)}
|
||||
}
|
||||
42
vendor/github.com/emersion/go-message/mail/address.go
generated
vendored
Normal file
42
vendor/github.com/emersion/go-message/mail/address.go
generated
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"net/mail"
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-message"
|
||||
)
|
||||
|
||||
// Address represents a single mail address.
|
||||
// The type alias ensures that a net/mail.Address can be used wherever an
|
||||
// Address is expected
|
||||
type Address = mail.Address
|
||||
|
||||
func formatAddressList(l []*Address) string {
|
||||
formatted := make([]string, len(l))
|
||||
for i, a := range l {
|
||||
formatted[i] = a.String()
|
||||
}
|
||||
return strings.Join(formatted, ", ")
|
||||
}
|
||||
|
||||
// ParseAddress parses a single RFC 5322 address, e.g. "Barry Gibbs <bg@example.com>"
|
||||
// Use this function only if you parse from a string, if you have a Header use
|
||||
// Header.AddressList instead
|
||||
func ParseAddress(address string) (*Address, error) {
|
||||
parser := mail.AddressParser{
|
||||
&mime.WordDecoder{message.CharsetReader},
|
||||
}
|
||||
return parser.Parse(address)
|
||||
}
|
||||
|
||||
// ParseAddressList parses the given string as a list of addresses.
|
||||
// Use this function only if you parse from a string, if you have a Header use
|
||||
// Header.AddressList instead
|
||||
func ParseAddressList(list string) ([]*Address, error) {
|
||||
parser := mail.AddressParser{
|
||||
&mime.WordDecoder{message.CharsetReader},
|
||||
}
|
||||
return parser.ParseList(list)
|
||||
}
|
||||
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)
|
||||
}
|
||||
381
vendor/github.com/emersion/go-message/mail/header.go
generated
vendored
Normal file
381
vendor/github.com/emersion/go-message/mail/header.go
generated
vendored
Normal file
@@ -0,0 +1,381 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/emersion/go-message"
|
||||
)
|
||||
|
||||
const dateLayout = "Mon, 02 Jan 2006 15:04:05 -0700"
|
||||
|
||||
type headerParser struct {
|
||||
s string
|
||||
}
|
||||
|
||||
func (p *headerParser) len() int {
|
||||
return len(p.s)
|
||||
}
|
||||
|
||||
func (p *headerParser) empty() bool {
|
||||
return p.len() == 0
|
||||
}
|
||||
|
||||
func (p *headerParser) peek() byte {
|
||||
return p.s[0]
|
||||
}
|
||||
|
||||
func (p *headerParser) consume(c byte) bool {
|
||||
if p.empty() || p.peek() != c {
|
||||
return false
|
||||
}
|
||||
p.s = p.s[1:]
|
||||
return true
|
||||
}
|
||||
|
||||
// skipSpace skips the leading space and tab characters.
|
||||
func (p *headerParser) skipSpace() {
|
||||
p.s = strings.TrimLeft(p.s, " \t")
|
||||
}
|
||||
|
||||
// skipCFWS skips CFWS as defined in RFC5322. It returns false if the CFWS is
|
||||
// malformed.
|
||||
func (p *headerParser) skipCFWS() bool {
|
||||
p.skipSpace()
|
||||
|
||||
for {
|
||||
if !p.consume('(') {
|
||||
break
|
||||
}
|
||||
|
||||
if _, ok := p.consumeComment(); !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
p.skipSpace()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *headerParser) consumeComment() (string, bool) {
|
||||
// '(' already consumed.
|
||||
depth := 1
|
||||
|
||||
var comment string
|
||||
for {
|
||||
if p.empty() || depth == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
if p.peek() == '\\' && p.len() > 1 {
|
||||
p.s = p.s[1:]
|
||||
} else if p.peek() == '(' {
|
||||
depth++
|
||||
} else if p.peek() == ')' {
|
||||
depth--
|
||||
}
|
||||
|
||||
if depth > 0 {
|
||||
comment += p.s[:1]
|
||||
}
|
||||
|
||||
p.s = p.s[1:]
|
||||
}
|
||||
|
||||
return comment, depth == 0
|
||||
}
|
||||
|
||||
func (p *headerParser) parseAtomText(dot bool) (string, error) {
|
||||
i := 0
|
||||
for {
|
||||
r, size := utf8.DecodeRuneInString(p.s[i:])
|
||||
if size == 1 && r == utf8.RuneError {
|
||||
return "", fmt.Errorf("mail: invalid UTF-8 in atom-text: %q", p.s)
|
||||
} else if size == 0 || !isAtext(r, dot) {
|
||||
break
|
||||
}
|
||||
i += size
|
||||
}
|
||||
if i == 0 {
|
||||
return "", errors.New("mail: invalid string")
|
||||
}
|
||||
|
||||
var atom string
|
||||
atom, p.s = p.s[:i], p.s[i:]
|
||||
return atom, nil
|
||||
}
|
||||
|
||||
func isAtext(r rune, dot bool) bool {
|
||||
switch r {
|
||||
case '.':
|
||||
return dot
|
||||
// RFC 5322 3.2.3 specials
|
||||
case '(', ')', '[', ']', ';', '@', '\\', ',':
|
||||
return false
|
||||
case '<', '>', '"', ':':
|
||||
return false
|
||||
}
|
||||
return isVchar(r)
|
||||
}
|
||||
|
||||
// isVchar reports whether r is an RFC 5322 VCHAR character.
|
||||
func isVchar(r rune) bool {
|
||||
// Visible (printing) characters
|
||||
return '!' <= r && r <= '~' || isMultibyte(r)
|
||||
}
|
||||
|
||||
// isMultibyte reports whether r is a multi-byte UTF-8 character
|
||||
// as supported by RFC 6532
|
||||
func isMultibyte(r rune) bool {
|
||||
return r >= utf8.RuneSelf
|
||||
}
|
||||
|
||||
func (p *headerParser) parseNoFoldLiteral() (string, error) {
|
||||
if !p.consume('[') {
|
||||
return "", errors.New("mail: missing '[' in no-fold-literal")
|
||||
}
|
||||
|
||||
i := 0
|
||||
for {
|
||||
r, size := utf8.DecodeRuneInString(p.s[i:])
|
||||
if size == 1 && r == utf8.RuneError {
|
||||
return "", fmt.Errorf("mail: invalid UTF-8 in no-fold-literal: %q", p.s)
|
||||
} else if size == 0 || !isDtext(r) {
|
||||
break
|
||||
}
|
||||
i += size
|
||||
}
|
||||
var lit string
|
||||
lit, p.s = p.s[:i], p.s[i:]
|
||||
|
||||
if !p.consume(']') {
|
||||
return "", errors.New("mail: missing ']' in no-fold-literal")
|
||||
}
|
||||
return "[" + lit + "]", nil
|
||||
}
|
||||
|
||||
func isDtext(r rune) bool {
|
||||
switch r {
|
||||
case '[', ']', '\\':
|
||||
return false
|
||||
}
|
||||
return isVchar(r)
|
||||
}
|
||||
|
||||
func (p *headerParser) parseMsgID() (string, error) {
|
||||
if !p.skipCFWS() {
|
||||
return "", errors.New("mail: malformed parenthetical comment")
|
||||
}
|
||||
|
||||
if !p.consume('<') {
|
||||
return "", errors.New("mail: missing '<' in msg-id")
|
||||
}
|
||||
|
||||
left, err := p.parseAtomText(true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !p.consume('@') {
|
||||
return "", errors.New("mail: missing '@' in msg-id")
|
||||
}
|
||||
|
||||
var right string
|
||||
if !p.empty() && p.peek() == '[' {
|
||||
// no-fold-literal
|
||||
right, err = p.parseNoFoldLiteral()
|
||||
} else {
|
||||
right, err = p.parseAtomText(true)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !p.consume('>') {
|
||||
return "", errors.New("mail: missing '>' in msg-id")
|
||||
}
|
||||
|
||||
if !p.skipCFWS() {
|
||||
return "", errors.New("mail: malformed parenthetical comment")
|
||||
}
|
||||
|
||||
return left + "@" + right, nil
|
||||
}
|
||||
|
||||
// A Header is a mail header.
|
||||
type Header struct {
|
||||
message.Header
|
||||
}
|
||||
|
||||
// HeaderFromMap creates a header from a map of header fields.
|
||||
//
|
||||
// This function is provided for interoperability with the standard library.
|
||||
// If possible, ReadHeader should be used instead to avoid loosing information.
|
||||
// The map representation looses the ordering of the fields, the capitalization
|
||||
// of the header keys, and the whitespace of the original header.
|
||||
func HeaderFromMap(m map[string][]string) Header {
|
||||
return Header{message.HeaderFromMap(m)}
|
||||
}
|
||||
|
||||
// AddressList parses the named header field as a list of addresses. If the
|
||||
// header field is missing, it returns nil.
|
||||
//
|
||||
// This can be used on From, Sender, Reply-To, To, Cc and Bcc header fields.
|
||||
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 field to the provided list of
|
||||
// addresses.
|
||||
//
|
||||
// This can be used on From, Sender, Reply-To, To, Cc and Bcc header fields.
|
||||
func (h *Header) SetAddressList(key string, addrs []*Address) {
|
||||
if len(addrs) > 0 {
|
||||
h.Set(key, formatAddressList(addrs))
|
||||
} else {
|
||||
h.Del(key)
|
||||
}
|
||||
}
|
||||
|
||||
// Date parses the Date header field. If the header field is missing, it
|
||||
// returns the zero time.
|
||||
func (h *Header) Date() (time.Time, error) {
|
||||
v := h.Get("Date")
|
||||
if v == "" {
|
||||
return time.Time{}, nil
|
||||
}
|
||||
return mail.ParseDate(v)
|
||||
}
|
||||
|
||||
// SetDate formats the Date header field.
|
||||
func (h *Header) SetDate(t time.Time) {
|
||||
if !t.IsZero() {
|
||||
h.Set("Date", t.Format(dateLayout))
|
||||
} else {
|
||||
h.Del("Date")
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// MessageID parses the Message-ID field. It returns the message identifier,
|
||||
// without the angle brackets. If the message doesn't have a Message-ID header
|
||||
// field, it returns an empty string.
|
||||
func (h *Header) MessageID() (string, error) {
|
||||
v := h.Get("Message-Id")
|
||||
if v == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
p := headerParser{v}
|
||||
return p.parseMsgID()
|
||||
}
|
||||
|
||||
// MsgIDList parses a list of message identifiers. It returns message
|
||||
// identifiers without angle brackets. If the header field is missing, it
|
||||
// returns nil.
|
||||
//
|
||||
// This can be used on In-Reply-To and References header fields.
|
||||
func (h *Header) MsgIDList(key string) ([]string, error) {
|
||||
v := h.Get(key)
|
||||
if v == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
p := headerParser{v}
|
||||
var l []string
|
||||
for !p.empty() {
|
||||
msgID, err := p.parseMsgID()
|
||||
if err != nil {
|
||||
return l, err
|
||||
}
|
||||
l = append(l, msgID)
|
||||
}
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// GenerateMessageID wraps GenerateMessageIDWithHostname and therefore uses the
|
||||
// hostname of the local machine. This is done to not break existing software.
|
||||
// Wherever possible better use GenerateMessageIDWithHostname, because the local
|
||||
// hostname of a machine tends to not be unique nor a FQDN which especially
|
||||
// brings problems with spam filters.
|
||||
func (h *Header) GenerateMessageID() error {
|
||||
var err error
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return h.GenerateMessageIDWithHostname(hostname)
|
||||
}
|
||||
|
||||
// GenerateMessageIDWithHostname generates an RFC 2822-compliant Message-Id
|
||||
// based on the informational draft "Recommendations for generating Message
|
||||
// IDs", it takes an hostname as argument, so that software using this library
|
||||
// could use a hostname they know to be unique
|
||||
func (h *Header) GenerateMessageIDWithHostname(hostname string) error {
|
||||
now := uint64(time.Now().UnixNano())
|
||||
|
||||
nonceByte := make([]byte, 8)
|
||||
if _, err := rand.Read(nonceByte); err != nil {
|
||||
return err
|
||||
}
|
||||
nonce := binary.BigEndian.Uint64(nonceByte)
|
||||
|
||||
msgID := fmt.Sprintf("%s.%s@%s", base36(now), base36(nonce), hostname)
|
||||
h.SetMessageID(msgID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func base36(input uint64) string {
|
||||
return strings.ToUpper(strconv.FormatUint(input, 36))
|
||||
}
|
||||
|
||||
// SetMessageID sets the Message-ID field. id is the message identifier,
|
||||
// without the angle brackets.
|
||||
func (h *Header) SetMessageID(id string) {
|
||||
if id != "" {
|
||||
h.Set("Message-Id", "<"+id+">")
|
||||
} else {
|
||||
h.Del("Message-Id")
|
||||
}
|
||||
}
|
||||
|
||||
// SetMsgIDList formats a list of message identifiers. Message identifiers
|
||||
// don't include angle brackets.
|
||||
//
|
||||
// This can be used on In-Reply-To and References header fields.
|
||||
func (h *Header) SetMsgIDList(key string, l []string) {
|
||||
if len(l) > 0 {
|
||||
h.Set(key, "<"+strings.Join(l, "> <")+">")
|
||||
} else {
|
||||
h.Del(key)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy creates a stand-alone copy of the header.
|
||||
func (h *Header) Copy() Header {
|
||||
return Header{h.Header.Copy()}
|
||||
}
|
||||
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
|
||||
}
|
||||
9
vendor/github.com/emersion/go-message/mail/mail.go
generated
vendored
Normal file
9
vendor/github.com/emersion/go-message/mail/mail.go
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
// 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
|
||||
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
|
||||
}
|
||||
132
vendor/github.com/emersion/go-message/mail/writer.go
generated
vendored
Normal file
132
vendor/github.com/emersion/go-message/mail/writer.go
generated
vendored
Normal file
@@ -0,0 +1,132 @@
|
||||
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 = header.Copy() // don't modify the caller's view
|
||||
header.Set("Content-Type", "multipart/mixed")
|
||||
|
||||
mw, err := message.CreateWriter(w, header.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Writer{mw}, nil
|
||||
}
|
||||
|
||||
// CreateInlineWriter writes a mail header to w. The mail will contain an
|
||||
// inline part, allowing to represent the same text in different formats.
|
||||
// Attachments cannot be included.
|
||||
func CreateInlineWriter(w io.Writer, header Header) (*InlineWriter, error) {
|
||||
header = header.Copy() // don't modify the caller's view
|
||||
header.Set("Content-Type", "multipart/alternative")
|
||||
|
||||
mw, err := message.CreateWriter(w, header.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &InlineWriter{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) {
|
||||
header = header.Copy() // don't modify the caller's view
|
||||
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) {
|
||||
h = InlineHeader{h.Header.Copy()} // don't modify the caller's view
|
||||
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) {
|
||||
h = AttachmentHeader{h.Header.Copy()} // don't modify the caller's view
|
||||
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) {
|
||||
h = InlineHeader{h.Header.Copy()} // don't modify the caller's view
|
||||
initInlineHeader(&h)
|
||||
return w.mw.CreatePart(h.Header)
|
||||
}
|
||||
|
||||
// Close finishes the InlineWriter.
|
||||
func (w *InlineWriter) Close() error {
|
||||
return w.mw.Close()
|
||||
}
|
||||
15
vendor/github.com/emersion/go-message/message.go
generated
vendored
Normal file
15
vendor/github.com/emersion/go-message/message.go
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
// 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"
|
||||
// )
|
||||
//
|
||||
// Note, non-UTF-8 charsets are only supported when reading messages. Only
|
||||
// UTF-8 is supported when writing messages.
|
||||
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
|
||||
}
|
||||
677
vendor/github.com/emersion/go-message/textproto/header.go
generated
vendored
Normal file
677
vendor/github.com/emersion/go-message/textproto/header.go
generated
vendored
Normal file
@@ -0,0 +1,677 @@
|
||||
package textproto
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/textproto"
|
||||
"sort"
|
||||
"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}
|
||||
}
|
||||
|
||||
func (f *headerField) raw() ([]byte, error) {
|
||||
if f.b != nil {
|
||||
return f.b, nil
|
||||
} else {
|
||||
for pos, ch := range f.k {
|
||||
// check if character is a printable US-ASCII except ':'
|
||||
if !(ch >= '!' && ch < ':' || ch > ':' && ch <= '~') {
|
||||
return nil, fmt.Errorf("field name contains incorrect symbols (\\x%x at %v)", ch, pos)
|
||||
}
|
||||
}
|
||||
|
||||
if pos := strings.IndexAny(f.v, "\r\n"); pos != -1 {
|
||||
return nil, fmt.Errorf("field value contains \\r\\n (at %v)", pos)
|
||||
}
|
||||
|
||||
return []byte(formatHeaderField(f.k, f.v)), nil
|
||||
}
|
||||
}
|
||||
|
||||
// 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 and header field ordering). 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, len(fs))
|
||||
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]
|
||||
}
|
||||
|
||||
return Header{l: fs, m: makeHeaderMap(fs)}
|
||||
}
|
||||
|
||||
// HeaderFromMap creates a header from a map of header fields.
|
||||
//
|
||||
// This function is provided for interoperability with the standard library.
|
||||
// If possible, ReadHeader should be used instead to avoid loosing information.
|
||||
// The map representation looses the ordering of the fields, the capitalization
|
||||
// of the header keys, and the whitespace of the original header.
|
||||
func HeaderFromMap(m map[string][]string) Header {
|
||||
fs := make([]*headerField, 0, len(m))
|
||||
for k, vs := range m {
|
||||
for _, v := range vs {
|
||||
fs = append(fs, newHeaderField(k, v, nil))
|
||||
}
|
||||
}
|
||||
|
||||
sort.SliceStable(fs, func(i, j int) bool {
|
||||
return fs[i].k < fs[j].k
|
||||
})
|
||||
|
||||
return newHeader(fs)
|
||||
}
|
||||
|
||||
// AddRaw adds the raw key, value pair to the header.
|
||||
//
|
||||
// The supplied byte slice should be a complete field in the "Key: Value" form
|
||||
// including trailing CRLF. If there is no comma in the input - AddRaw panics.
|
||||
// No changes are made to kv contents and it will be copied into WriteHeader
|
||||
// output as is.
|
||||
//
|
||||
// kv is directly added to the underlying structure and therefore should not be
|
||||
// modified after the AddRaw call.
|
||||
func (h *Header) AddRaw(kv []byte) {
|
||||
colon := bytes.IndexByte(kv, ':')
|
||||
if colon == -1 {
|
||||
panic("textproto: Header.AddRaw: missing colon")
|
||||
}
|
||||
k := textproto.CanonicalMIMEHeaderKey(string(trim(kv[:colon])))
|
||||
v := trimAroundNewlines(kv[colon+1:])
|
||||
|
||||
if h.m == nil {
|
||||
h.m = make(map[string][]*headerField)
|
||||
}
|
||||
|
||||
f := newHeaderField(k, v, kv)
|
||||
h.l = append(h.l, f)
|
||||
h.m[k] = append(h.m[k], f)
|
||||
}
|
||||
|
||||
// Add adds the key, value pair to the header. It prepends to any existing
|
||||
// fields associated with key.
|
||||
//
|
||||
// Key and value should obey character requirements of RFC 6532.
|
||||
// If you need to format or fold lines manually, use AddRaw.
|
||||
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
|
||||
}
|
||||
|
||||
// Raw gets the first raw header field associated with the given key.
|
||||
//
|
||||
// The returned bytes contain a complete field in the "Key: value" form,
|
||||
// including trailing CRLF.
|
||||
//
|
||||
// The returned slice should not be modified and becomes invalid when the
|
||||
// header is updated.
|
||||
//
|
||||
// An error is returned if the header field contains incorrect characters (see
|
||||
// RFC 6532).
|
||||
func (h *Header) Raw(k string) ([]byte, error) {
|
||||
fields := h.m[textproto.CanonicalMIMEHeaderKey(k)]
|
||||
if len(fields) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return fields[len(fields)-1].raw()
|
||||
}
|
||||
|
||||
// Values returns all values associated with the given key.
|
||||
//
|
||||
// The returned slice should not be modified and becomes invalid when the
|
||||
// header is updated.
|
||||
func (h *Header) Values(k string) []string {
|
||||
fields := h.m[textproto.CanonicalMIMEHeaderKey(k)]
|
||||
if len(fields) == 0 {
|
||||
return nil
|
||||
}
|
||||
l := make([]string, len(fields))
|
||||
for i, field := range fields {
|
||||
l[len(fields)-i-1] = field.v
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// 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}
|
||||
}
|
||||
|
||||
// Len returns the number of fields in the header.
|
||||
func (h *Header) Len() int {
|
||||
return len(h.l)
|
||||
}
|
||||
|
||||
// Map returns all header fields as a map.
|
||||
//
|
||||
// This function is provided for interoperability with the standard library.
|
||||
// If possible, Fields should be used instead to avoid loosing information.
|
||||
// The map representation looses the ordering of the fields, the capitalization
|
||||
// of the header keys, and the whitespace of the original header.
|
||||
func (h *Header) Map() map[string][]string {
|
||||
m := make(map[string][]string, h.Len())
|
||||
fields := h.Fields()
|
||||
for fields.Next() {
|
||||
m[fields.Key()] = append(m[fields.Key()], fields.Value())
|
||||
}
|
||||
return 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
|
||||
// Raw returns the raw current header field. See Header.Raw.
|
||||
Raw() ([]byte, error)
|
||||
// Del deletes the current field.
|
||||
Del()
|
||||
// Len returns the amount of header fields in the subset of header iterated
|
||||
// by this HeaderFields instance.
|
||||
//
|
||||
// For Fields(), it will return the amount of fields in the whole header section.
|
||||
// For FieldsByKey(), it will return the amount of fields with certain key.
|
||||
Len() int
|
||||
}
|
||||
|
||||
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) Raw() ([]byte, error) {
|
||||
return fs.field().raw()
|
||||
}
|
||||
|
||||
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--
|
||||
}
|
||||
|
||||
func (fs *headerFields) Len() int {
|
||||
return len(fs.h.l)
|
||||
}
|
||||
|
||||
// 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) Raw() ([]byte, error) {
|
||||
return fs.field().raw()
|
||||
}
|
||||
|
||||
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--
|
||||
}
|
||||
|
||||
func (fs *headerFieldsByKey) Len() int {
|
||||
return len(fs.h.m[fs.k])
|
||||
}
|
||||
|
||||
// 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()
|
||||
line = append(line, l...)
|
||||
if err != nil {
|
||||
return line, err
|
||||
}
|
||||
|
||||
if !more {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return line, nil
|
||||
}
|
||||
|
||||
func isSpace(c byte) bool {
|
||||
return c == ' ' || c == '\t'
|
||||
}
|
||||
|
||||
func validHeaderKeyByte(b byte) bool {
|
||||
c := int(b)
|
||||
return c >= 33 && c <= 126 && c != ':'
|
||||
}
|
||||
|
||||
// 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]
|
||||
}
|
||||
|
||||
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. We preallocate slice that it enough
|
||||
// for most fields.
|
||||
line, err := readLineSlice(r, make([]byte, 0, 256))
|
||||
if err == io.EOF && len(line) == 0 {
|
||||
// Header without a body
|
||||
return nil, nil
|
||||
} else 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
|
||||
b.Grow(len(v))
|
||||
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.
|
||||
//
|
||||
// To avoid denial of service attacks, the provided bufio.Reader should be
|
||||
// reading from an io.LimitedReader or a similar Reader to bound the size of
|
||||
// headers.
|
||||
func ReadHeader(r *bufio.Reader) (Header, error) {
|
||||
fs := make([]*headerField, 0, 32)
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
keyBytes := trim(kv[:i])
|
||||
|
||||
// Verify that there are no invalid characters in the header key.
|
||||
// See RFC 5322 Section 2.2
|
||||
for _, c := range keyBytes {
|
||||
if !validHeaderKeyByte(c) {
|
||||
return newHeader(fs), fmt.Errorf("message: malformed MIME header key: %v", string(keyBytes))
|
||||
}
|
||||
}
|
||||
|
||||
key := textproto.CanonicalMIMEHeaderKey(string(keyBytes))
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func foldLine(v string, maxlen int) (line, next string, ok bool) {
|
||||
ok = true
|
||||
|
||||
// We'll need to fold before maxlen
|
||||
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 closest whitespace before maxlen
|
||||
foldAt = strings.LastIndexAny(v[:foldBefore], " \t\n")
|
||||
|
||||
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:
|
||||
// Another char, we need to insert CRLF + WSP. This will insert an
|
||||
// extra space in the string, so this should be avoided if
|
||||
// possible.
|
||||
folding = "\r\n "
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
|
||||
return v[:foldAt] + folding, v[foldAt:], ok
|
||||
}
|
||||
|
||||
const (
|
||||
preferredHeaderLen = 76
|
||||
maxHeaderLen = 998
|
||||
)
|
||||
|
||||
// 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 {
|
||||
// If this is the first line, substract the length of the key
|
||||
keylen := 0
|
||||
if first {
|
||||
keylen = len(s)
|
||||
}
|
||||
|
||||
// First try with a soft limit
|
||||
l, next, ok := foldLine(v, preferredHeaderLen-keylen)
|
||||
if !ok {
|
||||
// Folding failed to preserve the original header field value. Try
|
||||
// with a larger, hard limit.
|
||||
l, next, _ = foldLine(v, maxHeaderLen-keylen)
|
||||
}
|
||||
v = next
|
||||
s += l
|
||||
first = false
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// WriteHeader writes a MIME header to w.
|
||||
func WriteHeader(w io.Writer, h Header) error {
|
||||
for i := len(h.l) - 1; i >= 0; i-- {
|
||||
f := h.l[i]
|
||||
if rawField, err := f.raw(); err == nil {
|
||||
if _, err := w.Write(rawField); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("failed to write header field #%v (%q): %w", len(h.l)-i, f.k, err)
|
||||
}
|
||||
}
|
||||
|
||||
_, err := w.Write([]byte{'\r', '\n'})
|
||||
return err
|
||||
}
|
||||
474
vendor/github.com/emersion/go-message/textproto/multipart.go
generated
vendored
Normal file
474
vendor/github.com/emersion/go-message/textproto/multipart.go
generated
vendored
Normal file
@@ -0,0 +1,474 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
2
vendor/github.com/emersion/go-message/textproto/textproto.go
generated
vendored
Normal file
2
vendor/github.com/emersion/go-message/textproto/textproto.go
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package textproto implements low-level manipulation of MIME messages.
|
||||
package textproto
|
||||
134
vendor/github.com/emersion/go-message/writer.go
generated
vendored
Normal file
134
vendor/github.com/emersion/go-message/writer.go
generated
vendored
Normal file
@@ -0,0 +1,134 @@
|
||||
package message
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-message/textproto"
|
||||
)
|
||||
|
||||
// Writer writes message entities.
|
||||
//
|
||||
// If the message is not multipart, it should be used as a WriteCloser. Don't
|
||||
// forget to call Close.
|
||||
//
|
||||
// If the message is multipart, users can either use CreatePart to write child
|
||||
// parts or Write to directly pipe a multipart message. In any case, Close must
|
||||
// be called at the end.
|
||||
type Writer struct {
|
||||
w io.Writer
|
||||
c io.Closer
|
||||
mw *textproto.MultipartWriter
|
||||
}
|
||||
|
||||
// createWriter creates a new Writer writing to w with the provided header.
|
||||
// Nothing is written to w when it is called. header is modified in-place.
|
||||
func createWriter(w io.Writer, header *Header) (*Writer, error) {
|
||||
ww := &Writer{w: w}
|
||||
|
||||
mediaType, mediaParams, _ := header.ContentType()
|
||||
if strings.HasPrefix(mediaType, "multipart/") {
|
||||
ww.mw = textproto.NewMultipartWriter(ww.w)
|
||||
|
||||
// Do not set ww's io.Closer for now: if this is a multipart entity but
|
||||
// CreatePart is not used (only Write is used), then the final boundary
|
||||
// is expected to be written by the user too. In this case, ww.Close
|
||||
// shouldn't write the final boundary.
|
||||
|
||||
if mediaParams["boundary"] != "" {
|
||||
ww.mw.SetBoundary(mediaParams["boundary"])
|
||||
} else {
|
||||
mediaParams["boundary"] = ww.mw.Boundary()
|
||||
header.SetContentType(mediaType, mediaParams)
|
||||
}
|
||||
|
||||
header.Del("Content-Transfer-Encoding")
|
||||
} else {
|
||||
wc, err := encodingWriter(header.Get("Content-Transfer-Encoding"), ww.w)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ww.w = wc
|
||||
ww.c = wc
|
||||
}
|
||||
|
||||
switch strings.ToLower(mediaParams["charset"]) {
|
||||
case "", "us-ascii", "utf-8":
|
||||
// This is OK
|
||||
default:
|
||||
// Anything else is invalid
|
||||
return nil, fmt.Errorf("unhandled charset %q", mediaParams["charset"])
|
||||
}
|
||||
|
||||
return ww, nil
|
||||
}
|
||||
|
||||
// CreateWriter creates a new message writer to w. If header contains an
|
||||
// encoding, data written to the Writer will automatically be encoded with it.
|
||||
// The charset needs to be utf-8 or us-ascii.
|
||||
func CreateWriter(w io.Writer, header Header) (*Writer, error) {
|
||||
// Ensure that modifications are invisible to the caller
|
||||
header = header.Copy()
|
||||
|
||||
// If the message uses MIME, it has to include MIME-Version
|
||||
if !header.Has("Mime-Version") {
|
||||
header.Set("MIME-Version", "1.0")
|
||||
}
|
||||
|
||||
ww, err := createWriter(w, &header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := textproto.WriteHeader(w, header.Header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ww, nil
|
||||
}
|
||||
|
||||
// Write implements io.Writer.
|
||||
func (w *Writer) Write(b []byte) (int, error) {
|
||||
return w.w.Write(b)
|
||||
}
|
||||
|
||||
// Close implements io.Closer.
|
||||
func (w *Writer) Close() error {
|
||||
if w.c != nil {
|
||||
return w.c.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreatePart returns a Writer to a new part in this multipart entity. If this
|
||||
// entity is not multipart, it fails. The body of the part should be written to
|
||||
// the returned io.WriteCloser.
|
||||
func (w *Writer) CreatePart(header Header) (*Writer, error) {
|
||||
if w.mw == nil {
|
||||
return nil, errors.New("cannot create a part in a non-multipart message")
|
||||
}
|
||||
|
||||
if w.c == nil {
|
||||
// We know that the user calls CreatePart so Close should write the final
|
||||
// boundary
|
||||
w.c = w.mw
|
||||
}
|
||||
|
||||
// cw -> ww -> pw -> w.mw -> w.w
|
||||
|
||||
ww := &struct{ io.Writer }{nil}
|
||||
|
||||
// ensure that modifications are invisible to the caller
|
||||
header = header.Copy()
|
||||
cw, err := createWriter(ww, &header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pw, err := w.mw.CreatePart(header.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ww.Writer = pw
|
||||
return cw, nil
|
||||
}
|
||||
19
vendor/github.com/emersion/go-sasl/.build.yml
generated
vendored
Normal file
19
vendor/github.com/emersion/go-sasl/.build.yml
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
image: alpine/latest
|
||||
packages:
|
||||
- go
|
||||
# Required by codecov
|
||||
- bash
|
||||
- findutils
|
||||
sources:
|
||||
- https://github.com/emersion/go-sasl
|
||||
tasks:
|
||||
- build: |
|
||||
cd go-sasl
|
||||
go build -v ./...
|
||||
- test: |
|
||||
cd go-sasl
|
||||
go test -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
- upload-coverage: |
|
||||
cd go-sasl
|
||||
export CODECOV_TOKEN=3f257f71-a128-4834-8f68-2b534e9f4cb1
|
||||
curl -s https://codecov.io/bash | bash
|
||||
24
vendor/github.com/emersion/go-sasl/.gitignore
generated
vendored
Normal file
24
vendor/github.com/emersion/go-sasl/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
21
vendor/github.com/emersion/go-sasl/LICENSE
generated
vendored
Normal file
21
vendor/github.com/emersion/go-sasl/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 emersion
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
17
vendor/github.com/emersion/go-sasl/README.md
generated
vendored
Normal file
17
vendor/github.com/emersion/go-sasl/README.md
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
# go-sasl
|
||||
|
||||
[](https://pkg.go.dev/github.com/emersion/go-sasl)
|
||||
|
||||
A [SASL](https://tools.ietf.org/html/rfc4422) library written in Go.
|
||||
|
||||
Implemented mechanisms:
|
||||
|
||||
* [ANONYMOUS](https://tools.ietf.org/html/rfc4505)
|
||||
* [EXTERNAL](https://tools.ietf.org/html/rfc4422#appendix-A)
|
||||
* [LOGIN](https://tools.ietf.org/html/draft-murchison-sasl-login-00) (obsolete, use PLAIN instead)
|
||||
* [PLAIN](https://tools.ietf.org/html/rfc4616)
|
||||
* [OAUTHBEARER](https://tools.ietf.org/html/rfc7628)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
56
vendor/github.com/emersion/go-sasl/anonymous.go
generated
vendored
Normal file
56
vendor/github.com/emersion/go-sasl/anonymous.go
generated
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
package sasl
|
||||
|
||||
// The ANONYMOUS mechanism name.
|
||||
const Anonymous = "ANONYMOUS"
|
||||
|
||||
type anonymousClient struct {
|
||||
Trace string
|
||||
}
|
||||
|
||||
func (c *anonymousClient) Start() (mech string, ir []byte, err error) {
|
||||
mech = Anonymous
|
||||
ir = []byte(c.Trace)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *anonymousClient) Next(challenge []byte) (response []byte, err error) {
|
||||
return nil, ErrUnexpectedServerChallenge
|
||||
}
|
||||
|
||||
// A client implementation of the ANONYMOUS authentication mechanism, as
|
||||
// described in RFC 4505.
|
||||
func NewAnonymousClient(trace string) Client {
|
||||
return &anonymousClient{trace}
|
||||
}
|
||||
|
||||
// Get trace information from clients logging in anonymously.
|
||||
type AnonymousAuthenticator func(trace string) error
|
||||
|
||||
type anonymousServer struct {
|
||||
done bool
|
||||
authenticate AnonymousAuthenticator
|
||||
}
|
||||
|
||||
func (s *anonymousServer) Next(response []byte) (challenge []byte, done bool, err error) {
|
||||
if s.done {
|
||||
err = ErrUnexpectedClientResponse
|
||||
return
|
||||
}
|
||||
|
||||
// No initial response, send an empty challenge
|
||||
if response == nil {
|
||||
return []byte{}, false, nil
|
||||
}
|
||||
|
||||
s.done = true
|
||||
|
||||
err = s.authenticate(string(response))
|
||||
done = true
|
||||
return
|
||||
}
|
||||
|
||||
// A server implementation of the ANONYMOUS authentication mechanism, as
|
||||
// described in RFC 4505.
|
||||
func NewAnonymousServer(authenticator AnonymousAuthenticator) Server {
|
||||
return &anonymousServer{authenticate: authenticator}
|
||||
}
|
||||
67
vendor/github.com/emersion/go-sasl/external.go
generated
vendored
Normal file
67
vendor/github.com/emersion/go-sasl/external.go
generated
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
package sasl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// The EXTERNAL mechanism name.
|
||||
const External = "EXTERNAL"
|
||||
|
||||
type externalClient struct {
|
||||
Identity string
|
||||
}
|
||||
|
||||
func (a *externalClient) Start() (mech string, ir []byte, err error) {
|
||||
mech = External
|
||||
ir = []byte(a.Identity)
|
||||
return
|
||||
}
|
||||
|
||||
func (a *externalClient) Next(challenge []byte) (response []byte, err error) {
|
||||
return nil, ErrUnexpectedServerChallenge
|
||||
}
|
||||
|
||||
// An implementation of the EXTERNAL authentication mechanism, as described in
|
||||
// RFC 4422. Authorization identity may be left blank to indicate that the
|
||||
// client is requesting to act as the identity associated with the
|
||||
// authentication credentials.
|
||||
func NewExternalClient(identity string) Client {
|
||||
return &externalClient{identity}
|
||||
}
|
||||
|
||||
// ExternalAuthenticator authenticates users with the EXTERNAL mechanism. If
|
||||
// the identity is left blank, it indicates that it is the same as the one used
|
||||
// in the external credentials. If identity is not empty and the server doesn't
|
||||
// support it, an error must be returned.
|
||||
type ExternalAuthenticator func(identity string) error
|
||||
|
||||
type externalServer struct {
|
||||
done bool
|
||||
authenticate ExternalAuthenticator
|
||||
}
|
||||
|
||||
func (a *externalServer) Next(response []byte) (challenge []byte, done bool, err error) {
|
||||
if a.done {
|
||||
return nil, false, ErrUnexpectedClientResponse
|
||||
}
|
||||
|
||||
// No initial response, send an empty challenge
|
||||
if response == nil {
|
||||
return []byte{}, false, nil
|
||||
}
|
||||
|
||||
a.done = true
|
||||
|
||||
if bytes.Contains(response, []byte("\x00")) {
|
||||
return nil, false, errors.New("sasl: identity contains a NUL character")
|
||||
}
|
||||
|
||||
return nil, true, a.authenticate(string(response))
|
||||
}
|
||||
|
||||
// NewExternalServer creates a server implementation of the EXTERNAL
|
||||
// authentication mechanism, as described in RFC 4422.
|
||||
func NewExternalServer(authenticator ExternalAuthenticator) Server {
|
||||
return &externalServer{authenticate: authenticator}
|
||||
}
|
||||
38
vendor/github.com/emersion/go-sasl/login.go
generated
vendored
Normal file
38
vendor/github.com/emersion/go-sasl/login.go
generated
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
package sasl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
// The LOGIN mechanism name.
|
||||
const Login = "LOGIN"
|
||||
|
||||
var expectedChallenge = []byte("Password:")
|
||||
|
||||
type loginClient struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
func (a *loginClient) Start() (mech string, ir []byte, err error) {
|
||||
mech = "LOGIN"
|
||||
ir = []byte(a.Username)
|
||||
return
|
||||
}
|
||||
|
||||
func (a *loginClient) Next(challenge []byte) (response []byte, err error) {
|
||||
if bytes.Compare(challenge, expectedChallenge) != 0 {
|
||||
return nil, ErrUnexpectedServerChallenge
|
||||
} else {
|
||||
return []byte(a.Password), nil
|
||||
}
|
||||
}
|
||||
|
||||
// A client implementation of the LOGIN authentication mechanism for SMTP,
|
||||
// as described in http://www.iana.org/go/draft-murchison-sasl-login
|
||||
//
|
||||
// It is considered obsolete, and should not be used when other mechanisms are
|
||||
// available. For plaintext password authentication use PLAIN mechanism.
|
||||
func NewLoginClient(username, password string) Client {
|
||||
return &loginClient{username, password}
|
||||
}
|
||||
198
vendor/github.com/emersion/go-sasl/oauthbearer.go
generated
vendored
Normal file
198
vendor/github.com/emersion/go-sasl/oauthbearer.go
generated
vendored
Normal file
@@ -0,0 +1,198 @@
|
||||
package sasl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// The OAUTHBEARER mechanism name.
|
||||
const OAuthBearer = "OAUTHBEARER"
|
||||
|
||||
type OAuthBearerError struct {
|
||||
Status string `json:"status"`
|
||||
Schemes string `json:"schemes"`
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
|
||||
type OAuthBearerOptions struct {
|
||||
Username string
|
||||
Token string
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
// Implements error
|
||||
func (err *OAuthBearerError) Error() string {
|
||||
return fmt.Sprintf("OAUTHBEARER authentication error (%v)", err.Status)
|
||||
}
|
||||
|
||||
type oauthBearerClient struct {
|
||||
OAuthBearerOptions
|
||||
}
|
||||
|
||||
func (a *oauthBearerClient) Start() (mech string, ir []byte, err error) {
|
||||
var authzid string
|
||||
if a.Username != "" {
|
||||
authzid = "a=" + a.Username
|
||||
}
|
||||
str := "n," + authzid + ","
|
||||
|
||||
if a.Host != "" {
|
||||
str += "\x01host=" + a.Host
|
||||
}
|
||||
|
||||
if a.Port != 0 {
|
||||
str += "\x01port=" + strconv.Itoa(a.Port)
|
||||
}
|
||||
str += "\x01auth=Bearer " + a.Token + "\x01\x01"
|
||||
ir = []byte(str)
|
||||
return OAuthBearer, ir, nil
|
||||
}
|
||||
|
||||
func (a *oauthBearerClient) Next(challenge []byte) ([]byte, error) {
|
||||
authBearerErr := &OAuthBearerError{}
|
||||
if err := json.Unmarshal(challenge, authBearerErr); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return nil, authBearerErr
|
||||
}
|
||||
}
|
||||
|
||||
// An implementation of the OAUTHBEARER authentication mechanism, as
|
||||
// described in RFC 7628.
|
||||
func NewOAuthBearerClient(opt *OAuthBearerOptions) Client {
|
||||
return &oauthBearerClient{*opt}
|
||||
}
|
||||
|
||||
type OAuthBearerAuthenticator func(opts OAuthBearerOptions) *OAuthBearerError
|
||||
|
||||
type oauthBearerServer struct {
|
||||
done bool
|
||||
failErr error
|
||||
authenticate OAuthBearerAuthenticator
|
||||
}
|
||||
|
||||
func (a *oauthBearerServer) fail(descr string) ([]byte, bool, error) {
|
||||
blob, err := json.Marshal(OAuthBearerError{
|
||||
Status: "invalid_request",
|
||||
Schemes: "bearer",
|
||||
})
|
||||
if err != nil {
|
||||
panic(err) // wtf
|
||||
}
|
||||
a.failErr = errors.New("sasl: client error: " + descr)
|
||||
return blob, false, nil
|
||||
}
|
||||
|
||||
func (a *oauthBearerServer) Next(response []byte) (challenge []byte, done bool, err error) {
|
||||
// Per RFC, we cannot just send an error, we need to return JSON-structured
|
||||
// value as a challenge and then after getting dummy response from the
|
||||
// client stop the exchange.
|
||||
if a.failErr != nil {
|
||||
// Server libraries (go-smtp, go-imap) will not call Next on
|
||||
// protocol-specific SASL cancel response ('*'). However, GS2 (and
|
||||
// indirectly OAUTHBEARER) defines a protocol-independent way to do so
|
||||
// using 0x01.
|
||||
if len(response) != 1 && response[0] != 0x01 {
|
||||
return nil, true, errors.New("sasl: invalid response")
|
||||
}
|
||||
return nil, true, a.failErr
|
||||
}
|
||||
|
||||
if a.done {
|
||||
err = ErrUnexpectedClientResponse
|
||||
return
|
||||
}
|
||||
|
||||
// Generate empty challenge.
|
||||
if response == nil {
|
||||
return []byte{}, false, nil
|
||||
}
|
||||
|
||||
a.done = true
|
||||
|
||||
// Cut n,a=username,\x01host=...\x01auth=...
|
||||
// into
|
||||
// n
|
||||
// a=username
|
||||
// \x01host=...\x01auth=...\x01\x01
|
||||
parts := bytes.SplitN(response, []byte{','}, 3)
|
||||
if len(parts) != 3 {
|
||||
return a.fail("Invalid response")
|
||||
}
|
||||
flag := parts[0]
|
||||
authzid := parts[1]
|
||||
if !bytes.Equal(flag, []byte{'n'}) {
|
||||
return a.fail("Invalid response, missing 'n' in gs2-cb-flag")
|
||||
}
|
||||
opts := OAuthBearerOptions{}
|
||||
if len(authzid) > 0 {
|
||||
if !bytes.HasPrefix(authzid, []byte("a=")) {
|
||||
return a.fail("Invalid response, missing 'a=' in gs2-authzid")
|
||||
}
|
||||
opts.Username = string(bytes.TrimPrefix(authzid, []byte("a=")))
|
||||
}
|
||||
|
||||
// Cut \x01host=...\x01auth=...\x01\x01
|
||||
// into
|
||||
// *empty*
|
||||
// host=...
|
||||
// auth=...
|
||||
// *empty*
|
||||
//
|
||||
// Note that this code does not do a lot of checks to make sure the input
|
||||
// follows the exact format specified by RFC.
|
||||
params := bytes.Split(parts[2], []byte{0x01})
|
||||
for _, p := range params {
|
||||
// Skip empty fields (one at start and end).
|
||||
if len(p) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
pParts := bytes.SplitN(p, []byte{'='}, 2)
|
||||
if len(pParts) != 2 {
|
||||
return a.fail("Invalid response, missing '='")
|
||||
}
|
||||
|
||||
switch string(pParts[0]) {
|
||||
case "host":
|
||||
opts.Host = string(pParts[1])
|
||||
case "port":
|
||||
port, err := strconv.ParseUint(string(pParts[1]), 10, 16)
|
||||
if err != nil {
|
||||
return a.fail("Invalid response, malformed 'port' value")
|
||||
}
|
||||
opts.Port = int(port)
|
||||
case "auth":
|
||||
const prefix = "bearer "
|
||||
strValue := string(pParts[1])
|
||||
// Token type is case-insensitive.
|
||||
if !strings.HasPrefix(strings.ToLower(strValue), prefix) {
|
||||
return a.fail("Unsupported token type")
|
||||
}
|
||||
opts.Token = strValue[len(prefix):]
|
||||
default:
|
||||
return a.fail("Invalid response, unknown parameter: " + string(pParts[0]))
|
||||
}
|
||||
}
|
||||
|
||||
authzErr := a.authenticate(opts)
|
||||
if authzErr != nil {
|
||||
blob, err := json.Marshal(authzErr)
|
||||
if err != nil {
|
||||
panic(err) // wtf
|
||||
}
|
||||
a.failErr = authzErr
|
||||
return blob, false, nil
|
||||
}
|
||||
|
||||
return nil, true, nil
|
||||
}
|
||||
|
||||
func NewOAuthBearerServer(auth OAuthBearerAuthenticator) Server {
|
||||
return &oauthBearerServer{authenticate: auth}
|
||||
}
|
||||
77
vendor/github.com/emersion/go-sasl/plain.go
generated
vendored
Normal file
77
vendor/github.com/emersion/go-sasl/plain.go
generated
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
package sasl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// The PLAIN mechanism name.
|
||||
const Plain = "PLAIN"
|
||||
|
||||
type plainClient struct {
|
||||
Identity string
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
func (a *plainClient) Start() (mech string, ir []byte, err error) {
|
||||
mech = "PLAIN"
|
||||
ir = []byte(a.Identity + "\x00" + a.Username + "\x00" + a.Password)
|
||||
return
|
||||
}
|
||||
|
||||
func (a *plainClient) Next(challenge []byte) (response []byte, err error) {
|
||||
return nil, ErrUnexpectedServerChallenge
|
||||
}
|
||||
|
||||
// A client implementation of the PLAIN authentication mechanism, as described
|
||||
// in RFC 4616. Authorization identity may be left blank to indicate that it is
|
||||
// the same as the username.
|
||||
func NewPlainClient(identity, username, password string) Client {
|
||||
return &plainClient{identity, username, password}
|
||||
}
|
||||
|
||||
// Authenticates users with an identity, a username and a password. If the
|
||||
// identity is left blank, it indicates that it is the same as the username.
|
||||
// If identity is not empty and the server doesn't support it, an error must be
|
||||
// returned.
|
||||
type PlainAuthenticator func(identity, username, password string) error
|
||||
|
||||
type plainServer struct {
|
||||
done bool
|
||||
authenticate PlainAuthenticator
|
||||
}
|
||||
|
||||
func (a *plainServer) Next(response []byte) (challenge []byte, done bool, err error) {
|
||||
if a.done {
|
||||
err = ErrUnexpectedClientResponse
|
||||
return
|
||||
}
|
||||
|
||||
// No initial response, send an empty challenge
|
||||
if response == nil {
|
||||
return []byte{}, false, nil
|
||||
}
|
||||
|
||||
a.done = true
|
||||
|
||||
parts := bytes.Split(response, []byte("\x00"))
|
||||
if len(parts) != 3 {
|
||||
err = errors.New("sasl: invalid response")
|
||||
return
|
||||
}
|
||||
|
||||
identity := string(parts[0])
|
||||
username := string(parts[1])
|
||||
password := string(parts[2])
|
||||
|
||||
err = a.authenticate(identity, username, password)
|
||||
done = true
|
||||
return
|
||||
}
|
||||
|
||||
// A server implementation of the PLAIN authentication mechanism, as described
|
||||
// in RFC 4616.
|
||||
func NewPlainServer(authenticator PlainAuthenticator) Server {
|
||||
return &plainServer{authenticate: authenticator}
|
||||
}
|
||||
45
vendor/github.com/emersion/go-sasl/sasl.go
generated
vendored
Normal file
45
vendor/github.com/emersion/go-sasl/sasl.go
generated
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
// Library for Simple Authentication and Security Layer (SASL) defined in RFC 4422.
|
||||
package sasl
|
||||
|
||||
// Note:
|
||||
// Most of this code was copied, with some modifications, from net/smtp. It
|
||||
// would be better if Go provided a standard package (e.g. crypto/sasl) that
|
||||
// could be shared by SMTP, IMAP, and other packages.
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// Common SASL errors.
|
||||
var (
|
||||
ErrUnexpectedClientResponse = errors.New("sasl: unexpected client response")
|
||||
ErrUnexpectedServerChallenge = errors.New("sasl: unexpected server challenge")
|
||||
)
|
||||
|
||||
// Client interface to perform challenge-response authentication.
|
||||
type Client interface {
|
||||
// Begins SASL authentication with the server. It returns the
|
||||
// authentication mechanism name and "initial response" data (if required by
|
||||
// the selected mechanism). A non-nil error causes the client to abort the
|
||||
// authentication attempt.
|
||||
//
|
||||
// A nil ir value is different from a zero-length value. The nil value
|
||||
// indicates that the selected mechanism does not use an initial response,
|
||||
// while a zero-length value indicates an empty initial response, which must
|
||||
// be sent to the server.
|
||||
Start() (mech string, ir []byte, err error)
|
||||
|
||||
// Continues challenge-response authentication. A non-nil error causes
|
||||
// the client to abort the authentication attempt.
|
||||
Next(challenge []byte) (response []byte, err error)
|
||||
}
|
||||
|
||||
// Server interface to perform challenge-response authentication.
|
||||
type Server interface {
|
||||
// Begins or continues challenge-response authentication. If the client
|
||||
// supplies an initial response, response is non-nil.
|
||||
//
|
||||
// If the authentication is finished, done is set to true. If the
|
||||
// authentication has failed, an error is returned.
|
||||
Next(response []byte) (challenge []byte, done bool, err error)
|
||||
}
|
||||
4
vendor/github.com/sashabaranov/go-openai/.codecov.yml
generated
vendored
Normal file
4
vendor/github.com/sashabaranov/go-openai/.codecov.yml
generated
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
coverage:
|
||||
ignore:
|
||||
- "examples/**"
|
||||
- "internal/test/**"
|
||||
22
vendor/github.com/sashabaranov/go-openai/.gitignore
generated
vendored
Normal file
22
vendor/github.com/sashabaranov/go-openai/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Auth token for tests
|
||||
.openai-token
|
||||
.idea
|
||||
|
||||
# Generated by tests
|
||||
test.mp3
|
||||
168
vendor/github.com/sashabaranov/go-openai/.golangci.yml
generated
vendored
Normal file
168
vendor/github.com/sashabaranov/go-openai/.golangci.yml
generated
vendored
Normal file
@@ -0,0 +1,168 @@
|
||||
version: "2"
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- asciicheck
|
||||
- bidichk
|
||||
- bodyclose
|
||||
- contextcheck
|
||||
- cyclop
|
||||
- dupl
|
||||
- durationcheck
|
||||
- errcheck
|
||||
- errname
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- forbidigo
|
||||
- funlen
|
||||
- gochecknoinits
|
||||
- gocognit
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godot
|
||||
- gomoddirectives
|
||||
- gomodguard
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- govet
|
||||
- ineffassign
|
||||
- lll
|
||||
- makezero
|
||||
- mnd
|
||||
- nestif
|
||||
- nilerr
|
||||
- nilnil
|
||||
- nolintlint
|
||||
- nosprintfhostport
|
||||
- predeclared
|
||||
- promlinter
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- sqlclosecheck
|
||||
- staticcheck
|
||||
- testpackage
|
||||
- tparallel
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- usetesting
|
||||
- wastedassign
|
||||
- whitespace
|
||||
settings:
|
||||
cyclop:
|
||||
max-complexity: 30
|
||||
package-average: 10
|
||||
errcheck:
|
||||
check-type-assertions: true
|
||||
funlen:
|
||||
lines: 100
|
||||
statements: 50
|
||||
gocognit:
|
||||
min-complexity: 20
|
||||
gocritic:
|
||||
settings:
|
||||
captLocal:
|
||||
paramsOnly: false
|
||||
underef:
|
||||
skipRecvDeref: false
|
||||
gomodguard:
|
||||
blocked:
|
||||
modules:
|
||||
- github.com/golang/protobuf:
|
||||
recommendations:
|
||||
- google.golang.org/protobuf
|
||||
reason: see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules
|
||||
- github.com/satori/go.uuid:
|
||||
recommendations:
|
||||
- github.com/google/uuid
|
||||
reason: satori's package is not maintained
|
||||
- github.com/gofrs/uuid:
|
||||
recommendations:
|
||||
- github.com/google/uuid
|
||||
reason: 'see recommendation from dev-infra team: https://confluence.gtforge.com/x/gQI6Aw'
|
||||
govet:
|
||||
disable:
|
||||
- fieldalignment
|
||||
enable-all: true
|
||||
settings:
|
||||
shadow:
|
||||
strict: true
|
||||
mnd:
|
||||
ignored-functions:
|
||||
- os.Chmod
|
||||
- os.Mkdir
|
||||
- os.MkdirAll
|
||||
- os.OpenFile
|
||||
- os.WriteFile
|
||||
- prometheus.ExponentialBuckets
|
||||
- prometheus.ExponentialBucketsRange
|
||||
- prometheus.LinearBuckets
|
||||
- strconv.FormatFloat
|
||||
- strconv.FormatInt
|
||||
- strconv.FormatUint
|
||||
- strconv.ParseFloat
|
||||
- strconv.ParseInt
|
||||
- strconv.ParseUint
|
||||
nakedret:
|
||||
max-func-lines: 0
|
||||
nolintlint:
|
||||
require-explanation: true
|
||||
require-specific: true
|
||||
allow-no-explanation:
|
||||
- funlen
|
||||
- gocognit
|
||||
- lll
|
||||
rowserrcheck:
|
||||
packages:
|
||||
- github.com/jmoiron/sqlx
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
- linters:
|
||||
- forbidigo
|
||||
- mnd
|
||||
- revive
|
||||
path : ^examples/.*\.go$
|
||||
- linters:
|
||||
- lll
|
||||
source: ^//\s*go:generate\s
|
||||
- linters:
|
||||
- godot
|
||||
source: (noinspection|TODO)
|
||||
- linters:
|
||||
- gocritic
|
||||
source: //noinspection
|
||||
- linters:
|
||||
- errorlint
|
||||
source: ^\s+if _, ok := err\.\([^.]+\.InternalError\); ok {
|
||||
- linters:
|
||||
- bodyclose
|
||||
- dupl
|
||||
- funlen
|
||||
- goconst
|
||||
- gosec
|
||||
- noctx
|
||||
- wrapcheck
|
||||
- staticcheck
|
||||
path: _test\.go
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
issues:
|
||||
max-same-issues: 50
|
||||
formatters:
|
||||
enable:
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
88
vendor/github.com/sashabaranov/go-openai/CONTRIBUTING.md
generated
vendored
Normal file
88
vendor/github.com/sashabaranov/go-openai/CONTRIBUTING.md
generated
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
# Contributing Guidelines
|
||||
|
||||
## Overview
|
||||
Thank you for your interest in contributing to the "Go OpenAI" project! By following this guideline, we hope to ensure that your contributions are made smoothly and efficiently. The Go OpenAI project is licensed under the [Apache 2.0 License](https://github.com/sashabaranov/go-openai/blob/master/LICENSE), and we welcome contributions through GitHub pull requests.
|
||||
|
||||
## Reporting Bugs
|
||||
If you discover a bug, first check the [GitHub Issues page](https://github.com/sashabaranov/go-openai/issues) to see if the issue has already been reported. If you're reporting a new issue, please use the "Bug report" template and provide detailed information about the problem, including steps to reproduce it.
|
||||
|
||||
## Suggesting Features
|
||||
If you want to suggest a new feature or improvement, first check the [GitHub Issues page](https://github.com/sashabaranov/go-openai/issues) to ensure a similar suggestion hasn't already been made. Use the "Feature request" template to provide a detailed description of your suggestion.
|
||||
|
||||
## Reporting Vulnerabilities
|
||||
If you identify a security concern, please use the "Report a security vulnerability" template on the [GitHub Issues page](https://github.com/sashabaranov/go-openai/issues) to share the details. This report will only be viewable to repository maintainers. You will be credited if the advisory is published.
|
||||
|
||||
## Questions for Users
|
||||
If you have questions, please utilize [StackOverflow](https://stackoverflow.com/) or the [GitHub Discussions page](https://github.com/sashabaranov/go-openai/discussions).
|
||||
|
||||
## Contributing Code
|
||||
There might already be a similar pull requests submitted! Please search for [pull requests](https://github.com/sashabaranov/go-openai/pulls) before creating one.
|
||||
|
||||
### Requirements for Merging a Pull Request
|
||||
|
||||
The requirements to accept a pull request are as follows:
|
||||
|
||||
- Features not provided by the OpenAI API will not be accepted.
|
||||
- The functionality of the feature must match that of the official OpenAI API.
|
||||
- All pull requests should be written in Go according to common conventions, formatted with `goimports`, and free of warnings from tools like `golangci-lint`.
|
||||
- Include tests and ensure all tests pass.
|
||||
- Maintain test coverage without any reduction.
|
||||
- All pull requests require approval from at least one Go OpenAI maintainer.
|
||||
|
||||
**Note:**
|
||||
The merging method for pull requests in this repository is squash merge.
|
||||
|
||||
### Creating a Pull Request
|
||||
- Fork the repository.
|
||||
- Create a new branch and commit your changes.
|
||||
- Push that branch to GitHub.
|
||||
- Start a new Pull Request on GitHub. (Please use the pull request template to provide detailed information.)
|
||||
|
||||
**Note:**
|
||||
If your changes introduce breaking changes, please prefix your pull request title with "[BREAKING_CHANGES]".
|
||||
|
||||
### Code Style
|
||||
In this project, we adhere to the standard coding style of Go. Your code should maintain consistency with the rest of the codebase. To achieve this, please format your code using tools like `goimports` and resolve any syntax or style issues with `golangci-lint`.
|
||||
|
||||
**Run goimports:**
|
||||
```
|
||||
go install golang.org/x/tools/cmd/goimports@latest
|
||||
```
|
||||
|
||||
```
|
||||
goimports -w .
|
||||
```
|
||||
|
||||
**Run golangci-lint:**
|
||||
```
|
||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||
```
|
||||
|
||||
```
|
||||
golangci-lint run --out-format=github-actions
|
||||
```
|
||||
|
||||
### Unit Test
|
||||
Please create or update tests relevant to your changes. Ensure all tests run successfully to verify that your modifications do not adversely affect other functionalities.
|
||||
|
||||
**Run test:**
|
||||
```
|
||||
go test -v ./...
|
||||
```
|
||||
|
||||
### Integration Test
|
||||
Integration tests are requested against the production version of the OpenAI API. These tests will verify that the library is properly coded against the actual behavior of the API, and will fail upon any incompatible change in the API.
|
||||
|
||||
**Notes:**
|
||||
These tests send real network traffic to the OpenAI API and may reach rate limits. Temporary network problems may also cause the test to fail.
|
||||
|
||||
**Run integration test:**
|
||||
```
|
||||
OPENAI_TOKEN=XXX go test -v -tags=integration ./api_integration_test.go
|
||||
```
|
||||
|
||||
If the `OPENAI_TOKEN` environment variable is not available, integration tests will be skipped.
|
||||
|
||||
---
|
||||
|
||||
We wholeheartedly welcome your active participation. Let's build an amazing project together!
|
||||
201
vendor/github.com/sashabaranov/go-openai/LICENSE
generated
vendored
Normal file
201
vendor/github.com/sashabaranov/go-openai/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
913
vendor/github.com/sashabaranov/go-openai/README.md
generated
vendored
Normal file
913
vendor/github.com/sashabaranov/go-openai/README.md
generated
vendored
Normal file
@@ -0,0 +1,913 @@
|
||||
# Go OpenAI
|
||||
[](https://pkg.go.dev/github.com/sashabaranov/go-openai)
|
||||
[](https://goreportcard.com/report/github.com/sashabaranov/go-openai)
|
||||
[](https://codecov.io/gh/sashabaranov/go-openai)
|
||||
|
||||
This library provides unofficial Go clients for [OpenAI API](https://platform.openai.com/). We support:
|
||||
|
||||
* ChatGPT 4o, o1
|
||||
* GPT-3, GPT-4
|
||||
* DALL·E 2, DALL·E 3, GPT Image 1
|
||||
* Whisper
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
go get github.com/sashabaranov/go-openai
|
||||
```
|
||||
Currently, go-openai requires Go version 1.18 or greater.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
### ChatGPT example usage:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
openai "github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := openai.NewClient("your token")
|
||||
resp, err := client.CreateChatCompletion(
|
||||
context.Background(),
|
||||
openai.ChatCompletionRequest{
|
||||
Model: openai.GPT3Dot5Turbo,
|
||||
Messages: []openai.ChatCompletionMessage{
|
||||
{
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
Content: "Hello!",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("ChatCompletion error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(resp.Choices[0].Message.Content)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Getting an OpenAI API Key:
|
||||
|
||||
1. Visit the OpenAI website at [https://platform.openai.com/account/api-keys](https://platform.openai.com/account/api-keys).
|
||||
2. If you don't have an account, click on "Sign Up" to create one. If you do, click "Log In".
|
||||
3. Once logged in, navigate to your API key management page.
|
||||
4. Click on "Create new secret key".
|
||||
5. Enter a name for your new key, then click "Create secret key".
|
||||
6. Your new API key will be displayed. Use this key to interact with the OpenAI API.
|
||||
|
||||
**Note:** Your API key is sensitive information. Do not share it with anyone.
|
||||
|
||||
### Other examples:
|
||||
|
||||
<details>
|
||||
<summary>ChatGPT streaming completion</summary>
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
openai "github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
func main() {
|
||||
c := openai.NewClient("your token")
|
||||
ctx := context.Background()
|
||||
|
||||
req := openai.ChatCompletionRequest{
|
||||
Model: openai.GPT3Dot5Turbo,
|
||||
MaxTokens: 20,
|
||||
Messages: []openai.ChatCompletionMessage{
|
||||
{
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
Content: "Lorem ipsum",
|
||||
},
|
||||
},
|
||||
Stream: true,
|
||||
}
|
||||
stream, err := c.CreateChatCompletionStream(ctx, req)
|
||||
if err != nil {
|
||||
fmt.Printf("ChatCompletionStream error: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer stream.Close()
|
||||
|
||||
fmt.Printf("Stream response: ")
|
||||
for {
|
||||
response, err := stream.Recv()
|
||||
if errors.Is(err, io.EOF) {
|
||||
fmt.Println("\nStream finished")
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("\nStream error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(response.Choices[0].Delta.Content)
|
||||
}
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>GPT-3 completion</summary>
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
openai "github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
func main() {
|
||||
c := openai.NewClient("your token")
|
||||
ctx := context.Background()
|
||||
|
||||
req := openai.CompletionRequest{
|
||||
Model: openai.GPT3Babbage002,
|
||||
MaxTokens: 5,
|
||||
Prompt: "Lorem ipsum",
|
||||
}
|
||||
resp, err := c.CreateCompletion(ctx, req)
|
||||
if err != nil {
|
||||
fmt.Printf("Completion error: %v\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Println(resp.Choices[0].Text)
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>GPT-3 streaming completion</summary>
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
openai "github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
func main() {
|
||||
c := openai.NewClient("your token")
|
||||
ctx := context.Background()
|
||||
|
||||
req := openai.CompletionRequest{
|
||||
Model: openai.GPT3Babbage002,
|
||||
MaxTokens: 5,
|
||||
Prompt: "Lorem ipsum",
|
||||
Stream: true,
|
||||
}
|
||||
stream, err := c.CreateCompletionStream(ctx, req)
|
||||
if err != nil {
|
||||
fmt.Printf("CompletionStream error: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer stream.Close()
|
||||
|
||||
for {
|
||||
response, err := stream.Recv()
|
||||
if errors.Is(err, io.EOF) {
|
||||
fmt.Println("Stream finished")
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Stream error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
fmt.Printf("Stream response: %v\n", response)
|
||||
}
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Audio Speech-To-Text</summary>
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
openai "github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
func main() {
|
||||
c := openai.NewClient("your token")
|
||||
ctx := context.Background()
|
||||
|
||||
req := openai.AudioRequest{
|
||||
Model: openai.Whisper1,
|
||||
FilePath: "recording.mp3",
|
||||
}
|
||||
resp, err := c.CreateTranscription(ctx, req)
|
||||
if err != nil {
|
||||
fmt.Printf("Transcription error: %v\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Println(resp.Text)
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Audio Captions</summary>
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
openai "github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
func main() {
|
||||
c := openai.NewClient(os.Getenv("OPENAI_KEY"))
|
||||
|
||||
req := openai.AudioRequest{
|
||||
Model: openai.Whisper1,
|
||||
FilePath: os.Args[1],
|
||||
Format: openai.AudioResponseFormatSRT,
|
||||
}
|
||||
resp, err := c.CreateTranscription(context.Background(), req)
|
||||
if err != nil {
|
||||
fmt.Printf("Transcription error: %v\n", err)
|
||||
return
|
||||
}
|
||||
f, err := os.Create(os.Args[1] + ".srt")
|
||||
if err != nil {
|
||||
fmt.Printf("Could not open file: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
if _, err := f.WriteString(resp.Text); err != nil {
|
||||
fmt.Printf("Error writing to file: %v\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>DALL-E 2 image generation</summary>
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
openai "github.com/sashabaranov/go-openai"
|
||||
"image/png"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
c := openai.NewClient("your token")
|
||||
ctx := context.Background()
|
||||
|
||||
// Sample image by link
|
||||
reqUrl := openai.ImageRequest{
|
||||
Prompt: "Parrot on a skateboard performs a trick, cartoon style, natural light, high detail",
|
||||
Size: openai.CreateImageSize256x256,
|
||||
ResponseFormat: openai.CreateImageResponseFormatURL,
|
||||
N: 1,
|
||||
}
|
||||
|
||||
respUrl, err := c.CreateImage(ctx, reqUrl)
|
||||
if err != nil {
|
||||
fmt.Printf("Image creation error: %v\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Println(respUrl.Data[0].URL)
|
||||
|
||||
// Example image as base64
|
||||
reqBase64 := openai.ImageRequest{
|
||||
Prompt: "Portrait of a humanoid parrot in a classic costume, high detail, realistic light, unreal engine",
|
||||
Size: openai.CreateImageSize256x256,
|
||||
ResponseFormat: openai.CreateImageResponseFormatB64JSON,
|
||||
N: 1,
|
||||
}
|
||||
|
||||
respBase64, err := c.CreateImage(ctx, reqBase64)
|
||||
if err != nil {
|
||||
fmt.Printf("Image creation error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
imgBytes, err := base64.StdEncoding.DecodeString(respBase64.Data[0].B64JSON)
|
||||
if err != nil {
|
||||
fmt.Printf("Base64 decode error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
r := bytes.NewReader(imgBytes)
|
||||
imgData, err := png.Decode(r)
|
||||
if err != nil {
|
||||
fmt.Printf("PNG decode error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
file, err := os.Create("example.png")
|
||||
if err != nil {
|
||||
fmt.Printf("File creation error: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if err := png.Encode(file, imgData); err != nil {
|
||||
fmt.Printf("PNG encode error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("The image was saved as example.png")
|
||||
}
|
||||
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>GPT Image 1 image generation</summary>
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
openai "github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
func main() {
|
||||
c := openai.NewClient("your token")
|
||||
ctx := context.Background()
|
||||
|
||||
req := openai.ImageRequest{
|
||||
Prompt: "Parrot on a skateboard performing a trick. Large bold text \"SKATE MASTER\" banner at the bottom of the image. Cartoon style, natural light, high detail, 1:1 aspect ratio.",
|
||||
Background: openai.CreateImageBackgroundOpaque,
|
||||
Model: openai.CreateImageModelGptImage1,
|
||||
Size: openai.CreateImageSize1024x1024,
|
||||
N: 1,
|
||||
Quality: openai.CreateImageQualityLow,
|
||||
OutputCompression: 100,
|
||||
OutputFormat: openai.CreateImageOutputFormatJPEG,
|
||||
// Moderation: openai.CreateImageModerationLow,
|
||||
// User: "",
|
||||
}
|
||||
|
||||
resp, err := c.CreateImage(ctx, req)
|
||||
if err != nil {
|
||||
fmt.Printf("Image creation Image generation with GPT Image 1error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Image Base64:", resp.Data[0].B64JSON)
|
||||
|
||||
// Decode the base64 data
|
||||
imgBytes, err := base64.StdEncoding.DecodeString(resp.Data[0].B64JSON)
|
||||
if err != nil {
|
||||
fmt.Printf("Base64 decode error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Write image to file
|
||||
outputPath := "generated_image.jpg"
|
||||
err = os.WriteFile(outputPath, imgBytes, 0644)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to write image file: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("The image was saved as %s\n", outputPath)
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Configuring proxy</summary>
|
||||
|
||||
```go
|
||||
config := openai.DefaultConfig("token")
|
||||
proxyUrl, err := url.Parse("http://localhost:{port}")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
transport := &http.Transport{
|
||||
Proxy: http.ProxyURL(proxyUrl),
|
||||
}
|
||||
config.HTTPClient = &http.Client{
|
||||
Transport: transport,
|
||||
}
|
||||
|
||||
c := openai.NewClientWithConfig(config)
|
||||
```
|
||||
|
||||
See also: https://pkg.go.dev/github.com/sashabaranov/go-openai#ClientConfig
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>ChatGPT support context</summary>
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := openai.NewClient("your token")
|
||||
messages := make([]openai.ChatCompletionMessage, 0)
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
fmt.Println("Conversation")
|
||||
fmt.Println("---------------------")
|
||||
|
||||
for {
|
||||
fmt.Print("-> ")
|
||||
text, _ := reader.ReadString('\n')
|
||||
// convert CRLF to LF
|
||||
text = strings.Replace(text, "\n", "", -1)
|
||||
messages = append(messages, openai.ChatCompletionMessage{
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
Content: text,
|
||||
})
|
||||
|
||||
resp, err := client.CreateChatCompletion(
|
||||
context.Background(),
|
||||
openai.ChatCompletionRequest{
|
||||
Model: openai.GPT3Dot5Turbo,
|
||||
Messages: messages,
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("ChatCompletion error: %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
content := resp.Choices[0].Message.Content
|
||||
messages = append(messages, openai.ChatCompletionMessage{
|
||||
Role: openai.ChatMessageRoleAssistant,
|
||||
Content: content,
|
||||
})
|
||||
fmt.Println(content)
|
||||
}
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Azure OpenAI ChatGPT</summary>
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
openai "github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
func main() {
|
||||
config := openai.DefaultAzureConfig("your Azure OpenAI Key", "https://your Azure OpenAI Endpoint")
|
||||
// If you use a deployment name different from the model name, you can customize the AzureModelMapperFunc function
|
||||
// config.AzureModelMapperFunc = func(model string) string {
|
||||
// azureModelMapping := map[string]string{
|
||||
// "gpt-3.5-turbo": "your gpt-3.5-turbo deployment name",
|
||||
// }
|
||||
// return azureModelMapping[model]
|
||||
// }
|
||||
|
||||
client := openai.NewClientWithConfig(config)
|
||||
resp, err := client.CreateChatCompletion(
|
||||
context.Background(),
|
||||
openai.ChatCompletionRequest{
|
||||
Model: openai.GPT3Dot5Turbo,
|
||||
Messages: []openai.ChatCompletionMessage{
|
||||
{
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
Content: "Hello Azure OpenAI!",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("ChatCompletion error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(resp.Choices[0].Message.Content)
|
||||
}
|
||||
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Embedding Semantic Similarity</summary>
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
openai "github.com/sashabaranov/go-openai"
|
||||
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := openai.NewClient("your-token")
|
||||
|
||||
// Create an EmbeddingRequest for the user query
|
||||
queryReq := openai.EmbeddingRequest{
|
||||
Input: []string{"How many chucks would a woodchuck chuck"},
|
||||
Model: openai.AdaEmbeddingV2,
|
||||
}
|
||||
|
||||
// Create an embedding for the user query
|
||||
queryResponse, err := client.CreateEmbeddings(context.Background(), queryReq)
|
||||
if err != nil {
|
||||
log.Fatal("Error creating query embedding:", err)
|
||||
}
|
||||
|
||||
// Create an EmbeddingRequest for the target text
|
||||
targetReq := openai.EmbeddingRequest{
|
||||
Input: []string{"How many chucks would a woodchuck chuck if the woodchuck could chuck wood"},
|
||||
Model: openai.AdaEmbeddingV2,
|
||||
}
|
||||
|
||||
// Create an embedding for the target text
|
||||
targetResponse, err := client.CreateEmbeddings(context.Background(), targetReq)
|
||||
if err != nil {
|
||||
log.Fatal("Error creating target embedding:", err)
|
||||
}
|
||||
|
||||
// Now that we have the embeddings for the user query and the target text, we
|
||||
// can calculate their similarity.
|
||||
queryEmbedding := queryResponse.Data[0]
|
||||
targetEmbedding := targetResponse.Data[0]
|
||||
|
||||
similarity, err := queryEmbedding.DotProduct(&targetEmbedding)
|
||||
if err != nil {
|
||||
log.Fatal("Error calculating dot product:", err)
|
||||
}
|
||||
|
||||
log.Printf("The similarity score between the query and the target is %f", similarity)
|
||||
}
|
||||
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Azure OpenAI Embeddings</summary>
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
openai "github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
config := openai.DefaultAzureConfig("your Azure OpenAI Key", "https://your Azure OpenAI Endpoint")
|
||||
config.APIVersion = "2023-05-15" // optional update to latest API version
|
||||
|
||||
//If you use a deployment name different from the model name, you can customize the AzureModelMapperFunc function
|
||||
//config.AzureModelMapperFunc = func(model string) string {
|
||||
// azureModelMapping := map[string]string{
|
||||
// "gpt-3.5-turbo":"your gpt-3.5-turbo deployment name",
|
||||
// }
|
||||
// return azureModelMapping[model]
|
||||
//}
|
||||
|
||||
input := "Text to vectorize"
|
||||
|
||||
client := openai.NewClientWithConfig(config)
|
||||
resp, err := client.CreateEmbeddings(
|
||||
context.Background(),
|
||||
openai.EmbeddingRequest{
|
||||
Input: []string{input},
|
||||
Model: openai.AdaEmbeddingV2,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("CreateEmbeddings error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
vectors := resp.Data[0].Embedding // []float32 with 1536 dimensions
|
||||
|
||||
fmt.Println(vectors[:10], "...", vectors[len(vectors)-10:])
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>JSON Schema for function calling</summary>
|
||||
|
||||
It is now possible for chat completion to choose to call a function for more information ([see developer docs here](https://platform.openai.com/docs/guides/gpt/function-calling)).
|
||||
|
||||
In order to describe the type of functions that can be called, a JSON schema must be provided. Many JSON schema libraries exist and are more advanced than what we can offer in this library, however we have included a simple `jsonschema` package for those who want to use this feature without formatting their own JSON schema payload.
|
||||
|
||||
The developer documents give this JSON schema definition as an example:
|
||||
|
||||
```json
|
||||
{
|
||||
"name":"get_current_weather",
|
||||
"description":"Get the current weather in a given location",
|
||||
"parameters":{
|
||||
"type":"object",
|
||||
"properties":{
|
||||
"location":{
|
||||
"type":"string",
|
||||
"description":"The city and state, e.g. San Francisco, CA"
|
||||
},
|
||||
"unit":{
|
||||
"type":"string",
|
||||
"enum":[
|
||||
"celsius",
|
||||
"fahrenheit"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required":[
|
||||
"location"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Using the `jsonschema` package, this schema could be created using structs as such:
|
||||
|
||||
```go
|
||||
FunctionDefinition{
|
||||
Name: "get_current_weather",
|
||||
Parameters: jsonschema.Definition{
|
||||
Type: jsonschema.Object,
|
||||
Properties: map[string]jsonschema.Definition{
|
||||
"location": {
|
||||
Type: jsonschema.String,
|
||||
Description: "The city and state, e.g. San Francisco, CA",
|
||||
},
|
||||
"unit": {
|
||||
Type: jsonschema.String,
|
||||
Enum: []string{"celsius", "fahrenheit"},
|
||||
},
|
||||
},
|
||||
Required: []string{"location"},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The `Parameters` field of a `FunctionDefinition` can accept either of the above styles, or even a nested struct from another library (as long as it can be marshalled into JSON).
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Error handling</summary>
|
||||
|
||||
Open-AI maintains clear documentation on how to [handle API errors](https://platform.openai.com/docs/guides/error-codes/api-errors)
|
||||
|
||||
example:
|
||||
```
|
||||
e := &openai.APIError{}
|
||||
if errors.As(err, &e) {
|
||||
switch e.HTTPStatusCode {
|
||||
case 401:
|
||||
// invalid auth or key (do not retry)
|
||||
case 429:
|
||||
// rate limiting or engine overload (wait and retry)
|
||||
case 500:
|
||||
// openai server error (retry)
|
||||
default:
|
||||
// unhandled
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Fine Tune Model</summary>
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := openai.NewClient("your token")
|
||||
ctx := context.Background()
|
||||
|
||||
// create a .jsonl file with your training data for conversational model
|
||||
// {"prompt": "<prompt text>", "completion": "<ideal generated text>"}
|
||||
// {"prompt": "<prompt text>", "completion": "<ideal generated text>"}
|
||||
// {"prompt": "<prompt text>", "completion": "<ideal generated text>"}
|
||||
|
||||
// chat models are trained using the following file format:
|
||||
// {"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "What's the capital of France?"}, {"role": "assistant", "content": "Paris, as if everyone doesn't know that already."}]}
|
||||
// {"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "Who wrote 'Romeo and Juliet'?"}, {"role": "assistant", "content": "Oh, just some guy named William Shakespeare. Ever heard of him?"}]}
|
||||
// {"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "How far is the Moon from Earth?"}, {"role": "assistant", "content": "Around 384,400 kilometers. Give or take a few, like that really matters."}]}
|
||||
|
||||
// you can use openai cli tool to validate the data
|
||||
// For more info - https://platform.openai.com/docs/guides/fine-tuning
|
||||
|
||||
file, err := client.CreateFile(ctx, openai.FileRequest{
|
||||
FilePath: "training_prepared.jsonl",
|
||||
Purpose: "fine-tune",
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Upload JSONL file error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// create a fine tuning job
|
||||
// Streams events until the job is done (this often takes minutes, but can take hours if there are many jobs in the queue or your dataset is large)
|
||||
// use below get method to know the status of your model
|
||||
fineTuningJob, err := client.CreateFineTuningJob(ctx, openai.FineTuningJobRequest{
|
||||
TrainingFile: file.ID,
|
||||
Model: "davinci-002", // gpt-3.5-turbo-0613, babbage-002.
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Creating new fine tune model error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fineTuningJob, err = client.RetrieveFineTuningJob(ctx, fineTuningJob.ID)
|
||||
if err != nil {
|
||||
fmt.Printf("Getting fine tune model error: %v\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Println(fineTuningJob.FineTunedModel)
|
||||
|
||||
// once the status of fineTuningJob is `succeeded`, you can use your fine tune model in Completion Request or Chat Completion Request
|
||||
|
||||
// resp, err := client.CreateCompletion(ctx, openai.CompletionRequest{
|
||||
// Model: fineTuningJob.FineTunedModel,
|
||||
// Prompt: "your prompt",
|
||||
// })
|
||||
// if err != nil {
|
||||
// fmt.Printf("Create completion error %v\n", err)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// fmt.Println(resp.Choices[0].Text)
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Structured Outputs</summary>
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/sashabaranov/go-openai"
|
||||
"github.com/sashabaranov/go-openai/jsonschema"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client := openai.NewClient("your token")
|
||||
ctx := context.Background()
|
||||
|
||||
type Result struct {
|
||||
Steps []struct {
|
||||
Explanation string `json:"explanation"`
|
||||
Output string `json:"output"`
|
||||
} `json:"steps"`
|
||||
FinalAnswer string `json:"final_answer"`
|
||||
}
|
||||
var result Result
|
||||
schema, err := jsonschema.GenerateSchemaForType(result)
|
||||
if err != nil {
|
||||
log.Fatalf("GenerateSchemaForType error: %v", err)
|
||||
}
|
||||
resp, err := client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
|
||||
Model: openai.GPT4oMini,
|
||||
Messages: []openai.ChatCompletionMessage{
|
||||
{
|
||||
Role: openai.ChatMessageRoleSystem,
|
||||
Content: "You are a helpful math tutor. Guide the user through the solution step by step.",
|
||||
},
|
||||
{
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
Content: "how can I solve 8x + 7 = -23",
|
||||
},
|
||||
},
|
||||
ResponseFormat: &openai.ChatCompletionResponseFormat{
|
||||
Type: openai.ChatCompletionResponseFormatTypeJSONSchema,
|
||||
JSONSchema: &openai.ChatCompletionResponseFormatJSONSchema{
|
||||
Name: "math_reasoning",
|
||||
Schema: schema,
|
||||
Strict: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("CreateChatCompletion error: %v", err)
|
||||
}
|
||||
err = schema.Unmarshal(resp.Choices[0].Message.Content, &result)
|
||||
if err != nil {
|
||||
log.Fatalf("Unmarshal schema error: %v", err)
|
||||
}
|
||||
fmt.Println(result)
|
||||
}
|
||||
```
|
||||
</details>
|
||||
See the `examples/` folder for more.
|
||||
|
||||
## Frequently Asked Questions
|
||||
|
||||
### Why don't we get the same answer when specifying a temperature field of 0 and asking the same question?
|
||||
|
||||
Even when specifying a temperature field of 0, it doesn't guarantee that you'll always get the same response. Several factors come into play.
|
||||
|
||||
1. Go OpenAI Behavior: When you specify a temperature field of 0 in Go OpenAI, the omitempty tag causes that field to be removed from the request. Consequently, the OpenAI API applies the default value of 1.
|
||||
2. Token Count for Input/Output: If there's a large number of tokens in the input and output, setting the temperature to 0 can still result in non-deterministic behavior. In particular, when using around 32k tokens, the likelihood of non-deterministic behavior becomes highest even with a temperature of 0.
|
||||
|
||||
Due to the factors mentioned above, different answers may be returned even for the same question.
|
||||
|
||||
**Workarounds:**
|
||||
1. As of November 2023, use [the new `seed` parameter](https://platform.openai.com/docs/guides/text-generation/reproducible-outputs) in conjunction with the `system_fingerprint` response field, alongside Temperature management.
|
||||
2. Try using `math.SmallestNonzeroFloat32`: By specifying `math.SmallestNonzeroFloat32` in the temperature field instead of 0, you can mimic the behavior of setting it to 0.
|
||||
3. Limiting Token Count: By limiting the number of tokens in the input and output and especially avoiding large requests close to 32k tokens, you can reduce the risk of non-deterministic behavior.
|
||||
|
||||
By adopting these strategies, you can expect more consistent results.
|
||||
|
||||
**Related Issues:**
|
||||
[omitempty option of request struct will generate incorrect request when parameter is 0.](https://github.com/sashabaranov/go-openai/issues/9)
|
||||
|
||||
### Does Go OpenAI provide a method to count tokens?
|
||||
|
||||
No, Go OpenAI does not offer a feature to count tokens, and there are no plans to provide such a feature in the future. However, if there's a way to implement a token counting feature with zero dependencies, it might be possible to merge that feature into Go OpenAI. Otherwise, it would be more appropriate to implement it in a dedicated library or repository.
|
||||
|
||||
For counting tokens, you might find the following links helpful:
|
||||
- [Counting Tokens For Chat API Calls](https://github.com/pkoukk/tiktoken-go#counting-tokens-for-chat-api-calls)
|
||||
- [How to count tokens with tiktoken](https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb)
|
||||
|
||||
**Related Issues:**
|
||||
[Is it possible to join the implementation of GPT3 Tokenizer](https://github.com/sashabaranov/go-openai/issues/62)
|
||||
|
||||
## Contributing
|
||||
|
||||
By following [Contributing Guidelines](https://github.com/sashabaranov/go-openai/blob/master/CONTRIBUTING.md), we hope to ensure that your contributions are made smoothly and efficiently.
|
||||
|
||||
## Thank you
|
||||
|
||||
We want to take a moment to express our deepest gratitude to the [contributors](https://github.com/sashabaranov/go-openai/graphs/contributors) and sponsors of this project:
|
||||
- [Carson Kahn](https://carsonkahn.com) of [Spindle AI](https://spindleai.com)
|
||||
|
||||
To all of you: thank you. You've helped us achieve more than we ever imagined possible. Can't wait to see where we go next, together!
|
||||
325
vendor/github.com/sashabaranov/go-openai/assistant.go
generated
vendored
Normal file
325
vendor/github.com/sashabaranov/go-openai/assistant.go
generated
vendored
Normal file
@@ -0,0 +1,325 @@
|
||||
package openai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
const (
|
||||
assistantsSuffix = "/assistants"
|
||||
assistantsFilesSuffix = "/files"
|
||||
)
|
||||
|
||||
type Assistant struct {
|
||||
ID string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Model string `json:"model"`
|
||||
Instructions *string `json:"instructions,omitempty"`
|
||||
Tools []AssistantTool `json:"tools"`
|
||||
ToolResources *AssistantToolResource `json:"tool_resources,omitempty"`
|
||||
FileIDs []string `json:"file_ids,omitempty"` // Deprecated in v2
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
Temperature *float32 `json:"temperature,omitempty"`
|
||||
TopP *float32 `json:"top_p,omitempty"`
|
||||
ResponseFormat any `json:"response_format,omitempty"`
|
||||
|
||||
httpHeader
|
||||
}
|
||||
|
||||
type AssistantToolType string
|
||||
|
||||
const (
|
||||
AssistantToolTypeCodeInterpreter AssistantToolType = "code_interpreter"
|
||||
AssistantToolTypeRetrieval AssistantToolType = "retrieval"
|
||||
AssistantToolTypeFunction AssistantToolType = "function"
|
||||
AssistantToolTypeFileSearch AssistantToolType = "file_search"
|
||||
)
|
||||
|
||||
type AssistantTool struct {
|
||||
Type AssistantToolType `json:"type"`
|
||||
Function *FunctionDefinition `json:"function,omitempty"`
|
||||
}
|
||||
|
||||
type AssistantToolFileSearch struct {
|
||||
VectorStoreIDs []string `json:"vector_store_ids"`
|
||||
}
|
||||
|
||||
type AssistantToolCodeInterpreter struct {
|
||||
FileIDs []string `json:"file_ids"`
|
||||
}
|
||||
|
||||
type AssistantToolResource struct {
|
||||
FileSearch *AssistantToolFileSearch `json:"file_search,omitempty"`
|
||||
CodeInterpreter *AssistantToolCodeInterpreter `json:"code_interpreter,omitempty"`
|
||||
}
|
||||
|
||||
// AssistantRequest provides the assistant request parameters.
|
||||
// When modifying the tools the API functions as the following:
|
||||
// If Tools is undefined, no changes are made to the Assistant's tools.
|
||||
// If Tools is empty slice it will effectively delete all of the Assistant's tools.
|
||||
// If Tools is populated, it will replace all of the existing Assistant's tools with the provided tools.
|
||||
type AssistantRequest struct {
|
||||
Model string `json:"model"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Instructions *string `json:"instructions,omitempty"`
|
||||
Tools []AssistantTool `json:"-"`
|
||||
FileIDs []string `json:"file_ids,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
ToolResources *AssistantToolResource `json:"tool_resources,omitempty"`
|
||||
ResponseFormat any `json:"response_format,omitempty"`
|
||||
Temperature *float32 `json:"temperature,omitempty"`
|
||||
TopP *float32 `json:"top_p,omitempty"`
|
||||
}
|
||||
|
||||
// MarshalJSON provides a custom marshaller for the assistant request to handle the API use cases
|
||||
// If Tools is nil, the field is omitted from the JSON.
|
||||
// If Tools is an empty slice, it's included in the JSON as an empty array ([]).
|
||||
// If Tools is populated, it's included in the JSON with the elements.
|
||||
func (a AssistantRequest) MarshalJSON() ([]byte, error) {
|
||||
type Alias AssistantRequest
|
||||
assistantAlias := &struct {
|
||||
Tools *[]AssistantTool `json:"tools,omitempty"`
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(&a),
|
||||
}
|
||||
|
||||
if a.Tools != nil {
|
||||
assistantAlias.Tools = &a.Tools
|
||||
}
|
||||
|
||||
return json.Marshal(assistantAlias)
|
||||
}
|
||||
|
||||
// AssistantsList is a list of assistants.
|
||||
type AssistantsList struct {
|
||||
Assistants []Assistant `json:"data"`
|
||||
LastID *string `json:"last_id"`
|
||||
FirstID *string `json:"first_id"`
|
||||
HasMore bool `json:"has_more"`
|
||||
httpHeader
|
||||
}
|
||||
|
||||
type AssistantDeleteResponse struct {
|
||||
ID string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
Deleted bool `json:"deleted"`
|
||||
|
||||
httpHeader
|
||||
}
|
||||
|
||||
type AssistantFile struct {
|
||||
ID string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
AssistantID string `json:"assistant_id"`
|
||||
|
||||
httpHeader
|
||||
}
|
||||
|
||||
type AssistantFileRequest struct {
|
||||
FileID string `json:"file_id"`
|
||||
}
|
||||
|
||||
type AssistantFilesList struct {
|
||||
AssistantFiles []AssistantFile `json:"data"`
|
||||
|
||||
httpHeader
|
||||
}
|
||||
|
||||
// CreateAssistant creates a new assistant.
|
||||
func (c *Client) CreateAssistant(ctx context.Context, request AssistantRequest) (response Assistant, err error) {
|
||||
req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(assistantsSuffix), withBody(request),
|
||||
withBetaAssistantVersion(c.config.AssistantVersion))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = c.sendRequest(req, &response)
|
||||
return
|
||||
}
|
||||
|
||||
// RetrieveAssistant retrieves an assistant.
|
||||
func (c *Client) RetrieveAssistant(
|
||||
ctx context.Context,
|
||||
assistantID string,
|
||||
) (response Assistant, err error) {
|
||||
urlSuffix := fmt.Sprintf("%s/%s", assistantsSuffix, assistantID)
|
||||
req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix),
|
||||
withBetaAssistantVersion(c.config.AssistantVersion))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = c.sendRequest(req, &response)
|
||||
return
|
||||
}
|
||||
|
||||
// ModifyAssistant modifies an assistant.
|
||||
func (c *Client) ModifyAssistant(
|
||||
ctx context.Context,
|
||||
assistantID string,
|
||||
request AssistantRequest,
|
||||
) (response Assistant, err error) {
|
||||
urlSuffix := fmt.Sprintf("%s/%s", assistantsSuffix, assistantID)
|
||||
req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix), withBody(request),
|
||||
withBetaAssistantVersion(c.config.AssistantVersion))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = c.sendRequest(req, &response)
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteAssistant deletes an assistant.
|
||||
func (c *Client) DeleteAssistant(
|
||||
ctx context.Context,
|
||||
assistantID string,
|
||||
) (response AssistantDeleteResponse, err error) {
|
||||
urlSuffix := fmt.Sprintf("%s/%s", assistantsSuffix, assistantID)
|
||||
req, err := c.newRequest(ctx, http.MethodDelete, c.fullURL(urlSuffix),
|
||||
withBetaAssistantVersion(c.config.AssistantVersion))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = c.sendRequest(req, &response)
|
||||
return
|
||||
}
|
||||
|
||||
// ListAssistants Lists the currently available assistants.
|
||||
func (c *Client) ListAssistants(
|
||||
ctx context.Context,
|
||||
limit *int,
|
||||
order *string,
|
||||
after *string,
|
||||
before *string,
|
||||
) (response AssistantsList, err error) {
|
||||
urlValues := url.Values{}
|
||||
if limit != nil {
|
||||
urlValues.Add("limit", fmt.Sprintf("%d", *limit))
|
||||
}
|
||||
if order != nil {
|
||||
urlValues.Add("order", *order)
|
||||
}
|
||||
if after != nil {
|
||||
urlValues.Add("after", *after)
|
||||
}
|
||||
if before != nil {
|
||||
urlValues.Add("before", *before)
|
||||
}
|
||||
|
||||
encodedValues := ""
|
||||
if len(urlValues) > 0 {
|
||||
encodedValues = "?" + urlValues.Encode()
|
||||
}
|
||||
|
||||
urlSuffix := fmt.Sprintf("%s%s", assistantsSuffix, encodedValues)
|
||||
req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix),
|
||||
withBetaAssistantVersion(c.config.AssistantVersion))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = c.sendRequest(req, &response)
|
||||
return
|
||||
}
|
||||
|
||||
// CreateAssistantFile creates a new assistant file.
|
||||
func (c *Client) CreateAssistantFile(
|
||||
ctx context.Context,
|
||||
assistantID string,
|
||||
request AssistantFileRequest,
|
||||
) (response AssistantFile, err error) {
|
||||
urlSuffix := fmt.Sprintf("%s/%s%s", assistantsSuffix, assistantID, assistantsFilesSuffix)
|
||||
req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix),
|
||||
withBody(request),
|
||||
withBetaAssistantVersion(c.config.AssistantVersion))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = c.sendRequest(req, &response)
|
||||
return
|
||||
}
|
||||
|
||||
// RetrieveAssistantFile retrieves an assistant file.
|
||||
func (c *Client) RetrieveAssistantFile(
|
||||
ctx context.Context,
|
||||
assistantID string,
|
||||
fileID string,
|
||||
) (response AssistantFile, err error) {
|
||||
urlSuffix := fmt.Sprintf("%s/%s%s/%s", assistantsSuffix, assistantID, assistantsFilesSuffix, fileID)
|
||||
req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix),
|
||||
withBetaAssistantVersion(c.config.AssistantVersion))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = c.sendRequest(req, &response)
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteAssistantFile deletes an existing file.
|
||||
func (c *Client) DeleteAssistantFile(
|
||||
ctx context.Context,
|
||||
assistantID string,
|
||||
fileID string,
|
||||
) (err error) {
|
||||
urlSuffix := fmt.Sprintf("%s/%s%s/%s", assistantsSuffix, assistantID, assistantsFilesSuffix, fileID)
|
||||
req, err := c.newRequest(ctx, http.MethodDelete, c.fullURL(urlSuffix),
|
||||
withBetaAssistantVersion(c.config.AssistantVersion))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = c.sendRequest(req, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// ListAssistantFiles Lists the currently available files for an assistant.
|
||||
func (c *Client) ListAssistantFiles(
|
||||
ctx context.Context,
|
||||
assistantID string,
|
||||
limit *int,
|
||||
order *string,
|
||||
after *string,
|
||||
before *string,
|
||||
) (response AssistantFilesList, err error) {
|
||||
urlValues := url.Values{}
|
||||
if limit != nil {
|
||||
urlValues.Add("limit", fmt.Sprintf("%d", *limit))
|
||||
}
|
||||
if order != nil {
|
||||
urlValues.Add("order", *order)
|
||||
}
|
||||
if after != nil {
|
||||
urlValues.Add("after", *after)
|
||||
}
|
||||
if before != nil {
|
||||
urlValues.Add("before", *before)
|
||||
}
|
||||
|
||||
encodedValues := ""
|
||||
if len(urlValues) > 0 {
|
||||
encodedValues = "?" + urlValues.Encode()
|
||||
}
|
||||
|
||||
urlSuffix := fmt.Sprintf("%s/%s%s%s", assistantsSuffix, assistantID, assistantsFilesSuffix, encodedValues)
|
||||
req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix),
|
||||
withBetaAssistantVersion(c.config.AssistantVersion))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = c.sendRequest(req, &response)
|
||||
return
|
||||
}
|
||||
234
vendor/github.com/sashabaranov/go-openai/audio.go
generated
vendored
Normal file
234
vendor/github.com/sashabaranov/go-openai/audio.go
generated
vendored
Normal file
@@ -0,0 +1,234 @@
|
||||
package openai
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
utils "github.com/sashabaranov/go-openai/internal"
|
||||
)
|
||||
|
||||
// Whisper Defines the models provided by OpenAI to use when processing audio with OpenAI.
|
||||
const (
|
||||
Whisper1 = "whisper-1"
|
||||
)
|
||||
|
||||
// Response formats; Whisper uses AudioResponseFormatJSON by default.
|
||||
type AudioResponseFormat string
|
||||
|
||||
const (
|
||||
AudioResponseFormatJSON AudioResponseFormat = "json"
|
||||
AudioResponseFormatText AudioResponseFormat = "text"
|
||||
AudioResponseFormatSRT AudioResponseFormat = "srt"
|
||||
AudioResponseFormatVerboseJSON AudioResponseFormat = "verbose_json"
|
||||
AudioResponseFormatVTT AudioResponseFormat = "vtt"
|
||||
)
|
||||
|
||||
type TranscriptionTimestampGranularity string
|
||||
|
||||
const (
|
||||
TranscriptionTimestampGranularityWord TranscriptionTimestampGranularity = "word"
|
||||
TranscriptionTimestampGranularitySegment TranscriptionTimestampGranularity = "segment"
|
||||
)
|
||||
|
||||
// AudioRequest represents a request structure for audio API.
|
||||
type AudioRequest struct {
|
||||
Model string
|
||||
|
||||
// FilePath is either an existing file in your filesystem or a filename representing the contents of Reader.
|
||||
FilePath string
|
||||
|
||||
// Reader is an optional io.Reader when you do not want to use an existing file.
|
||||
Reader io.Reader
|
||||
|
||||
Prompt string
|
||||
Temperature float32
|
||||
Language string // Only for transcription.
|
||||
Format AudioResponseFormat
|
||||
TimestampGranularities []TranscriptionTimestampGranularity // Only for transcription.
|
||||
}
|
||||
|
||||
// AudioResponse represents a response structure for audio API.
|
||||
type AudioResponse struct {
|
||||
Task string `json:"task"`
|
||||
Language string `json:"language"`
|
||||
Duration float64 `json:"duration"`
|
||||
Segments []struct {
|
||||
ID int `json:"id"`
|
||||
Seek int `json:"seek"`
|
||||
Start float64 `json:"start"`
|
||||
End float64 `json:"end"`
|
||||
Text string `json:"text"`
|
||||
Tokens []int `json:"tokens"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
AvgLogprob float64 `json:"avg_logprob"`
|
||||
CompressionRatio float64 `json:"compression_ratio"`
|
||||
NoSpeechProb float64 `json:"no_speech_prob"`
|
||||
Transient bool `json:"transient"`
|
||||
} `json:"segments"`
|
||||
Words []struct {
|
||||
Word string `json:"word"`
|
||||
Start float64 `json:"start"`
|
||||
End float64 `json:"end"`
|
||||
} `json:"words"`
|
||||
Text string `json:"text"`
|
||||
|
||||
httpHeader
|
||||
}
|
||||
|
||||
type audioTextResponse struct {
|
||||
Text string `json:"text"`
|
||||
|
||||
httpHeader
|
||||
}
|
||||
|
||||
func (r *audioTextResponse) ToAudioResponse() AudioResponse {
|
||||
return AudioResponse{
|
||||
Text: r.Text,
|
||||
httpHeader: r.httpHeader,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTranscription — API call to create a transcription. Returns transcribed text.
|
||||
func (c *Client) CreateTranscription(
|
||||
ctx context.Context,
|
||||
request AudioRequest,
|
||||
) (response AudioResponse, err error) {
|
||||
return c.callAudioAPI(ctx, request, "transcriptions")
|
||||
}
|
||||
|
||||
// CreateTranslation — API call to translate audio into English.
|
||||
func (c *Client) CreateTranslation(
|
||||
ctx context.Context,
|
||||
request AudioRequest,
|
||||
) (response AudioResponse, err error) {
|
||||
return c.callAudioAPI(ctx, request, "translations")
|
||||
}
|
||||
|
||||
// callAudioAPI — API call to an audio endpoint.
|
||||
func (c *Client) callAudioAPI(
|
||||
ctx context.Context,
|
||||
request AudioRequest,
|
||||
endpointSuffix string,
|
||||
) (response AudioResponse, err error) {
|
||||
var formBody bytes.Buffer
|
||||
builder := c.createFormBuilder(&formBody)
|
||||
|
||||
if err = audioMultipartForm(request, builder); err != nil {
|
||||
return AudioResponse{}, err
|
||||
}
|
||||
|
||||
urlSuffix := fmt.Sprintf("/audio/%s", endpointSuffix)
|
||||
req, err := c.newRequest(
|
||||
ctx,
|
||||
http.MethodPost,
|
||||
c.fullURL(urlSuffix, withModel(request.Model)),
|
||||
withBody(&formBody),
|
||||
withContentType(builder.FormDataContentType()),
|
||||
)
|
||||
if err != nil {
|
||||
return AudioResponse{}, err
|
||||
}
|
||||
|
||||
if request.HasJSONResponse() {
|
||||
err = c.sendRequest(req, &response)
|
||||
} else {
|
||||
var textResponse audioTextResponse
|
||||
err = c.sendRequest(req, &textResponse)
|
||||
response = textResponse.ToAudioResponse()
|
||||
}
|
||||
if err != nil {
|
||||
return AudioResponse{}, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// HasJSONResponse returns true if the response format is JSON.
|
||||
func (r AudioRequest) HasJSONResponse() bool {
|
||||
return r.Format == "" || r.Format == AudioResponseFormatJSON || r.Format == AudioResponseFormatVerboseJSON
|
||||
}
|
||||
|
||||
// audioMultipartForm creates a form with audio file contents and the name of the model to use for
|
||||
// audio processing.
|
||||
func audioMultipartForm(request AudioRequest, b utils.FormBuilder) error {
|
||||
err := createFileField(request, b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = b.WriteField("model", request.Model)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing model name: %w", err)
|
||||
}
|
||||
|
||||
// Create a form field for the prompt (if provided)
|
||||
if request.Prompt != "" {
|
||||
err = b.WriteField("prompt", request.Prompt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing prompt: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a form field for the format (if provided)
|
||||
if request.Format != "" {
|
||||
err = b.WriteField("response_format", string(request.Format))
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing format: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a form field for the temperature (if provided)
|
||||
if request.Temperature != 0 {
|
||||
err = b.WriteField("temperature", fmt.Sprintf("%.2f", request.Temperature))
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing temperature: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a form field for the language (if provided)
|
||||
if request.Language != "" {
|
||||
err = b.WriteField("language", request.Language)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing language: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(request.TimestampGranularities) > 0 {
|
||||
for _, tg := range request.TimestampGranularities {
|
||||
err = b.WriteField("timestamp_granularities[]", string(tg))
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing timestamp_granularities[]: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close the multipart writer
|
||||
return b.Close()
|
||||
}
|
||||
|
||||
// createFileField creates the "file" form field from either an existing file or by using the reader.
|
||||
func createFileField(request AudioRequest, b utils.FormBuilder) error {
|
||||
if request.Reader != nil {
|
||||
err := b.CreateFormFileReader("file", request.Reader, request.FilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating form using reader: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
f, err := os.Open(request.FilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening audio file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
err = b.CreateFormFile("file", f)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating form file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
271
vendor/github.com/sashabaranov/go-openai/batch.go
generated
vendored
Normal file
271
vendor/github.com/sashabaranov/go-openai/batch.go
generated
vendored
Normal file
@@ -0,0 +1,271 @@
|
||||
package openai
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
const batchesSuffix = "/batches"
|
||||
|
||||
type BatchEndpoint string
|
||||
|
||||
const (
|
||||
BatchEndpointChatCompletions BatchEndpoint = "/v1/chat/completions"
|
||||
BatchEndpointCompletions BatchEndpoint = "/v1/completions"
|
||||
BatchEndpointEmbeddings BatchEndpoint = "/v1/embeddings"
|
||||
)
|
||||
|
||||
type BatchLineItem interface {
|
||||
MarshalBatchLineItem() []byte
|
||||
}
|
||||
|
||||
type BatchChatCompletionRequest struct {
|
||||
CustomID string `json:"custom_id"`
|
||||
Body ChatCompletionRequest `json:"body"`
|
||||
Method string `json:"method"`
|
||||
URL BatchEndpoint `json:"url"`
|
||||
}
|
||||
|
||||
func (r BatchChatCompletionRequest) MarshalBatchLineItem() []byte {
|
||||
marshal, _ := json.Marshal(r)
|
||||
return marshal
|
||||
}
|
||||
|
||||
type BatchCompletionRequest struct {
|
||||
CustomID string `json:"custom_id"`
|
||||
Body CompletionRequest `json:"body"`
|
||||
Method string `json:"method"`
|
||||
URL BatchEndpoint `json:"url"`
|
||||
}
|
||||
|
||||
func (r BatchCompletionRequest) MarshalBatchLineItem() []byte {
|
||||
marshal, _ := json.Marshal(r)
|
||||
return marshal
|
||||
}
|
||||
|
||||
type BatchEmbeddingRequest struct {
|
||||
CustomID string `json:"custom_id"`
|
||||
Body EmbeddingRequest `json:"body"`
|
||||
Method string `json:"method"`
|
||||
URL BatchEndpoint `json:"url"`
|
||||
}
|
||||
|
||||
func (r BatchEmbeddingRequest) MarshalBatchLineItem() []byte {
|
||||
marshal, _ := json.Marshal(r)
|
||||
return marshal
|
||||
}
|
||||
|
||||
type Batch struct {
|
||||
ID string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
Endpoint BatchEndpoint `json:"endpoint"`
|
||||
Errors *struct {
|
||||
Object string `json:"object,omitempty"`
|
||||
Data []struct {
|
||||
Code string `json:"code,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Param *string `json:"param,omitempty"`
|
||||
Line *int `json:"line,omitempty"`
|
||||
} `json:"data"`
|
||||
} `json:"errors"`
|
||||
InputFileID string `json:"input_file_id"`
|
||||
CompletionWindow string `json:"completion_window"`
|
||||
Status string `json:"status"`
|
||||
OutputFileID *string `json:"output_file_id"`
|
||||
ErrorFileID *string `json:"error_file_id"`
|
||||
CreatedAt int `json:"created_at"`
|
||||
InProgressAt *int `json:"in_progress_at"`
|
||||
ExpiresAt *int `json:"expires_at"`
|
||||
FinalizingAt *int `json:"finalizing_at"`
|
||||
CompletedAt *int `json:"completed_at"`
|
||||
FailedAt *int `json:"failed_at"`
|
||||
ExpiredAt *int `json:"expired_at"`
|
||||
CancellingAt *int `json:"cancelling_at"`
|
||||
CancelledAt *int `json:"cancelled_at"`
|
||||
RequestCounts BatchRequestCounts `json:"request_counts"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
}
|
||||
|
||||
type BatchRequestCounts struct {
|
||||
Total int `json:"total"`
|
||||
Completed int `json:"completed"`
|
||||
Failed int `json:"failed"`
|
||||
}
|
||||
|
||||
type CreateBatchRequest struct {
|
||||
InputFileID string `json:"input_file_id"`
|
||||
Endpoint BatchEndpoint `json:"endpoint"`
|
||||
CompletionWindow string `json:"completion_window"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
}
|
||||
|
||||
type BatchResponse struct {
|
||||
httpHeader
|
||||
Batch
|
||||
}
|
||||
|
||||
// CreateBatch — API call to Create batch.
|
||||
func (c *Client) CreateBatch(
|
||||
ctx context.Context,
|
||||
request CreateBatchRequest,
|
||||
) (response BatchResponse, err error) {
|
||||
if request.CompletionWindow == "" {
|
||||
request.CompletionWindow = "24h"
|
||||
}
|
||||
|
||||
req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(batchesSuffix), withBody(request))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = c.sendRequest(req, &response)
|
||||
return
|
||||
}
|
||||
|
||||
type UploadBatchFileRequest struct {
|
||||
FileName string
|
||||
Lines []BatchLineItem
|
||||
}
|
||||
|
||||
func (r *UploadBatchFileRequest) MarshalJSONL() []byte {
|
||||
buff := bytes.Buffer{}
|
||||
for i, line := range r.Lines {
|
||||
if i != 0 {
|
||||
buff.Write([]byte("\n"))
|
||||
}
|
||||
buff.Write(line.MarshalBatchLineItem())
|
||||
}
|
||||
return buff.Bytes()
|
||||
}
|
||||
|
||||
func (r *UploadBatchFileRequest) AddChatCompletion(customerID string, body ChatCompletionRequest) {
|
||||
r.Lines = append(r.Lines, BatchChatCompletionRequest{
|
||||
CustomID: customerID,
|
||||
Body: body,
|
||||
Method: "POST",
|
||||
URL: BatchEndpointChatCompletions,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *UploadBatchFileRequest) AddCompletion(customerID string, body CompletionRequest) {
|
||||
r.Lines = append(r.Lines, BatchCompletionRequest{
|
||||
CustomID: customerID,
|
||||
Body: body,
|
||||
Method: "POST",
|
||||
URL: BatchEndpointCompletions,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *UploadBatchFileRequest) AddEmbedding(customerID string, body EmbeddingRequest) {
|
||||
r.Lines = append(r.Lines, BatchEmbeddingRequest{
|
||||
CustomID: customerID,
|
||||
Body: body,
|
||||
Method: "POST",
|
||||
URL: BatchEndpointEmbeddings,
|
||||
})
|
||||
}
|
||||
|
||||
// UploadBatchFile — upload batch file.
|
||||
func (c *Client) UploadBatchFile(ctx context.Context, request UploadBatchFileRequest) (File, error) {
|
||||
if request.FileName == "" {
|
||||
request.FileName = "@batchinput.jsonl"
|
||||
}
|
||||
return c.CreateFileBytes(ctx, FileBytesRequest{
|
||||
Name: request.FileName,
|
||||
Bytes: request.MarshalJSONL(),
|
||||
Purpose: PurposeBatch,
|
||||
})
|
||||
}
|
||||
|
||||
type CreateBatchWithUploadFileRequest struct {
|
||||
Endpoint BatchEndpoint `json:"endpoint"`
|
||||
CompletionWindow string `json:"completion_window"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
UploadBatchFileRequest
|
||||
}
|
||||
|
||||
// CreateBatchWithUploadFile — API call to Create batch with upload file.
|
||||
func (c *Client) CreateBatchWithUploadFile(
|
||||
ctx context.Context,
|
||||
request CreateBatchWithUploadFileRequest,
|
||||
) (response BatchResponse, err error) {
|
||||
var file File
|
||||
file, err = c.UploadBatchFile(ctx, UploadBatchFileRequest{
|
||||
FileName: request.FileName,
|
||||
Lines: request.Lines,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return c.CreateBatch(ctx, CreateBatchRequest{
|
||||
InputFileID: file.ID,
|
||||
Endpoint: request.Endpoint,
|
||||
CompletionWindow: request.CompletionWindow,
|
||||
Metadata: request.Metadata,
|
||||
})
|
||||
}
|
||||
|
||||
// RetrieveBatch — API call to Retrieve batch.
|
||||
func (c *Client) RetrieveBatch(
|
||||
ctx context.Context,
|
||||
batchID string,
|
||||
) (response BatchResponse, err error) {
|
||||
urlSuffix := fmt.Sprintf("%s/%s", batchesSuffix, batchID)
|
||||
req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = c.sendRequest(req, &response)
|
||||
return
|
||||
}
|
||||
|
||||
// CancelBatch — API call to Cancel batch.
|
||||
func (c *Client) CancelBatch(
|
||||
ctx context.Context,
|
||||
batchID string,
|
||||
) (response BatchResponse, err error) {
|
||||
urlSuffix := fmt.Sprintf("%s/%s/cancel", batchesSuffix, batchID)
|
||||
req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = c.sendRequest(req, &response)
|
||||
return
|
||||
}
|
||||
|
||||
type ListBatchResponse struct {
|
||||
httpHeader
|
||||
Object string `json:"object"`
|
||||
Data []Batch `json:"data"`
|
||||
FirstID string `json:"first_id"`
|
||||
LastID string `json:"last_id"`
|
||||
HasMore bool `json:"has_more"`
|
||||
}
|
||||
|
||||
// ListBatch API call to List batch.
|
||||
func (c *Client) ListBatch(ctx context.Context, after *string, limit *int) (response ListBatchResponse, err error) {
|
||||
urlValues := url.Values{}
|
||||
if limit != nil {
|
||||
urlValues.Add("limit", fmt.Sprintf("%d", *limit))
|
||||
}
|
||||
if after != nil {
|
||||
urlValues.Add("after", *after)
|
||||
}
|
||||
encodedValues := ""
|
||||
if len(urlValues) > 0 {
|
||||
encodedValues = "?" + urlValues.Encode()
|
||||
}
|
||||
|
||||
urlSuffix := fmt.Sprintf("%s%s", batchesSuffix, encodedValues)
|
||||
req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = c.sendRequest(req, &response)
|
||||
return
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user