Initialize module and dependencies
This commit is contained in:
20
vendor/github.com/emersion/go-message/.build.yml
generated
vendored
Normal file
20
vendor/github.com/emersion/go-message/.build.yml
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
image: alpine/latest
|
||||
packages:
|
||||
- go
|
||||
sources:
|
||||
- https://github.com/emersion/go-message
|
||||
artifacts:
|
||||
- coverage.html
|
||||
tasks:
|
||||
- build: |
|
||||
cd go-message
|
||||
go build -v ./...
|
||||
- test: |
|
||||
cd go-message
|
||||
go test -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
- coverage: |
|
||||
cd go-message
|
||||
go tool cover -html=coverage.txt -o ~/coverage.html
|
||||
- gofmt: |
|
||||
cd go-message
|
||||
test -z $(gofmt -l .)
|
||||
24
vendor/github.com/emersion/go-message/.gitignore
generated
vendored
Normal file
24
vendor/github.com/emersion/go-message/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
21
vendor/github.com/emersion/go-message/LICENSE
generated
vendored
Normal file
21
vendor/github.com/emersion/go-message/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016 emersion
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
30
vendor/github.com/emersion/go-message/README.md
generated
vendored
Normal file
30
vendor/github.com/emersion/go-message/README.md
generated
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
# go-message
|
||||
|
||||
[](https://pkg.go.dev/github.com/emersion/go-message)
|
||||
|
||||
A Go library for the Internet Message Format. It implements:
|
||||
|
||||
* [RFC 5322]: Internet Message Format
|
||||
* [RFC 2045], [RFC 2046] and [RFC 2047]: Multipurpose Internet Mail Extensions
|
||||
* [RFC 2183]: Content-Disposition Header Field
|
||||
|
||||
## Features
|
||||
|
||||
* Streaming API
|
||||
* Automatic encoding and charset handling (to decode all charsets, add
|
||||
`import _ "github.com/emersion/go-message/charset"` to your application)
|
||||
* A [`mail`](https://godocs.io/github.com/emersion/go-message/mail) subpackage
|
||||
to read and write mail messages
|
||||
* DKIM-friendly
|
||||
* A [`textproto`](https://godocs.io/github.com/emersion/go-message/textproto)
|
||||
subpackage that just implements the wire format
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
[RFC 5322]: https://tools.ietf.org/html/rfc5322
|
||||
[RFC 2045]: https://tools.ietf.org/html/rfc2045
|
||||
[RFC 2046]: https://tools.ietf.org/html/rfc2046
|
||||
[RFC 2047]: https://tools.ietf.org/html/rfc2047
|
||||
[RFC 2183]: https://tools.ietf.org/html/rfc2183
|
||||
66
vendor/github.com/emersion/go-message/charset.go
generated
vendored
Normal file
66
vendor/github.com/emersion/go-message/charset.go
generated
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
package message
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type UnknownCharsetError struct {
|
||||
e error
|
||||
}
|
||||
|
||||
func (u UnknownCharsetError) Unwrap() error { return u.e }
|
||||
|
||||
func (u UnknownCharsetError) Error() string {
|
||||
return "unknown charset: " + u.e.Error()
|
||||
}
|
||||
|
||||
// IsUnknownCharset returns a boolean indicating whether the error is known to
|
||||
// report that the charset advertised by the entity is unknown.
|
||||
func IsUnknownCharset(err error) bool {
|
||||
return errors.As(err, new(UnknownCharsetError))
|
||||
}
|
||||
|
||||
// CharsetReader, if non-nil, defines a function to generate charset-conversion
|
||||
// readers, converting from the provided charset into UTF-8. Charsets are always
|
||||
// lower-case. utf-8 and us-ascii charsets are handled by default. One of the
|
||||
// the CharsetReader's result values must be non-nil.
|
||||
//
|
||||
// Importing github.com/emersion/go-message/charset will set CharsetReader to
|
||||
// a function that handles most common charsets. Alternatively, CharsetReader
|
||||
// can be set to e.g. golang.org/x/net/html/charset.NewReaderLabel.
|
||||
var CharsetReader func(charset string, input io.Reader) (io.Reader, error)
|
||||
|
||||
// charsetReader calls CharsetReader if non-nil.
|
||||
func charsetReader(charset string, input io.Reader) (io.Reader, error) {
|
||||
charset = strings.ToLower(charset)
|
||||
if charset == "utf-8" || charset == "us-ascii" {
|
||||
return input, nil
|
||||
}
|
||||
if CharsetReader != nil {
|
||||
r, err := CharsetReader(charset, input)
|
||||
if err != nil {
|
||||
return r, UnknownCharsetError{err}
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
return input, UnknownCharsetError{fmt.Errorf("message: unhandled charset %q", charset)}
|
||||
}
|
||||
|
||||
// decodeHeader decodes an internationalized header field. If it fails, it
|
||||
// returns the input string and the error.
|
||||
func decodeHeader(s string) (string, error) {
|
||||
wordDecoder := mime.WordDecoder{CharsetReader: charsetReader}
|
||||
dec, err := wordDecoder.DecodeHeader(s)
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
return dec, nil
|
||||
}
|
||||
|
||||
func encodeHeader(s string) string {
|
||||
return mime.QEncoding.Encode("utf-8", s)
|
||||
}
|
||||
64
vendor/github.com/emersion/go-message/charset/charset.go
generated
vendored
Normal file
64
vendor/github.com/emersion/go-message/charset/charset.go
generated
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
// Package charset provides functions to decode and encode charsets.
|
||||
//
|
||||
// It imports all supported charsets, which adds about 1MiB to binaries size.
|
||||
// Importing the package automatically sets message.CharsetReader.
|
||||
package charset
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-message"
|
||||
"golang.org/x/text/encoding"
|
||||
"golang.org/x/text/encoding/charmap"
|
||||
"golang.org/x/text/encoding/htmlindex"
|
||||
"golang.org/x/text/encoding/ianaindex"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
)
|
||||
|
||||
// Quirks table for charsets not handled by ianaindex
|
||||
//
|
||||
// A nil entry disables the charset.
|
||||
//
|
||||
// For aliases, see
|
||||
// https://www.iana.org/assignments/character-sets/character-sets.xhtml
|
||||
var charsets = map[string]encoding.Encoding{
|
||||
"ansi_x3.110-1983": charmap.ISO8859_1, // see RFC 1345 page 62, mostly superset of ISO 8859-1
|
||||
"x-utf_8j": unicode.UTF8, // alias for UTF-8, see https://icu4c-demos.unicode.org/icu-bin/convexp?s=ALL
|
||||
}
|
||||
|
||||
func init() {
|
||||
message.CharsetReader = Reader
|
||||
}
|
||||
|
||||
// Reader returns an io.Reader that converts the provided charset to UTF-8.
|
||||
func Reader(charset string, input io.Reader) (io.Reader, error) {
|
||||
var err error
|
||||
enc, ok := charsets[strings.ToLower(charset)]
|
||||
if ok && enc == nil {
|
||||
return nil, fmt.Errorf("charset %q: charset is disabled", charset)
|
||||
} else if !ok {
|
||||
enc, err = ianaindex.MIME.Encoding(charset)
|
||||
}
|
||||
if enc == nil {
|
||||
enc, err = ianaindex.MIME.Encoding("cs" + charset)
|
||||
}
|
||||
if enc == nil {
|
||||
enc, err = htmlindex.Get(charset)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("charset %q: %v", charset, err)
|
||||
}
|
||||
// See https://github.com/golang/go/issues/19421
|
||||
if enc == nil {
|
||||
return nil, fmt.Errorf("charset %q: unsupported charset", charset)
|
||||
}
|
||||
return enc.NewDecoder().Reader(input), nil
|
||||
}
|
||||
|
||||
// RegisterEncoding registers an encoding. This is intended to be called from
|
||||
// the init function in packages that want to support additional charsets.
|
||||
func RegisterEncoding(name string, enc encoding.Encoding) {
|
||||
charsets[name] = enc
|
||||
}
|
||||
155
vendor/github.com/emersion/go-message/encoding.go
generated
vendored
Normal file
155
vendor/github.com/emersion/go-message/encoding.go
generated
vendored
Normal file
@@ -0,0 +1,155 @@
|
||||
package message
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/quotedprintable"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type UnknownEncodingError struct {
|
||||
e error
|
||||
}
|
||||
|
||||
func (u UnknownEncodingError) Unwrap() error { return u.e }
|
||||
|
||||
func (u UnknownEncodingError) Error() string {
|
||||
return "encoding error: " + u.e.Error()
|
||||
}
|
||||
|
||||
// IsUnknownEncoding returns a boolean indicating whether the error is known to
|
||||
// report that the encoding advertised by the entity is unknown.
|
||||
func IsUnknownEncoding(err error) bool {
|
||||
return errors.As(err, new(UnknownEncodingError))
|
||||
}
|
||||
|
||||
func encodingReader(enc string, r io.Reader) (io.Reader, error) {
|
||||
var dec io.Reader
|
||||
switch strings.ToLower(enc) {
|
||||
case "quoted-printable":
|
||||
dec = quotedprintable.NewReader(r)
|
||||
case "base64":
|
||||
wrapped := &whitespaceReplacingReader{wrapped: r}
|
||||
dec = base64.NewDecoder(base64.StdEncoding, wrapped)
|
||||
case "7bit", "8bit", "binary", "":
|
||||
dec = r
|
||||
default:
|
||||
return nil, fmt.Errorf("unhandled encoding %q", enc)
|
||||
}
|
||||
return dec, nil
|
||||
}
|
||||
|
||||
type nopCloser struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func (nopCloser) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodingWriter(enc string, w io.Writer) (io.WriteCloser, error) {
|
||||
var wc io.WriteCloser
|
||||
switch strings.ToLower(enc) {
|
||||
case "quoted-printable":
|
||||
wc = quotedprintable.NewWriter(w)
|
||||
case "base64":
|
||||
wc = base64.NewEncoder(base64.StdEncoding, &lineWrapper{w: w, maxLineLen: 76})
|
||||
case "7bit", "8bit":
|
||||
wc = nopCloser{&lineWrapper{w: w, maxLineLen: 998}}
|
||||
case "binary", "":
|
||||
wc = nopCloser{w}
|
||||
default:
|
||||
return nil, fmt.Errorf("unhandled encoding %q", enc)
|
||||
}
|
||||
return wc, nil
|
||||
}
|
||||
|
||||
// whitespaceReplacingReader replaces space and tab characters with a LF so
|
||||
// base64 bodies with a continuation indent can be decoded by the base64 decoder
|
||||
// even though it is against the spec.
|
||||
type whitespaceReplacingReader struct {
|
||||
wrapped io.Reader
|
||||
}
|
||||
|
||||
func (r *whitespaceReplacingReader) Read(p []byte) (int, error) {
|
||||
n, err := r.wrapped.Read(p)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
if p[i] == ' ' || p[i] == '\t' {
|
||||
p[i] = '\n'
|
||||
}
|
||||
}
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
type lineWrapper struct {
|
||||
w io.Writer
|
||||
maxLineLen int
|
||||
|
||||
curLineLen int
|
||||
cr bool
|
||||
}
|
||||
|
||||
func (w *lineWrapper) Write(b []byte) (int, error) {
|
||||
var written int
|
||||
for len(b) > 0 {
|
||||
var l []byte
|
||||
l, b = cutLine(b, w.maxLineLen-w.curLineLen)
|
||||
|
||||
lf := bytes.HasSuffix(l, []byte("\n"))
|
||||
l = bytes.TrimSuffix(l, []byte("\n"))
|
||||
|
||||
n, err := w.w.Write(l)
|
||||
if err != nil {
|
||||
return written, err
|
||||
}
|
||||
written += n
|
||||
|
||||
cr := bytes.HasSuffix(l, []byte("\r"))
|
||||
if len(l) == 0 {
|
||||
cr = w.cr
|
||||
}
|
||||
|
||||
if !lf && len(b) == 0 {
|
||||
w.curLineLen += len(l)
|
||||
w.cr = cr
|
||||
break
|
||||
}
|
||||
w.curLineLen = 0
|
||||
|
||||
ending := []byte("\r\n")
|
||||
if cr {
|
||||
ending = []byte("\n")
|
||||
}
|
||||
_, err = w.w.Write(ending)
|
||||
if err != nil {
|
||||
return written, err
|
||||
}
|
||||
// If the written `\n` was part of the input bytes slice, then account for it.
|
||||
if lf {
|
||||
written++
|
||||
}
|
||||
w.cr = false
|
||||
}
|
||||
|
||||
return written, nil
|
||||
}
|
||||
|
||||
func cutLine(b []byte, max int) ([]byte, []byte) {
|
||||
for i := 0; i < len(b); i++ {
|
||||
if b[i] == '\r' && i == max {
|
||||
continue
|
||||
}
|
||||
if b[i] == '\n' {
|
||||
return b[:i+1], b[i+1:]
|
||||
}
|
||||
if i >= max {
|
||||
return b[:i], b[i:]
|
||||
}
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
264
vendor/github.com/emersion/go-message/entity.go
generated
vendored
Normal file
264
vendor/github.com/emersion/go-message/entity.go
generated
vendored
Normal file
@@ -0,0 +1,264 @@
|
||||
package message
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-message/textproto"
|
||||
)
|
||||
|
||||
// An Entity is either a whole message or a one of the parts in the body of a
|
||||
// multipart entity.
|
||||
type Entity struct {
|
||||
Header Header // The entity's header.
|
||||
Body io.Reader // The decoded entity's body.
|
||||
|
||||
mediaType string
|
||||
mediaParams map[string]string
|
||||
}
|
||||
|
||||
// New makes a new message with the provided header and body. The entity's
|
||||
// transfer encoding and charset are automatically decoded to UTF-8.
|
||||
//
|
||||
// If the message uses an unknown transfer encoding or charset, New returns an
|
||||
// error that verifies IsUnknownCharset, but also returns an Entity that can
|
||||
// be read.
|
||||
func New(header Header, body io.Reader) (*Entity, error) {
|
||||
var err error
|
||||
|
||||
mediaType, mediaParams, _ := header.ContentType()
|
||||
|
||||
// QUIRK: RFC 2045 section 6.4 specifies that multipart messages can't have
|
||||
// a Content-Transfer-Encoding other than "7bit", "8bit" or "binary".
|
||||
// However some messages in the wild are non-conformant and have it set to
|
||||
// e.g. "quoted-printable". So we just ignore it for multipart.
|
||||
// See https://github.com/emersion/go-message/issues/48
|
||||
if !strings.HasPrefix(mediaType, "multipart/") {
|
||||
enc := header.Get("Content-Transfer-Encoding")
|
||||
if decoded, encErr := encodingReader(enc, body); encErr != nil {
|
||||
err = UnknownEncodingError{encErr}
|
||||
} else {
|
||||
body = decoded
|
||||
}
|
||||
}
|
||||
|
||||
// RFC 2046 section 4.1.2: charset only applies to text/*
|
||||
if strings.HasPrefix(mediaType, "text/") {
|
||||
if ch, ok := mediaParams["charset"]; ok {
|
||||
if converted, charsetErr := charsetReader(ch, body); charsetErr != nil {
|
||||
err = UnknownCharsetError{charsetErr}
|
||||
} else {
|
||||
body = converted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &Entity{
|
||||
Header: header,
|
||||
Body: body,
|
||||
mediaType: mediaType,
|
||||
mediaParams: mediaParams,
|
||||
}, err
|
||||
}
|
||||
|
||||
// NewMultipart makes a new multipart message with the provided header and
|
||||
// parts. The Content-Type header must begin with "multipart/".
|
||||
//
|
||||
// If the message uses an unknown transfer encoding, NewMultipart returns an
|
||||
// error that verifies IsUnknownCharset, but also returns an Entity that can
|
||||
// be read.
|
||||
func NewMultipart(header Header, parts []*Entity) (*Entity, error) {
|
||||
r := &multipartBody{
|
||||
header: header,
|
||||
parts: parts,
|
||||
}
|
||||
|
||||
return New(header, r)
|
||||
}
|
||||
|
||||
const defaultMaxHeaderBytes = 1 << 20 // 1 MB
|
||||
|
||||
var errHeaderTooBig = errors.New("message: header exceeds maximum size")
|
||||
|
||||
// limitedReader is the same as io.LimitedReader, but returns a custom error.
|
||||
type limitedReader struct {
|
||||
R io.Reader
|
||||
N int64
|
||||
}
|
||||
|
||||
func (lr *limitedReader) Read(p []byte) (int, error) {
|
||||
if lr.N <= 0 {
|
||||
return 0, errHeaderTooBig
|
||||
}
|
||||
if int64(len(p)) > lr.N {
|
||||
p = p[0:lr.N]
|
||||
}
|
||||
n, err := lr.R.Read(p)
|
||||
lr.N -= int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// ReadOptions are options for ReadWithOptions.
|
||||
type ReadOptions struct {
|
||||
// MaxHeaderBytes limits the maximum permissible size of a message header
|
||||
// block. If exceeded, an error will be returned.
|
||||
//
|
||||
// Set to -1 for no limit, set to 0 for the default value (1MB).
|
||||
MaxHeaderBytes int64
|
||||
}
|
||||
|
||||
// withDefaults returns a sanitised version of the options with defaults/special
|
||||
// values accounted for.
|
||||
func (o *ReadOptions) withDefaults() *ReadOptions {
|
||||
var out ReadOptions
|
||||
if o != nil {
|
||||
out = *o
|
||||
}
|
||||
if out.MaxHeaderBytes == 0 {
|
||||
out.MaxHeaderBytes = defaultMaxHeaderBytes
|
||||
} else if out.MaxHeaderBytes < 0 {
|
||||
out.MaxHeaderBytes = math.MaxInt64
|
||||
}
|
||||
return &out
|
||||
}
|
||||
|
||||
// ReadWithOptions see Read, but allows overriding some parameters with
|
||||
// ReadOptions.
|
||||
//
|
||||
// If the message uses an unknown transfer encoding or charset, ReadWithOptions
|
||||
// returns an error that verifies IsUnknownCharset or IsUnknownEncoding, but
|
||||
// also returns an Entity that can be read.
|
||||
func ReadWithOptions(r io.Reader, opts *ReadOptions) (*Entity, error) {
|
||||
opts = opts.withDefaults()
|
||||
|
||||
lr := &limitedReader{R: r, N: opts.MaxHeaderBytes}
|
||||
br := bufio.NewReader(lr)
|
||||
|
||||
h, err := textproto.ReadHeader(br)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lr.N = math.MaxInt64
|
||||
|
||||
return New(Header{h}, br)
|
||||
}
|
||||
|
||||
// Read reads a message from r. The message's encoding and charset are
|
||||
// automatically decoded to raw UTF-8. Note that this function only reads the
|
||||
// message header.
|
||||
//
|
||||
// If the message uses an unknown transfer encoding or charset, Read returns an
|
||||
// error that verifies IsUnknownCharset or IsUnknownEncoding, but also returns
|
||||
// an Entity that can be read.
|
||||
func Read(r io.Reader) (*Entity, error) {
|
||||
return ReadWithOptions(r, nil)
|
||||
}
|
||||
|
||||
// MultipartReader returns a MultipartReader that reads parts from this entity's
|
||||
// body. If this entity is not multipart, it returns nil.
|
||||
func (e *Entity) MultipartReader() MultipartReader {
|
||||
if !strings.HasPrefix(e.mediaType, "multipart/") {
|
||||
return nil
|
||||
}
|
||||
if mb, ok := e.Body.(*multipartBody); ok {
|
||||
return mb
|
||||
}
|
||||
return &multipartReader{textproto.NewMultipartReader(e.Body, e.mediaParams["boundary"])}
|
||||
}
|
||||
|
||||
// writeBodyTo writes this entity's body to w (without the header).
|
||||
func (e *Entity) writeBodyTo(w *Writer) error {
|
||||
var err error
|
||||
if mb, ok := e.Body.(*multipartBody); ok {
|
||||
err = mb.writeBodyTo(w)
|
||||
} else {
|
||||
_, err = io.Copy(w, e.Body)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// WriteTo writes this entity's header and body to w.
|
||||
func (e *Entity) WriteTo(w io.Writer) error {
|
||||
ew, err := CreateWriter(w, e.Header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := e.writeBodyTo(ew); err != nil {
|
||||
ew.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
return ew.Close()
|
||||
}
|
||||
|
||||
// WalkFunc is the type of the function called for each part visited by Walk.
|
||||
//
|
||||
// The path argument is a list of multipart indices leading to the part. The
|
||||
// root part has a nil path.
|
||||
//
|
||||
// If there was an encoding error walking to a part, the incoming error will
|
||||
// describe the problem and the function can decide how to handle that error.
|
||||
//
|
||||
// Unlike IMAP part paths, indices start from 0 (instead of 1) and a
|
||||
// non-multipart message has a nil path (instead of {1}).
|
||||
//
|
||||
// If an error is returned, processing stops.
|
||||
type WalkFunc func(path []int, entity *Entity, err error) error
|
||||
|
||||
// Walk walks the entity's multipart tree, calling walkFunc for each part in
|
||||
// the tree, including the root entity.
|
||||
//
|
||||
// Walk consumes the entity.
|
||||
func (e *Entity) Walk(walkFunc WalkFunc) error {
|
||||
var multipartReaders []MultipartReader
|
||||
var path []int
|
||||
part := e
|
||||
for {
|
||||
var err error
|
||||
if part == nil {
|
||||
if len(multipartReaders) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// Get the next part from the last multipart reader
|
||||
mr := multipartReaders[len(multipartReaders)-1]
|
||||
part, err = mr.NextPart()
|
||||
if err == io.EOF {
|
||||
multipartReaders = multipartReaders[:len(multipartReaders)-1]
|
||||
path = path[:len(path)-1]
|
||||
continue
|
||||
} else if IsUnknownEncoding(err) || IsUnknownCharset(err) {
|
||||
// Forward the error to walkFunc
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path[len(path)-1]++
|
||||
}
|
||||
|
||||
// Copy the path since we'll mutate it on the next iteration
|
||||
var pathCopy []int
|
||||
if len(path) > 0 {
|
||||
pathCopy = make([]int, len(path))
|
||||
copy(pathCopy, path)
|
||||
}
|
||||
|
||||
if err := walkFunc(pathCopy, part, err); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if mr := part.MultipartReader(); mr != nil {
|
||||
multipartReaders = append(multipartReaders, mr)
|
||||
path = append(path, -1)
|
||||
}
|
||||
|
||||
part = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
118
vendor/github.com/emersion/go-message/header.go
generated
vendored
Normal file
118
vendor/github.com/emersion/go-message/header.go
generated
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
package message
|
||||
|
||||
import (
|
||||
"mime"
|
||||
|
||||
"github.com/emersion/go-message/textproto"
|
||||
)
|
||||
|
||||
func parseHeaderWithParams(s string) (f string, params map[string]string, err error) {
|
||||
f, params, err = mime.ParseMediaType(s)
|
||||
if err != nil {
|
||||
return s, nil, err
|
||||
}
|
||||
for k, v := range params {
|
||||
params[k], _ = decodeHeader(v)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func formatHeaderWithParams(f string, params map[string]string) string {
|
||||
encParams := make(map[string]string)
|
||||
for k, v := range params {
|
||||
encParams[k] = encodeHeader(v)
|
||||
}
|
||||
return mime.FormatMediaType(f, encParams)
|
||||
}
|
||||
|
||||
// HeaderFields iterates over header fields.
|
||||
type HeaderFields interface {
|
||||
textproto.HeaderFields
|
||||
|
||||
// Text parses the value of the current field as plaintext. The field
|
||||
// charset is decoded to UTF-8. If the header field's charset is unknown,
|
||||
// the raw field value is returned and the error verifies IsUnknownCharset.
|
||||
Text() (string, error)
|
||||
}
|
||||
|
||||
type headerFields struct {
|
||||
textproto.HeaderFields
|
||||
}
|
||||
|
||||
func (hf *headerFields) Text() (string, error) {
|
||||
return decodeHeader(hf.Value())
|
||||
}
|
||||
|
||||
// A Header represents the key-value pairs in a message header.
|
||||
type Header struct {
|
||||
textproto.Header
|
||||
}
|
||||
|
||||
// HeaderFromMap creates a header from a map of header fields.
|
||||
//
|
||||
// This function is provided for interoperability with the standard library.
|
||||
// If possible, ReadHeader should be used instead to avoid loosing information.
|
||||
// The map representation looses the ordering of the fields, the capitalization
|
||||
// of the header keys, and the whitespace of the original header.
|
||||
func HeaderFromMap(m map[string][]string) Header {
|
||||
return Header{textproto.HeaderFromMap(m)}
|
||||
}
|
||||
|
||||
// ContentType parses the Content-Type header field.
|
||||
//
|
||||
// If no Content-Type is specified, it returns "text/plain".
|
||||
func (h *Header) ContentType() (t string, params map[string]string, err error) {
|
||||
v := h.Get("Content-Type")
|
||||
if v == "" {
|
||||
return "text/plain", nil, nil
|
||||
}
|
||||
return parseHeaderWithParams(v)
|
||||
}
|
||||
|
||||
// SetContentType formats the Content-Type header field.
|
||||
func (h *Header) SetContentType(t string, params map[string]string) {
|
||||
h.Set("Content-Type", formatHeaderWithParams(t, params))
|
||||
}
|
||||
|
||||
// ContentDisposition parses the Content-Disposition header field, as defined in
|
||||
// RFC 2183.
|
||||
func (h *Header) ContentDisposition() (disp string, params map[string]string, err error) {
|
||||
return parseHeaderWithParams(h.Get("Content-Disposition"))
|
||||
}
|
||||
|
||||
// SetContentDisposition formats the Content-Disposition header field, as
|
||||
// defined in RFC 2183.
|
||||
func (h *Header) SetContentDisposition(disp string, params map[string]string) {
|
||||
h.Set("Content-Disposition", formatHeaderWithParams(disp, params))
|
||||
}
|
||||
|
||||
// Text parses a plaintext header field. The field charset is automatically
|
||||
// decoded to UTF-8. If the header field's charset is unknown, the raw field
|
||||
// value is returned and the error verifies IsUnknownCharset.
|
||||
func (h *Header) Text(k string) (string, error) {
|
||||
return decodeHeader(h.Get(k))
|
||||
}
|
||||
|
||||
// SetText sets a plaintext header field.
|
||||
func (h *Header) SetText(k, v string) {
|
||||
h.Set(k, encodeHeader(v))
|
||||
}
|
||||
|
||||
// Copy creates a stand-alone copy of the header.
|
||||
func (h *Header) Copy() Header {
|
||||
return Header{h.Header.Copy()}
|
||||
}
|
||||
|
||||
// Fields iterates over all the header fields.
|
||||
//
|
||||
// The header may not be mutated while iterating, except using HeaderFields.Del.
|
||||
func (h *Header) Fields() HeaderFields {
|
||||
return &headerFields{h.Header.Fields()}
|
||||
}
|
||||
|
||||
// FieldsByKey iterates over all fields having the specified key.
|
||||
//
|
||||
// The header may not be mutated while iterating, except using HeaderFields.Del.
|
||||
func (h *Header) FieldsByKey(k string) HeaderFields {
|
||||
return &headerFields{h.Header.FieldsByKey(k)}
|
||||
}
|
||||
42
vendor/github.com/emersion/go-message/mail/address.go
generated
vendored
Normal file
42
vendor/github.com/emersion/go-message/mail/address.go
generated
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"net/mail"
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-message"
|
||||
)
|
||||
|
||||
// Address represents a single mail address.
|
||||
// The type alias ensures that a net/mail.Address can be used wherever an
|
||||
// Address is expected
|
||||
type Address = mail.Address
|
||||
|
||||
func formatAddressList(l []*Address) string {
|
||||
formatted := make([]string, len(l))
|
||||
for i, a := range l {
|
||||
formatted[i] = a.String()
|
||||
}
|
||||
return strings.Join(formatted, ", ")
|
||||
}
|
||||
|
||||
// ParseAddress parses a single RFC 5322 address, e.g. "Barry Gibbs <bg@example.com>"
|
||||
// Use this function only if you parse from a string, if you have a Header use
|
||||
// Header.AddressList instead
|
||||
func ParseAddress(address string) (*Address, error) {
|
||||
parser := mail.AddressParser{
|
||||
&mime.WordDecoder{message.CharsetReader},
|
||||
}
|
||||
return parser.Parse(address)
|
||||
}
|
||||
|
||||
// ParseAddressList parses the given string as a list of addresses.
|
||||
// Use this function only if you parse from a string, if you have a Header use
|
||||
// Header.AddressList instead
|
||||
func ParseAddressList(list string) ([]*Address, error) {
|
||||
parser := mail.AddressParser{
|
||||
&mime.WordDecoder{message.CharsetReader},
|
||||
}
|
||||
return parser.ParseList(list)
|
||||
}
|
||||
30
vendor/github.com/emersion/go-message/mail/attachment.go
generated
vendored
Normal file
30
vendor/github.com/emersion/go-message/mail/attachment.go
generated
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-message"
|
||||
)
|
||||
|
||||
// An AttachmentHeader represents an attachment's header.
|
||||
type AttachmentHeader struct {
|
||||
message.Header
|
||||
}
|
||||
|
||||
// Filename parses the attachment's filename.
|
||||
func (h *AttachmentHeader) Filename() (string, error) {
|
||||
_, params, err := h.ContentDisposition()
|
||||
|
||||
filename, ok := params["filename"]
|
||||
if !ok {
|
||||
// Using "name" in Content-Type is discouraged
|
||||
_, params, err = h.ContentType()
|
||||
filename = params["name"]
|
||||
}
|
||||
|
||||
return filename, err
|
||||
}
|
||||
|
||||
// SetFilename formats the attachment's filename.
|
||||
func (h *AttachmentHeader) SetFilename(filename string) {
|
||||
dispParams := map[string]string{"filename": filename}
|
||||
h.SetContentDisposition("attachment", dispParams)
|
||||
}
|
||||
381
vendor/github.com/emersion/go-message/mail/header.go
generated
vendored
Normal file
381
vendor/github.com/emersion/go-message/mail/header.go
generated
vendored
Normal file
@@ -0,0 +1,381 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/emersion/go-message"
|
||||
)
|
||||
|
||||
const dateLayout = "Mon, 02 Jan 2006 15:04:05 -0700"
|
||||
|
||||
type headerParser struct {
|
||||
s string
|
||||
}
|
||||
|
||||
func (p *headerParser) len() int {
|
||||
return len(p.s)
|
||||
}
|
||||
|
||||
func (p *headerParser) empty() bool {
|
||||
return p.len() == 0
|
||||
}
|
||||
|
||||
func (p *headerParser) peek() byte {
|
||||
return p.s[0]
|
||||
}
|
||||
|
||||
func (p *headerParser) consume(c byte) bool {
|
||||
if p.empty() || p.peek() != c {
|
||||
return false
|
||||
}
|
||||
p.s = p.s[1:]
|
||||
return true
|
||||
}
|
||||
|
||||
// skipSpace skips the leading space and tab characters.
|
||||
func (p *headerParser) skipSpace() {
|
||||
p.s = strings.TrimLeft(p.s, " \t")
|
||||
}
|
||||
|
||||
// skipCFWS skips CFWS as defined in RFC5322. It returns false if the CFWS is
|
||||
// malformed.
|
||||
func (p *headerParser) skipCFWS() bool {
|
||||
p.skipSpace()
|
||||
|
||||
for {
|
||||
if !p.consume('(') {
|
||||
break
|
||||
}
|
||||
|
||||
if _, ok := p.consumeComment(); !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
p.skipSpace()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *headerParser) consumeComment() (string, bool) {
|
||||
// '(' already consumed.
|
||||
depth := 1
|
||||
|
||||
var comment string
|
||||
for {
|
||||
if p.empty() || depth == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
if p.peek() == '\\' && p.len() > 1 {
|
||||
p.s = p.s[1:]
|
||||
} else if p.peek() == '(' {
|
||||
depth++
|
||||
} else if p.peek() == ')' {
|
||||
depth--
|
||||
}
|
||||
|
||||
if depth > 0 {
|
||||
comment += p.s[:1]
|
||||
}
|
||||
|
||||
p.s = p.s[1:]
|
||||
}
|
||||
|
||||
return comment, depth == 0
|
||||
}
|
||||
|
||||
func (p *headerParser) parseAtomText(dot bool) (string, error) {
|
||||
i := 0
|
||||
for {
|
||||
r, size := utf8.DecodeRuneInString(p.s[i:])
|
||||
if size == 1 && r == utf8.RuneError {
|
||||
return "", fmt.Errorf("mail: invalid UTF-8 in atom-text: %q", p.s)
|
||||
} else if size == 0 || !isAtext(r, dot) {
|
||||
break
|
||||
}
|
||||
i += size
|
||||
}
|
||||
if i == 0 {
|
||||
return "", errors.New("mail: invalid string")
|
||||
}
|
||||
|
||||
var atom string
|
||||
atom, p.s = p.s[:i], p.s[i:]
|
||||
return atom, nil
|
||||
}
|
||||
|
||||
func isAtext(r rune, dot bool) bool {
|
||||
switch r {
|
||||
case '.':
|
||||
return dot
|
||||
// RFC 5322 3.2.3 specials
|
||||
case '(', ')', '[', ']', ';', '@', '\\', ',':
|
||||
return false
|
||||
case '<', '>', '"', ':':
|
||||
return false
|
||||
}
|
||||
return isVchar(r)
|
||||
}
|
||||
|
||||
// isVchar reports whether r is an RFC 5322 VCHAR character.
|
||||
func isVchar(r rune) bool {
|
||||
// Visible (printing) characters
|
||||
return '!' <= r && r <= '~' || isMultibyte(r)
|
||||
}
|
||||
|
||||
// isMultibyte reports whether r is a multi-byte UTF-8 character
|
||||
// as supported by RFC 6532
|
||||
func isMultibyte(r rune) bool {
|
||||
return r >= utf8.RuneSelf
|
||||
}
|
||||
|
||||
func (p *headerParser) parseNoFoldLiteral() (string, error) {
|
||||
if !p.consume('[') {
|
||||
return "", errors.New("mail: missing '[' in no-fold-literal")
|
||||
}
|
||||
|
||||
i := 0
|
||||
for {
|
||||
r, size := utf8.DecodeRuneInString(p.s[i:])
|
||||
if size == 1 && r == utf8.RuneError {
|
||||
return "", fmt.Errorf("mail: invalid UTF-8 in no-fold-literal: %q", p.s)
|
||||
} else if size == 0 || !isDtext(r) {
|
||||
break
|
||||
}
|
||||
i += size
|
||||
}
|
||||
var lit string
|
||||
lit, p.s = p.s[:i], p.s[i:]
|
||||
|
||||
if !p.consume(']') {
|
||||
return "", errors.New("mail: missing ']' in no-fold-literal")
|
||||
}
|
||||
return "[" + lit + "]", nil
|
||||
}
|
||||
|
||||
func isDtext(r rune) bool {
|
||||
switch r {
|
||||
case '[', ']', '\\':
|
||||
return false
|
||||
}
|
||||
return isVchar(r)
|
||||
}
|
||||
|
||||
func (p *headerParser) parseMsgID() (string, error) {
|
||||
if !p.skipCFWS() {
|
||||
return "", errors.New("mail: malformed parenthetical comment")
|
||||
}
|
||||
|
||||
if !p.consume('<') {
|
||||
return "", errors.New("mail: missing '<' in msg-id")
|
||||
}
|
||||
|
||||
left, err := p.parseAtomText(true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !p.consume('@') {
|
||||
return "", errors.New("mail: missing '@' in msg-id")
|
||||
}
|
||||
|
||||
var right string
|
||||
if !p.empty() && p.peek() == '[' {
|
||||
// no-fold-literal
|
||||
right, err = p.parseNoFoldLiteral()
|
||||
} else {
|
||||
right, err = p.parseAtomText(true)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !p.consume('>') {
|
||||
return "", errors.New("mail: missing '>' in msg-id")
|
||||
}
|
||||
|
||||
if !p.skipCFWS() {
|
||||
return "", errors.New("mail: malformed parenthetical comment")
|
||||
}
|
||||
|
||||
return left + "@" + right, nil
|
||||
}
|
||||
|
||||
// A Header is a mail header.
|
||||
type Header struct {
|
||||
message.Header
|
||||
}
|
||||
|
||||
// HeaderFromMap creates a header from a map of header fields.
|
||||
//
|
||||
// This function is provided for interoperability with the standard library.
|
||||
// If possible, ReadHeader should be used instead to avoid loosing information.
|
||||
// The map representation looses the ordering of the fields, the capitalization
|
||||
// of the header keys, and the whitespace of the original header.
|
||||
func HeaderFromMap(m map[string][]string) Header {
|
||||
return Header{message.HeaderFromMap(m)}
|
||||
}
|
||||
|
||||
// AddressList parses the named header field as a list of addresses. If the
|
||||
// header field is missing, it returns nil.
|
||||
//
|
||||
// This can be used on From, Sender, Reply-To, To, Cc and Bcc header fields.
|
||||
func (h *Header) AddressList(key string) ([]*Address, error) {
|
||||
v := h.Get(key)
|
||||
if v == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return ParseAddressList(v)
|
||||
}
|
||||
|
||||
// SetAddressList formats the named header field to the provided list of
|
||||
// addresses.
|
||||
//
|
||||
// This can be used on From, Sender, Reply-To, To, Cc and Bcc header fields.
|
||||
func (h *Header) SetAddressList(key string, addrs []*Address) {
|
||||
if len(addrs) > 0 {
|
||||
h.Set(key, formatAddressList(addrs))
|
||||
} else {
|
||||
h.Del(key)
|
||||
}
|
||||
}
|
||||
|
||||
// Date parses the Date header field. If the header field is missing, it
|
||||
// returns the zero time.
|
||||
func (h *Header) Date() (time.Time, error) {
|
||||
v := h.Get("Date")
|
||||
if v == "" {
|
||||
return time.Time{}, nil
|
||||
}
|
||||
return mail.ParseDate(v)
|
||||
}
|
||||
|
||||
// SetDate formats the Date header field.
|
||||
func (h *Header) SetDate(t time.Time) {
|
||||
if !t.IsZero() {
|
||||
h.Set("Date", t.Format(dateLayout))
|
||||
} else {
|
||||
h.Del("Date")
|
||||
}
|
||||
}
|
||||
|
||||
// Subject parses the Subject header field. If there is an error, the raw field
|
||||
// value is returned alongside the error.
|
||||
func (h *Header) Subject() (string, error) {
|
||||
return h.Text("Subject")
|
||||
}
|
||||
|
||||
// SetSubject formats the Subject header field.
|
||||
func (h *Header) SetSubject(s string) {
|
||||
h.SetText("Subject", s)
|
||||
}
|
||||
|
||||
// MessageID parses the Message-ID field. It returns the message identifier,
|
||||
// without the angle brackets. If the message doesn't have a Message-ID header
|
||||
// field, it returns an empty string.
|
||||
func (h *Header) MessageID() (string, error) {
|
||||
v := h.Get("Message-Id")
|
||||
if v == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
p := headerParser{v}
|
||||
return p.parseMsgID()
|
||||
}
|
||||
|
||||
// MsgIDList parses a list of message identifiers. It returns message
|
||||
// identifiers without angle brackets. If the header field is missing, it
|
||||
// returns nil.
|
||||
//
|
||||
// This can be used on In-Reply-To and References header fields.
|
||||
func (h *Header) MsgIDList(key string) ([]string, error) {
|
||||
v := h.Get(key)
|
||||
if v == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
p := headerParser{v}
|
||||
var l []string
|
||||
for !p.empty() {
|
||||
msgID, err := p.parseMsgID()
|
||||
if err != nil {
|
||||
return l, err
|
||||
}
|
||||
l = append(l, msgID)
|
||||
}
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// GenerateMessageID wraps GenerateMessageIDWithHostname and therefore uses the
|
||||
// hostname of the local machine. This is done to not break existing software.
|
||||
// Wherever possible better use GenerateMessageIDWithHostname, because the local
|
||||
// hostname of a machine tends to not be unique nor a FQDN which especially
|
||||
// brings problems with spam filters.
|
||||
func (h *Header) GenerateMessageID() error {
|
||||
var err error
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return h.GenerateMessageIDWithHostname(hostname)
|
||||
}
|
||||
|
||||
// GenerateMessageIDWithHostname generates an RFC 2822-compliant Message-Id
|
||||
// based on the informational draft "Recommendations for generating Message
|
||||
// IDs", it takes an hostname as argument, so that software using this library
|
||||
// could use a hostname they know to be unique
|
||||
func (h *Header) GenerateMessageIDWithHostname(hostname string) error {
|
||||
now := uint64(time.Now().UnixNano())
|
||||
|
||||
nonceByte := make([]byte, 8)
|
||||
if _, err := rand.Read(nonceByte); err != nil {
|
||||
return err
|
||||
}
|
||||
nonce := binary.BigEndian.Uint64(nonceByte)
|
||||
|
||||
msgID := fmt.Sprintf("%s.%s@%s", base36(now), base36(nonce), hostname)
|
||||
h.SetMessageID(msgID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func base36(input uint64) string {
|
||||
return strings.ToUpper(strconv.FormatUint(input, 36))
|
||||
}
|
||||
|
||||
// SetMessageID sets the Message-ID field. id is the message identifier,
|
||||
// without the angle brackets.
|
||||
func (h *Header) SetMessageID(id string) {
|
||||
if id != "" {
|
||||
h.Set("Message-Id", "<"+id+">")
|
||||
} else {
|
||||
h.Del("Message-Id")
|
||||
}
|
||||
}
|
||||
|
||||
// SetMsgIDList formats a list of message identifiers. Message identifiers
|
||||
// don't include angle brackets.
|
||||
//
|
||||
// This can be used on In-Reply-To and References header fields.
|
||||
func (h *Header) SetMsgIDList(key string, l []string) {
|
||||
if len(l) > 0 {
|
||||
h.Set(key, "<"+strings.Join(l, "> <")+">")
|
||||
} else {
|
||||
h.Del(key)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy creates a stand-alone copy of the header.
|
||||
func (h *Header) Copy() Header {
|
||||
return Header{h.Header.Copy()}
|
||||
}
|
||||
10
vendor/github.com/emersion/go-message/mail/inline.go
generated
vendored
Normal file
10
vendor/github.com/emersion/go-message/mail/inline.go
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-message"
|
||||
)
|
||||
|
||||
// A InlineHeader represents a message text header.
|
||||
type InlineHeader struct {
|
||||
message.Header
|
||||
}
|
||||
9
vendor/github.com/emersion/go-message/mail/mail.go
generated
vendored
Normal file
9
vendor/github.com/emersion/go-message/mail/mail.go
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
// Package mail implements reading and writing mail messages.
|
||||
//
|
||||
// This package assumes that a mail message contains one or more text parts and
|
||||
// zero or more attachment parts. Each text part represents a different version
|
||||
// of the message content (e.g. a different type, a different language and so
|
||||
// on).
|
||||
//
|
||||
// RFC 5322 defines the Internet Message Format.
|
||||
package mail
|
||||
130
vendor/github.com/emersion/go-message/mail/reader.go
generated
vendored
Normal file
130
vendor/github.com/emersion/go-message/mail/reader.go
generated
vendored
Normal file
@@ -0,0 +1,130 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-message"
|
||||
)
|
||||
|
||||
// A PartHeader is a mail part header. It contains convenience functions to get
|
||||
// and set header fields.
|
||||
type PartHeader interface {
|
||||
// Add adds the key, value pair to the header.
|
||||
Add(key, value string)
|
||||
// Del deletes the values associated with key.
|
||||
Del(key string)
|
||||
// Get gets the first value associated with the given key. If there are no
|
||||
// values associated with the key, Get returns "".
|
||||
Get(key string) string
|
||||
// Set sets the header entries associated with key to the single element
|
||||
// value. It replaces any existing values associated with key.
|
||||
Set(key, value string)
|
||||
}
|
||||
|
||||
// A Part is either a mail text or an attachment. Header is either a InlineHeader
|
||||
// or an AttachmentHeader.
|
||||
type Part struct {
|
||||
Header PartHeader
|
||||
Body io.Reader
|
||||
}
|
||||
|
||||
// A Reader reads a mail message.
|
||||
type Reader struct {
|
||||
Header Header
|
||||
|
||||
e *message.Entity
|
||||
readers *list.List
|
||||
}
|
||||
|
||||
// NewReader creates a new mail reader.
|
||||
func NewReader(e *message.Entity) *Reader {
|
||||
mr := e.MultipartReader()
|
||||
if mr == nil {
|
||||
// Artificially create a multipart entity
|
||||
// With this header, no error will be returned by message.NewMultipart
|
||||
var h message.Header
|
||||
h.Set("Content-Type", "multipart/mixed")
|
||||
me, _ := message.NewMultipart(h, []*message.Entity{e})
|
||||
mr = me.MultipartReader()
|
||||
}
|
||||
|
||||
l := list.New()
|
||||
l.PushBack(mr)
|
||||
|
||||
return &Reader{Header{e.Header}, e, l}
|
||||
}
|
||||
|
||||
// CreateReader reads a mail header from r and returns a new mail reader.
|
||||
//
|
||||
// If the message uses an unknown transfer encoding or charset, CreateReader
|
||||
// returns an error that verifies message.IsUnknownCharset, but also returns a
|
||||
// Reader that can be used.
|
||||
func CreateReader(r io.Reader) (*Reader, error) {
|
||||
e, err := message.Read(r)
|
||||
if err != nil && !message.IsUnknownCharset(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewReader(e), err
|
||||
}
|
||||
|
||||
// NextPart returns the next mail part. If there is no more part, io.EOF is
|
||||
// returned as error.
|
||||
//
|
||||
// The returned Part.Body must be read completely before the next call to
|
||||
// NextPart, otherwise it will be discarded.
|
||||
//
|
||||
// If the part uses an unknown transfer encoding or charset, NextPart returns an
|
||||
// error that verifies message.IsUnknownCharset, but also returns a Part that
|
||||
// can be used.
|
||||
func (r *Reader) NextPart() (*Part, error) {
|
||||
for r.readers.Len() > 0 {
|
||||
e := r.readers.Back()
|
||||
mr := e.Value.(message.MultipartReader)
|
||||
|
||||
p, err := mr.NextPart()
|
||||
if err == io.EOF {
|
||||
// This whole multipart entity has been read, continue with the next one
|
||||
r.readers.Remove(e)
|
||||
continue
|
||||
} else if err != nil && !message.IsUnknownCharset(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pmr := p.MultipartReader(); pmr != nil {
|
||||
// This is a multipart part, read it
|
||||
r.readers.PushBack(pmr)
|
||||
} else {
|
||||
// This is a non-multipart part, return a mail part
|
||||
mp := &Part{Body: p.Body}
|
||||
t, _, _ := p.Header.ContentType()
|
||||
disp, _, _ := p.Header.ContentDisposition()
|
||||
if disp == "inline" || (disp != "attachment" && strings.HasPrefix(t, "text/")) {
|
||||
mp.Header = &InlineHeader{p.Header}
|
||||
} else {
|
||||
mp.Header = &AttachmentHeader{p.Header}
|
||||
}
|
||||
return mp, err
|
||||
}
|
||||
}
|
||||
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
// Close finishes the reader.
|
||||
func (r *Reader) Close() error {
|
||||
for r.readers.Len() > 0 {
|
||||
e := r.readers.Back()
|
||||
mr := e.Value.(message.MultipartReader)
|
||||
|
||||
if err := mr.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.readers.Remove(e)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
132
vendor/github.com/emersion/go-message/mail/writer.go
generated
vendored
Normal file
132
vendor/github.com/emersion/go-message/mail/writer.go
generated
vendored
Normal file
@@ -0,0 +1,132 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-message"
|
||||
)
|
||||
|
||||
func initInlineContentTransferEncoding(h *message.Header) {
|
||||
if !h.Has("Content-Transfer-Encoding") {
|
||||
t, _, _ := h.ContentType()
|
||||
if strings.HasPrefix(t, "text/") {
|
||||
h.Set("Content-Transfer-Encoding", "quoted-printable")
|
||||
} else {
|
||||
h.Set("Content-Transfer-Encoding", "base64")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initInlineHeader(h *InlineHeader) {
|
||||
h.Set("Content-Disposition", "inline")
|
||||
initInlineContentTransferEncoding(&h.Header)
|
||||
}
|
||||
|
||||
func initAttachmentHeader(h *AttachmentHeader) {
|
||||
disp, _, _ := h.ContentDisposition()
|
||||
if disp != "attachment" {
|
||||
h.Set("Content-Disposition", "attachment")
|
||||
}
|
||||
if !h.Has("Content-Transfer-Encoding") {
|
||||
h.Set("Content-Transfer-Encoding", "base64")
|
||||
}
|
||||
}
|
||||
|
||||
// A Writer writes a mail message. A mail message contains one or more text
|
||||
// parts and zero or more attachments.
|
||||
type Writer struct {
|
||||
mw *message.Writer
|
||||
}
|
||||
|
||||
// CreateWriter writes a mail header to w and creates a new Writer.
|
||||
func CreateWriter(w io.Writer, header Header) (*Writer, error) {
|
||||
header = header.Copy() // don't modify the caller's view
|
||||
header.Set("Content-Type", "multipart/mixed")
|
||||
|
||||
mw, err := message.CreateWriter(w, header.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Writer{mw}, nil
|
||||
}
|
||||
|
||||
// CreateInlineWriter writes a mail header to w. The mail will contain an
|
||||
// inline part, allowing to represent the same text in different formats.
|
||||
// Attachments cannot be included.
|
||||
func CreateInlineWriter(w io.Writer, header Header) (*InlineWriter, error) {
|
||||
header = header.Copy() // don't modify the caller's view
|
||||
header.Set("Content-Type", "multipart/alternative")
|
||||
|
||||
mw, err := message.CreateWriter(w, header.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &InlineWriter{mw}, nil
|
||||
}
|
||||
|
||||
// CreateSingleInlineWriter writes a mail header to w. The mail will contain a
|
||||
// single inline part. The body of the part should be written to the returned
|
||||
// io.WriteCloser. Only one single inline part should be written, use
|
||||
// CreateWriter if you want multiple parts.
|
||||
func CreateSingleInlineWriter(w io.Writer, header Header) (io.WriteCloser, error) {
|
||||
header = header.Copy() // don't modify the caller's view
|
||||
initInlineContentTransferEncoding(&header.Header)
|
||||
return message.CreateWriter(w, header.Header)
|
||||
}
|
||||
|
||||
// CreateInline creates a InlineWriter. One or more parts representing the same
|
||||
// text in different formats can be written to a InlineWriter.
|
||||
func (w *Writer) CreateInline() (*InlineWriter, error) {
|
||||
var h message.Header
|
||||
h.Set("Content-Type", "multipart/alternative")
|
||||
|
||||
mw, err := w.mw.CreatePart(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &InlineWriter{mw}, nil
|
||||
}
|
||||
|
||||
// CreateSingleInline creates a new single text part with the provided header.
|
||||
// The body of the part should be written to the returned io.WriteCloser. Only
|
||||
// one single text part should be written, use CreateInline if you want multiple
|
||||
// text parts.
|
||||
func (w *Writer) CreateSingleInline(h InlineHeader) (io.WriteCloser, error) {
|
||||
h = InlineHeader{h.Header.Copy()} // don't modify the caller's view
|
||||
initInlineHeader(&h)
|
||||
return w.mw.CreatePart(h.Header)
|
||||
}
|
||||
|
||||
// CreateAttachment creates a new attachment with the provided header. The body
|
||||
// of the part should be written to the returned io.WriteCloser.
|
||||
func (w *Writer) CreateAttachment(h AttachmentHeader) (io.WriteCloser, error) {
|
||||
h = AttachmentHeader{h.Header.Copy()} // don't modify the caller's view
|
||||
initAttachmentHeader(&h)
|
||||
return w.mw.CreatePart(h.Header)
|
||||
}
|
||||
|
||||
// Close finishes the Writer.
|
||||
func (w *Writer) Close() error {
|
||||
return w.mw.Close()
|
||||
}
|
||||
|
||||
// InlineWriter writes a mail message's text.
|
||||
type InlineWriter struct {
|
||||
mw *message.Writer
|
||||
}
|
||||
|
||||
// CreatePart creates a new text part with the provided header. The body of the
|
||||
// part should be written to the returned io.WriteCloser.
|
||||
func (w *InlineWriter) CreatePart(h InlineHeader) (io.WriteCloser, error) {
|
||||
h = InlineHeader{h.Header.Copy()} // don't modify the caller's view
|
||||
initInlineHeader(&h)
|
||||
return w.mw.CreatePart(h.Header)
|
||||
}
|
||||
|
||||
// Close finishes the InlineWriter.
|
||||
func (w *InlineWriter) Close() error {
|
||||
return w.mw.Close()
|
||||
}
|
||||
15
vendor/github.com/emersion/go-message/message.go
generated
vendored
Normal file
15
vendor/github.com/emersion/go-message/message.go
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
// Package message implements reading and writing multipurpose messages.
|
||||
//
|
||||
// RFC 2045, RFC 2046 and RFC 2047 defines MIME, and RFC 2183 defines the
|
||||
// Content-Disposition header field.
|
||||
//
|
||||
// Add this import to your package if you want to handle most common charsets
|
||||
// by default:
|
||||
//
|
||||
// import (
|
||||
// _ "github.com/emersion/go-message/charset"
|
||||
// )
|
||||
//
|
||||
// Note, non-UTF-8 charsets are only supported when reading messages. Only
|
||||
// UTF-8 is supported when writing messages.
|
||||
package message
|
||||
116
vendor/github.com/emersion/go-message/multipart.go
generated
vendored
Normal file
116
vendor/github.com/emersion/go-message/multipart.go
generated
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
package message
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/emersion/go-message/textproto"
|
||||
)
|
||||
|
||||
// MultipartReader is an iterator over parts in a MIME multipart body.
|
||||
type MultipartReader interface {
|
||||
io.Closer
|
||||
|
||||
// NextPart returns the next part in the multipart or an error. When there are
|
||||
// no more parts, the error io.EOF is returned.
|
||||
//
|
||||
// Entity.Body must be read completely before the next call to NextPart,
|
||||
// otherwise it will be discarded.
|
||||
NextPart() (*Entity, error)
|
||||
}
|
||||
|
||||
type multipartReader struct {
|
||||
r *textproto.MultipartReader
|
||||
}
|
||||
|
||||
// NextPart implements MultipartReader.
|
||||
func (r *multipartReader) NextPart() (*Entity, error) {
|
||||
p, err := r.r.NextPart()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return New(Header{p.Header}, p)
|
||||
}
|
||||
|
||||
// Close implements io.Closer.
|
||||
func (r *multipartReader) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type multipartBody struct {
|
||||
header Header
|
||||
parts []*Entity
|
||||
|
||||
r *io.PipeReader
|
||||
w *Writer
|
||||
|
||||
i int
|
||||
}
|
||||
|
||||
// Read implements io.Reader.
|
||||
func (m *multipartBody) Read(p []byte) (n int, err error) {
|
||||
if m.r == nil {
|
||||
r, w := io.Pipe()
|
||||
m.r = r
|
||||
|
||||
var err error
|
||||
m.w, err = createWriter(w, &m.header)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Prevent calls to NextPart to succeed
|
||||
m.i = len(m.parts)
|
||||
|
||||
go func() {
|
||||
if err := m.writeBodyTo(m.w); err != nil {
|
||||
w.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.w.Close(); err != nil {
|
||||
w.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Close()
|
||||
}()
|
||||
}
|
||||
|
||||
return m.r.Read(p)
|
||||
}
|
||||
|
||||
// Close implements io.Closer.
|
||||
func (m *multipartBody) Close() error {
|
||||
if m.r != nil {
|
||||
m.r.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NextPart implements MultipartReader.
|
||||
func (m *multipartBody) NextPart() (*Entity, error) {
|
||||
if m.i >= len(m.parts) {
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
part := m.parts[m.i]
|
||||
m.i++
|
||||
return part, nil
|
||||
}
|
||||
|
||||
func (m *multipartBody) writeBodyTo(w *Writer) error {
|
||||
for _, p := range m.parts {
|
||||
pw, err := w.CreatePart(p.Header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.writeBodyTo(pw); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := pw.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
677
vendor/github.com/emersion/go-message/textproto/header.go
generated
vendored
Normal file
677
vendor/github.com/emersion/go-message/textproto/header.go
generated
vendored
Normal file
@@ -0,0 +1,677 @@
|
||||
package textproto
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/textproto"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type headerField struct {
|
||||
b []byte // Raw header field, including whitespace
|
||||
|
||||
k string
|
||||
v string
|
||||
}
|
||||
|
||||
func newHeaderField(k, v string, b []byte) *headerField {
|
||||
return &headerField{k: textproto.CanonicalMIMEHeaderKey(k), v: v, b: b}
|
||||
}
|
||||
|
||||
func (f *headerField) raw() ([]byte, error) {
|
||||
if f.b != nil {
|
||||
return f.b, nil
|
||||
} else {
|
||||
for pos, ch := range f.k {
|
||||
// check if character is a printable US-ASCII except ':'
|
||||
if !(ch >= '!' && ch < ':' || ch > ':' && ch <= '~') {
|
||||
return nil, fmt.Errorf("field name contains incorrect symbols (\\x%x at %v)", ch, pos)
|
||||
}
|
||||
}
|
||||
|
||||
if pos := strings.IndexAny(f.v, "\r\n"); pos != -1 {
|
||||
return nil, fmt.Errorf("field value contains \\r\\n (at %v)", pos)
|
||||
}
|
||||
|
||||
return []byte(formatHeaderField(f.k, f.v)), nil
|
||||
}
|
||||
}
|
||||
|
||||
// A Header represents the key-value pairs in a message header.
|
||||
//
|
||||
// The header representation is idempotent: if the header can be read and
|
||||
// written, the result will be exactly the same as the original (including
|
||||
// whitespace and header field ordering). This is required for e.g. DKIM.
|
||||
//
|
||||
// Mutating the header is restricted: the only two allowed operations are
|
||||
// inserting a new header field at the top and deleting a header field. This is
|
||||
// again necessary for DKIM.
|
||||
type Header struct {
|
||||
// Fields are in reverse order so that inserting a new field at the top is
|
||||
// cheap.
|
||||
l []*headerField
|
||||
m map[string][]*headerField
|
||||
}
|
||||
|
||||
func makeHeaderMap(fs []*headerField) map[string][]*headerField {
|
||||
if len(fs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
m := make(map[string][]*headerField, len(fs))
|
||||
for i, f := range fs {
|
||||
m[f.k] = append(m[f.k], fs[i])
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func newHeader(fs []*headerField) Header {
|
||||
// Reverse order
|
||||
for i := len(fs)/2 - 1; i >= 0; i-- {
|
||||
opp := len(fs) - 1 - i
|
||||
fs[i], fs[opp] = fs[opp], fs[i]
|
||||
}
|
||||
|
||||
return Header{l: fs, m: makeHeaderMap(fs)}
|
||||
}
|
||||
|
||||
// HeaderFromMap creates a header from a map of header fields.
|
||||
//
|
||||
// This function is provided for interoperability with the standard library.
|
||||
// If possible, ReadHeader should be used instead to avoid loosing information.
|
||||
// The map representation looses the ordering of the fields, the capitalization
|
||||
// of the header keys, and the whitespace of the original header.
|
||||
func HeaderFromMap(m map[string][]string) Header {
|
||||
fs := make([]*headerField, 0, len(m))
|
||||
for k, vs := range m {
|
||||
for _, v := range vs {
|
||||
fs = append(fs, newHeaderField(k, v, nil))
|
||||
}
|
||||
}
|
||||
|
||||
sort.SliceStable(fs, func(i, j int) bool {
|
||||
return fs[i].k < fs[j].k
|
||||
})
|
||||
|
||||
return newHeader(fs)
|
||||
}
|
||||
|
||||
// AddRaw adds the raw key, value pair to the header.
|
||||
//
|
||||
// The supplied byte slice should be a complete field in the "Key: Value" form
|
||||
// including trailing CRLF. If there is no comma in the input - AddRaw panics.
|
||||
// No changes are made to kv contents and it will be copied into WriteHeader
|
||||
// output as is.
|
||||
//
|
||||
// kv is directly added to the underlying structure and therefore should not be
|
||||
// modified after the AddRaw call.
|
||||
func (h *Header) AddRaw(kv []byte) {
|
||||
colon := bytes.IndexByte(kv, ':')
|
||||
if colon == -1 {
|
||||
panic("textproto: Header.AddRaw: missing colon")
|
||||
}
|
||||
k := textproto.CanonicalMIMEHeaderKey(string(trim(kv[:colon])))
|
||||
v := trimAroundNewlines(kv[colon+1:])
|
||||
|
||||
if h.m == nil {
|
||||
h.m = make(map[string][]*headerField)
|
||||
}
|
||||
|
||||
f := newHeaderField(k, v, kv)
|
||||
h.l = append(h.l, f)
|
||||
h.m[k] = append(h.m[k], f)
|
||||
}
|
||||
|
||||
// Add adds the key, value pair to the header. It prepends to any existing
|
||||
// fields associated with key.
|
||||
//
|
||||
// Key and value should obey character requirements of RFC 6532.
|
||||
// If you need to format or fold lines manually, use AddRaw.
|
||||
func (h *Header) Add(k, v string) {
|
||||
k = textproto.CanonicalMIMEHeaderKey(k)
|
||||
|
||||
if h.m == nil {
|
||||
h.m = make(map[string][]*headerField)
|
||||
}
|
||||
|
||||
f := newHeaderField(k, v, nil)
|
||||
h.l = append(h.l, f)
|
||||
h.m[k] = append(h.m[k], f)
|
||||
}
|
||||
|
||||
// Get gets the first value associated with the given key. If there are no
|
||||
// values associated with the key, Get returns "".
|
||||
func (h *Header) Get(k string) string {
|
||||
fields := h.m[textproto.CanonicalMIMEHeaderKey(k)]
|
||||
if len(fields) == 0 {
|
||||
return ""
|
||||
}
|
||||
return fields[len(fields)-1].v
|
||||
}
|
||||
|
||||
// Raw gets the first raw header field associated with the given key.
|
||||
//
|
||||
// The returned bytes contain a complete field in the "Key: value" form,
|
||||
// including trailing CRLF.
|
||||
//
|
||||
// The returned slice should not be modified and becomes invalid when the
|
||||
// header is updated.
|
||||
//
|
||||
// An error is returned if the header field contains incorrect characters (see
|
||||
// RFC 6532).
|
||||
func (h *Header) Raw(k string) ([]byte, error) {
|
||||
fields := h.m[textproto.CanonicalMIMEHeaderKey(k)]
|
||||
if len(fields) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return fields[len(fields)-1].raw()
|
||||
}
|
||||
|
||||
// Values returns all values associated with the given key.
|
||||
//
|
||||
// The returned slice should not be modified and becomes invalid when the
|
||||
// header is updated.
|
||||
func (h *Header) Values(k string) []string {
|
||||
fields := h.m[textproto.CanonicalMIMEHeaderKey(k)]
|
||||
if len(fields) == 0 {
|
||||
return nil
|
||||
}
|
||||
l := make([]string, len(fields))
|
||||
for i, field := range fields {
|
||||
l[len(fields)-i-1] = field.v
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// Set sets the header fields associated with key to the single field value.
|
||||
// It replaces any existing values associated with key.
|
||||
func (h *Header) Set(k, v string) {
|
||||
h.Del(k)
|
||||
h.Add(k, v)
|
||||
}
|
||||
|
||||
// Del deletes the values associated with key.
|
||||
func (h *Header) Del(k string) {
|
||||
k = textproto.CanonicalMIMEHeaderKey(k)
|
||||
|
||||
delete(h.m, k)
|
||||
|
||||
// Delete existing keys
|
||||
for i := len(h.l) - 1; i >= 0; i-- {
|
||||
if h.l[i].k == k {
|
||||
h.l = append(h.l[:i], h.l[i+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Has checks whether the header has a field with the specified key.
|
||||
func (h *Header) Has(k string) bool {
|
||||
_, ok := h.m[textproto.CanonicalMIMEHeaderKey(k)]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Copy creates an independent copy of the header.
|
||||
func (h *Header) Copy() Header {
|
||||
l := make([]*headerField, len(h.l))
|
||||
copy(l, h.l)
|
||||
m := makeHeaderMap(l)
|
||||
return Header{l: l, m: m}
|
||||
}
|
||||
|
||||
// Len returns the number of fields in the header.
|
||||
func (h *Header) Len() int {
|
||||
return len(h.l)
|
||||
}
|
||||
|
||||
// Map returns all header fields as a map.
|
||||
//
|
||||
// This function is provided for interoperability with the standard library.
|
||||
// If possible, Fields should be used instead to avoid loosing information.
|
||||
// The map representation looses the ordering of the fields, the capitalization
|
||||
// of the header keys, and the whitespace of the original header.
|
||||
func (h *Header) Map() map[string][]string {
|
||||
m := make(map[string][]string, h.Len())
|
||||
fields := h.Fields()
|
||||
for fields.Next() {
|
||||
m[fields.Key()] = append(m[fields.Key()], fields.Value())
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// HeaderFields iterates over header fields. Its cursor starts before the first
|
||||
// field of the header. Use Next to advance from field to field.
|
||||
type HeaderFields interface {
|
||||
// Next advances to the next header field. It returns true on success, or
|
||||
// false if there is no next field.
|
||||
Next() (more bool)
|
||||
// Key returns the key of the current field.
|
||||
Key() string
|
||||
// Value returns the value of the current field.
|
||||
Value() string
|
||||
// Raw returns the raw current header field. See Header.Raw.
|
||||
Raw() ([]byte, error)
|
||||
// Del deletes the current field.
|
||||
Del()
|
||||
// Len returns the amount of header fields in the subset of header iterated
|
||||
// by this HeaderFields instance.
|
||||
//
|
||||
// For Fields(), it will return the amount of fields in the whole header section.
|
||||
// For FieldsByKey(), it will return the amount of fields with certain key.
|
||||
Len() int
|
||||
}
|
||||
|
||||
type headerFields struct {
|
||||
h *Header
|
||||
cur int
|
||||
}
|
||||
|
||||
func (fs *headerFields) Next() bool {
|
||||
fs.cur++
|
||||
return fs.cur < len(fs.h.l)
|
||||
}
|
||||
|
||||
func (fs *headerFields) index() int {
|
||||
if fs.cur < 0 {
|
||||
panic("message: HeaderFields method called before Next")
|
||||
}
|
||||
if fs.cur >= len(fs.h.l) {
|
||||
panic("message: HeaderFields method called after Next returned false")
|
||||
}
|
||||
return len(fs.h.l) - fs.cur - 1
|
||||
}
|
||||
|
||||
func (fs *headerFields) field() *headerField {
|
||||
return fs.h.l[fs.index()]
|
||||
}
|
||||
|
||||
func (fs *headerFields) Key() string {
|
||||
return fs.field().k
|
||||
}
|
||||
|
||||
func (fs *headerFields) Value() string {
|
||||
return fs.field().v
|
||||
}
|
||||
|
||||
func (fs *headerFields) Raw() ([]byte, error) {
|
||||
return fs.field().raw()
|
||||
}
|
||||
|
||||
func (fs *headerFields) Del() {
|
||||
f := fs.field()
|
||||
|
||||
ok := false
|
||||
for i, ff := range fs.h.m[f.k] {
|
||||
if ff == f {
|
||||
ok = true
|
||||
fs.h.m[f.k] = append(fs.h.m[f.k][:i], fs.h.m[f.k][i+1:]...)
|
||||
if len(fs.h.m[f.k]) == 0 {
|
||||
delete(fs.h.m, f.k)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
panic("message: field not found in Header.m")
|
||||
}
|
||||
|
||||
fs.h.l = append(fs.h.l[:fs.index()], fs.h.l[fs.index()+1:]...)
|
||||
fs.cur--
|
||||
}
|
||||
|
||||
func (fs *headerFields) Len() int {
|
||||
return len(fs.h.l)
|
||||
}
|
||||
|
||||
// Fields iterates over all the header fields.
|
||||
//
|
||||
// The header may not be mutated while iterating, except using HeaderFields.Del.
|
||||
func (h *Header) Fields() HeaderFields {
|
||||
return &headerFields{h, -1}
|
||||
}
|
||||
|
||||
type headerFieldsByKey struct {
|
||||
h *Header
|
||||
k string
|
||||
cur int
|
||||
}
|
||||
|
||||
func (fs *headerFieldsByKey) Next() bool {
|
||||
fs.cur++
|
||||
return fs.cur < len(fs.h.m[fs.k])
|
||||
}
|
||||
|
||||
func (fs *headerFieldsByKey) index() int {
|
||||
if fs.cur < 0 {
|
||||
panic("message: headerfields.key or value called before next")
|
||||
}
|
||||
if fs.cur >= len(fs.h.m[fs.k]) {
|
||||
panic("message: headerfields.key or value called after next returned false")
|
||||
}
|
||||
return len(fs.h.m[fs.k]) - fs.cur - 1
|
||||
}
|
||||
|
||||
func (fs *headerFieldsByKey) field() *headerField {
|
||||
return fs.h.m[fs.k][fs.index()]
|
||||
}
|
||||
|
||||
func (fs *headerFieldsByKey) Key() string {
|
||||
return fs.field().k
|
||||
}
|
||||
|
||||
func (fs *headerFieldsByKey) Value() string {
|
||||
return fs.field().v
|
||||
}
|
||||
|
||||
func (fs *headerFieldsByKey) Raw() ([]byte, error) {
|
||||
return fs.field().raw()
|
||||
}
|
||||
|
||||
func (fs *headerFieldsByKey) Del() {
|
||||
f := fs.field()
|
||||
|
||||
ok := false
|
||||
for i := range fs.h.l {
|
||||
if f == fs.h.l[i] {
|
||||
ok = true
|
||||
fs.h.l = append(fs.h.l[:i], fs.h.l[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
panic("message: field not found in Header.l")
|
||||
}
|
||||
|
||||
fs.h.m[fs.k] = append(fs.h.m[fs.k][:fs.index()], fs.h.m[fs.k][fs.index()+1:]...)
|
||||
if len(fs.h.m[fs.k]) == 0 {
|
||||
delete(fs.h.m, fs.k)
|
||||
}
|
||||
fs.cur--
|
||||
}
|
||||
|
||||
func (fs *headerFieldsByKey) Len() int {
|
||||
return len(fs.h.m[fs.k])
|
||||
}
|
||||
|
||||
// FieldsByKey iterates over all fields having the specified key.
|
||||
//
|
||||
// The header may not be mutated while iterating, except using HeaderFields.Del.
|
||||
func (h *Header) FieldsByKey(k string) HeaderFields {
|
||||
return &headerFieldsByKey{h, textproto.CanonicalMIMEHeaderKey(k), -1}
|
||||
}
|
||||
|
||||
func readLineSlice(r *bufio.Reader, line []byte) ([]byte, error) {
|
||||
for {
|
||||
l, more, err := r.ReadLine()
|
||||
line = append(line, l...)
|
||||
if err != nil {
|
||||
return line, err
|
||||
}
|
||||
|
||||
if !more {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return line, nil
|
||||
}
|
||||
|
||||
func isSpace(c byte) bool {
|
||||
return c == ' ' || c == '\t'
|
||||
}
|
||||
|
||||
func validHeaderKeyByte(b byte) bool {
|
||||
c := int(b)
|
||||
return c >= 33 && c <= 126 && c != ':'
|
||||
}
|
||||
|
||||
// trim returns s with leading and trailing spaces and tabs removed.
|
||||
// It does not assume Unicode or UTF-8.
|
||||
func trim(s []byte) []byte {
|
||||
i := 0
|
||||
for i < len(s) && isSpace(s[i]) {
|
||||
i++
|
||||
}
|
||||
n := len(s)
|
||||
for n > i && isSpace(s[n-1]) {
|
||||
n--
|
||||
}
|
||||
return s[i:n]
|
||||
}
|
||||
|
||||
func hasContinuationLine(r *bufio.Reader) bool {
|
||||
c, err := r.ReadByte()
|
||||
if err != nil {
|
||||
return false // bufio will keep err until next read.
|
||||
}
|
||||
r.UnreadByte()
|
||||
return isSpace(c)
|
||||
}
|
||||
|
||||
func readContinuedLineSlice(r *bufio.Reader) ([]byte, error) {
|
||||
// Read the first line. We preallocate slice that it enough
|
||||
// for most fields.
|
||||
line, err := readLineSlice(r, make([]byte, 0, 256))
|
||||
if err == io.EOF && len(line) == 0 {
|
||||
// Header without a body
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(line) == 0 { // blank line - no continuation
|
||||
return line, nil
|
||||
}
|
||||
|
||||
line = append(line, '\r', '\n')
|
||||
|
||||
// Read continuation lines.
|
||||
for hasContinuationLine(r) {
|
||||
line, err = readLineSlice(r, line)
|
||||
if err != nil {
|
||||
break // bufio will keep err until next read.
|
||||
}
|
||||
|
||||
line = append(line, '\r', '\n')
|
||||
}
|
||||
|
||||
return line, nil
|
||||
}
|
||||
|
||||
func writeContinued(b *strings.Builder, l []byte) {
|
||||
// Strip trailing \r, if any
|
||||
if len(l) > 0 && l[len(l)-1] == '\r' {
|
||||
l = l[:len(l)-1]
|
||||
}
|
||||
l = trim(l)
|
||||
if len(l) == 0 {
|
||||
return
|
||||
}
|
||||
if b.Len() > 0 {
|
||||
b.WriteByte(' ')
|
||||
}
|
||||
b.Write(l)
|
||||
}
|
||||
|
||||
// Strip newlines and spaces around newlines.
|
||||
func trimAroundNewlines(v []byte) string {
|
||||
var b strings.Builder
|
||||
b.Grow(len(v))
|
||||
for {
|
||||
i := bytes.IndexByte(v, '\n')
|
||||
if i < 0 {
|
||||
writeContinued(&b, v)
|
||||
break
|
||||
}
|
||||
writeContinued(&b, v[:i])
|
||||
v = v[i+1:]
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// ReadHeader reads a MIME header from r. The header is a sequence of possibly
|
||||
// continued "Key: Value" lines ending in a blank line.
|
||||
//
|
||||
// To avoid denial of service attacks, the provided bufio.Reader should be
|
||||
// reading from an io.LimitedReader or a similar Reader to bound the size of
|
||||
// headers.
|
||||
func ReadHeader(r *bufio.Reader) (Header, error) {
|
||||
fs := make([]*headerField, 0, 32)
|
||||
|
||||
// The first line cannot start with a leading space.
|
||||
if buf, err := r.Peek(1); err == nil && isSpace(buf[0]) {
|
||||
line, err := readLineSlice(r, nil)
|
||||
if err != nil {
|
||||
return newHeader(fs), err
|
||||
}
|
||||
|
||||
return newHeader(fs), fmt.Errorf("message: malformed MIME header initial line: %v", string(line))
|
||||
}
|
||||
|
||||
for {
|
||||
kv, err := readContinuedLineSlice(r)
|
||||
if len(kv) == 0 {
|
||||
return newHeader(fs), err
|
||||
}
|
||||
|
||||
// Key ends at first colon; should not have trailing spaces but they
|
||||
// appear in the wild, violating specs, so we remove them if present.
|
||||
i := bytes.IndexByte(kv, ':')
|
||||
if i < 0 {
|
||||
return newHeader(fs), fmt.Errorf("message: malformed MIME header line: %v", string(kv))
|
||||
}
|
||||
|
||||
keyBytes := trim(kv[:i])
|
||||
|
||||
// Verify that there are no invalid characters in the header key.
|
||||
// See RFC 5322 Section 2.2
|
||||
for _, c := range keyBytes {
|
||||
if !validHeaderKeyByte(c) {
|
||||
return newHeader(fs), fmt.Errorf("message: malformed MIME header key: %v", string(keyBytes))
|
||||
}
|
||||
}
|
||||
|
||||
key := textproto.CanonicalMIMEHeaderKey(string(keyBytes))
|
||||
|
||||
// As per RFC 7230 field-name is a token, tokens consist of one or more
|
||||
// chars. We could return a an error here, but better to be liberal in
|
||||
// what we accept, so if we get an empty key, skip it.
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
i++ // skip colon
|
||||
v := kv[i:]
|
||||
|
||||
value := trimAroundNewlines(v)
|
||||
fs = append(fs, newHeaderField(key, value, kv))
|
||||
|
||||
if err != nil {
|
||||
return newHeader(fs), err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func foldLine(v string, maxlen int) (line, next string, ok bool) {
|
||||
ok = true
|
||||
|
||||
// We'll need to fold before maxlen
|
||||
foldBefore := maxlen + 1
|
||||
foldAt := len(v)
|
||||
|
||||
var folding string
|
||||
if foldBefore > len(v) {
|
||||
// We reached the end of the string
|
||||
if v[len(v)-1] != '\n' {
|
||||
// If there isn't already a trailing CRLF, insert one
|
||||
folding = "\r\n"
|
||||
}
|
||||
} else {
|
||||
// Find the closest whitespace before maxlen
|
||||
foldAt = strings.LastIndexAny(v[:foldBefore], " \t\n")
|
||||
|
||||
if foldAt == 0 {
|
||||
// The whitespace we found was the previous folding WSP
|
||||
foldAt = foldBefore - 1
|
||||
} else if foldAt < 0 {
|
||||
// We didn't find any whitespace, we have to insert one
|
||||
foldAt = foldBefore - 2
|
||||
}
|
||||
|
||||
switch v[foldAt] {
|
||||
case ' ', '\t':
|
||||
if v[foldAt-1] != '\n' {
|
||||
folding = "\r\n" // The next char will be a WSP, don't need to insert one
|
||||
}
|
||||
case '\n':
|
||||
folding = "" // There is already a CRLF, nothing to do
|
||||
default:
|
||||
// Another char, we need to insert CRLF + WSP. This will insert an
|
||||
// extra space in the string, so this should be avoided if
|
||||
// possible.
|
||||
folding = "\r\n "
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
|
||||
return v[:foldAt] + folding, v[foldAt:], ok
|
||||
}
|
||||
|
||||
const (
|
||||
preferredHeaderLen = 76
|
||||
maxHeaderLen = 998
|
||||
)
|
||||
|
||||
// formatHeaderField formats a header field, ensuring each line is no longer
|
||||
// than 76 characters. It tries to fold lines at whitespace characters if
|
||||
// possible. If the header contains a word longer than this limit, it will be
|
||||
// split.
|
||||
func formatHeaderField(k, v string) string {
|
||||
s := k + ": "
|
||||
|
||||
if v == "" {
|
||||
return s + "\r\n"
|
||||
}
|
||||
|
||||
first := true
|
||||
for len(v) > 0 {
|
||||
// If this is the first line, substract the length of the key
|
||||
keylen := 0
|
||||
if first {
|
||||
keylen = len(s)
|
||||
}
|
||||
|
||||
// First try with a soft limit
|
||||
l, next, ok := foldLine(v, preferredHeaderLen-keylen)
|
||||
if !ok {
|
||||
// Folding failed to preserve the original header field value. Try
|
||||
// with a larger, hard limit.
|
||||
l, next, _ = foldLine(v, maxHeaderLen-keylen)
|
||||
}
|
||||
v = next
|
||||
s += l
|
||||
first = false
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// WriteHeader writes a MIME header to w.
|
||||
func WriteHeader(w io.Writer, h Header) error {
|
||||
for i := len(h.l) - 1; i >= 0; i-- {
|
||||
f := h.l[i]
|
||||
if rawField, err := f.raw(); err == nil {
|
||||
if _, err := w.Write(rawField); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("failed to write header field #%v (%q): %w", len(h.l)-i, f.k, err)
|
||||
}
|
||||
}
|
||||
|
||||
_, err := w.Write([]byte{'\r', '\n'})
|
||||
return err
|
||||
}
|
||||
474
vendor/github.com/emersion/go-message/textproto/multipart.go
generated
vendored
Normal file
474
vendor/github.com/emersion/go-message/textproto/multipart.go
generated
vendored
Normal file
@@ -0,0 +1,474 @@
|
||||
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
//
|
||||
|
||||
package textproto
|
||||
|
||||
// Multipart is defined in RFC 2046.
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
var emptyParams = make(map[string]string)
|
||||
|
||||
// This constant needs to be at least 76 for this package to work correctly.
|
||||
// This is because \r\n--separator_of_len_70- would fill the buffer and it
|
||||
// wouldn't be safe to consume a single byte from it.
|
||||
const peekBufferSize = 4096
|
||||
|
||||
// A Part represents a single part in a multipart body.
|
||||
type Part struct {
|
||||
Header Header
|
||||
|
||||
mr *MultipartReader
|
||||
|
||||
// r is either a reader directly reading from mr
|
||||
r io.Reader
|
||||
|
||||
n int // known data bytes waiting in mr.bufReader
|
||||
total int64 // total data bytes read already
|
||||
err error // error to return when n == 0
|
||||
readErr error // read error observed from mr.bufReader
|
||||
}
|
||||
|
||||
// NewMultipartReader creates a new multipart reader reading from r using the
|
||||
// given MIME boundary.
|
||||
//
|
||||
// The boundary is usually obtained from the "boundary" parameter of
|
||||
// the message's "Content-Type" header. Use mime.ParseMediaType to
|
||||
// parse such headers.
|
||||
func NewMultipartReader(r io.Reader, boundary string) *MultipartReader {
|
||||
b := []byte("\r\n--" + boundary + "--")
|
||||
return &MultipartReader{
|
||||
bufReader: bufio.NewReaderSize(&stickyErrorReader{r: r}, peekBufferSize),
|
||||
nl: b[:2],
|
||||
nlDashBoundary: b[:len(b)-2],
|
||||
dashBoundaryDash: b[2:],
|
||||
dashBoundary: b[2 : len(b)-2],
|
||||
}
|
||||
}
|
||||
|
||||
// stickyErrorReader is an io.Reader which never calls Read on its
|
||||
// underlying Reader once an error has been seen. (the io.Reader
|
||||
// interface's contract promises nothing about the return values of
|
||||
// Read calls after an error, yet this package does do multiple Reads
|
||||
// after error)
|
||||
type stickyErrorReader struct {
|
||||
r io.Reader
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *stickyErrorReader) Read(p []byte) (n int, _ error) {
|
||||
if r.err != nil {
|
||||
return 0, r.err
|
||||
}
|
||||
n, r.err = r.r.Read(p)
|
||||
return n, r.err
|
||||
}
|
||||
|
||||
func newPart(mr *MultipartReader) (*Part, error) {
|
||||
bp := &Part{mr: mr}
|
||||
if err := bp.populateHeaders(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bp.r = partReader{bp}
|
||||
return bp, nil
|
||||
}
|
||||
|
||||
func (bp *Part) populateHeaders() error {
|
||||
header, err := ReadHeader(bp.mr.bufReader)
|
||||
if err == nil {
|
||||
bp.Header = header
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Read reads the body of a part, after its headers and before the
|
||||
// next part (if any) begins.
|
||||
func (p *Part) Read(d []byte) (n int, err error) {
|
||||
return p.r.Read(d)
|
||||
}
|
||||
|
||||
// partReader implements io.Reader by reading raw bytes directly from the
|
||||
// wrapped *Part, without doing any Transfer-Encoding decoding.
|
||||
type partReader struct {
|
||||
p *Part
|
||||
}
|
||||
|
||||
func (pr partReader) Read(d []byte) (int, error) {
|
||||
p := pr.p
|
||||
br := p.mr.bufReader
|
||||
|
||||
// Read into buffer until we identify some data to return,
|
||||
// or we find a reason to stop (boundary or read error).
|
||||
for p.n == 0 && p.err == nil {
|
||||
peek, _ := br.Peek(br.Buffered())
|
||||
p.n, p.err = scanUntilBoundary(peek, p.mr.dashBoundary, p.mr.nlDashBoundary, p.total, p.readErr)
|
||||
if p.n == 0 && p.err == nil {
|
||||
// Force buffered I/O to read more into buffer.
|
||||
_, p.readErr = br.Peek(len(peek) + 1)
|
||||
if p.readErr == io.EOF {
|
||||
p.readErr = io.ErrUnexpectedEOF
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read out from "data to return" part of buffer.
|
||||
if p.n == 0 {
|
||||
return 0, p.err
|
||||
}
|
||||
n := len(d)
|
||||
if n > p.n {
|
||||
n = p.n
|
||||
}
|
||||
n, _ = br.Read(d[:n])
|
||||
p.total += int64(n)
|
||||
p.n -= n
|
||||
if p.n == 0 {
|
||||
return n, p.err
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// scanUntilBoundary scans buf to identify how much of it can be safely
|
||||
// returned as part of the Part body.
|
||||
// dashBoundary is "--boundary".
|
||||
// nlDashBoundary is "\r\n--boundary" or "\n--boundary", depending on what mode we are in.
|
||||
// The comments below (and the name) assume "\n--boundary", but either is accepted.
|
||||
// total is the number of bytes read out so far. If total == 0, then a leading "--boundary" is recognized.
|
||||
// readErr is the read error, if any, that followed reading the bytes in buf.
|
||||
// scanUntilBoundary returns the number of data bytes from buf that can be
|
||||
// returned as part of the Part body and also the error to return (if any)
|
||||
// once those data bytes are done.
|
||||
func scanUntilBoundary(buf, dashBoundary, nlDashBoundary []byte, total int64, readErr error) (int, error) {
|
||||
if total == 0 {
|
||||
// At beginning of body, allow dashBoundary.
|
||||
if bytes.HasPrefix(buf, dashBoundary) {
|
||||
switch matchAfterPrefix(buf, dashBoundary, readErr) {
|
||||
case -1:
|
||||
return len(dashBoundary), nil
|
||||
case 0:
|
||||
return 0, nil
|
||||
case +1:
|
||||
return 0, io.EOF
|
||||
}
|
||||
}
|
||||
if bytes.HasPrefix(dashBoundary, buf) {
|
||||
return 0, readErr
|
||||
}
|
||||
}
|
||||
|
||||
// Search for "\n--boundary".
|
||||
if i := bytes.Index(buf, nlDashBoundary); i >= 0 {
|
||||
switch matchAfterPrefix(buf[i:], nlDashBoundary, readErr) {
|
||||
case -1:
|
||||
return i + len(nlDashBoundary), nil
|
||||
case 0:
|
||||
return i, nil
|
||||
case +1:
|
||||
return i, io.EOF
|
||||
}
|
||||
}
|
||||
if bytes.HasPrefix(nlDashBoundary, buf) {
|
||||
return 0, readErr
|
||||
}
|
||||
|
||||
// Otherwise, anything up to the final \n is not part of the boundary
|
||||
// and so must be part of the body.
|
||||
// Also if the section from the final \n onward is not a prefix of the boundary,
|
||||
// it too must be part of the body.
|
||||
i := bytes.LastIndexByte(buf, nlDashBoundary[0])
|
||||
if i >= 0 && bytes.HasPrefix(nlDashBoundary, buf[i:]) {
|
||||
return i, nil
|
||||
}
|
||||
return len(buf), readErr
|
||||
}
|
||||
|
||||
// matchAfterPrefix checks whether buf should be considered to match the boundary.
|
||||
// The prefix is "--boundary" or "\r\n--boundary" or "\n--boundary",
|
||||
// and the caller has verified already that bytes.HasPrefix(buf, prefix) is true.
|
||||
//
|
||||
// matchAfterPrefix returns +1 if the buffer does match the boundary,
|
||||
// meaning the prefix is followed by a dash, space, tab, cr, nl, or end of input.
|
||||
// It returns -1 if the buffer definitely does NOT match the boundary,
|
||||
// meaning the prefix is followed by some other character.
|
||||
// For example, "--foobar" does not match "--foo".
|
||||
// It returns 0 more input needs to be read to make the decision,
|
||||
// meaning that len(buf) == len(prefix) and readErr == nil.
|
||||
func matchAfterPrefix(buf, prefix []byte, readErr error) int {
|
||||
if len(buf) == len(prefix) {
|
||||
if readErr != nil {
|
||||
return +1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
c := buf[len(prefix)]
|
||||
if c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '-' {
|
||||
return +1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (p *Part) Close() error {
|
||||
io.Copy(ioutil.Discard, p)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MultipartReader is an iterator over parts in a MIME multipart body.
|
||||
// MultipartReader's underlying parser consumes its input as needed. Seeking
|
||||
// isn't supported.
|
||||
type MultipartReader struct {
|
||||
bufReader *bufio.Reader
|
||||
|
||||
currentPart *Part
|
||||
partsRead int
|
||||
|
||||
nl []byte // "\r\n" or "\n" (set after seeing first boundary line)
|
||||
nlDashBoundary []byte // nl + "--boundary"
|
||||
dashBoundaryDash []byte // "--boundary--"
|
||||
dashBoundary []byte // "--boundary"
|
||||
}
|
||||
|
||||
// NextPart returns the next part in the multipart or an error.
|
||||
// When there are no more parts, the error io.EOF is returned.
|
||||
func (r *MultipartReader) NextPart() (*Part, error) {
|
||||
if r.currentPart != nil {
|
||||
r.currentPart.Close()
|
||||
}
|
||||
if string(r.dashBoundary) == "--" {
|
||||
return nil, fmt.Errorf("multipart: boundary is empty")
|
||||
}
|
||||
expectNewPart := false
|
||||
for {
|
||||
line, err := r.bufReader.ReadSlice('\n')
|
||||
|
||||
if err == io.EOF && r.isFinalBoundary(line) {
|
||||
// If the buffer ends in "--boundary--" without the
|
||||
// trailing "\r\n", ReadSlice will return an error
|
||||
// (since it's missing the '\n'), but this is a valid
|
||||
// multipart EOF so we need to return io.EOF instead of
|
||||
// a fmt-wrapped one.
|
||||
return nil, io.EOF
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("multipart: NextPart: %v", err)
|
||||
}
|
||||
|
||||
if r.isBoundaryDelimiterLine(line) {
|
||||
r.partsRead++
|
||||
bp, err := newPart(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.currentPart = bp
|
||||
return bp, nil
|
||||
}
|
||||
|
||||
if r.isFinalBoundary(line) {
|
||||
// Expected EOF
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
if expectNewPart {
|
||||
return nil, fmt.Errorf("multipart: expecting a new Part; got line %q", string(line))
|
||||
}
|
||||
|
||||
if r.partsRead == 0 {
|
||||
// skip line
|
||||
continue
|
||||
}
|
||||
|
||||
// Consume the "\n" or "\r\n" separator between the
|
||||
// body of the previous part and the boundary line we
|
||||
// now expect will follow. (either a new part or the
|
||||
// end boundary)
|
||||
if bytes.Equal(line, r.nl) {
|
||||
expectNewPart = true
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("multipart: unexpected line in Next(): %q", line)
|
||||
}
|
||||
}
|
||||
|
||||
// isFinalBoundary reports whether line is the final boundary line
|
||||
// indicating that all parts are over.
|
||||
// It matches `^--boundary--[ \t]*(\r\n)?$`
|
||||
func (mr *MultipartReader) isFinalBoundary(line []byte) bool {
|
||||
if !bytes.HasPrefix(line, mr.dashBoundaryDash) {
|
||||
return false
|
||||
}
|
||||
rest := line[len(mr.dashBoundaryDash):]
|
||||
rest = skipLWSPChar(rest)
|
||||
return len(rest) == 0 || bytes.Equal(rest, mr.nl)
|
||||
}
|
||||
|
||||
func (mr *MultipartReader) isBoundaryDelimiterLine(line []byte) (ret bool) {
|
||||
// https://tools.ietf.org/html/rfc2046#section-5.1
|
||||
// The boundary delimiter line is then defined as a line
|
||||
// consisting entirely of two hyphen characters ("-",
|
||||
// decimal value 45) followed by the boundary parameter
|
||||
// value from the Content-Type header field, optional linear
|
||||
// whitespace, and a terminating CRLF.
|
||||
if !bytes.HasPrefix(line, mr.dashBoundary) {
|
||||
return false
|
||||
}
|
||||
rest := line[len(mr.dashBoundary):]
|
||||
rest = skipLWSPChar(rest)
|
||||
|
||||
// On the first part, see our lines are ending in \n instead of \r\n
|
||||
// and switch into that mode if so. This is a violation of the spec,
|
||||
// but occurs in practice.
|
||||
if mr.partsRead == 0 && len(rest) == 1 && rest[0] == '\n' {
|
||||
mr.nl = mr.nl[1:]
|
||||
mr.nlDashBoundary = mr.nlDashBoundary[1:]
|
||||
}
|
||||
return bytes.Equal(rest, mr.nl)
|
||||
}
|
||||
|
||||
// skipLWSPChar returns b with leading spaces and tabs removed.
|
||||
// RFC 822 defines:
|
||||
//
|
||||
// LWSP-char = SPACE / HTAB
|
||||
func skipLWSPChar(b []byte) []byte {
|
||||
for len(b) > 0 && (b[0] == ' ' || b[0] == '\t') {
|
||||
b = b[1:]
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// A MultipartWriter generates multipart messages.
|
||||
type MultipartWriter struct {
|
||||
w io.Writer
|
||||
boundary string
|
||||
lastpart *part
|
||||
}
|
||||
|
||||
// NewMultipartWriter returns a new multipart Writer with a random boundary,
|
||||
// writing to w.
|
||||
func NewMultipartWriter(w io.Writer) *MultipartWriter {
|
||||
return &MultipartWriter{
|
||||
w: w,
|
||||
boundary: randomBoundary(),
|
||||
}
|
||||
}
|
||||
|
||||
// Boundary returns the Writer's boundary.
|
||||
func (w *MultipartWriter) Boundary() string {
|
||||
return w.boundary
|
||||
}
|
||||
|
||||
// SetBoundary overrides the Writer's default randomly-generated
|
||||
// boundary separator with an explicit value.
|
||||
//
|
||||
// SetBoundary must be called before any parts are created, may only
|
||||
// contain certain ASCII characters, and must be non-empty and
|
||||
// at most 70 bytes long.
|
||||
func (w *MultipartWriter) SetBoundary(boundary string) error {
|
||||
if w.lastpart != nil {
|
||||
return errors.New("mime: SetBoundary called after write")
|
||||
}
|
||||
// rfc2046#section-5.1.1
|
||||
if len(boundary) < 1 || len(boundary) > 70 {
|
||||
return errors.New("mime: invalid boundary length")
|
||||
}
|
||||
end := len(boundary) - 1
|
||||
for i, b := range boundary {
|
||||
if 'A' <= b && b <= 'Z' || 'a' <= b && b <= 'z' || '0' <= b && b <= '9' {
|
||||
continue
|
||||
}
|
||||
switch b {
|
||||
case '\'', '(', ')', '+', '_', ',', '-', '.', '/', ':', '=', '?':
|
||||
continue
|
||||
case ' ':
|
||||
if i != end {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return errors.New("mime: invalid boundary character")
|
||||
}
|
||||
w.boundary = boundary
|
||||
return nil
|
||||
}
|
||||
|
||||
func randomBoundary() string {
|
||||
var buf [30]byte
|
||||
_, err := io.ReadFull(rand.Reader, buf[:])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return fmt.Sprintf("%x", buf[:])
|
||||
}
|
||||
|
||||
// CreatePart creates a new multipart section with the provided
|
||||
// header. The body of the part should be written to the returned
|
||||
// Writer. After calling CreatePart, any previous part may no longer
|
||||
// be written to.
|
||||
func (w *MultipartWriter) CreatePart(header Header) (io.Writer, error) {
|
||||
if w.lastpart != nil {
|
||||
if err := w.lastpart.close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var b bytes.Buffer
|
||||
if w.lastpart != nil {
|
||||
fmt.Fprintf(&b, "\r\n--%s\r\n", w.boundary)
|
||||
} else {
|
||||
fmt.Fprintf(&b, "--%s\r\n", w.boundary)
|
||||
}
|
||||
|
||||
WriteHeader(&b, header)
|
||||
|
||||
_, err := io.Copy(w.w, &b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p := &part{
|
||||
mw: w,
|
||||
}
|
||||
w.lastpart = p
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Close finishes the multipart message and writes the trailing
|
||||
// boundary end line to the output.
|
||||
func (w *MultipartWriter) Close() error {
|
||||
if w.lastpart != nil {
|
||||
if err := w.lastpart.close(); err != nil {
|
||||
return err
|
||||
}
|
||||
w.lastpart = nil
|
||||
}
|
||||
_, err := fmt.Fprintf(w.w, "\r\n--%s--\r\n", w.boundary)
|
||||
return err
|
||||
}
|
||||
|
||||
type part struct {
|
||||
mw *MultipartWriter
|
||||
closed bool
|
||||
we error // last error that occurred writing
|
||||
}
|
||||
|
||||
func (p *part) close() error {
|
||||
p.closed = true
|
||||
return p.we
|
||||
}
|
||||
|
||||
func (p *part) Write(d []byte) (n int, err error) {
|
||||
if p.closed {
|
||||
return 0, errors.New("multipart: can't write to finished part")
|
||||
}
|
||||
n, err = p.mw.w.Write(d)
|
||||
if err != nil {
|
||||
p.we = err
|
||||
}
|
||||
return
|
||||
}
|
||||
2
vendor/github.com/emersion/go-message/textproto/textproto.go
generated
vendored
Normal file
2
vendor/github.com/emersion/go-message/textproto/textproto.go
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package textproto implements low-level manipulation of MIME messages.
|
||||
package textproto
|
||||
134
vendor/github.com/emersion/go-message/writer.go
generated
vendored
Normal file
134
vendor/github.com/emersion/go-message/writer.go
generated
vendored
Normal file
@@ -0,0 +1,134 @@
|
||||
package message
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-message/textproto"
|
||||
)
|
||||
|
||||
// Writer writes message entities.
|
||||
//
|
||||
// If the message is not multipart, it should be used as a WriteCloser. Don't
|
||||
// forget to call Close.
|
||||
//
|
||||
// If the message is multipart, users can either use CreatePart to write child
|
||||
// parts or Write to directly pipe a multipart message. In any case, Close must
|
||||
// be called at the end.
|
||||
type Writer struct {
|
||||
w io.Writer
|
||||
c io.Closer
|
||||
mw *textproto.MultipartWriter
|
||||
}
|
||||
|
||||
// createWriter creates a new Writer writing to w with the provided header.
|
||||
// Nothing is written to w when it is called. header is modified in-place.
|
||||
func createWriter(w io.Writer, header *Header) (*Writer, error) {
|
||||
ww := &Writer{w: w}
|
||||
|
||||
mediaType, mediaParams, _ := header.ContentType()
|
||||
if strings.HasPrefix(mediaType, "multipart/") {
|
||||
ww.mw = textproto.NewMultipartWriter(ww.w)
|
||||
|
||||
// Do not set ww's io.Closer for now: if this is a multipart entity but
|
||||
// CreatePart is not used (only Write is used), then the final boundary
|
||||
// is expected to be written by the user too. In this case, ww.Close
|
||||
// shouldn't write the final boundary.
|
||||
|
||||
if mediaParams["boundary"] != "" {
|
||||
ww.mw.SetBoundary(mediaParams["boundary"])
|
||||
} else {
|
||||
mediaParams["boundary"] = ww.mw.Boundary()
|
||||
header.SetContentType(mediaType, mediaParams)
|
||||
}
|
||||
|
||||
header.Del("Content-Transfer-Encoding")
|
||||
} else {
|
||||
wc, err := encodingWriter(header.Get("Content-Transfer-Encoding"), ww.w)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ww.w = wc
|
||||
ww.c = wc
|
||||
}
|
||||
|
||||
switch strings.ToLower(mediaParams["charset"]) {
|
||||
case "", "us-ascii", "utf-8":
|
||||
// This is OK
|
||||
default:
|
||||
// Anything else is invalid
|
||||
return nil, fmt.Errorf("unhandled charset %q", mediaParams["charset"])
|
||||
}
|
||||
|
||||
return ww, nil
|
||||
}
|
||||
|
||||
// CreateWriter creates a new message writer to w. If header contains an
|
||||
// encoding, data written to the Writer will automatically be encoded with it.
|
||||
// The charset needs to be utf-8 or us-ascii.
|
||||
func CreateWriter(w io.Writer, header Header) (*Writer, error) {
|
||||
// Ensure that modifications are invisible to the caller
|
||||
header = header.Copy()
|
||||
|
||||
// If the message uses MIME, it has to include MIME-Version
|
||||
if !header.Has("Mime-Version") {
|
||||
header.Set("MIME-Version", "1.0")
|
||||
}
|
||||
|
||||
ww, err := createWriter(w, &header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := textproto.WriteHeader(w, header.Header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ww, nil
|
||||
}
|
||||
|
||||
// Write implements io.Writer.
|
||||
func (w *Writer) Write(b []byte) (int, error) {
|
||||
return w.w.Write(b)
|
||||
}
|
||||
|
||||
// Close implements io.Closer.
|
||||
func (w *Writer) Close() error {
|
||||
if w.c != nil {
|
||||
return w.c.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreatePart returns a Writer to a new part in this multipart entity. If this
|
||||
// entity is not multipart, it fails. The body of the part should be written to
|
||||
// the returned io.WriteCloser.
|
||||
func (w *Writer) CreatePart(header Header) (*Writer, error) {
|
||||
if w.mw == nil {
|
||||
return nil, errors.New("cannot create a part in a non-multipart message")
|
||||
}
|
||||
|
||||
if w.c == nil {
|
||||
// We know that the user calls CreatePart so Close should write the final
|
||||
// boundary
|
||||
w.c = w.mw
|
||||
}
|
||||
|
||||
// cw -> ww -> pw -> w.mw -> w.w
|
||||
|
||||
ww := &struct{ io.Writer }{nil}
|
||||
|
||||
// ensure that modifications are invisible to the caller
|
||||
header = header.Copy()
|
||||
cw, err := createWriter(ww, &header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pw, err := w.mw.CreatePart(header.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ww.Writer = pw
|
||||
return cw, nil
|
||||
}
|
||||
Reference in New Issue
Block a user