From 1e469de52daf3f1e730b9352c9c1c58339830bd3 Mon Sep 17 00:00:00 2001 From: Giacomo Tartari Date: Thu, 2 May 2024 13:23:15 +0200 Subject: [PATCH] feat: format email according to RFC 5322 --- config.yaml | 2 +- go.mod | 1 + go.sum | 32 + main.go | 78 +- .../github.com/emersion/go-message/.build.yml | 20 + .../github.com/emersion/go-message/.gitignore | 24 + vendor/github.com/emersion/go-message/LICENSE | 21 + .../github.com/emersion/go-message/README.md | 31 + .../github.com/emersion/go-message/charset.go | 66 ++ .../emersion/go-message/encoding.go | 151 ++++ .../github.com/emersion/go-message/entity.go | 264 +++++++ .../github.com/emersion/go-message/header.go | 118 +++ .../emersion/go-message/mail/address.go | 42 ++ .../emersion/go-message/mail/attachment.go | 30 + .../emersion/go-message/mail/header.go | 381 ++++++++++ .../emersion/go-message/mail/inline.go | 10 + .../emersion/go-message/mail/mail.go | 9 + .../emersion/go-message/mail/reader.go | 130 ++++ .../emersion/go-message/mail/writer.go | 132 ++++ .../github.com/emersion/go-message/message.go | 15 + .../emersion/go-message/multipart.go | 116 +++ .../emersion/go-message/textproto/header.go | 677 ++++++++++++++++++ .../go-message/textproto/multipart.go | 474 ++++++++++++ .../go-message/textproto/textproto.go | 2 + .../github.com/emersion/go-message/writer.go | 134 ++++ vendor/modules.txt | 5 + 26 files changed, 2953 insertions(+), 12 deletions(-) create mode 100644 vendor/github.com/emersion/go-message/.build.yml create mode 100644 vendor/github.com/emersion/go-message/.gitignore create mode 100644 vendor/github.com/emersion/go-message/LICENSE create mode 100644 vendor/github.com/emersion/go-message/README.md create mode 100644 vendor/github.com/emersion/go-message/charset.go create mode 100644 vendor/github.com/emersion/go-message/encoding.go create mode 100644 vendor/github.com/emersion/go-message/entity.go create mode 100644 vendor/github.com/emersion/go-message/header.go create mode 100644 vendor/github.com/emersion/go-message/mail/address.go create mode 100644 vendor/github.com/emersion/go-message/mail/attachment.go create mode 100644 vendor/github.com/emersion/go-message/mail/header.go create mode 100644 vendor/github.com/emersion/go-message/mail/inline.go create mode 100644 vendor/github.com/emersion/go-message/mail/mail.go create mode 100644 vendor/github.com/emersion/go-message/mail/reader.go create mode 100644 vendor/github.com/emersion/go-message/mail/writer.go create mode 100644 vendor/github.com/emersion/go-message/message.go create mode 100644 vendor/github.com/emersion/go-message/multipart.go create mode 100644 vendor/github.com/emersion/go-message/textproto/header.go create mode 100644 vendor/github.com/emersion/go-message/textproto/multipart.go create mode 100644 vendor/github.com/emersion/go-message/textproto/textproto.go create mode 100644 vendor/github.com/emersion/go-message/writer.go diff --git a/config.yaml b/config.yaml index ff1721b..d419c03 100644 --- a/config.yaml +++ b/config.yaml @@ -3,6 +3,6 @@ email-smtp-svr-addr: 127.0.0.1:1025 email-smtp-username: admin email-smtp-password: admin email-addr-from: noreply@cycloid.io -email-addr-return-path: "admin+ydbounce@cycloid.io" +email-addr-return-path: "Cycloid Framework " email-tls-skip-verify: true email-addr-to: test@email.com diff --git a/go.mod b/go.mod index e1bee66..740cc00 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( ) require ( + github.com/emersion/go-message v0.18.1 github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect diff --git a/go.sum b/go.sum index 338adcb..4e30dcd 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emersion/go-message v0.18.1 h1:tfTxIoXFSFRwWaZsgnqS1DSZuGpYGzSmCZD8SK3QA2E= +github.com/emersion/go-message v0.18.1/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 h1:hH4PQfOndHDlpzYfLAAfl63E8Le6F2+EL/cdhlkyRJY= github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= @@ -60,16 +62,46 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +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/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +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/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/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.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/main.go b/main.go index e111604..85baade 100644 --- a/main.go +++ b/main.go @@ -1,12 +1,15 @@ package main import ( + "bytes" "crypto/tls" "fmt" + "io" "log" "os" - "strings" + "time" + "github.com/emersion/go-message/mail" "github.com/emersion/go-sasl" "github.com/emersion/go-smtp" "github.com/spf13/cobra" @@ -14,7 +17,7 @@ import ( ) const ( - cfg = "config-file" + cfile = "config-file" skiptls = "email-tls-skip-verify" server = "email-smtp-svr-addr" username = "email-smtp-username" @@ -24,7 +27,7 @@ const ( ) var ( - cfgFlag string + cfileFlag string skiptlsFlag bool serverFlag string usernameFlag string @@ -38,7 +41,7 @@ var RootCmd = &cobra.Command{ Short: "Send an email using Cycloid config", Long: "Send an email using Cycloid config file in order to test different SMTP servers integration", RunE: func(cmd *cobra.Command, _ []string) error { - viper.SetConfigFile(cfgFlag) + viper.SetConfigFile(cfileFlag) err := viper.ReadInConfig() if err != nil { return fmt.Errorf("error reading config file: %w", err) @@ -57,8 +60,8 @@ var RootCmd = &cobra.Command{ } func init() { - RootCmd.Flags().StringVarP(&cfgFlag, cfg, "c", "config.yaml", "The configuration file PATH.") - viper.BindPFlag(cfg, RootCmd.Flags().Lookup(cfg)) + RootCmd.Flags().StringVarP(&cfileFlag, cfile, "c", "config.yaml", "The configuration file PATH.") + viper.BindPFlag(cfile, RootCmd.Flags().Lookup(cfile)) RootCmd.Flags().StringVarP(&serverFlag, server, "s", "", "SMTP server address (host:port)") viper.BindPFlag(server, RootCmd.Flags().Lookup(server)) @@ -97,7 +100,7 @@ type config struct { func getConfig() config { ret := config{ - cfgFile: cfgFlag, + cfgFile: cfileFlag, skiptls: skiptlsFlag, server: serverFlag, username: usernameFlag, @@ -106,7 +109,7 @@ func getConfig() config { to: toFlag, } if ret.cfgFile == "" { - ret.cfgFile = viper.GetString(cfg) + ret.cfgFile = viper.GetString(cfile) } if ret.skiptls == false { ret.skiptls = viper.GetBool(skiptls) @@ -130,9 +133,21 @@ func getConfig() config { } func sendEmail(cfg config) error { - msg := strings.NewReader("Hello from cy-smtp!\nThis is a test message.") + from, err := mail.ParseAddress(cfg.from) + if err != nil { + return fmt.Errorf("error sender address: %w", err) + } + to, err := mail.ParseAddress(cfg.to) + if err != nil { + return fmt.Errorf("error recipient address: %w", err) + } + + msg, err := formatEmail(from, to) + if err != nil { + return fmt.Errorf("error formatting email: %w", err) + } - log.Println("Sending test email ") + log.Print("Sending test email... ") tlsCfg := &tls.Config{ InsecureSkipVerify: cfg.skiptls, } @@ -146,9 +161,50 @@ func sendEmail(cfg config) error { return fmt.Errorf("error authenticating to server: %w", err) } } - err = client.SendMail(cfg.from, []string{cfg.to}, msg) + err = client.SendMail(from.Address, []string{to.Address}, &msg) if err != nil { return fmt.Errorf("error sending email: %w", err) } + log.Println("Email sent!") + return nil } + +func formatEmail(fromAddrs, toAddrs *mail.Address) (bytes.Buffer, error) { + var b bytes.Buffer + + from := []*mail.Address{fromAddrs} + to := []*mail.Address{toAddrs} + + // Create our mail header + var h mail.Header + h.SetDate(time.Now()) + h.SetAddressList("From", from) + h.SetAddressList("To", to) + + // Create a new mail writer + mw, err := mail.CreateWriter(&b, h) + if err != nil { + return b, fmt.Errorf("error creating email writer: %w", err) + } + + // Create a text part + tw, err := mw.CreateInline() + if err != nil { + return b, fmt.Errorf("error creating email InlineWriter: %w", err) + } + var th mail.InlineHeader + th.Set("Content-Type", "text/plain") + w, err := tw.CreatePart(th) + if err != nil { + log.Fatal(err) + } + io.WriteString(w, "Hello from cy-smtp!\nThis is a test message.") + w.Close() + tw.Close() + + mw.Close() + + return b, nil + +} diff --git a/vendor/github.com/emersion/go-message/.build.yml b/vendor/github.com/emersion/go-message/.build.yml new file mode 100644 index 0000000..cc583f2 --- /dev/null +++ b/vendor/github.com/emersion/go-message/.build.yml @@ -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 .) diff --git a/vendor/github.com/emersion/go-message/.gitignore b/vendor/github.com/emersion/go-message/.gitignore new file mode 100644 index 0000000..daf913b --- /dev/null +++ b/vendor/github.com/emersion/go-message/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/vendor/github.com/emersion/go-message/LICENSE b/vendor/github.com/emersion/go-message/LICENSE new file mode 100644 index 0000000..0d50487 --- /dev/null +++ b/vendor/github.com/emersion/go-message/LICENSE @@ -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. diff --git a/vendor/github.com/emersion/go-message/README.md b/vendor/github.com/emersion/go-message/README.md new file mode 100644 index 0000000..20bf891 --- /dev/null +++ b/vendor/github.com/emersion/go-message/README.md @@ -0,0 +1,31 @@ +# go-message + +[![Go Reference](https://pkg.go.dev/badge/github.com/emersion/go-message.svg)](https://pkg.go.dev/github.com/emersion/go-message) +[![builds.sr.ht status](https://builds.sr.ht/~emersion/go-message/commits/master.svg)](https://builds.sr.ht/~emersion/go-message/commits/master?) + +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 diff --git a/vendor/github.com/emersion/go-message/charset.go b/vendor/github.com/emersion/go-message/charset.go new file mode 100644 index 0000000..9d4d10e --- /dev/null +++ b/vendor/github.com/emersion/go-message/charset.go @@ -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) +} diff --git a/vendor/github.com/emersion/go-message/encoding.go b/vendor/github.com/emersion/go-message/encoding.go new file mode 100644 index 0000000..a503276 --- /dev/null +++ b/vendor/github.com/emersion/go-message/encoding.go @@ -0,0 +1,151 @@ +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 + } + 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 +} diff --git a/vendor/github.com/emersion/go-message/entity.go b/vendor/github.com/emersion/go-message/entity.go new file mode 100644 index 0000000..ee2ebe6 --- /dev/null +++ b/vendor/github.com/emersion/go-message/entity.go @@ -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 +} diff --git a/vendor/github.com/emersion/go-message/header.go b/vendor/github.com/emersion/go-message/header.go new file mode 100644 index 0000000..1a98fe6 --- /dev/null +++ b/vendor/github.com/emersion/go-message/header.go @@ -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)} +} diff --git a/vendor/github.com/emersion/go-message/mail/address.go b/vendor/github.com/emersion/go-message/mail/address.go new file mode 100644 index 0000000..3d3bbca --- /dev/null +++ b/vendor/github.com/emersion/go-message/mail/address.go @@ -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 " +// 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) +} diff --git a/vendor/github.com/emersion/go-message/mail/attachment.go b/vendor/github.com/emersion/go-message/mail/attachment.go new file mode 100644 index 0000000..3fbbce2 --- /dev/null +++ b/vendor/github.com/emersion/go-message/mail/attachment.go @@ -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) +} diff --git a/vendor/github.com/emersion/go-message/mail/header.go b/vendor/github.com/emersion/go-message/mail/header.go new file mode 100644 index 0000000..4dd89ef --- /dev/null +++ b/vendor/github.com/emersion/go-message/mail/header.go @@ -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()} +} diff --git a/vendor/github.com/emersion/go-message/mail/inline.go b/vendor/github.com/emersion/go-message/mail/inline.go new file mode 100644 index 0000000..2aadfdc --- /dev/null +++ b/vendor/github.com/emersion/go-message/mail/inline.go @@ -0,0 +1,10 @@ +package mail + +import ( + "github.com/emersion/go-message" +) + +// A InlineHeader represents a message text header. +type InlineHeader struct { + message.Header +} diff --git a/vendor/github.com/emersion/go-message/mail/mail.go b/vendor/github.com/emersion/go-message/mail/mail.go new file mode 100644 index 0000000..2f9a12c --- /dev/null +++ b/vendor/github.com/emersion/go-message/mail/mail.go @@ -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 diff --git a/vendor/github.com/emersion/go-message/mail/reader.go b/vendor/github.com/emersion/go-message/mail/reader.go new file mode 100644 index 0000000..f721a45 --- /dev/null +++ b/vendor/github.com/emersion/go-message/mail/reader.go @@ -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 +} diff --git a/vendor/github.com/emersion/go-message/mail/writer.go b/vendor/github.com/emersion/go-message/mail/writer.go new file mode 100644 index 0000000..6e6a0d2 --- /dev/null +++ b/vendor/github.com/emersion/go-message/mail/writer.go @@ -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() +} diff --git a/vendor/github.com/emersion/go-message/message.go b/vendor/github.com/emersion/go-message/message.go new file mode 100644 index 0000000..52cf115 --- /dev/null +++ b/vendor/github.com/emersion/go-message/message.go @@ -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 diff --git a/vendor/github.com/emersion/go-message/multipart.go b/vendor/github.com/emersion/go-message/multipart.go new file mode 100644 index 0000000..c406a31 --- /dev/null +++ b/vendor/github.com/emersion/go-message/multipart.go @@ -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 +} diff --git a/vendor/github.com/emersion/go-message/textproto/header.go b/vendor/github.com/emersion/go-message/textproto/header.go new file mode 100644 index 0000000..10c04f3 --- /dev/null +++ b/vendor/github.com/emersion/go-message/textproto/header.go @@ -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 +} diff --git a/vendor/github.com/emersion/go-message/textproto/multipart.go b/vendor/github.com/emersion/go-message/textproto/multipart.go new file mode 100644 index 0000000..62824df --- /dev/null +++ b/vendor/github.com/emersion/go-message/textproto/multipart.go @@ -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 +} diff --git a/vendor/github.com/emersion/go-message/textproto/textproto.go b/vendor/github.com/emersion/go-message/textproto/textproto.go new file mode 100644 index 0000000..2fa994b --- /dev/null +++ b/vendor/github.com/emersion/go-message/textproto/textproto.go @@ -0,0 +1,2 @@ +// Package textproto implements low-level manipulation of MIME messages. +package textproto diff --git a/vendor/github.com/emersion/go-message/writer.go b/vendor/github.com/emersion/go-message/writer.go new file mode 100644 index 0000000..6a80da2 --- /dev/null +++ b/vendor/github.com/emersion/go-message/writer.go @@ -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 +} diff --git a/vendor/modules.txt b/vendor/modules.txt index d9cca24..941d9da 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,3 +1,8 @@ +# github.com/emersion/go-message v0.18.1 +## explicit; go 1.14 +github.com/emersion/go-message +github.com/emersion/go-message/mail +github.com/emersion/go-message/textproto # github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 ## explicit; go 1.12 github.com/emersion/go-sasl