Initialize module and dependencies

This commit is contained in:
dwrz
2026-01-04 20:57:40 +00:00
commit a3b390c008
514 changed files with 310495 additions and 0 deletions

28
go.mod Normal file
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,29 @@
# go-imap
[![Go Reference](https://pkg.go.dev/badge/github.com/emersion/go-imap/v2.svg)](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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View 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()
}

View 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
}

View 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
}

File diff suppressed because it is too large Load Diff

View 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
}

View 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
}

View 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
}

View 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

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
View 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()
}

View 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()
}

View 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
}
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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, &not)
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(&not) {
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
}

View 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
}

View 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
}

View 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)
}

View 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
}

View 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
}

View 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
View 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)
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
View 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)
}
}

View 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]
}

View 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()
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,30 @@
# go-message
[![Go Reference](https://pkg.go.dev/badge/github.com/emersion/go-message.svg)](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
View 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)
}

View 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
View 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
View 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
View 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
View 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)
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View 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
}

View 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
}

View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,17 @@
# go-sasl
[![Go Reference](https://pkg.go.dev/badge/github.com/emersion/go-sasl.svg)](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
View 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
View 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
View 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
View 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
View 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
View 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)
}

View File

@@ -0,0 +1,4 @@
coverage:
ignore:
- "examples/**"
- "internal/test/**"

22
vendor/github.com/sashabaranov/go-openai/.gitignore generated vendored Normal file
View 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
View 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$

View 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
View 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
View File

@@ -0,0 +1,913 @@
# Go OpenAI
[![Go Reference](https://pkg.go.dev/badge/github.com/sashabaranov/go-openai.svg)](https://pkg.go.dev/github.com/sashabaranov/go-openai)
[![Go Report Card](https://goreportcard.com/badge/github.com/sashabaranov/go-openai)](https://goreportcard.com/report/github.com/sashabaranov/go-openai)
[![codecov](https://codecov.io/gh/sashabaranov/go-openai/branch/master/graph/badge.svg?token=bCbIfHLIsW)](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
View 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
View 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
View 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