pax_global_header 0000666 0000000 0000000 00000000064 14060654707 0014523 g ustar 00root root 0000000 0000000 52 comment=87d83aac2f695c202fbb6acc422e3f04b27a9f96
go-message-0.15.0/ 0000775 0000000 0000000 00000000000 14060654707 0013635 5 ustar 00root root 0000000 0000000 go-message-0.15.0/.build.yml 0000664 0000000 0000000 00000000564 14060654707 0015542 0 ustar 00root root 0000000 0000000 image: alpine/edge
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
go-message-0.15.0/.gitignore 0000664 0000000 0000000 00000000412 14060654707 0015622 0 ustar 00root root 0000000 0000000 # 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
go-message-0.15.0/LICENSE 0000664 0000000 0000000 00000002051 14060654707 0014640 0 ustar 00root root 0000000 0000000 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.
go-message-0.15.0/README.md 0000664 0000000 0000000 00000002246 14060654707 0015120 0 ustar 00root root 0000000 0000000 # go-message
[](https://godocs.io/github.com/emersion/go-message)
[](https://builds.sr.ht/~emersion/go-message/commits?)
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
go-message-0.15.0/charset.go 0000664 0000000 0000000 00000003667 14060654707 0015631 0 ustar 00root root 0000000 0000000 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)
}
go-message-0.15.0/charset/ 0000775 0000000 0000000 00000000000 14060654707 0015266 5 ustar 00root root 0000000 0000000 go-message-0.15.0/charset/charset.go 0000664 0000000 0000000 00000003441 14060654707 0017250 0 ustar 00root root 0000000 0000000 // 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"
)
// 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
}
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
}
go-message-0.15.0/charset/charset_test.go 0000664 0000000 0000000 00000003606 14060654707 0020312 0 ustar 00root root 0000000 0000000 package charset
import (
"bytes"
"io/ioutil"
"strings"
"testing"
)
var testCharsets = []struct {
charset string
encoded []byte
decoded string
}{
{
charset: "us-ascii",
encoded: []byte("yuudachi"),
decoded: "yuudachi",
},
{
charset: "utf-8",
encoded: []byte("café"),
decoded: "café",
},
{
charset: "utf8",
encoded: []byte("café"),
decoded: "café",
},
{
charset: "windows-1250",
encoded: []byte{0x8c, 0x8d, 0x8f, 0x9c, 0x9d, 0x9f, 0xbc, 0xbe},
decoded: "ŚŤŹśťźĽľ",
},
{
charset: "windows-1252",
encoded: []byte{0x63, 0x61, 0x66, 0xE9, 0x20, 0x80},
decoded: "café €",
},
{
charset: "iso-8859-1",
encoded: []byte{0x63, 0x61, 0x66, 0xE9},
decoded: "café",
},
{
charset: "idontexist",
encoded: []byte{42},
},
{
charset: "gb2312",
encoded: []byte{178, 226, 202, 212},
decoded: "测试",
},
{
charset: "iso8859-2",
encoded: []byte{0x63, 0x61, 0x66, 0xE9, 0x20, 0xfb},
decoded: "café ű",
},
}
func TestCharsetReader(t *testing.T) {
for _, test := range testCharsets {
r, err := Reader(test.charset, bytes.NewReader(test.encoded))
if test.decoded == "" {
if err == nil {
t.Errorf("Expected an error when creating reader for charset %q", test.charset)
}
}
if test.decoded != "" {
if err != nil {
t.Errorf("Expected no error when creating reader for charset %q, but got: %v", test.charset, err)
} else if b, err := ioutil.ReadAll(r); err != nil {
t.Errorf("Expected no error when reading charset %q, but got: %v", test.charset, err)
} else if s := string(b); s != test.decoded {
t.Errorf("Expected decoded text to be %q but got %q", test.decoded, s)
}
}
}
}
func TestDisabledCharsetReader(t *testing.T) {
charsets["DISABLED"] = nil
_, err := Reader("DISABLED", strings.NewReader("Some dummy text"))
if err == nil {
t.Errorf("Reader(): expected disabled charset to return an error")
}
}
go-message-0.15.0/encoding.go 0000664 0000000 0000000 00000002760 14060654707 0015757 0 ustar 00root root 0000000 0000000 package message
import (
"encoding/base64"
"errors"
"fmt"
"io"
"mime/quotedprintable"
"strings"
"github.com/emersion/go-textwrapper"
)
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":
dec = base64.NewDecoder(base64.StdEncoding, r)
case "7bit", "8bit", "binary", "":
dec = r
default:
return nil, fmt.Errorf("unhandled encoding %q", enc)
}
return dec, nil
}
type nopCloser struct {
io.Writer
}
func (nopCloser) Close() error {
return nil
}
func encodingWriter(enc string, w io.Writer) (io.WriteCloser, error) {
var wc io.WriteCloser
switch strings.ToLower(enc) {
case "quoted-printable":
wc = quotedprintable.NewWriter(w)
case "base64":
wc = base64.NewEncoder(base64.StdEncoding, textwrapper.NewRFC822(w))
case "7bit", "8bit":
wc = nopCloser{textwrapper.New(w, "\r\n", 1000)}
case "binary", "":
wc = nopCloser{w}
default:
return nil, fmt.Errorf("unhandled encoding %q", enc)
}
return wc, nil
}
go-message-0.15.0/encoding_test.go 0000664 0000000 0000000 00000003001 14060654707 0017003 0 ustar 00root root 0000000 0000000 package message
import (
"bytes"
"io"
"io/ioutil"
"strings"
"testing"
)
var testEncodings = []struct {
enc string
encoded string
decoded string
}{
{
enc: "binary",
encoded: "café",
decoded: "café",
},
{
enc: "8bit",
encoded: "café",
decoded: "café",
},
{
enc: "7bit",
encoded: "hi there",
decoded: "hi there",
},
{
enc: "quoted-printable",
encoded: "caf=C3=A9",
decoded: "café",
},
{
enc: "base64",
encoded: "Y2Fmw6k=",
decoded: "café",
},
}
func TestDecode(t *testing.T) {
for _, test := range testEncodings {
r, err := encodingReader(test.enc, strings.NewReader(test.encoded))
if err != nil {
t.Errorf("Expected no error when creating decoder for encoding %q, but got: %v", test.enc, err)
} else if b, err := ioutil.ReadAll(r); err != nil {
t.Errorf("Expected no error when reading encoding %q, but got: %v", test.enc, err)
} else if s := string(b); s != test.decoded {
t.Errorf("Expected decoded text to be %q but got %q", test.decoded, s)
}
}
}
func TestDecode_error(t *testing.T) {
_, err := encodingReader("idontexist", nil)
if err == nil {
t.Errorf("Expected an error when creating decoder for invalid encoding")
}
}
func TestEncode(t *testing.T) {
for _, test := range testEncodings {
var b bytes.Buffer
wc, _ := encodingWriter(test.enc, &b)
io.WriteString(wc, test.decoded)
wc.Close()
if s := b.String(); s != test.encoded {
t.Errorf("Expected encoded text to be %q but got %q", test.encoded, s)
}
}
}
go-message-0.15.0/entity.go 0000664 0000000 0000000 00000013651 14060654707 0015506 0 ustar 00root root 0000000 0000000 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 maxHeaderBytes = 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
}
// 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) {
lr := &limitedReader{R: r, N: 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)
}
// MultipartReader returns a MultipartReader that reads parts from this entity's
// body. If this entity is not multipart, it returns nil.
func (e *Entity) MultipartReader() MultipartReader {
if !strings.HasPrefix(e.mediaType, "multipart/") {
return nil
}
if mb, ok := e.Body.(*multipartBody); ok {
return mb
}
return &multipartReader{textproto.NewMultipartReader(e.Body, e.mediaParams["boundary"])}
}
// writeBodyTo writes this entity's body to w (without the header).
func (e *Entity) writeBodyTo(w *Writer) error {
var err error
if mb, ok := e.Body.(*multipartBody); ok {
err = mb.writeBodyTo(w)
} else {
_, err = io.Copy(w, e.Body)
}
return err
}
// WriteTo writes this entity's header and body to w.
func (e *Entity) WriteTo(w io.Writer) error {
ew, err := CreateWriter(w, e.Header)
if err != nil {
return err
}
defer ew.Close()
return e.writeBodyTo(ew)
}
// 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
}
go-message-0.15.0/entity_test.go 0000664 0000000 0000000 00000020730 14060654707 0016541 0 ustar 00root root 0000000 0000000 package message
import (
"bytes"
"errors"
"io"
"io/ioutil"
"reflect"
"strings"
"testing"
)
func testMakeEntity() *Entity {
var h Header
h.Set("Content-Type", "text/plain; charset=US-ASCII")
h.Set("Content-Transfer-Encoding", "base64")
r := strings.NewReader("Y2Mgc2F2YQ==")
e, _ := New(h, r)
return e
}
func TestNewEntity(t *testing.T) {
e := testMakeEntity()
expected := "cc sava"
if b, err := ioutil.ReadAll(e.Body); err != nil {
t.Error("Expected no error while reading entity body, got", err)
} else if s := string(b); s != expected {
t.Errorf("Expected %q as entity body but got %q", expected, s)
}
}
func testMakeMultipart() *Entity {
var h1 Header
h1.Set("Content-Type", "text/plain")
r1 := strings.NewReader("Text part")
e1, _ := New(h1, r1)
var h2 Header
h2.Set("Content-Type", "text/html")
r2 := strings.NewReader("
HTML part
")
e2, _ := New(h2, r2)
var h Header
h.Set("Content-Type", "multipart/alternative; boundary=IMTHEBOUNDARY")
e, _ := NewMultipart(h, []*Entity{e1, e2})
return e
}
const testMultipartHeader = "Mime-Version: 1.0\r\n" +
"Content-Type: multipart/alternative; boundary=IMTHEBOUNDARY\r\n\r\n"
const testMultipartBody = "--IMTHEBOUNDARY\r\n" +
"Content-Type: text/plain\r\n" +
"\r\n" +
"Text part\r\n" +
"--IMTHEBOUNDARY\r\n" +
"Content-Type: text/html\r\n" +
"\r\n" +
"HTML part
\r\n" +
"--IMTHEBOUNDARY--\r\n"
var testMultipartText = testMultipartHeader + testMultipartBody
const testSingleText = "Content-Type: text/plain\r\n" +
"\r\n" +
"Message body"
func testMultipart(t *testing.T, e *Entity) {
mr := e.MultipartReader()
if mr == nil {
t.Fatalf("Expected MultipartReader not to return nil")
}
defer mr.Close()
i := 0
for {
p, err := mr.NextPart()
if err == io.EOF {
break
} else if err != nil {
t.Fatal("Expected no error while reading multipart entity, got", err)
}
var expectedType string
var expectedBody string
switch i {
case 0:
expectedType = "text/plain"
expectedBody = "Text part"
case 1:
expectedType = "text/html"
expectedBody = "HTML part
"
}
if mediaType := p.Header.Get("Content-Type"); mediaType != expectedType {
t.Errorf("Expected part Content-Type to be %q, got %q", expectedType, mediaType)
}
if b, err := ioutil.ReadAll(p.Body); err != nil {
t.Error("Expected no error while reading part body, got", err)
} else if s := string(b); s != expectedBody {
t.Errorf("Expected %q as part body but got %q", expectedBody, s)
}
i++
}
if i != 2 {
t.Fatalf("Expected multipart entity to contain exactly 2 parts, got %v", i)
}
}
func TestNewMultipart(t *testing.T) {
testMultipart(t, testMakeMultipart())
}
func TestNewMultipart_read(t *testing.T) {
e := testMakeMultipart()
if b, err := ioutil.ReadAll(e.Body); err != nil {
t.Error("Expected no error while reading multipart body, got", err)
} else if s := string(b); s != testMultipartBody {
t.Errorf("Expected %q as multipart body but got %q", testMultipartBody, s)
}
}
func TestRead_multipart(t *testing.T) {
e, err := Read(strings.NewReader(testMultipartText))
if err != nil {
t.Fatal("Expected no error while reading multipart, got", err)
}
testMultipart(t, e)
}
func TestRead_single(t *testing.T) {
e, err := Read(strings.NewReader(testSingleText))
if err != nil {
t.Fatalf("Read() = %v", err)
}
b, err := ioutil.ReadAll(e.Body)
if err != nil {
t.Fatalf("ioutil.ReadAll() = %v", err)
}
expected := "Message body"
if string(b) != expected {
t.Fatalf("Expected body to be %q, got %q", expected, string(b))
}
}
func TestRead_tooBig(t *testing.T) {
raw := "Subject: " + strings.Repeat("A", 4096*1024) + "\r\n" +
"\r\n" +
"This header is too big.\r\n"
_, err := Read(strings.NewReader(raw))
if err != errHeaderTooBig {
t.Fatalf("Read() = %q, want %q", err, errHeaderTooBig)
}
}
func TestEntity_WriteTo_decode(t *testing.T) {
e := testMakeEntity()
e.Header.SetContentType("text/plain", map[string]string{"charset": "utf-8"})
e.Header.Del("Content-Transfer-Encoding")
var b bytes.Buffer
if err := e.WriteTo(&b); err != nil {
t.Fatal("Expected no error while writing entity, got", err)
}
expected := "Mime-Version: 1.0\r\n" +
"Content-Type: text/plain; charset=utf-8\r\n" +
"\r\n" +
"cc sava"
if s := b.String(); s != expected {
t.Errorf("Expected written entity to be:\n%s\nbut got:\n%s", expected, s)
}
}
func TestEntity_WriteTo_convert(t *testing.T) {
var h Header
h.Set("Content-Type", "text/plain; charset=utf-8")
h.Set("Content-Transfer-Encoding", "base64")
r := strings.NewReader("Qm9uam91ciDDoCB0b3Vz")
e, _ := New(h, r)
e.Header.Set("Content-Transfer-Encoding", "quoted-printable")
var b bytes.Buffer
if err := e.WriteTo(&b); err != nil {
t.Fatal("Expected no error while writing entity, got", err)
}
expected := "Mime-Version: 1.0\r\n" +
"Content-Transfer-Encoding: quoted-printable\r\n" +
"Content-Type: text/plain; charset=utf-8\r\n" +
"\r\n" +
"Bonjour =C3=A0 tous"
if s := b.String(); s != expected {
t.Errorf("Expected written entity to be:\n%s\nbut got:\n%s", expected, s)
}
}
func TestEntity_WriteTo_multipart(t *testing.T) {
e := testMakeMultipart()
var b bytes.Buffer
if err := e.WriteTo(&b); err != nil {
t.Fatal("Expected no error while writing entity, got", err)
}
if s := b.String(); s != testMultipartText {
t.Errorf("Expected written entity to be:\n%s\nbut got:\n%s", testMultipartText, s)
}
}
func TestNew_unknownTransferEncoding(t *testing.T) {
var h Header
h.Set("Content-Transfer-Encoding", "i-dont-exist")
expected := "hey there"
r := strings.NewReader(expected)
e, err := New(h, r)
if err == nil {
t.Fatal("New(unknown transfer encoding): expected an error")
}
if !IsUnknownEncoding(err) {
t.Fatal("New(unknown transfer encoding): expected an error that verifies IsUnknownEncoding")
}
if !errors.As(err, &UnknownEncodingError{}) {
t.Fatal("New(unknown transfer encoding): expected an error that verifies errors.As(err, &EncodingError{})")
}
if b, err := ioutil.ReadAll(e.Body); err != nil {
t.Error("Expected no error while reading entity body, got", err)
} else if s := string(b); s != expected {
t.Errorf("Expected %q as entity body but got %q", expected, s)
}
}
func TestNew_unknownCharset(t *testing.T) {
var h Header
h.Set("Content-Type", "text/plain; charset=I-DONT-EXIST")
expected := "hey there"
r := strings.NewReader(expected)
e, err := New(h, r)
if err == nil {
t.Fatal("New(unknown charset): expected an error")
}
if !IsUnknownCharset(err) {
t.Fatal("New(unknown charset): expected an error that verifies IsUnknownCharset")
}
if b, err := ioutil.ReadAll(e.Body); err != nil {
t.Error("Expected no error while reading entity body, got", err)
} else if s := string(b); s != expected {
t.Errorf("Expected %q as entity body but got %q", expected, s)
}
}
func TestNewEntity_MultipartReader_notMultipart(t *testing.T) {
e := testMakeEntity()
mr := e.MultipartReader()
if mr != nil {
t.Fatal("(non-multipart).MultipartReader() != nil")
}
}
type testWalkPart struct {
path []int
mediaType string
body string
err error
}
func walkCollect(e *Entity) ([]testWalkPart, error) {
var l []testWalkPart
err := e.Walk(func(path []int, part *Entity, err error) error {
var body string
if part.MultipartReader() == nil {
b, err := ioutil.ReadAll(part.Body)
if err != nil {
return err
}
body = string(b)
}
mediaType, _, _ := part.Header.ContentType()
l = append(l, testWalkPart{
path: path,
mediaType: mediaType,
body: body,
err: err,
})
return nil
})
return l, err
}
func TestWalk_single(t *testing.T) {
e, err := Read(strings.NewReader(testSingleText))
if err != nil {
t.Fatalf("Read() = %v", err)
}
want := []testWalkPart{{
path: nil,
mediaType: "text/plain",
body: "Message body",
}}
got, err := walkCollect(e)
if err != nil {
t.Fatalf("Entity.Walk() = %v", err)
}
if !reflect.DeepEqual(got, want) {
t.Errorf("Entity.Walk() =\n%#v\nbut want:\n%#v", got, want)
}
}
func TestWalk_multipart(t *testing.T) {
e := testMakeMultipart()
want := []testWalkPart{
{
path: nil,
mediaType: "multipart/alternative",
},
{
path: []int{0},
mediaType: "text/plain",
body: "Text part",
},
{
path: []int{1},
mediaType: "text/html",
body: "HTML part
",
},
}
got, err := walkCollect(e)
if err != nil {
t.Fatalf("Entity.Walk() = %v", err)
}
if !reflect.DeepEqual(got, want) {
t.Errorf("Entity.Walk() =\n%#v\nbut want:\n%#v", got, want)
}
}
go-message-0.15.0/example_test.go 0000664 0000000 0000000 00000005442 14060654707 0016663 0 ustar 00root root 0000000 0000000 package message_test
import (
"bytes"
"io"
"log"
"strings"
"github.com/emersion/go-message"
)
func ExampleRead() {
// Let's assume r is an io.Reader that contains a message.
var r io.Reader
m, err := message.Read(r)
if message.IsUnknownCharset(err) {
// This error is not fatal
log.Println("Unknown encoding:", err)
} else if err != nil {
log.Fatal(err)
}
if mr := m.MultipartReader(); mr != nil {
// This is a multipart message
log.Println("This is a multipart message containing:")
for {
p, err := mr.NextPart()
if err == io.EOF {
break
} else if err != nil {
log.Fatal(err)
}
t, _, _ := p.Header.ContentType()
log.Println("A part with type", t)
}
} else {
t, _, _ := m.Header.ContentType()
log.Println("This is a non-multipart message with type", t)
}
}
func ExampleWriter() {
var b bytes.Buffer
var h message.Header
h.SetContentType("multipart/alternative", nil)
w, err := message.CreateWriter(&b, h)
if err != nil {
log.Fatal(err)
}
var h1 message.Header
h1.SetContentType("text/html", nil)
w1, err := w.CreatePart(h1)
if err != nil {
log.Fatal(err)
}
io.WriteString(w1, "Hello World!
This is an HTML part.
")
w1.Close()
var h2 message.Header
h1.SetContentType("text/plain", nil)
w2, err := w.CreatePart(h2)
if err != nil {
log.Fatal(err)
}
io.WriteString(w2, "Hello World!\n\nThis is a text part.")
w2.Close()
w.Close()
log.Println(b.String())
}
func Example_transform() {
// Let's assume r is an io.Reader that contains a message.
var r io.Reader
m, err := message.Read(r)
if message.IsUnknownCharset(err) {
log.Println("Unknown encoding:", err)
} else if err != nil {
log.Fatal(err)
}
// We'll add "This message is powered by Go" at the end of each text entity.
poweredBy := "\n\nThis message is powered by Go."
var b bytes.Buffer
w, err := message.CreateWriter(&b, m.Header)
if err != nil {
log.Fatal(err)
}
// Define a function that transforms message.
var transform func(w *message.Writer, e *message.Entity) error
transform = func(w *message.Writer, e *message.Entity) error {
if mr := e.MultipartReader(); mr != nil {
// This is a multipart entity, transform each of its parts
for {
p, err := mr.NextPart()
if err == io.EOF {
break
} else if err != nil {
return err
}
pw, err := w.CreatePart(p.Header)
if err != nil {
return err
}
if err := transform(pw, p); err != nil {
return err
}
pw.Close()
}
return nil
} else {
body := e.Body
if strings.HasPrefix(m.Header.Get("Content-Type"), "text/") {
body = io.MultiReader(body, strings.NewReader(poweredBy))
}
_, err := io.Copy(w, body)
return err
}
}
if err := transform(w, m); err != nil {
log.Fatal(err)
}
w.Close()
log.Println(b.String())
}
go-message-0.15.0/go.mod 0000664 0000000 0000000 00000000235 14060654707 0014743 0 ustar 00root root 0000000 0000000 module github.com/emersion/go-message
go 1.14
require (
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594
golang.org/x/text v0.3.6
)
go-message-0.15.0/go.sum 0000664 0000000 0000000 00000000771 14060654707 0014775 0 ustar 00root root 0000000 0000000 github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
go-message-0.15.0/header.go 0000664 0000000 0000000 00000007022 14060654707 0015415 0 ustar 00root root 0000000 0000000 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)}
}
go-message-0.15.0/header_test.go 0000664 0000000 0000000 00000005071 14060654707 0016456 0 ustar 00root root 0000000 0000000 package message
import (
"reflect"
"testing"
)
func TestHeader(t *testing.T) {
mediaType := "text/plain"
mediaParams := map[string]string{"charset": "utf-8"}
desc := "Plan de complémentarité de l'Homme"
disp := "attachment"
dispParams := map[string]string{"filename": "complémentarité.txt"}
var h Header
h.SetContentType(mediaType, mediaParams)
h.SetText("Content-Description", desc)
h.SetContentDisposition(disp, dispParams)
if gotMediaType, gotParams, err := h.ContentType(); err != nil {
t.Error("Expected no error when parsing content type, but got:", err)
} else if gotMediaType != mediaType {
t.Errorf("Expected media type %q but got %q", mediaType, gotMediaType)
} else if !reflect.DeepEqual(gotParams, mediaParams) {
t.Errorf("Expected media params %v but got %v", mediaParams, gotParams)
}
if gotDesc, err := h.Text("Content-Description"); err != nil {
t.Error("Expected no error when parsing content description, but got:", err)
} else if gotDesc != desc {
t.Errorf("Expected content description %q but got %q", desc, gotDesc)
}
if gotDisp, gotParams, err := h.ContentDisposition(); err != nil {
t.Error("Expected no error when parsing content disposition, but got:", err)
} else if gotDisp != disp {
t.Errorf("Expected disposition %q but got %q", disp, gotDisp)
} else if !reflect.DeepEqual(gotParams, dispParams) {
t.Errorf("Expected disposition params %v but got %v", dispParams, gotParams)
}
}
func TestEmptyContentType(t *testing.T) {
var h Header
mediaType := "text/plain"
if gotMediaType, _, err := h.ContentType(); err != nil {
t.Error("Expected no error when parsing empty content type, but got:", err)
} else if gotMediaType != mediaType {
t.Errorf("Expected media type %q but got %q", mediaType, gotMediaType)
}
}
func TestKnownCharset(t *testing.T) {
var h Header
h.Set("Subject", "=?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=")
fields := h.Fields()
if !fields.Next() {
t.Error("Expected to be able to advance to first header item")
}
_, err := fields.Text()
if err != nil {
t.Error("Expected no error when decoding header key of known charset, but got: ", err)
}
}
func TestUnknownCharset(t *testing.T) {
var h Header
h.Set("Subject", "=?INVALIDCHARSET?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=")
fields := h.Fields()
if !fields.Next() {
t.Error("Expected to be able to advance to first header item")
}
_, err := fields.Text()
if err == nil {
t.Error("Expected error decoding header key of unknown charset")
}
if !IsUnknownCharset(err) {
t.Error("Expected error to verify IsUnknownCharset")
}
}
go-message-0.15.0/mail/ 0000775 0000000 0000000 00000000000 14060654707 0014557 5 ustar 00root root 0000000 0000000 go-message-0.15.0/mail/address.go 0000664 0000000 0000000 00000002172 14060654707 0016535 0 ustar 00root root 0000000 0000000 package mail
import (
"mime"
"net/mail"
"strings"
"github.com/emersion/go-message"
)
// Address represents a single mail address.
// The type alias ensures that a net/mail.Address can be used wherever an
// Address is expected
type Address = mail.Address
func formatAddressList(l []*Address) string {
formatted := make([]string, len(l))
for i, a := range l {
formatted[i] = a.String()
}
return strings.Join(formatted, ", ")
}
// ParseAddress parses a single RFC 5322 address, e.g. "Barry Gibbs "
// Use this function only if you parse from a string, if you have a Header use
// Header.AddressList instead
func ParseAddress(address string) (*Address, error) {
parser := mail.AddressParser{
&mime.WordDecoder{message.CharsetReader},
}
return parser.Parse(address)
}
// ParseAddressList parses the given string as a list of addresses.
// Use this function only if you parse from a string, if you have a Header use
// Header.AddressList instead
func ParseAddressList(list string) ([]*Address, error) {
parser := mail.AddressParser{
&mime.WordDecoder{message.CharsetReader},
}
return parser.ParseList(list)
}
go-message-0.15.0/mail/address_test.go 0000664 0000000 0000000 00000001712 14060654707 0017573 0 ustar 00root root 0000000 0000000 package mail_test
import (
"net/mail"
"reflect"
"testing"
)
func TestParseAddressList(t *testing.T) {
want := []*mail.Address{{"Mitsuha Miyamizu", "mitsuha.miyamizu@example.org"},
{"Han Solo", "hanibunny@example.org"},
}
input := "Mitsuha Miyamizu ,Han Solo "
if got, err := mail.ParseAddressList(input); err != nil {
t.Error("Expected no error while parsing address list got:", err)
} else if !reflect.DeepEqual(got, want) {
t.Errorf("Expected address list to be %v, but got %v", want, got)
}
}
func TestParseAddress(t *testing.T) {
want := &mail.Address{"Mitsuha Miyamizu", "mitsuha.miyamizu@example.org"}
input := "Mitsuha Miyamizu "
if got, err := mail.ParseAddress(input); err != nil {
t.Error("Expected no error while parsing address got:", err)
} else if !reflect.DeepEqual(got, want) {
t.Errorf("Expected address to be %v, but got %v", want, got)
}
}
go-message-0.15.0/mail/attachment.go 0000664 0000000 0000000 00000001314 14060654707 0017235 0 ustar 00root root 0000000 0000000 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)
}
go-message-0.15.0/mail/attachment_test.go 0000664 0000000 0000000 00000003251 14060654707 0020276 0 ustar 00root root 0000000 0000000 package mail_test
import (
"testing"
"github.com/emersion/go-message/mail"
)
func TestAttachmentHeader_Filename(t *testing.T) {
var h mail.AttachmentHeader
h.Set("Content-Disposition", "attachment; filename=note.txt")
if filename, err := h.Filename(); err != nil {
t.Error("Expected no error while parsing filename, got:", err)
} else if filename != "note.txt" {
t.Errorf("Expected filename to be %q but got %q", "note.txt", filename)
}
}
func TestAttachmentHeader_Filename_inContentType(t *testing.T) {
// Note: putting the attachment's filename in Content-Type is discouraged.
var h mail.AttachmentHeader
h.Set("Content-Type", "text/plain; name=note.txt")
if filename, err := h.Filename(); err != nil {
t.Error("Expected no error while parsing filename, got:", err)
} else if filename != "note.txt" {
t.Errorf("Expected filename to be %q but got %q", "note.txt", filename)
}
}
func TestAttachmentHeader_Filename_none(t *testing.T) {
var h mail.AttachmentHeader
if filename, err := h.Filename(); err != nil {
t.Error("Expected no error while parsing filename, got:", err)
} else if filename != "" {
t.Errorf("Expected filename to be %q but got %q", "", filename)
}
}
func TestAttachmentHeader_Filename_encoded(t *testing.T) {
var h mail.AttachmentHeader
h.Set("Content-Disposition", "attachment; filename=\"=?UTF-8?Q?Opis_przedmiotu_zam=c3=b3wienia_-_za=c5=82=c4=85cznik_nr_1?= =?UTF-8?Q?=2epdf?=\"")
if filename, err := h.Filename(); err != nil {
t.Error("Expected no error while parsing filename, got:", err)
} else if filename != "Opis przedmiotu zamówienia - załącznik nr 1.pdf" {
t.Errorf("Expected filename to be %q but got %q", "", filename)
}
}
go-message-0.15.0/mail/header.go 0000664 0000000 0000000 00000017065 14060654707 0016347 0 ustar 00root root 0000000 0000000 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) {
h.Set(key, formatAddressList(addrs))
}
// Date parses the Date header field.
func (h *Header) Date() (time.Time, error) {
return mail.ParseDate(h.Get("Date"))
}
// SetDate formats the Date header field.
func (h *Header) SetDate(t time.Time) {
h.Set("Date", t.Format(dateLayout))
}
// Subject parses the Subject header field. If there is an error, the raw field
// value is returned alongside the error.
func (h *Header) Subject() (string, error) {
return h.Text("Subject")
}
// SetSubject formats the Subject header field.
func (h *Header) SetSubject(s string) {
h.SetText("Subject", s)
}
// 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 generates an RFC 2822-compliant Message-Id based on the
// informational draft "Recommendations for generating Message IDs", for lack
// of a better authoritative source.
func (h *Header) GenerateMessageID() error {
now := uint64(time.Now().UnixNano())
nonceByte := make([]byte, 8)
if _, err := rand.Read(nonceByte); err != nil {
return err
}
nonce := binary.BigEndian.Uint64(nonceByte)
hostname, err := os.Hostname()
if err != nil {
return err
}
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) {
h.Set("Message-Id", "<"+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) {
var v string
if len(l) > 0 {
v = "<" + strings.Join(l, "> <") + ">"
}
h.Set(key, v)
}
// Copy creates a stand-alone copy of the header.
func (h *Header) Copy() Header {
return Header{h.Header.Copy()}
}
go-message-0.15.0/mail/header_test.go 0000664 0000000 0000000 00000012612 14060654707 0017377 0 ustar 00root root 0000000 0000000 package mail_test
import (
netmail "net/mail"
"reflect"
"testing"
"time"
"github.com/emersion/go-message/mail"
)
func TestHeader(t *testing.T) {
date := time.Unix(1466253744, 0)
from := []*mail.Address{{"Mitsuha Miyamizu", "mitsuha.miyamizu@example.org"}}
subject := "Café"
var h mail.Header
h.SetAddressList("From", from)
h.SetDate(date)
h.SetSubject(subject)
if got, err := h.Date(); err != nil {
t.Error("Expected no error while parsing header date, got:", err)
} else if !got.Equal(date) {
t.Errorf("Expected header date to be %v, but got %v", date, got)
}
if got, err := h.AddressList("From"); err != nil {
t.Error("Expected no error while parsing header address list, got:", err)
} else if !reflect.DeepEqual(got, from) {
t.Errorf("Expected header address list to be %v, but got %v", from, got)
}
if got, err := h.AddressList("Cc"); err != nil {
t.Error("Expected no error while parsing missing header address list, got:", err)
} else if got != nil {
t.Errorf("Expected missing header address list to be %v, but got %v", nil, got)
}
if got, err := h.Subject(); err != nil {
t.Error("Expected no error while parsing header subject, got:", err)
} else if got != subject {
t.Errorf("Expected header subject to be %v, but got %v", subject, got)
}
}
func TestCFWSDates(t *testing.T) {
tc := []string{
"Mon, 22 Jul 2019 13:57:29 -0500 (GMT-05:00)",
"Mon, 22 Jul 2019 13:57:29 -0500",
"Mon, 2 Jan 06 15:04:05 MST (Some random stuff)",
"Mon, 2 Jan 06 15:04:05 MST",
}
var h mail.Header
for _, tt := range tc {
h.Set("Date", tt)
_, err := h.Date()
if err != nil {
t.Errorf("Failed to parse time %q: %v", tt, err)
}
}
}
func TestHeader_MessageID(t *testing.T) {
tests := []struct {
raw string
msgID string
}{
{"", ""},
{"<123@asdf>", "123@asdf"},
{
" \t ",
"DM6PR09MB253761A38B42C713082A7CE2C60C0@DM6PR09MB2537.namprd09.prod.outlook.com",
},
{
`<20200122161125.7enac4n5rsxfnhg7@example.com> (Christopher Wellons's message of "Wed, 22 Jan 2020 11:11:25 -0500")`,
"20200122161125.7enac4n5rsxfnhg7@example.com",
},
{
"<123@[2001:db8:85a3:8d3:1319:8a2e:370:7348]>",
"123@[2001:db8:85a3:8d3:1319:8a2e:370:7348]",
},
}
for _, test := range tests {
var h mail.Header
h.Set("Message-ID", test.raw)
msgID, err := h.MessageID()
if err != nil {
t.Errorf("Failed to parse Message-ID %q: Header.MessageID() = %v", test.raw, err)
} else if msgID != test.msgID {
t.Errorf("Failed to parse Message-ID %q: Header.MessageID() = %q, want %q", test.raw, msgID, test.msgID)
}
}
}
func TestHeader_MsgIDList(t *testing.T) {
tests := []struct {
raw string
msgIDs []string
}{
{"", nil},
{"<123@asdf>", []string{"123@asdf"}},
{
" \t ",
[]string{"DM6PR09MB253761A38B42C713082A7CE2C60C0@DM6PR09MB2537.namprd09.prod.outlook.com"},
},
{
`<20200122161125.7enac4n5rsxfnhg7@example.com> (Christopher Wellons's message of "Wed, 22 Jan 2020 11:11:25 -0500")`,
[]string{"20200122161125.7enac4n5rsxfnhg7@example.com"},
},
{
"<87pnfb69f3.fsf@bernat.ch> \t <20200122161125.7enac4n5rsxfnhg7@nullprogram.com>",
[]string{"87pnfb69f3.fsf@bernat.ch", "20200122161125.7enac4n5rsxfnhg7@nullprogram.com"},
},
{
"<87pnfb69f3.fsf@bernat.ch> (a comment) \t <20200122161125.7enac4n5rsxfnhg7@nullprogram.com> (another comment)",
[]string{"87pnfb69f3.fsf@bernat.ch", "20200122161125.7enac4n5rsxfnhg7@nullprogram.com"},
},
}
for _, test := range tests {
var h mail.Header
h.Set("In-Reply-To", test.raw)
msgIDs, err := h.MsgIDList("In-Reply-To")
if err != nil {
t.Errorf("Failed to parse In-Reply-To %q: Header.MsgIDList() = %v", test.raw, err)
} else if !reflect.DeepEqual(msgIDs, test.msgIDs) {
t.Errorf("Failed to parse In-Reply-To %q: Header.MsgIDList() = %q, want %q", test.raw, msgIDs, test.msgIDs)
}
}
}
func TestHeader_GenerateMessageID(t *testing.T) {
var h mail.Header
if err := h.GenerateMessageID(); err != nil {
t.Fatalf("Header.GenerateMessageID() = %v", err)
}
if _, err := h.MessageID(); err != nil {
t.Errorf("Failed to parse generated Message-Id: Header.MessageID() = %v", err)
}
}
func TestHeader_SetMsgIDList(t *testing.T) {
tests := []struct {
raw string
msgIDs []string
}{
{"", nil},
{"<123@asdf>", []string{"123@asdf"}},
{"<123@asdf> <456@asdf>", []string{"123@asdf", "456@asdf"}},
}
for _, test := range tests {
var h mail.Header
h.SetMsgIDList("In-Reply-To", test.msgIDs)
raw := h.Get("In-Reply-To")
if raw != test.raw {
t.Errorf("Failed to format In-Reply-To %q: Header.Get() = %q, want %q", test.msgIDs, raw, test.raw)
}
}
}
func TestHeader_CanUseNetMailAddress(t *testing.T) {
netfrom := []*netmail.Address{{"Mitsuha Miyamizu", "mitsuha.miyamizu@example.org"}}
mailfrom := []*mail.Address{{"Mitsuha Miyamizu", "mitsuha.miyamizu@example.org"}}
//sanity check that they types are identical
if !reflect.DeepEqual(netfrom, mailfrom) {
t.Error("[]*net/mail.Address differs from []*mail.Address")
}
//roundtrip
var h mail.Header
h.SetAddressList("From", netfrom)
if got, err := h.AddressList("From"); err != nil {
t.Error("Expected no error while parsing header address list, got:", err)
} else if !reflect.DeepEqual(got, netfrom) {
t.Errorf("Expected header address list to be %v, but got %v", netfrom, got)
}
}
go-message-0.15.0/mail/inline.go 0000664 0000000 0000000 00000000235 14060654707 0016364 0 ustar 00root root 0000000 0000000 package mail
import (
"github.com/emersion/go-message"
)
// A InlineHeader represents a message text header.
type InlineHeader struct {
message.Header
}
go-message-0.15.0/mail/mail.go 0000664 0000000 0000000 00000000570 14060654707 0016032 0 ustar 00root root 0000000 0000000 // 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
go-message-0.15.0/mail/mail_test.go 0000664 0000000 0000000 00000002045 14060654707 0017070 0 ustar 00root root 0000000 0000000 package mail_test
const mailString = "Subject: Your Name\r\n" +
"Content-Type: multipart/mixed; boundary=message-boundary\r\n" +
"\r\n" +
"--message-boundary\r\n" +
"Content-Type: multipart/alternative; boundary=text-boundary\r\n" +
"\r\n" +
"--text-boundary\r\n" +
"Content-Type: text/plain\r\n" +
"Content-Disposition: inline\r\n" +
"\r\n" +
"Who are you?\r\n" +
"--text-boundary--\r\n" +
"--message-boundary\r\n" +
"Content-Type: text/plain\r\n" +
"Content-Disposition: attachment; filename=note.txt\r\n" +
"\r\n" +
"I'm Mitsuha.\r\n" +
"--message-boundary--\r\n"
const nestedMailString = "Subject: Fwd: Your Name\r\n" +
"Content-Type: multipart/mixed; boundary=outer-message-boundary\r\n" +
"\r\n" +
"--outer-message-boundary\r\n" +
"Content-Type: text/plain\r\n" +
"Content-Disposition: inline\r\n" +
"\r\n" +
"I forgot.\r\n" +
"--outer-message-boundary\r\n" +
"Content-Type: message/rfc822\r\n" +
"Content-Disposition: attachment; filename=attached-message.eml\r\n" +
"\r\n" +
mailString +
"--outer-message-boundary--\r\n"
go-message-0.15.0/mail/reader.go 0000664 0000000 0000000 00000006564 14060654707 0016363 0 ustar 00root root 0000000 0000000 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
}
go-message-0.15.0/mail/reader_test.go 0000664 0000000 0000000 00000010767 14060654707 0017422 0 ustar 00root root 0000000 0000000 package mail_test
import (
"io"
"io/ioutil"
"log"
"strings"
"testing"
"github.com/emersion/go-message/mail"
)
func ExampleReader() {
// Let's assume r is an io.Reader that contains a mail.
var r io.Reader
// Create a new mail reader
mr, err := mail.CreateReader(r)
if err != nil {
log.Fatal(err)
}
// Read each mail's part
for {
p, err := mr.NextPart()
if err == io.EOF {
break
} else if err != nil {
log.Fatal(err)
}
switch h := p.Header.(type) {
case *mail.InlineHeader:
b, _ := ioutil.ReadAll(p.Body)
log.Printf("Got text: %v\n", string(b))
case *mail.AttachmentHeader:
filename, _ := h.Filename()
log.Printf("Got attachment: %v\n", filename)
}
}
}
func testReader(t *testing.T, r io.Reader) {
mr, err := mail.CreateReader(r)
if err != nil {
t.Fatalf("mail.CreateReader(r) = %v", err)
}
defer mr.Close()
wantSubject := "Your Name"
subject, err := mr.Header.Subject()
if err != nil {
t.Errorf("mr.Header.Subject() = %v", err)
} else if subject != wantSubject {
t.Errorf("mr.Header.Subject() = %v, want %v", subject, wantSubject)
}
i := 0
for {
p, err := mr.NextPart()
if err == io.EOF {
break
} else if err != nil {
t.Fatal(err)
}
var expectedBody string
switch i {
case 0:
h, ok := p.Header.(*mail.InlineHeader)
if !ok {
t.Fatalf("Expected a InlineHeader, but got a %T", p.Header)
}
if mediaType, _, _ := h.ContentType(); mediaType != "text/plain" {
t.Errorf("Expected a plaintext part, not an HTML part")
}
expectedBody = "Who are you?"
case 1:
h, ok := p.Header.(*mail.AttachmentHeader)
if !ok {
t.Fatalf("Expected an AttachmentHeader, but got a %T", p.Header)
}
if filename, err := h.Filename(); err != nil {
t.Error("Expected no error while parsing filename, but got:", err)
} else if filename != "note.txt" {
t.Errorf("Expected filename to be %q but got %q", "note.txt", filename)
}
expectedBody = "I'm Mitsuha."
}
if b, err := ioutil.ReadAll(p.Body); err != nil {
t.Error("Expected no error while reading part body, but got:", err)
} else if string(b) != expectedBody {
t.Errorf("Expected part body to be:\n%v\nbut got:\n%v", expectedBody, string(b))
}
i++
}
if i != 2 {
t.Errorf("Expected exactly two parts but got %v", i)
}
}
func TestReader(t *testing.T) {
testReader(t, strings.NewReader(mailString))
}
func TestReader_nonMultipart(t *testing.T) {
s := "Subject: Your Name\r\n" +
"\r\n" +
"Who are you?"
mr, err := mail.CreateReader(strings.NewReader(s))
if err != nil {
t.Fatal("Expected no error while creating reader, got:", err)
}
defer mr.Close()
p, err := mr.NextPart()
if err != nil {
t.Fatal("Expected no error while reading part, got:", err)
}
if _, ok := p.Header.(*mail.InlineHeader); !ok {
t.Fatalf("Expected a InlineHeader, but got a %T", p.Header)
}
expectedBody := "Who are you?"
if b, err := ioutil.ReadAll(p.Body); err != nil {
t.Error("Expected no error while reading part body, but got:", err)
} else if string(b) != expectedBody {
t.Errorf("Expected part body to be:\n%v\nbut got:\n%v", expectedBody, string(b))
}
if _, err := mr.NextPart(); err != io.EOF {
t.Fatal("Expected io.EOF while reading part, but got:", err)
}
}
func TestReader_closeImmediately(t *testing.T) {
s := "Content-Type: text/plain\r\n" +
"\r\n" +
"Who are you?"
mr, err := mail.CreateReader(strings.NewReader(s))
if err != nil {
t.Fatal("Expected no error while creating reader, got:", err)
}
mr.Close()
if _, err := mr.NextPart(); err != io.EOF {
t.Fatal("Expected io.EOF while reading part, but got:", err)
}
}
func TestReader_nested(t *testing.T) {
r := strings.NewReader(nestedMailString)
mr, err := mail.CreateReader(r)
if err != nil {
t.Fatalf("mail.CreateReader(r) = %v", err)
}
defer mr.Close()
i := 0
for {
p, err := mr.NextPart()
if err == io.EOF {
break
} else if err != nil {
t.Fatal(err)
}
switch i {
case 0:
_, ok := p.Header.(*mail.InlineHeader)
if !ok {
t.Fatalf("Expected a InlineHeader, but got a %T", p.Header)
}
expectedBody := "I forgot."
if b, err := ioutil.ReadAll(p.Body); err != nil {
t.Error("Expected no error while reading part body, but got:", err)
} else if string(b) != expectedBody {
t.Errorf("Expected part body to be:\n%v\nbut got:\n%v", expectedBody, string(b))
}
case 1:
_, ok := p.Header.(*mail.AttachmentHeader)
if !ok {
t.Fatalf("Expected an AttachmentHeader, but got a %T", p.Header)
}
testReader(t, p.Body)
}
i++
}
}
go-message-0.15.0/mail/writer.go 0000664 0000000 0000000 00000010010 14060654707 0016412 0 ustar 00root root 0000000 0000000 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()
}
go-message-0.15.0/mail/writer_test.go 0000664 0000000 0000000 00000004744 14060654707 0017472 0 ustar 00root root 0000000 0000000 package mail_test
import (
"bytes"
"io"
"log"
"testing"
"time"
"github.com/emersion/go-message/mail"
)
func ExampleWriter() {
var b bytes.Buffer
from := []*mail.Address{{"Mitsuha Miyamizu", "mitsuha.miyamizu@example.org"}}
to := []*mail.Address{{"Taki Tachibana", "taki.tachibana@example.org"}}
// Create our mail header
var h mail.Header
h.SetDate(time.Now())
h.SetAddressList("From", from)
h.SetAddressList("To", to)
// Create a new mail writer
mw, err := mail.CreateWriter(&b, h)
if err != nil {
log.Fatal(err)
}
// Create a text part
tw, err := mw.CreateInline()
if err != nil {
log.Fatal(err)
}
var th mail.InlineHeader
th.Set("Content-Type", "text/plain")
w, err := tw.CreatePart(th)
if err != nil {
log.Fatal(err)
}
io.WriteString(w, "Who are you?")
w.Close()
tw.Close()
// Create an attachment
var ah mail.AttachmentHeader
ah.Set("Content-Type", "image/jpeg")
ah.SetFilename("picture.jpg")
w, err = mw.CreateAttachment(ah)
if err != nil {
log.Fatal(err)
}
// TODO: write a JPEG file to w
w.Close()
mw.Close()
log.Println(b.String())
}
func TestWriter(t *testing.T) {
var b bytes.Buffer
var h mail.Header
h.SetSubject("Your Name")
mw, err := mail.CreateWriter(&b, h)
if err != nil {
t.Fatal(err)
}
// Create a text part
tw, err := mw.CreateInline()
if err != nil {
t.Fatal(err)
}
var th mail.InlineHeader
th.Set("Content-Type", "text/plain")
w, err := tw.CreatePart(th)
if err != nil {
t.Fatal(err)
}
io.WriteString(w, "Who are you?")
w.Close()
tw.Close()
// Create an attachment
var ah mail.AttachmentHeader
ah.Set("Content-Type", "text/plain")
ah.SetFilename("note.txt")
w, err = mw.CreateAttachment(ah)
if err != nil {
t.Fatal(err)
}
io.WriteString(w, "I'm Mitsuha.")
w.Close()
mw.Close()
testReader(t, &b)
}
func TestWriter_singleInline(t *testing.T) {
var b bytes.Buffer
var h mail.Header
h.SetSubject("Your Name")
mw, err := mail.CreateWriter(&b, h)
if err != nil {
t.Fatal(err)
}
// Create a text part
var th mail.InlineHeader
th.Set("Content-Type", "text/plain")
w, err := mw.CreateSingleInline(th)
if err != nil {
t.Fatal(err)
}
io.WriteString(w, "Who are you?")
w.Close()
// Create an attachment
var ah mail.AttachmentHeader
ah.Set("Content-Type", "text/plain")
ah.SetFilename("note.txt")
w, err = mw.CreateAttachment(ah)
if err != nil {
t.Fatal(err)
}
io.WriteString(w, "I'm Mitsuha.")
w.Close()
mw.Close()
t.Logf("Formatted message: \n%v", b.String())
testReader(t, &b)
}
go-message-0.15.0/message.go 0000664 0000000 0000000 00000000564 14060654707 0015615 0 ustar 00root root 0000000 0000000 // Package message implements reading and writing multipurpose messages.
//
// RFC 2045, RFC 2046 and RFC 2047 defines MIME, and RFC 2183 defines the
// Content-Disposition header field.
//
// Add this import to your package if you want to handle most common charsets
// by default:
//
// import (
// _ "github.com/emersion/go-message/charset"
// )
package message
go-message-0.15.0/multipart.go 0000664 0000000 0000000 00000004007 14060654707 0016206 0 ustar 00root root 0000000 0000000 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
}
go-message-0.15.0/textproto/ 0000775 0000000 0000000 00000000000 14060654707 0015705 5 ustar 00root root 0000000 0000000 go-message-0.15.0/textproto/bench_test.go 0000664 0000000 0000000 00000033757 14060654707 0020371 0 ustar 00root root 0000000 0000000 package textproto
import (
"bufio"
"net/textproto"
"strings"
"testing"
)
const testHeaderString1 = "Return-Path: \r\n" +
"Delivered-To: aaaaaaa@example.com\r\n" +
"Received: from localhost (localhost [127.0.0.1])\r\n" +
" by example.com (Postfix) with ESMTP id 27A702E253\r\n" +
" for ; Fri, 11 Oct 2019 22:25:14 +0200 (CEST)\r\n" +
"X-Virus-Scanned: Debian amavisd-new at example.com\r\n" +
"X-Spam-Flag: NO\r\n" +
"X-Spam-Score: -2.1\r\n" +
"X-Spam-Level:\r\n" +
"X-Spam-Status: No, score=-2.1 tagged_above=-9999 required=5\r\n" +
" tests=[BAYES_00=-1.9, DKIMWL_WL_HIGH=-0.001, DKIM_SIGNED=0.1,\r\n" +
" DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1,\r\n" +
" HTML_MESSAGE=0.001, SPF_HELO_NONE=0.001, SPF_PASS=-0.001]\r\n" +
" autolearn=ham autolearn_force=no\r\n" +
"Received: from knopi.example.com ([127.0.0.1])\r\n" +
" by localhost (example.com [127.0.0.1]) (amavisd-new, port 10024)\r\n" +
" with ESMTP id 7VNTFhobvC5w for ;\r\n" +
" Fri, 11 Oct 2019 22:25:12 +0200 (CEST)\r\n" +
"Received: from aaaaaaa.example.com (aaaaaaa.example.com [208.64.202.54])\r\n" +
" by example.com (Postfix) with ESMTPS id 498DF26A80\r\n" +
" for ; Fri, 11 Oct 2019 22:25:11 +0200 (CEST)\r\n" +
"Authentication-Results: example.com;\r\n" +
" dkim=pass (1024-bit key; unprotected) header.d=example.com header.i=@example.com header.b=\"q+sL9woc\";\r\n" +
" dkim-atps=neutral\r\n" +
"DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed;\r\n" +
" d=example.com; s=smtp; h=Date:Message-Id:Content-Type:Subject:\r\n" +
" MIME-Version:Reply-To:From:To:Sender:Cc:Content-Transfer-Encoding:Content-ID:\r\n" +
" Content-Description:Resent-Date:Resent-From:Resent-Sender:Resent-To:Resent-Cc\r\n" +
" :Resent-Message-ID:In-Reply-To:References:List-Id:List-Help:List-Unsubscribe:\r\n" +
" List-Subscribe:List-Post:List-Owner:List-Archive;\r\n" +
" bh=oE7DVQcCo/SZapqnvRiosGrj0I7XI3GdKR8JEFu0l1U=; b=q+sL9wocDMrSmDdrErStDEN4AD\r\n" +
" tZQmJUGprHWQAuc4b+r5H/yKHqgGRMKm91qvxLfPtBbLIIAilDZk7Q6HAMko/qt9msj26eCO8a5/+\r\n" +
" QtT94z5b+p5OckmgpQTK9k5n3MlaHkLannUrMvr0+fKI/Nw5OMgmrlxzfFWS3gtri7ME=;\r\n" +
"Received: from [208.64.202.21] (helo=example.com)\r\n" +
" by smtp-04-tuk1.example.com with smtp (Exim 4.90_1)\r\n" +
" (envelope-from )\r\n" +
" id 1iJ1TK-0001P5-61\r\n" +
" for aaaaaaa@example.com; Fri, 11 Oct 2019 13:25:10 -0700\r\n" +
"To: aaaaaaa@example.com\r\n" +
"From: \"whatever\" \r\n" +
"Reply-To: \r\n" +
"Errors-To: \r\n" +
"X-whatever-Message-Type: AAAAAAAAAAAAAAAAAAAAAAAA\r\n" +
"Mime-Version: 1.0\r\n" +
"Subject: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA!\r\n" +
"Content-Type: multipart/alternative;\r\n" +
" boundary=\"np5da0e524c5cff\"\r\n" +
"Message-Id: \r\n" +
"Date: Fri, 11 Oct 2019 13:25:10 -0700\r\n" +
"\r\n"
const testHeaderString2 = "Return-Path: \r\n" +
"Delivered-To: aaaaaaa@example.com\r\n" +
"Received: from localhost (localhost [127.0.0.1])\r\n" +
" by example.com (Postfix) with ESMTP id 6674C25DD7\r\n" +
" for ; Sat, 12 Oct 2019 23:27:19 +0200 (CEST)\r\n" +
"X-Virus-Scanned: Debian amavisd-new at example.com\r\n" +
"X-Spam-Flag: NO\r\n" +
"X-Spam-Score: -1.698\r\n" +
"X-Spam-Level:\r\n" +
"X-Spam-Status: No, score=-1.698 tagged_above=-9999 required=5\r\n" +
" tests=[BAYES_00=-1.9, DKIM_INVALID=0.1, DKIM_SIGNED=0.1,\r\n" +
" HTML_FONT_LOW_CONTRAST=0.001, HTML_MESSAGE=0.001, SPF_HELO_NONE=0.001,\r\n" +
" SPF_PASS=-0.001] autolearn=no autolearn_force=no\r\n" +
"Received: from aaaaa.example.com ([127.0.0.1])\r\n" +
" by localhost (example.com [127.0.0.1]) (amavisd-new, port 10024)\r\n" +
" with ESMTP id rreNxtBzkKKg for ;\r\n" +
" Sat, 12 Oct 2019 23:27:17 +0200 (CEST)\r\n" +
"Received: from aaaaaaaaa.example.com (aaaaaaaaa.example.com [52.21.114.224])\r\n" +
" by example.com (Postfix) with ESMTPS id 04078252B0\r\n" +
" for ; Sat, 12 Oct 2019 23:27:16 +0200 (CEST)\r\n" +
"Authentication-Results: example.com;\r\n" +
" dkim=fail reason=\"signature verification failed\" (2048-bit key; unprotected) header.d=example.com header.i=@example.com header.b=\"dZdXFfdO\";\r\n" +
" dkim-atps=neutral\r\n" +
"Received:\r\n" +
" by pigeon-at-10005 (OpenSMTPD) with ESMTP id 8c8bc170\r\n" +
" for ;\r\n" +
" Sat, 12 Oct 2019 21:27:15 +0000 (UTC)\r\n" +
"DKIM-Signature: v=1; a=rsa-sha1; c=relaxed/relaxed; d=example.com; h=\r\n" +
" content-type:mime-version:from:to:subject:list-unsubscribe:date\r\n" +
" :message-id; s=pigeon; bh=KSoP6Qz2pPwRYI9o8UOCFcgfqoA=; b=dZdXFf\r\n" +
" dOFuwK7RaGOspcDR+26a2iQRLO7WuXahe+X/deW0tvmoaRGyF18ei3nwM7lZdHyZ\r\n" +
" gztpqTsZtYHfyhqf7lMplMt4uGoxo1iofM4GFRiJy2A+umfOnLRYcAb5Hulyn83c\r\n" +
" YldmKy0Cmy4B1uRYozyv6doMScYaIiB9STNnaaJh3oaApx4wrk5r0kav2CGc7e0/\r\n" +
" rLij61X8nyLFOnPzHNi1ByXsAZWxZtOe7H7mzF+Xh/WQ6y6SQnkksg+AhsJGQ1b/\r\n" +
" usdLollf47EJaYEuHOMKsrvIqRCWACq7Mhzu3KMi81Tl0TGhk4KfSp+6ldSIc/39\r\n" +
" 0z8EFXV5TSoJA0dg==\r\n" +
"Content-Type: multipart/alternative;\r\n" +
" boundary=\"===============7074720964622344361==\"\r\n" +
"Mime-Version: 1.0\r\n" +
"From: example \r\n" +
"To: aaaaaaa@example.com\r\n" +
"Subject: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=\r\n" +
" =?utf-8?q?aaaaaaaaaaaaaaaaaaaaaaaaaaaaa?=\r\n" +
"List-Unsubscribe: \r\n" +
"X-CID: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n" +
"Date: Sat, 12 Oct 2019 21:27:15 +0000\r\n" +
"Message-ID: \r\n" +
"X-SMTPAPI: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n" +
" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n" +
" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n" +
" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n" +
" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n" +
" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n" +
" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n" +
" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n" +
" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n" +
" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n" +
" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n" +
" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n" +
" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n" +
" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n" +
" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n" +
" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n" +
" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n" +
" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n" +
"X-QMSG: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n" +
" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" +
" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" +
" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n" +
" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n" +
" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n" +
" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n" +
" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n" +
"\r\n"
const testHeaderString3 = "Return-Path: \r\n" +
"Delivered-To: aaaaaaa@example.com\r\n" +
"Received: from localhost (localhost [127.0.0.1])\r\n" +
" by example.com (Postfix) with ESMTP id C263620992\r\n" +
" for ; Sun, 22 Sep 2019 14:16:09 +0200 (CEST)\r\n" +
"X-Virus-Scanned: Debian amavisd-new at example.com\r\n" +
"X-Spam-Flag: NO\r\n" +
"X-Spam-Score: -1.9\r\n" +
"X-Spam-Level:\r\n" +
"X-Spam-Status: No, score=-1.9 tagged_above=-9999 required=5\r\n" +
" tests=[BAYES_00=-1.9, SPF_HELO_NONE=0.001, SPF_PASS=-0.001]\r\n" +
" autolearn=ham autolearn_force=no\r\n" +
"Received: from aaaaa.example.com ([127.0.0.1])\r\n" +
" by localhost (example.com [127.0.0.1]) (amavisd-new, port 10024)\r\n" +
" with ESMTP id HwPdIXhwQogO for ;\r\n" +
" Sun, 22 Sep 2019 14:16:08 +0200 (CEST)\r\n" +
"Received-SPF: Pass (mailfrom) identity=mailfrom; client-ip=XXX.XXX.XXX.XXX; helo=aaaaaaaaa.example.com; envelope-from=aaaaaaaaaaa@example.com; receiver= \r\n" +
"Received: from aaaaaaaaa.example.com (aaaaaaaaaa.example.com [XXX.XXX.XXX.XXX])\r\n" +
" by example.com (Postfix) with ESMTPS id 7C70420990\r\n" +
" for ; Sun, 22 Sep 2019 14:16:07 +0200 (CEST)\r\n" +
"X-Note: This Email was scanned by AppRiver SecureTide\r\n" +
"X-Note-AR-ScanTimeLocal: 09/22/2019 8:01:04 AM\r\n" +
"X-Note: SecureTide Build: 9/5/2019 3:33:32 PM UTC (2.8.5.0)\r\n" +
"X-Note: Filtered by 10.246.0.223\r\n" +
"X-Note-AR-Scan: None - PIPE\r\n" +
"Received: by aaaaaaaaa.example.com (CommuniGate Pro PIPE 6.2.4)\r\n" +
" with PIPE id 12280785; Sun, 22 Sep 2019 08:01:04 -0400\r\n" +
"Received: from [XXX.XXX.XXX.XXX] (HELO aaaaaaaaaaaa.example.com)\r\n" +
" by aaaaaaaaa.example.com (CommuniGate Pro SMTP 6.2.4)\r\n" +
" with ESMTP id 12280776 for aaaaaaa@example.com; Sun, 22 Sep 2019 08:01:00 -0400\r\n" +
"Received: from aaaaaaaaaaaaaaaaaaaaaaaaaa.local (XXX.XXX.XXX.XXX) by\r\n" +
" aaaaaaaaaaaaaaaaaaaaaaaaaa.local (XXX.XXX.XXX.XXX) with Microsoft SMTP Server\r\n" +
" (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256_P256) id\r\n" +
" X.X.XXXX.X; Sun, 22 Sep 2019 08:01:00 -0400\r\n" +
"Received: from aaaaaaaaaaaaaaaaaaaaaaaaaa.local ([XXX.XXX.XXX.XXX]) by\r\n" +
" aaaaaaaaaaaaaaaaaaaaaaaaaa.local ([XXX.XXX.XXX.XXX]) with mapi id\r\n" +
" 15.01.1779.000; Sun, 22 Sep 2019 08:01:00 -0400\r\n" +
"From: \"AAAAAAAAAAAAAA\" \r\n" +
"To: AAAAAAAAAAA \r\n" +
"Subject: RE: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n" +
" AAAAAAAAAAAAAAA\r\n" +
"Thread-Topic: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n" +
" AAAAAAAAAAAAAAA\r\n" +
"Thread-Index: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\r\n" +
"Date: Sun, 22 Sep 2019 12:01:00 +0000\r\n" +
"Message-ID: \r\n" +
"References: \r\n" +
" \r\n" +
"In-Reply-To: \r\n" +
"Accept-Language: en-US\r\n" +
"Content-Language: en-US\r\n" +
"X-MS-Has-Attach:\r\n" +
"X-MS-TNEF-Correlator:\r\n" +
"x-rerouted-by-exchange:\r\n" +
"Content-Type: text/plain; charset=\"utf-8\"\r\n" +
"Content-Transfer-Encoding: base64\r\n" +
"Mime-Version: 1.0\r\n" +
"X-Note: This Email was scanned by AppRiver SecureTide\r\n" +
"X-Note-AR-ScanTimeLocal: 09/22/2019 8:01:00 AM\r\n" +
"X-Note: SecureTide Build: 9/5/2019 3:33:32 PM UTC (2.8.5.0)\r\n" +
"X-Note: Filtered by XXX.XXX.XXX.XXX\r\n" +
"X-Policy: example.com\r\n" +
"X-Primary: example.com@example.com\r\n" +
"X-Note-Sender: \r\n" +
"X-Virus-Scan: V-\r\n" +
"X-Note-SnifferID: 0\r\n" +
"X-GBUdb-Analysis: 1, XXX.XXX.XXX.XXX, Ugly c=0.765185 p=-0.992849 Source White\r\n" +
"X-Signature-Violations:\r\n" +
" 0-0-0-2154-c\r\n" +
"X-Note-419: 0 ms. Fail:1 Chk:1354 of 1354 total\r\n" +
"X-Note: VSCH-CT/SI: 1-1354/SG:1 9/22/2019 8:00:50 AM\r\n" +
"X-Note: Spam Tests Failed: \r\n" +
"X-Country-Path: PRIVATE->PRIVATE->\r\n" +
"X-Note-Sending-IP: XXX.XXX.XXX.XXX\r\n" +
"X-Note-Reverse-DNS: \r\n" +
"X-Note-Return-Path: shahid.shah@example.com\r\n" +
"X-Note: User Rule Hits: \r\n" +
"X-Note: Global Rule Hits: G694 G695 G696 G697 G715 G716 G717 G870 \r\n" +
"X-Note: Encrypt Rule Hits: \r\n" +
"X-Note: Mail Class: VALID\r\n" +
"X-Note-ECS-IP:\r\n" +
"X-Note-ECS-Recip: aaaaaaa@example.com\r\n" +
"\r\n"
// Assign to global variable to prevent compiler optimizations.
var hdr Header
func BenchmarkTextprotoReadHeader(b *testing.B) {
bench := func(name, blob string) {
b.Run(name, func(b *testing.B) {
b.ReportAllocs()
r := bufio.NewReader(strings.NewReader(blob))
var err error
for i := 0; i < b.N; i++ {
r.Reset(strings.NewReader(blob))
hdr, err = ReadHeader(r)
if err != nil {
b.Fatal(err)
}
}
})
}
bench("1", testHeaderString1)
bench("2", testHeaderString2)
bench("3", testHeaderString3)
}
var mimeHdr textproto.MIMEHeader
func BenchmarkStdlibReadHeader(b *testing.B) {
bench := func(name, blob string) {
b.Run(name, func(b *testing.B) {
b.ReportAllocs()
r := bufio.NewReader(strings.NewReader(blob))
tr := textproto.NewReader(r)
var err error
for i := 0; i < b.N; i++ {
r.Reset(strings.NewReader(blob))
mimeHdr, err = tr.ReadMIMEHeader()
if err != nil {
b.Fatal(err)
}
}
})
}
bench("1", testHeaderString1)
bench("2", testHeaderString2)
bench("3", testHeaderString3)
}
go-message-0.15.0/textproto/example_test.go 0000664 0000000 0000000 00000000674 14060654707 0020735 0 ustar 00root root 0000000 0000000 package textproto_test
import (
"fmt"
"github.com/emersion/go-message/textproto"
)
func ExampleHeader() {
var h textproto.Header
h.Add("From", "")
h.Add("To", "")
h.Set("Subject", "Tonight's dinner")
fmt.Println("From: ", h.Get("From"))
fmt.Println("Has Received: ", h.Has("Received"))
fmt.Println("Header fields:")
fields := h.Fields()
for fields.Next() {
fmt.Println(" ", fields.Key())
}
}
go-message-0.15.0/textproto/header.go 0000664 0000000 0000000 00000040316 14060654707 0017470 0 ustar 00root root 0000000 0000000 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
}
go-message-0.15.0/textproto/header_test.go 0000664 0000000 0000000 00000044716 14060654707 0020537 0 ustar 00root root 0000000 0000000 package textproto
import (
"bufio"
"bytes"
"io"
"reflect"
"strings"
"testing"
)
var from = "Mitsuha Miyamizu "
var to = "Taki Tachibana "
var received2 = "from example.com by example.org"
func newTestHeader() Header {
var h Header
h.Add("From", from)
h.Add("To", to)
h.Add("Received", "from localhost by example.com")
h.Add("Received", received2)
return h
}
func collectHeaderFields(fields HeaderFields) []string {
var l []string
for fields.Next() {
l = append(l, fields.Key()+": "+fields.Value())
}
return l
}
func TestHeader(t *testing.T) {
h := newTestHeader()
if got := h.Get("From"); got != from {
t.Errorf("Get(\"From\") = %#v, want %#v", got, from)
}
if got := h.Get("Received"); got != received2 {
t.Errorf("Get(\"Received\") = %#v, want %#v", got, received2)
}
if got := h.Get("X-I-Dont-Exist"); got != "" {
t.Errorf("Get(non-existing) = %#v, want \"\"", got)
}
if !h.Has("From") {
t.Errorf("Has(\"From\") = false, want true")
}
if h.Has("X-I-Dont-Exist") {
t.Errorf("Has(non-existing) = true, want false")
}
if got := h.FieldsByKey("Received").Len(); got != 2 {
t.Errorf("FieldsByKey(\"Received\").Len() = %v, want %v", got, 2)
}
if got := h.FieldsByKey("X-I-Dont-Exist").Len(); got != 0 {
t.Errorf("FieldsByKey(non-existing).Len() = %v, want %v", got, 0)
}
l := collectHeaderFields(h.Fields())
want := []string{
"Received: from example.com by example.org",
"Received: from localhost by example.com",
"To: Taki Tachibana ",
"From: Mitsuha Miyamizu ",
}
if !reflect.DeepEqual(l, want) {
t.Errorf("Fields() reported incorrect values: got \n%#v\n but want \n%#v", l, want)
}
l = collectHeaderFields(h.FieldsByKey("Received"))
want = []string{
"Received: from example.com by example.org",
"Received: from localhost by example.com",
}
if !reflect.DeepEqual(l, want) {
t.Errorf("FieldsByKey(\"Received\") reported incorrect values: got \n%#v\n but want \n%#v", l, want)
}
if h.FieldsByKey("X-I-Dont-Exist").Next() {
t.Errorf("FieldsByKey(non-existing).Next() returned true, want false")
}
want = []string{
"from example.com by example.org",
"from localhost by example.com",
}
if l := h.Values("Received"); !reflect.DeepEqual(l, want) {
t.Errorf("Values(\"Received\") reported incorrect values: got \n%#v\n but want \n%#v", l, want)
}
}
func TestHeader_Set(t *testing.T) {
h := newTestHeader()
h.Set("From", to)
if got := h.Get("From"); got != to {
t.Errorf("Get(\"From\") = %#v after Set(), want %#v", got, to)
}
l := collectHeaderFields(h.FieldsByKey("From"))
want := []string{"From: Taki Tachibana "}
if !reflect.DeepEqual(l, want) {
t.Errorf("FieldsByKey(\"From\") reported incorrect values after Set(): got \n%#v\n but want \n%#v", l, want)
}
}
func TestHeader_Del(t *testing.T) {
h := newTestHeader()
h.Del("Received")
if h.Has("Received") {
t.Errorf("Has(\"Received\") = true after Del(), want false")
}
l := collectHeaderFields(h.FieldsByKey("Received"))
var want []string = nil
if !reflect.DeepEqual(l, want) {
t.Errorf("FieldsByKey(\"Received\") reported incorrect values after Del(): got \n%#v\n but want \n%#v", l, want)
}
}
func TestHeader_Fields_Del_multiple(t *testing.T) {
h := newTestHeader()
ok := false
fields := h.Fields()
for fields.Next() {
if fields.Key() == "Received" {
fields.Del()
ok = true
break
}
}
if !ok {
t.Fatal("Fields() didn't yield \"Received\"")
}
l := collectHeaderFields(h.FieldsByKey("Received"))
want := []string{"Received: from localhost by example.com"}
if !reflect.DeepEqual(l, want) {
t.Errorf("FieldsByKey(\"Received\") reported incorrect values after HeaderFields.Del(): got \n%#v\n but want \n%#v", l, want)
}
}
func TestHeader_Fields_Del_single(t *testing.T) {
h := newTestHeader()
ok := false
fields := h.Fields()
for fields.Next() {
if fields.Key() == "To" {
fields.Del()
ok = true
break
}
}
if !ok {
t.Fatal("Fields() didn't yield \"To\"")
}
if h.FieldsByKey("To").Next() {
t.Errorf("FieldsByKey(\"To\") returned a non-empty set")
}
}
func TestHeader_Fields_Del_all(t *testing.T) {
h := newTestHeader()
fields := h.Fields()
for fields.Next() {
fields.Del()
}
if h.Fields().Next() {
t.Errorf("Fields() returned a non-empty set")
}
}
func TestHeader_FieldsByKey_Del(t *testing.T) {
h := newTestHeader()
fields := h.FieldsByKey("Received")
if !fields.Next() {
t.Fatal("FieldsByKey(\"Received\").Next() = false, want true")
}
fields.Del()
l := collectHeaderFields(h.FieldsByKey("Received"))
want := []string{"Received: from localhost by example.com"}
if !reflect.DeepEqual(l, want) {
t.Errorf("FieldsByKey(\"Received\") reported incorrect values after HeaderFields.Del(): got \n%#v\n but want \n%#v", l, want)
}
}
const testHeader = "Received: from example.com by example.org\r\n" +
"Received: from localhost by example.com\r\n" +
"To: Taki Tachibana \r\n" +
"From: Mitsuha Miyamizu \r\n\r\n"
func TestReadHeader(t *testing.T) {
r := bufio.NewReader(strings.NewReader(testHeader))
h, err := ReadHeader(r)
if err != nil {
t.Fatalf("readHeader() returned error: %v", err)
}
l := collectHeaderFields(h.Fields())
want := []string{
"Received: from example.com by example.org",
"Received: from localhost by example.com",
"To: Taki Tachibana ",
"From: Mitsuha Miyamizu ",
}
if !reflect.DeepEqual(l, want) {
t.Errorf("Fields() reported incorrect values: got \n%#v\n but want \n%#v", l, want)
}
b := make([]byte, 1)
if _, err := r.Read(b); err != io.EOF {
t.Errorf("Read() didn't return EOF: %v", err)
}
}
const testInvalidHeader = "not valid: example\r\n"
func TestInvalidHeader(t *testing.T) {
r := bufio.NewReader(strings.NewReader(testInvalidHeader))
_, err := ReadHeader(r)
if err == nil {
t.Errorf("No error thrown")
}
}
const testHeaderWithoutBody = "Received: from example.com by example.org\r\n" +
"Received: from localhost by example.com\r\n" +
"To: Taki Tachibana \r\n" +
"From: Mitsuha Miyamizu \r\n"
func TestReadHeaderWithoutBody(t *testing.T) {
r := bufio.NewReader(strings.NewReader(testHeaderWithoutBody))
h, err := ReadHeader(r)
if err != nil {
t.Fatalf("readHeader() returned error: %v", err)
}
l := collectHeaderFields(h.Fields())
want := []string{
"Received: from example.com by example.org",
"Received: from localhost by example.com",
"To: Taki Tachibana ",
"From: Mitsuha Miyamizu ",
}
if !reflect.DeepEqual(l, want) {
t.Errorf("Fields() reported incorrect values: got \n%#v\n but want \n%#v", l, want)
}
b := make([]byte, 1)
if _, err := r.Read(b); err != io.EOF {
t.Errorf("Read() didn't return EOF: %v", err)
}
}
const testLFHeader = `From: contact@example.org
To: contact@example.org
Subject: A little message, just for you
Date: Wed, 11 May 2016 14:31:59 +0000
Message-ID: <0000000@localhost/>
Content-Type: text/plain
`
func TestReadHeader_lf(t *testing.T) {
r := bufio.NewReader(strings.NewReader(testLFHeader))
h, err := ReadHeader(r)
if err != nil {
t.Fatalf("readHeader() returned error: %v", err)
}
l := collectHeaderFields(h.Fields())
want := []string{
"From: contact@example.org",
"To: contact@example.org",
"Subject: A little message, just for you",
"Date: Wed, 11 May 2016 14:31:59 +0000",
"Message-Id: <0000000@localhost/>",
"Content-Type: text/plain",
}
if !reflect.DeepEqual(l, want) {
t.Errorf("Fields() reported incorrect values: got \n%#v\n but want \n%#v", l, want)
}
b := make([]byte, 1)
if _, err := r.Read(b); err != io.EOF {
t.Errorf("Read() didn't return EOF: %v", err)
}
}
func TestHeader_AddRaw(t *testing.T) {
dkimLine := `DKIM-Signature: a=rsa-sha256; bh=uI/rVH7mLBSWkJVvQYKz3TbpdI2BLZWTIMKcuo0KHO
I=; c=simple/simple; d=example.org; h=Subject:To:From; s=default; t=1577562184; v=1; b=;` + "\r\n"
valUnfolded := `a=rsa-sha256; bh=uI/rVH7mLBSWkJVvQYKz3TbpdI2BLZWTIMKcuo0KHO I=; c=simple/simple; d=example.org; h=Subject:To:From; s=default; t=1577562184; v=1; b=;`
h := newTestHeader()
h.AddRaw([]byte(dkimLine))
// 1. It should be possible to get value using any key case.
// 2. It should be un-folded.
if v := h.Get("Dkim-Signature"); v != valUnfolded {
t.Errorf("Get returned wrong value: got \n%v\n but want \n%v", v, valUnfolded)
}
var b bytes.Buffer
if err := WriteHeader(&b, h); err != nil {
t.Fatalf("WriteHeader() returned error: %v", err)
}
// 1. Header field name is not changed (to Dkim-Signature).
// 2. No folding is done.
wantHdr := dkimLine + testHeader
if b.String() != wantHdr {
t.Errorf("WriteHeader() wrote invalid data: got \n%v\n but want \n%v", b.String(), wantHdr)
}
}
func TestWriteHeader(t *testing.T) {
h := newTestHeader()
var b bytes.Buffer
if err := WriteHeader(&b, h); err != nil {
t.Fatalf("writeHeader() returned error: %v", err)
}
if b.String() != testHeader {
t.Errorf("writeHeader() wrote invalid data: got \n%v\n but want \n%v", b.String(), testHeader)
}
}
// RFC says key shouldn't have trailing spaces, but those appear in the wild, so
// we need to handle them.
const testHeaderWithWhitespace = "Subject \t : \t Hey \r\n" +
" \t there\r\n" +
"From: Mitsuha Miyamizu \r\n\r\n"
func TestHeaderWithWhitespace(t *testing.T) {
h, err := ReadHeader(bufio.NewReader(strings.NewReader(testHeaderWithWhitespace)))
if err != nil {
t.Fatalf("readHeader() returned error: %v", err)
}
l := collectHeaderFields(h.Fields())
want := []string{
"Subject: Hey there",
"From: Mitsuha Miyamizu ",
}
if !reflect.DeepEqual(l, want) {
t.Errorf("Fields() reported incorrect values: got \n%#v\n but want \n%#v", l, want)
}
var b bytes.Buffer
if err := WriteHeader(&b, h); err != nil {
t.Fatalf("writeHeader() returned error: %v", err)
}
if b.String() != testHeaderWithWhitespace {
t.Errorf("writeHeader() wrote invalid data: got \n%v\n but want \n%v", b.String(), testHeaderWithWhitespace)
}
}
var formatHeaderFieldTests = []struct {
k, v string
formatted string
}{
{
k: "From",
v: "Mitsuha Miyamizu ",
formatted: "From: Mitsuha Miyamizu \r\n",
},
{
k: "Subject",
v: "This is a very long subject, much longer than just the 76 characters limit that applies to message header fields",
formatted: "Subject: This is a very long subject, much longer than just the 76\r\n characters limit that applies to message header fields\r\n",
},
{
k: "Subject",
v: "This is yet \t another subject \t with many whitespace characters",
formatted: "Subject: This is yet \t another subject \t \r\n with many whitespace characters\r\n",
},
{
k: "In-Reply-To",
v: "",
formatted: "In-Reply-To: \r\n",
},
{
k: "Subject",
v: "=?utf-8?q?=E2=80=9CDeveloper_reads_customer_requested_change.=E2=80=9D=0A?= =?utf-8?q?=0ACaravaggio=0A=0AOil_on...?=",
formatted: "Subject: =?utf-8?q?=E2=80=9CDeveloper_reads_customer_requested_change.=E2=80=9D=0A?= =?utf-8?q?=0ACaravaggio=0A=0AOil_on...?=\r\n",
},
// Spaces should not appear in RFC2047-encoded string but it should be fine in case they do
{
k: "Subject",
v: "=?utf-8?q?=E2=80=9CShort subject=E2=80=9D=0A?= =?utf-8?q?=0AAuthor=0A=0AOil_on...?=",
formatted: "Subject: =?utf-8?q?=E2=80=9CShort subject=E2=80=9D=0A?=\r\n =?utf-8?q?=0AAuthor=0A=0AOil_on...?=\r\n",
},
{
k: "Subject",
v: "=?utf-8?q?=E2=80=9CVery long subject very long subject very long subject very long subject=E2=80=9D=0A?= =?utf-8?q?=0ALong second part of subject long second part of subject long second part of subject long subject=0A=0AOil_on...?=",
formatted: "Subject: =?utf-8?q?=E2=80=9CVery long subject very long subject very long\r\n subject very long subject=E2=80=9D=0A?= =?utf-8?q?=0ALong second part of\r\n subject long second part of subject long second part of subject long\r\n subject=0A=0AOil_on...?=\r\n",
},
{
k: "Subject",
v: "InCaseOfVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongStringWeStillShouldComplyToTheHardLimitOf998Symbols",
formatted: "Subject: InCaseOfVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongStringWeStillSho\r\n uldComplyToTheHardLimitOf998Symbols\r\n",
},
{
k: "Bcc",
v: "",
formatted: "Bcc: \r\n",
},
{
k: "Bcc",
v: " ",
formatted: "Bcc: \r\n",
},
}
func TestWriteHeader_continued(t *testing.T) {
for _, test := range formatHeaderFieldTests {
var h Header
h.Add(test.k, test.v)
var b bytes.Buffer
if err := WriteHeader(&b, h); err != nil {
t.Fatalf("writeHeader() returned error: %v", err)
}
if b.String() != test.formatted+"\r\n" {
t.Errorf("Expected formatted header to be \n%v\n but got \n%v", test.formatted+"\r\n", b.String())
}
}
}
var incorrectFormatHeaderFieldTests = []struct {
k, v string
}{
{
k: "DKIM Signature",
v: "v=1; h=From; d=example.org; b=AuUoFEfDxTDkHlLXSZEpZj79LICEps6eda7W3deTVFOk4yAUoqOB4nujc7YopdG5dWLSdNg6x NAZpOPr+kHxt1IrE+NahM6L/LbvaHutKVdkLLkpVaVVQPzeRDI009SO2Il5Lu7rDNH6mZckBdrI x0orEtZV4bmp/YzhwvcubU4=\r\n",
},
{
// Unicode, Cyrillic
k: "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a",
v: "Value",
},
{
k: "Header:",
v: "Value",
},
{
k: "DKIM-Signature",
v: "v=1;\r\n h=From:To:Reply-To:Subject:Message-ID:References:In-Reply-To:MIME-Version;\r\n d=example.org\r\n",
},
{
k: "DKIM-Signature",
v: "v=1;\n h=From:To:Reply-To:Subject:Message-ID:References:In-Reply-To:MIME-Version; d=example.org",
},
{
k: "DKIM-Signature",
v: "v=1;\r h=From:To:Reply-To:Subject:Message-ID:References:In-Reply-To:MIME-Version; d=example.org",
},
}
func TestWriteHeader_failed(t *testing.T) {
for _, test := range incorrectFormatHeaderFieldTests {
var h Header
h.Add(test.k, test.v)
var b bytes.Buffer
if err := WriteHeader(&b, h); err == nil {
t.Errorf("Expected header \n%v: %v\n to be incorrect, but it was accepted", test.k, test.v)
}
}
}
var incorrectFormatMultipleHeaderFieldTests = []struct {
k1, k2, v1, v2 string
}{
{
// Incorrect first
k1: "DKIM Signature",
v1: "v=1; h=From; d=example.org; b=AuUoFEfDxTDkHlLXSZEpZj79LICEps6eda7W3deTVFOk4yAUoqOB4nujc7YopdG5dWLSdNg6x NAZpOPr+kHxt1IrE+NahM6L/LbvaHutKVdkLLkpVaVVQPzeRDI009SO2Il5Lu7rDNH6mZckBdrI x0orEtZV4bmp/YzhwvcubU4=\r\n",
k2: "From",
v2: "alice@example.com",
},
{
// Incorrect both
k1: "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a",
v1: "Value",
k2: "Header:",
v2: "Value",
},
{
// Incorrect second
k1: "DKIM-Signature",
v1: "v=1; h=From:To:Reply-To:Subject:Message-ID:References:In-Reply-To:MIME-Version; d=example.org",
k2: "DKIM-Signature",
v2: "v=1;\r\n h=From:To:Reply-To:Subject:Message-ID:References:In-Reply-To:MIME-Version;\r\n d=example.org\r\n",
},
}
func TestWriteHeader_failed_multiple(t *testing.T) {
for _, test := range incorrectFormatMultipleHeaderFieldTests {
var h Header
h.Add(test.k1, test.v1)
h.Add(test.k2, test.v2)
var b bytes.Buffer
if err := WriteHeader(&b, h); err == nil {
t.Errorf("Expected headers \n%v: %v\n%v: %v\n to be incorrect, but it was accepted", test.k1, test.v2, test.k2, test.v2)
}
}
}
func TestHeaderFromMap(t *testing.T) {
m := map[string][]string{
"Received": []string{"from example.com by example.org", "from localhost by example.com"},
"To": []string{"Taki Tachibana "},
"From": []string{"Mitsuha Miyamizu "},
}
h := HeaderFromMap(m)
l := collectHeaderFields(h.Fields())
want := []string{
"From: Mitsuha Miyamizu ",
"Received: from example.com by example.org",
"Received: from localhost by example.com",
"To: Taki Tachibana ",
}
if !reflect.DeepEqual(l, want) {
t.Errorf("Fields() reported incorrect values: got \n%#v\n but want \n%#v", l, want)
}
}
func TestHeader_Map(t *testing.T) {
want := map[string][]string{
"Received": []string{"from example.com by example.org", "from localhost by example.com"},
"To": []string{"Taki Tachibana "},
"From": []string{"Mitsuha Miyamizu "},
}
h := newTestHeader()
m := h.Map()
if !reflect.DeepEqual(m, want) {
t.Errorf("Fields(): got \n%#v\n but want \n%#v", m, want)
}
}
go-message-0.15.0/textproto/multipart.go 0000664 0000000 0000000 00000031233 14060654707 0020257 0 ustar 00root root 0000000 0000000 // 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
}
go-message-0.15.0/textproto/multipart_test.go 0000664 0000000 0000000 00000063263 14060654707 0021326 0 ustar 00root root 0000000 0000000 // 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
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/textproto"
"reflect"
"strings"
"testing"
)
func TestBoundaryLine(t *testing.T) {
mr := NewMultipartReader(strings.NewReader(""), "myBoundary")
if !mr.isBoundaryDelimiterLine([]byte("--myBoundary\r\n")) {
t.Error("expected")
}
if !mr.isBoundaryDelimiterLine([]byte("--myBoundary \r\n")) {
t.Error("expected")
}
if !mr.isBoundaryDelimiterLine([]byte("--myBoundary \n")) {
t.Error("expected")
}
if mr.isBoundaryDelimiterLine([]byte("--myBoundary bogus \n")) {
t.Error("expected fail")
}
if mr.isBoundaryDelimiterLine([]byte("--myBoundary bogus--")) {
t.Error("expected fail")
}
}
func escapeString(v string) string {
bytes, _ := json.Marshal(v)
return string(bytes)
}
func expectEq(t *testing.T, expected, actual, what string) {
if expected == actual {
return
}
t.Errorf("Unexpected value for %s; got %s (len %d) but expected: %s (len %d)",
what, escapeString(actual), len(actual), escapeString(expected), len(expected))
}
var longLine = strings.Repeat("\n\n\r\r\r\n\r\000", (1<<20)/8)
func testMultipartBody(sep string) string {
testBody := `
This is a multi-part message. This line is ignored.
--MyBoundary
Header1: value1
HEADER2: value2
foo-bar: baz
My value
The end.
--MyBoundary
name: bigsection
[longline]
--MyBoundary
Header1: value1b
HEADER2: value2b
foo-bar: bazb
Line 1
Line 2
Line 3 ends in a newline, but just one.
--MyBoundary
never read data
--MyBoundary--
useless trailer
`
testBody = strings.ReplaceAll(testBody, "\n", sep)
return strings.Replace(testBody, "[longline]", longLine, 1)
}
func TestMultipart(t *testing.T) {
bodyReader := strings.NewReader(testMultipartBody("\r\n"))
testMultipart(t, bodyReader, false)
}
func TestMultipartOnlyNewlines(t *testing.T) {
bodyReader := strings.NewReader(testMultipartBody("\n"))
testMultipart(t, bodyReader, true)
}
func TestMultipartSlowInput(t *testing.T) {
bodyReader := strings.NewReader(testMultipartBody("\r\n"))
testMultipart(t, &slowReader{bodyReader}, false)
}
func testMultipart(t *testing.T, r io.Reader, onlyNewlines bool) {
t.Parallel()
reader := NewMultipartReader(r, "MyBoundary")
buf := new(bytes.Buffer)
// Part1
part, err := reader.NextPart()
if part == nil || err != nil {
t.Error("Expected part1")
return
}
if x := part.Header.Get("Header1"); x != "value1" {
t.Errorf("part.Header.Get(%q) = %q, want %q", "Header1", x, "value1")
}
if x := part.Header.Get("foo-bar"); x != "baz" {
t.Errorf("part.Header.Get(%q) = %q, want %q", "foo-bar", x, "baz")
}
if x := part.Header.Get("Foo-Bar"); x != "baz" {
t.Errorf("part.Header.Get(%q) = %q, want %q", "Foo-Bar", x, "baz")
}
buf.Reset()
if _, err := io.Copy(buf, part); err != nil {
t.Errorf("part 1 copy: %v", err)
}
adjustNewlines := func(s string) string {
if onlyNewlines {
return strings.ReplaceAll(s, "\r\n", "\n")
}
return s
}
expectEq(t, adjustNewlines("My value\r\nThe end."), buf.String(), "Value of first part")
// Part2
part, err = reader.NextPart()
if err != nil {
t.Fatalf("Expected part2; got: %v", err)
return
}
if e, g := "bigsection", part.Header.Get("name"); e != g {
t.Errorf("part2's name header: expected %q, got %q", e, g)
}
buf.Reset()
if _, err := io.Copy(buf, part); err != nil {
t.Errorf("part 2 copy: %v", err)
}
s := buf.String()
if len(s) != len(longLine) {
t.Errorf("part2 body expected long line of length %d; got length %d",
len(longLine), len(s))
}
if s != longLine {
t.Errorf("part2 long body didn't match")
}
// Part3
part, err = reader.NextPart()
if part == nil || err != nil {
t.Error("Expected part3")
return
}
if part.Header.Get("foo-bar") != "bazb" {
t.Error("Expected foo-bar: bazb")
}
buf.Reset()
if _, err := io.Copy(buf, part); err != nil {
t.Errorf("part 3 copy: %v", err)
}
expectEq(t, adjustNewlines("Line 1\r\nLine 2\r\nLine 3 ends in a newline, but just one.\r\n"),
buf.String(), "body of part 3")
// Part4
part, err = reader.NextPart()
if part == nil || err != nil {
t.Error("Expected part 4 without errors")
return
}
// Non-existent part5
part, err = reader.NextPart()
if part != nil {
t.Error("Didn't expect a fifth part.")
}
if err != io.EOF {
t.Errorf("On fifth part expected io.EOF; got %v", err)
}
}
func TestVariousTextLineEndings(t *testing.T) {
tests := [...]string{
"Foo\nBar",
"Foo\nBar\n",
"Foo\r\nBar",
"Foo\r\nBar\r\n",
"Foo\rBar",
"Foo\rBar\r",
"\x00\x01\x02\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10",
}
for testNum, expectedBody := range tests {
body := "--BOUNDARY\r\n" +
"Content-Disposition: form-data; name=\"value\"\r\n" +
"\r\n" +
expectedBody +
"\r\n--BOUNDARY--\r\n"
bodyReader := strings.NewReader(body)
reader := NewMultipartReader(bodyReader, "BOUNDARY")
buf := new(bytes.Buffer)
part, err := reader.NextPart()
if part == nil {
t.Errorf("Expected a body part on text %d", testNum)
continue
}
if err != nil {
t.Errorf("Unexpected error on text %d: %v", testNum, err)
continue
}
written, err := io.Copy(buf, part)
expectEq(t, expectedBody, buf.String(), fmt.Sprintf("test %d", testNum))
if err != nil {
t.Errorf("Error copying multipart; bytes=%v, error=%v", written, err)
}
part, err = reader.NextPart()
if part != nil {
t.Errorf("Unexpected part in test %d", testNum)
}
if err != io.EOF {
t.Errorf("On test %d expected io.EOF; got %v", testNum, err)
}
}
}
type maliciousReader struct {
t *testing.T
n int
}
const maxReadThreshold = 1 << 20
func (mr *maliciousReader) Read(b []byte) (n int, err error) {
mr.n += len(b)
if mr.n >= maxReadThreshold {
mr.t.Fatal("too much was read")
return 0, io.EOF
}
return len(b), nil
}
func TestLineLimit(t *testing.T) {
mr := &maliciousReader{t: t}
r := NewMultipartReader(mr, "fooBoundary")
part, err := r.NextPart()
if part != nil {
t.Errorf("unexpected part read")
}
if err == nil {
t.Errorf("expected an error")
}
if mr.n >= maxReadThreshold {
t.Errorf("expected to read < %d bytes; read %d", maxReadThreshold, mr.n)
}
}
func TestMultipartTruncated(t *testing.T) {
testBody := `
This is a multi-part message. This line is ignored.
--MyBoundary
foo-bar: baz
Oh no, premature EOF!
`
body := strings.ReplaceAll(testBody, "\n", "\r\n")
bodyReader := strings.NewReader(body)
r := NewMultipartReader(bodyReader, "MyBoundary")
part, err := r.NextPart()
if err != nil {
t.Fatalf("didn't get a part")
}
_, err = io.Copy(ioutil.Discard, part)
if err != io.ErrUnexpectedEOF {
t.Fatalf("expected error io.ErrUnexpectedEOF; got %v", err)
}
}
type slowReader struct {
r io.Reader
}
func (s *slowReader) Read(p []byte) (int, error) {
if len(p) == 0 {
return s.r.Read(p)
}
return s.r.Read(p[:1])
}
type sentinelReader struct {
// done is closed when this reader is read from.
done chan struct{}
}
func (s *sentinelReader) Read([]byte) (int, error) {
if s.done != nil {
close(s.done)
s.done = nil
}
return 0, io.EOF
}
// TestMultipartStreamReadahead tests that PartReader does not block
// on reading past the end of a part, ensuring that it can be used on
// a stream like multipart/x-mixed-replace. See golang.org/issue/15431
func TestMultipartStreamReadahead(t *testing.T) {
testBody1 := `
This is a multi-part message. This line is ignored.
--MyBoundary
foo-bar: baz
Body
--MyBoundary
`
testBody2 := `foo-bar: bop
Body 2
--MyBoundary--
`
done1 := make(chan struct{})
reader := NewMultipartReader(
io.MultiReader(
strings.NewReader(testBody1),
&sentinelReader{done1},
strings.NewReader(testBody2)),
"MyBoundary")
var i int
readPart := func(hdr textproto.MIMEHeader, body string) {
part, err := reader.NextPart()
if part == nil || err != nil {
t.Fatalf("Part %d: NextPart failed: %v", i, err)
}
if !reflect.DeepEqual(headerToMap(part.Header), hdr) {
t.Errorf("Part %d: part.Header = %v, want %v", i, part.Header, hdr)
}
data, err := ioutil.ReadAll(part)
expectEq(t, body, string(data), fmt.Sprintf("Part %d body", i))
if err != nil {
t.Fatalf("Part %d: ReadAll failed: %v", i, err)
}
i++
}
readPart(textproto.MIMEHeader{"Foo-Bar": {"baz"}}, "Body")
select {
case <-done1:
t.Errorf("Reader read past second boundary")
default:
}
readPart(textproto.MIMEHeader{"Foo-Bar": {"bop"}}, "Body 2")
}
func TestLineContinuation(t *testing.T) {
// This body, extracted from an email, contains headers that span multiple
// lines.
// TODO: The original mail ended with a double-newline before the
// final delimiter; this was manually edited to use a CRLF.
testBody :=
"\n--Apple-Mail-2-292336769\nContent-Transfer-Encoding: 7bit\nContent-Type: text/plain;\n\tcharset=US-ASCII;\n\tdelsp=yes;\n\tformat=flowed\n\nI'm finding the same thing happening on my system (10.4.1).\n\n\n--Apple-Mail-2-292336769\nContent-Transfer-Encoding: quoted-printable\nContent-Type: text/html;\n\tcharset=ISO-8859-1\n\nI'm finding the same thing =\nhappening on my system (10.4.1).=A0 But I built it with XCode =\n2.0.=\nHTML>=\n\r\n--Apple-Mail-2-292336769--\n"
r := NewMultipartReader(strings.NewReader(testBody), "Apple-Mail-2-292336769")
for i := 0; i < 2; i++ {
part, err := r.NextPart()
if err != nil {
t.Fatalf("didn't get a part")
}
var buf bytes.Buffer
n, err := io.Copy(&buf, part)
if err != nil {
t.Errorf("error reading part: %v\nread so far: %q", err, buf.String())
}
if n <= 0 {
t.Errorf("read %d bytes; expected >0", n)
}
}
}
// Test parsing an image attachment from gmail, which previously failed.
func TestNested(t *testing.T) {
r := strings.NewReader(`--e89a8ff1c1e83553e304be640612
Content-Type: multipart/alternative; boundary=e89a8ff1c1e83553e004be640610
--e89a8ff1c1e83553e004be640610
Content-Type: text/plain; charset=UTF-8
*body*
--e89a8ff1c1e83553e004be640610
Content-Type: text/html; charset=UTF-8
body
--e89a8ff1c1e83553e004be640610--
--e89a8ff1c1e83553e304be640612
Content-Type: image/png; name="x.png"
Content-Disposition: attachment;
filename="x.png"
Content-Transfer-Encoding: base64
X-Attachment-Id: f_h1edgigu0
iVBORw0KGgoAAAANSUhEUgAAAagAAADrCAIAAACza5XhAAAKMWlDQ1BJQ0MgUHJvZmlsZQAASImd
lndUU9kWh8+9N71QkhCKlNBraFICSA29SJEuKjEJEErAkAAiNkRUcERRkaYIMijggKNDkbEiioUB
8b2kqeGaj4aTNftesu5mob4pr07ecMywRwLBvDCJOksqlUyldAZD7g9fxIZRWWPMvXRNJROJRBIG
Y7Vx0mva1HAwYqibdKONXye3dW4iUonhWFJnqK7OaanU1gGkErFYEgaj0cg8wK+zVPh2ziwnHy07
U8lYTNapezSzOuevRwLB7CFkqQQCwaJDiBQIBIJFhwh8AoFg0SHUqQUCASRJKkwkhMy/JfODWPEJ
BIJFhwh8AoFg0TFnQqQ55GtPFopcJsN97e1nYtNuIBYeGBgYCmYrmE3jZ05iaGAoMX0xzxkWz6Hv
yO7WvrlwzA0uLzrD+VkKqViwl9IfTBVNFMyc/x9alloiPPlqhQAAAABJRU5ErkJggg==
--e89a8ff1c1e83553e304be640612--`)
mr := NewMultipartReader(r, "e89a8ff1c1e83553e304be640612")
p, err := mr.NextPart()
if err != nil {
t.Fatalf("error reading first section (alternative): %v", err)
}
// Read the inner text/plain and text/html sections of the multipart/alternative.
mr2 := NewMultipartReader(p, "e89a8ff1c1e83553e004be640610")
p, err = mr2.NextPart()
if err != nil {
t.Fatalf("reading text/plain part: %v", err)
}
if b, err := ioutil.ReadAll(p); string(b) != "*body*\n" || err != nil {
t.Fatalf("reading text/plain part: got %q, %v", b, err)
}
p, err = mr2.NextPart()
if err != nil {
t.Fatalf("reading text/html part: %v", err)
}
if b, err := ioutil.ReadAll(p); string(b) != "body\n" || err != nil {
t.Fatalf("reading text/html part: got %q, %v", b, err)
}
p, err = mr2.NextPart()
if err != io.EOF {
t.Fatalf("final inner NextPart = %v; want io.EOF", err)
}
// Back to the outer multipart/mixed, reading the image attachment.
_, err = mr.NextPart()
if err != nil {
t.Fatalf("error reading the image attachment at the end: %v", err)
}
_, err = mr.NextPart()
if err != io.EOF {
t.Fatalf("final outer NextPart = %v; want io.EOF", err)
}
}
func headerToMap(h Header) textproto.MIMEHeader {
m := make(textproto.MIMEHeader, h.Len())
fields := h.Fields()
for fields.Next() {
m[fields.Key()] = append(m[fields.Key()], fields.Value())
}
return m
}
func mapToHeader(m textproto.MIMEHeader) Header {
var h Header
for k, values := range m {
for _, v := range values {
h.Add(k, v)
}
}
return h
}
type headerBody struct {
header textproto.MIMEHeader
body string
}
func formData(key, value string) headerBody {
return headerBody{
textproto.MIMEHeader{
"Content-Type": {"text/plain; charset=ISO-8859-1"},
"Content-Disposition": {"form-data; name=" + key},
},
value,
}
}
type parseTest struct {
name string
in, sep string
want []headerBody
}
var parseTests = []parseTest{
// Actual body from App Engine on a blob upload. The final part (the
// Content-Type: message/external-body) is what App Engine replaces
// the uploaded file with. The other form fields (prefixed with
// "other" in their form-data name) are unchanged. A bug was
// reported with blob uploads failing when the other fields were
// empty. This was the MIME POST body that previously failed.
{
name: "App Engine post",
sep: "00151757727e9583fd04bfbca4c6",
in: "--00151757727e9583fd04bfbca4c6\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Disposition: form-data; name=otherEmpty1\r\n\r\n--00151757727e9583fd04bfbca4c6\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Disposition: form-data; name=otherFoo1\r\n\r\nfoo\r\n--00151757727e9583fd04bfbca4c6\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Disposition: form-data; name=otherFoo2\r\n\r\nfoo\r\n--00151757727e9583fd04bfbca4c6\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Disposition: form-data; name=otherEmpty2\r\n\r\n--00151757727e9583fd04bfbca4c6\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Disposition: form-data; name=otherRepeatFoo\r\n\r\nfoo\r\n--00151757727e9583fd04bfbca4c6\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Disposition: form-data; name=otherRepeatFoo\r\n\r\nfoo\r\n--00151757727e9583fd04bfbca4c6\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Disposition: form-data; name=otherRepeatEmpty\r\n\r\n--00151757727e9583fd04bfbca4c6\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Disposition: form-data; name=otherRepeatEmpty\r\n\r\n--00151757727e9583fd04bfbca4c6\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Disposition: form-data; name=submit\r\n\r\nSubmit\r\n--00151757727e9583fd04bfbca4c6\r\nContent-Type: message/external-body; charset=ISO-8859-1; blob-key=AHAZQqG84qllx7HUqO_oou5EvdYQNS3Mbbkb0RjjBoM_Kc1UqEN2ygDxWiyCPulIhpHRPx-VbpB6RX4MrsqhWAi_ZxJ48O9P2cTIACbvATHvg7IgbvZytyGMpL7xO1tlIvgwcM47JNfv_tGhy1XwyEUO8oldjPqg5Q\r\nContent-Disposition: form-data; name=file; filename=\"fall.png\"\r\n\r\nContent-Type: image/png\r\nContent-Length: 232303\r\nX-AppEngine-Upload-Creation: 2012-05-10 23:14:02.715173\r\nContent-MD5: MzRjODU1ZDZhZGU1NmRlOWEwZmMwMDdlODBmZTA0NzA=\r\nContent-Disposition: form-data; name=file; filename=\"fall.png\"\r\n\r\n\r\n--00151757727e9583fd04bfbca4c6--",
want: []headerBody{
formData("otherEmpty1", ""),
formData("otherFoo1", "foo"),
formData("otherFoo2", "foo"),
formData("otherEmpty2", ""),
formData("otherRepeatFoo", "foo"),
formData("otherRepeatFoo", "foo"),
formData("otherRepeatEmpty", ""),
formData("otherRepeatEmpty", ""),
formData("submit", "Submit"),
{textproto.MIMEHeader{
"Content-Type": {"message/external-body; charset=ISO-8859-1; blob-key=AHAZQqG84qllx7HUqO_oou5EvdYQNS3Mbbkb0RjjBoM_Kc1UqEN2ygDxWiyCPulIhpHRPx-VbpB6RX4MrsqhWAi_ZxJ48O9P2cTIACbvATHvg7IgbvZytyGMpL7xO1tlIvgwcM47JNfv_tGhy1XwyEUO8oldjPqg5Q"},
"Content-Disposition": {"form-data; name=file; filename=\"fall.png\""},
}, "Content-Type: image/png\r\nContent-Length: 232303\r\nX-AppEngine-Upload-Creation: 2012-05-10 23:14:02.715173\r\nContent-MD5: MzRjODU1ZDZhZGU1NmRlOWEwZmMwMDdlODBmZTA0NzA=\r\nContent-Disposition: form-data; name=file; filename=\"fall.png\"\r\n\r\n"},
},
},
// Single empty part, ended with --boundary immediately after headers.
{
name: "single empty part, --boundary",
sep: "abc",
in: "--abc\r\nFoo: bar\r\n\r\n--abc--",
want: []headerBody{
{textproto.MIMEHeader{"Foo": {"bar"}}, ""},
},
},
// Single empty part, ended with \r\n--boundary immediately after headers.
{
name: "single empty part, \r\n--boundary",
sep: "abc",
in: "--abc\r\nFoo: bar\r\n\r\n\r\n--abc--",
want: []headerBody{
{textproto.MIMEHeader{"Foo": {"bar"}}, ""},
},
},
// Final part empty.
{
name: "final part empty",
sep: "abc",
in: "--abc\r\nFoo: bar\r\n\r\n--abc\r\nFoo2: bar2\r\n\r\n--abc--",
want: []headerBody{
{textproto.MIMEHeader{"Foo": {"bar"}}, ""},
{textproto.MIMEHeader{"Foo2": {"bar2"}}, ""},
},
},
// Final part empty with newlines after final separator.
{
name: "final part empty then crlf",
sep: "abc",
in: "--abc\r\nFoo: bar\r\n\r\n--abc--\r\n",
want: []headerBody{
{textproto.MIMEHeader{"Foo": {"bar"}}, ""},
},
},
// Final part empty with lwsp-chars after final separator.
{
name: "final part empty then lwsp",
sep: "abc",
in: "--abc\r\nFoo: bar\r\n\r\n--abc-- \t",
want: []headerBody{
{textproto.MIMEHeader{"Foo": {"bar"}}, ""},
},
},
// No parts (empty form as submitted by Chrome)
{
name: "no parts",
sep: "----WebKitFormBoundaryQfEAfzFOiSemeHfA",
in: "------WebKitFormBoundaryQfEAfzFOiSemeHfA--\r\n",
want: []headerBody{},
},
// Part containing data starting with the boundary, but with additional suffix.
{
name: "fake separator as data",
sep: "sep",
in: "--sep\r\nFoo: bar\r\n\r\n--sepFAKE\r\n--sep--",
want: []headerBody{
{textproto.MIMEHeader{"Foo": {"bar"}}, "--sepFAKE"},
},
},
// Part containing a boundary with whitespace following it.
{
name: "boundary with whitespace",
sep: "sep",
in: "--sep \r\nFoo: bar\r\n\r\ntext\r\n--sep--",
want: []headerBody{
{textproto.MIMEHeader{"Foo": {"bar"}}, "text"},
},
},
// With ignored leading line.
{
name: "leading line",
sep: "MyBoundary",
in: strings.Replace(`This is a multi-part message. This line is ignored.
--MyBoundary
foo: bar
--MyBoundary--`, "\n", "\r\n", -1),
want: []headerBody{
{textproto.MIMEHeader{"Foo": {"bar"}}, ""},
},
},
// Issue 10616; minimal
{
name: "issue 10616 minimal",
sep: "sep",
in: "--sep \r\nFoo: bar\r\n\r\n" +
"a\r\n" +
"--sep_alt\r\n" +
"b\r\n" +
"\r\n--sep--",
want: []headerBody{
{textproto.MIMEHeader{"Foo": {"bar"}}, "a\r\n--sep_alt\r\nb\r\n"},
},
},
// Issue 10616; full example from bug.
{
name: "nested separator prefix is outer separator",
sep: "----=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9",
in: strings.Replace(`------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9
Content-Type: multipart/alternative; boundary="----=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt"
------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 8bit
This is a multi-part message in MIME format.
------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: 8bit
html things
------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt--
------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9--`, "\n", "\r\n", -1),
want: []headerBody{
{textproto.MIMEHeader{"Content-Type": {`multipart/alternative; boundary="----=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt"`}},
strings.Replace(`------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 8bit
This is a multi-part message in MIME format.
------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: 8bit
html things
------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt--`, "\n", "\r\n", -1),
},
},
},
// Issue 12662: Check that we don't consume the leading \r if the peekBuffer
// ends in '\r\n--separator-'
{
name: "peek buffer boundary condition",
sep: "00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db",
in: strings.Replace(`--00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db
Content-Disposition: form-data; name="block"; filename="block"
Content-Type: application/octet-stream
`+strings.Repeat("A", peekBufferSize-65)+"\n--00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db--", "\n", "\r\n", -1),
want: []headerBody{
{textproto.MIMEHeader{"Content-Type": {`application/octet-stream`}, "Content-Disposition": {`form-data; name="block"; filename="block"`}},
strings.Repeat("A", peekBufferSize-65),
},
},
},
// Issue 12662: Same test as above with \r\n at the end
{
name: "peek buffer boundary condition",
sep: "00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db",
in: strings.Replace(`--00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db
Content-Disposition: form-data; name="block"; filename="block"
Content-Type: application/octet-stream
`+strings.Repeat("A", peekBufferSize-65)+"\n--00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db--\n", "\n", "\r\n", -1),
want: []headerBody{
{textproto.MIMEHeader{"Content-Type": {`application/octet-stream`}, "Content-Disposition": {`form-data; name="block"; filename="block"`}},
strings.Repeat("A", peekBufferSize-65),
},
},
},
// Issue 12662v2: We want to make sure that for short buffers that end with
// '\r\n--separator-' we always consume at least one (valid) symbol from the
// peekBuffer
{
name: "peek buffer boundary condition",
sep: "aaaaaaaaaa00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db",
in: strings.Replace(`--aaaaaaaaaa00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db
Content-Disposition: form-data; name="block"; filename="block"
Content-Type: application/octet-stream
`+strings.Repeat("A", peekBufferSize)+"\n--aaaaaaaaaa00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db--", "\n", "\r\n", -1),
want: []headerBody{
{textproto.MIMEHeader{"Content-Type": {`application/octet-stream`}, "Content-Disposition": {`form-data; name="block"; filename="block"`}},
strings.Repeat("A", peekBufferSize),
},
},
},
// Context: https://github.com/camlistore/camlistore/issues/642
// If the file contents in the form happens to have a size such as:
// size = peekBufferSize - (len("\n--") + len(boundary) + len("\r") + 1), (modulo peekBufferSize)
// then peekBufferSeparatorIndex was wrongly returning (-1, false), which was leading to an nCopy
// cut such as:
// "somedata\r| |\n--Boundary\r" (instead of "somedata| |\r\n--Boundary\r"), which was making the
// subsequent Read miss the boundary.
{
name: "safeCount off by one",
sep: "08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74",
in: strings.Replace(`--08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74
Content-Disposition: form-data; name="myfile"; filename="my-file.txt"
Content-Type: application/octet-stream
`, "\n", "\r\n", -1) +
strings.Repeat("A", peekBufferSize-(len("\n--")+len("08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74")+len("\r")+1)) +
strings.Replace(`
--08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74
Content-Disposition: form-data; name="key"
val
--08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74--
`, "\n", "\r\n", -1),
want: []headerBody{
{textproto.MIMEHeader{"Content-Type": {`application/octet-stream`}, "Content-Disposition": {`form-data; name="myfile"; filename="my-file.txt"`}},
strings.Repeat("A", peekBufferSize-(len("\n--")+len("08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74")+len("\r")+1)),
},
{textproto.MIMEHeader{"Content-Disposition": {`form-data; name="key"`}},
"val",
},
},
},
roundTripParseTest(),
}
func TestParse(t *testing.T) {
Cases:
for _, tt := range parseTests {
r := NewMultipartReader(strings.NewReader(tt.in), tt.sep)
got := []headerBody{}
for {
p, err := r.NextPart()
if err == io.EOF {
break
}
if err != nil {
t.Errorf("in test %q, NextPart: %v", tt.name, err)
continue Cases
}
pbody, err := ioutil.ReadAll(p)
if err != nil {
t.Errorf("in test %q, error reading part: %v", tt.name, err)
continue Cases
}
got = append(got, headerBody{headerToMap(p.Header), string(pbody)})
}
if !reflect.DeepEqual(tt.want, got) {
t.Errorf("test %q:\n got: %v\nwant: %v", tt.name, got, tt.want)
if len(tt.want) != len(got) {
t.Errorf("test %q: got %d parts, want %d", tt.name, len(got), len(tt.want))
} else if len(got) > 1 {
for pi, wantPart := range tt.want {
if !reflect.DeepEqual(wantPart, got[pi]) {
t.Errorf("test %q, part %d:\n got: %v\nwant: %v", tt.name, pi, got[pi], wantPart)
}
}
}
}
}
}
func partsFromReader(r *MultipartReader) ([]headerBody, error) {
got := []headerBody{}
for {
p, err := r.NextPart()
if err == io.EOF {
return got, nil
}
if err != nil {
return nil, fmt.Errorf("NextPart: %v", err)
}
pbody, err := ioutil.ReadAll(p)
if err != nil {
return nil, fmt.Errorf("error reading part: %v", err)
}
got = append(got, headerBody{headerToMap(p.Header), string(pbody)})
}
}
func roundTripParseTest() parseTest {
t := parseTest{
name: "round trip",
want: []headerBody{
formData("empty", ""),
formData("lf", "\n"),
formData("cr", "\r"),
formData("crlf", "\r\n"),
formData("foo", "bar"),
},
}
var buf bytes.Buffer
w := NewMultipartWriter(&buf)
for _, p := range t.want {
pw, err := w.CreatePart(mapToHeader(p.header))
if err != nil {
panic(err)
}
_, err = pw.Write([]byte(p.body))
if err != nil {
panic(err)
}
}
w.Close()
t.in = buf.String()
t.sep = w.Boundary()
return t
}
func TestNoBoundary(t *testing.T) {
mr := NewMultipartReader(strings.NewReader(""), "")
_, err := mr.NextPart()
if got, want := fmt.Sprint(err), "multipart: boundary is empty"; got != want {
t.Errorf("NextPart error = %v; want %v", got, want)
}
}
go-message-0.15.0/textproto/textproto.go 0000664 0000000 0000000 00000000133 14060654707 0020301 0 ustar 00root root 0000000 0000000 // Package textproto implements low-level manipulation of MIME messages.
package textproto
go-message-0.15.0/writer.go 0000664 0000000 0000000 00000006643 14060654707 0015511 0 ustar 00root root 0000000 0000000 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
}
go-message-0.15.0/writer_test.go 0000664 0000000 0000000 00000002465 14060654707 0016546 0 ustar 00root root 0000000 0000000 package message
import (
"bytes"
"io"
"testing"
)
func TestWriter_multipartWithoutCreatePart(t *testing.T) {
var h Header
h.Set("Content-Type", "multipart/alternative; boundary=IMTHEBOUNDARY")
var b bytes.Buffer
mw, err := CreateWriter(&b, h)
if err != nil {
t.Fatal("Expected no error while creating message writer, got:", err)
}
io.WriteString(mw, testMultipartBody)
mw.Close()
if s := b.String(); s != testMultipartText {
t.Errorf("Expected output to be \n%s\n but go \n%s", testMultipartText, s)
}
}
func TestWriter_multipartWithoutBoundary(t *testing.T) {
var h Header
h.Set("Content-Type", "multipart/alternative")
var b bytes.Buffer
mw, err := CreateWriter(&b, h)
if err != nil {
t.Fatal("Expected no error while creating message writer, got:", err)
}
mw.Close()
e, err := Read(&b)
if err != nil {
t.Fatal("Expected no error while reading message, got:", err)
}
mediaType, mediaParams, err := e.Header.ContentType()
if err != nil {
t.Fatal("Expected no error while parsing Content-Type, got:", err)
} else if mediaType != "multipart/alternative" {
t.Errorf("Expected media type to be %q, but got %q", "multipart/alternative", mediaType)
} else if boundary, ok := mediaParams["boundary"]; !ok || boundary == "" {
t.Error("Expected boundary to be automatically generated")
}
}