pax_global_header00006660000000000000000000000064141753264340014523gustar00rootroot0000000000000052 comment=41326393d2ae42a07b3728e34608e352deef3e42 enmime-0.9.3/000077500000000000000000000000001417532643400130065ustar00rootroot00000000000000enmime-0.9.3/.gitattributes000066400000000000000000000010011417532643400156710ustar00rootroot00000000000000# Auto detect text files and perform LF normalization * text=auto *.golden -text *.raw -text # Custom for Visual Studio *.cs diff=csharp *.sln merge=union *.csproj merge=union *.vbproj merge=union *.fsproj merge=union *.dbproj merge=union # Standard to msysgit *.doc diff=astextplain *.DOC diff=astextplain *.docx diff=astextplain *.DOCX diff=astextplain *.dot diff=astextplain *.DOT diff=astextplain *.pdf diff=astextplain *.PDF diff=astextplain *.rtf diff=astextplain *.RTF diff=astextplain enmime-0.9.3/.github/000077500000000000000000000000001417532643400143465ustar00rootroot00000000000000enmime-0.9.3/.github/ISSUE_TEMPLATE.md000066400000000000000000000002311417532643400170470ustar00rootroot00000000000000What I did: What I expected: What I got: Release or branch I am using: (Please attach a sample message if you feel it will help reproduce the issue) enmime-0.9.3/.github/workflows/000077500000000000000000000000001417532643400164035ustar00rootroot00000000000000enmime-0.9.3/.github/workflows/build-and-test.yml000066400000000000000000000015451417532643400217470ustar00rootroot00000000000000name: Build and Test on: pull_request: jobs: build: runs-on: ubuntu-latest strategy: matrix: go: [ '1.17', '1.16' ] name: Go ${{ matrix.go }} build steps: - uses: actions/checkout@v2 with: fetch-depth: 0 - name: Setup Go uses: actions/setup-go@v2 with: go-version: ${{ matrix.go }} - name: Build and Test run: | go build go test -race -coverprofile=profile.cov ./... - name: Send coverage uses: shogo82148/actions-goveralls@v1 with: path-to-profile: profile.cov flag-name: Go-${{ matrix.go }} parallel: true coverage: needs: build name: Test Coverage runs-on: ubuntu-latest steps: - uses: shogo82148/actions-goveralls@v1 with: parallel-finished: true enmime-0.9.3/.github/workflows/pull-checks.yml000066400000000000000000000021621417532643400213410ustar00rootroot00000000000000name: Pull Request Checks on: pull_request: jobs: checks: name: Checks runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: fetch-depth: 0 - name: Format uses: grandcolline/golang-github-actions@v1.1.0 with: run: fmt token: ${{ secrets.GITHUB_TOKEN }} - name: Imports uses: grandcolline/golang-github-actions@v1.1.0 with: run: imports token: ${{ secrets.GITHUB_TOKEN }} - name: Lint uses: grandcolline/golang-github-actions@v1.1.0 with: run: lint token: ${{ secrets.GITHUB_TOKEN }} - name: Shadow uses: grandcolline/golang-github-actions@v1.1.0 with: run: shadow token: ${{ secrets.GITHUB_TOKEN }} - name: Static Check uses: grandcolline/golang-github-actions@v1.1.0 with: run: staticcheck token: ${{ secrets.GITHUB_TOKEN }} - name: Vet uses: grandcolline/golang-github-actions@v1.1.0 with: run: vet token: ${{ secrets.GITHUB_TOKEN }} enmime-0.9.3/.gitignore000066400000000000000000000005421417532643400147770ustar00rootroot00000000000000# 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 # goland ide .idea # vim swp files *.swp cmd/mime-dump/mime-dump cmd/mime-extractor/mime-extractor enmime-0.9.3/CHANGELOG.md000066400000000000000000000203601417532643400146200ustar00rootroot00000000000000Change Log ========== All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ## [0.9.3] - 2022-01-29 ### Added - Support for more charsets (#230) - fixMangledMediaType now removes extra content-type parts (#225) ### Fixed - Fix new lines (ie in filenames) in mediatype.Parse (#224) - Fix crash in QPCleaner, when line is too long and buffer is almost full (#220) ## [0.9.2] - 2021-08-21 ### Added - Auto-quote header parameters containing whitespace (#209) ### Fixed - Remove leading header parameter whitespace (#208) ### Changed - Move ParseMediaType to its own `mediatype` package to reduce the length of header.go. Introduce wrapper func to preserve public API. ## [0.9.1] - 2021-07-31 ### Added - `mime-dump` now prints a stack trace when parsing fails for easier debugging ### Fixed - Handle trailing whitespace in `;` separated headers (#195, thanks demofrager) - Ignore empty sections in `;` separated headers (#199, thanks pavelbazika) - Handle very long lines inside mime boundaries (#200, thanks pavelbazika) - Handle 8-bit characters in unencoded media type params (#201, thanks pavelbazika) - Handle tiny destination buffers and long lines in quoted-printable blocks (#203) ### Changed - Encoder now uses QP or b64 encoding for 8-bit filenames instead of flattening to ASCII (#197, thanks Alexfilus) ## [0.9.0] - 2021-05-01 ### Added - `SendWithReversePath` method to builder, allows specifying a reverse-path that differs from the from address (#179, thanks cgroschupp) - A `Sender` interface that allows our users to provide their own mail sending routines, or mock them in tests. #182 ### Fixed - Reject empty addresses during builder validation (#187, thanks jawr) - Allow unset subject line during builder validation (#191, thanks psanford) ### Changed - Updated dependencies ## [0.8.4] - 2020-12-18 ### Fixed - Attachment file names containing semicolons are no longer truncated (#174) ## [0.8.3] - 2020-11-05 ### Fixed - Reverted folded header parsing changes due to compatibility problems (#172) - Improved performance and memory consumption of boundary reader (#170, thanks bttrfl and dcormier) ## [0.8.2] - 2020-10-10 ### Fixed - Use DFS instead of BFS to locate HTML body to match behavior of popular email clients (#157, thanks huaconghub) - Improvements to media type parsing - Improvements to unescaping quotes with higher codepoints (#165, thanks pavelbazika) - Improvements to folded header parsing (#166, thanks pacellig) ## [0.8.1] - 2020-05-25 ### Fixed - Handle incorrectly indented headers (#149, thanks requaos) - Handle trailing separator characters in header (#154, thanks joekamibeppu) ### Changed - enmime no longer uses git-flow, and will now accept PRs against master ## [0.8.0] - 2020-02-23 ### Added - Inject a `application/octet-stream` as default content type when none is present (#140, thanks requaos) - Add support for content-type params to part & encoding (#148, thanks pzeinlinger) - UTF-7 support (#17) ### Fixed - Handle missing parameter values in the middle of the media parameter list (#139, thanks requaos) - Fix boundaryReader to respect length instead of capacity (#145, thanks dcormier) - Handle very empty mime parts (#144, thanks dcormier) ## [0.7.0] - 2019-11-24 ### Added - Public DecodeHeaders function for getting header data without processing the body parts (thanks requaos.) - Test coverage over 90% (thanks requaos!) ### Changed - Update dependencies ### Fixed - Do not attempt to detect character set for short messages (#131, thanks requaos.) - Possible slice out of bounds error (#134, thanks requaos.) - Tests on Go 1.13 no longer fail due to textproto change (#137, thanks to requaos.) ## [0.6.0] - 2019-08-10 ### Added - Make ParseMediaType public. ### Fixed - Improve quoted display name handling (#112, thanks to requaos.) - Refactor MIME part boundary detection (thanks to requaos.) - Several improvements to MIME attribute decoding (thanks to requaos.) - Detect text/plain attachments properly (thanks to davrux.) ## [0.5.0] - 2018-12-15 ### Added - Use github.com/pkg/errors to decorate errors with stack traces (thanks to dcomier.) - Several improvements to Content-Type header decoding (thanks to dcormier.) - File modification date to encode/decode (thanks to dann7387.) - Handle non-delimited address lists (thanks to requaos.) - RFC-2047 attribute name deocding (thanks to requaos.) ### Fixed - Only detect charset on `text/*` parts (thanks to dcormier.) - Stop adding extra newline during encode (thanks to dann7387.) - Math bug in selecting QP or base64 encoding (thanks to dann7387.) ## [0.4.0] - 2018-11-21 ### Added - Override declared character set if another is detected with high confidence (thanks to nerdlich.) - Handle unquoted specials in media type parameters (thanks to requaos.) - Handle barren Content-Type headers (thanks to dcormier.) - Better handle malformed media type parameters (thanks to dcormier.) ### Changed - Use iso-8859-1 character map when implicitly declared (thanks to requaos.) - Treat "inline" disposition as message content, not attachment unless it is accompanied by parameters (e.g. a filename, thanks to requaos.) ## [0.3.0] - 2018-11-01 ### Added - CLI utils now output inlines and other parts in addition to attachments. - Clone() method to Envelope and Part (thanks to nerdlich.) - GetHeaderKeys() method to Envelope (thanks to allenluce.) - GetHeaderValues() plus a suite of setters for Envelope (thanks to nerdlich.) ### Changed - Use value instead of pointer receivers and return types on MailBuilder methods. Cleaner API, but may break some users. - `enmime.Error` now conforms to the Go error interface, its `String()` method is now deprecated. - `NewPart()` constructor no longer takes a parent parameter. - Part.Errors now holds pointers, matching Envelope.Errors. ### Fixed - Content is now populated for binary-only mails root part (thank to ostcar.) ### Removed - Part no longer implements `io.Reader`, content is stored as a byte slice in `Part.Content` instead. ## [0.2.1] - 2018-10-20 ### Added - Go modules support for reproducible builds. ## [0.2.0] - 2018-02-24 ### Changed - Encoded filenames now have unicode accents stripped instead of escaped, making them more readable. - Part.ContentID - is now properly encoded into the headers when using the builder. - is now populated from headers when decoding messages. - Update go doc, add info about headers and errors. ### Fixed - Part.Read() and Part.Utf8Reader, they are deprecated but should continue to function until 1.0.0. ## 0.1.0 - 2018-02-10 ### Added - Initial implementation of MIME encoding, using `enmime.MailBuilder` [Unreleased]: https://github.com/jhillyerd/enmime/compare/v0.9.3...master [0.9.3]: https://github.com/jhillyerd/enmime/compare/v0.9.2...v0.9.3 [0.9.2]: https://github.com/jhillyerd/enmime/compare/v0.9.1...v0.9.2 [0.9.1]: https://github.com/jhillyerd/enmime/compare/v0.9.0...v0.9.1 [0.9.0]: https://github.com/jhillyerd/enmime/compare/v0.8.4...v0.9.0 [0.8.4]: https://github.com/jhillyerd/enmime/compare/v0.8.3...v0.8.4 [0.8.3]: https://github.com/jhillyerd/enmime/compare/v0.8.2...v0.8.3 [0.8.2]: https://github.com/jhillyerd/enmime/compare/v0.8.1...v0.8.2 [0.8.1]: https://github.com/jhillyerd/enmime/compare/v0.8.0...v0.8.1 [0.8.0]: https://github.com/jhillyerd/enmime/compare/v0.7.0...v0.8.0 [0.7.0]: https://github.com/jhillyerd/enmime/compare/v0.6.0...v0.7.0 [0.6.0]: https://github.com/jhillyerd/enmime/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/jhillyerd/enmime/compare/v0.4.0...v0.5.0 [0.4.0]: https://github.com/jhillyerd/enmime/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/jhillyerd/enmime/compare/v0.2.1...v0.3.0 [0.2.1]: https://github.com/jhillyerd/enmime/compare/v0.2.0...v0.2.1 [0.2.0]: https://github.com/jhillyerd/enmime/compare/v0.1.0...v0.2.0 ## Release Checklist 1. Update CHANGELOG.md: - Ensure *Unreleased* section is up to date - Rename *Unreleased* section to release name and date - Add new GitHub `/compare` link 2. Run tests 3. Tag release with `v` prefix See http://keep change log.com/ for additional instructions on how to update this file. enmime-0.9.3/CONTRIBUTING.md000066400000000000000000000021151417532643400152360ustar00rootroot00000000000000How to Contribute ================= Enmime highly encourages third-party patches. There is a great deal of MIME encoded email out there, so it's likely you will encounter a scenario we haven't. ### tl;dr: - Please add a unit test for your fix or feature - Ensure clean run of `make test lint` ## Getting Started If you anticipate your issue requiring a large patch, please first submit a GitHub issue describing the problem or feature. Attach an email that illustrates the scenario you are trying to improve if possible. You are also encouraged to outline the process you would like to use to resolve the issue. I will attempt to provide validation and/or guidance on your suggested approach. ## Making Changes Create a topic branch based on our `master` branch. 1. Make commits of logical units. 2. Add unit tests to exercise your changes. 3. **Scrub personally identifying information** from test case emails, and keep attachments short. 4. Ensure the code builds and tests with `make test` 5. Run the updated code through `make lint` ## Thanks Thank you for contributing to enmime! enmime-0.9.3/LICENSE000066400000000000000000000021231417532643400140110ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2012-2016 James Hillyerd, All Rights Reserved 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. enmime-0.9.3/Makefile000066400000000000000000000011621417532643400144460ustar00rootroot00000000000000SHELL := /bin/sh SRC := $(shell find . -type f -name '*.go' -not -path "./vendor/*") PKGS := $(shell go list ./... | grep -v /vendor/) .PHONY: all build clean fmt lint reflex simplify test all: clean test lint build clean: go clean $(PKGS) deps: go get ./... build: go build test: go test -race ./... fmt: @gofmt -l -w $(SRC) simplify: @gofmt -s -l -w $(SRC) lint: @test -z "$(shell gofmt -l . | tee /dev/stderr)" || echo "[WARN] Fix formatting issues with 'make fmt'" @golint -set_exit_status $(PKGS) @go vet $(PKGS) reflex: reflex -r '\.go$$' -- sh -c 'echo; date; echo; go test ./... && echo ALL PASS' enmime-0.9.3/README.md000066400000000000000000000032161417532643400142670ustar00rootroot00000000000000# enmime [![PkgGoDev](https://pkg.go.dev/badge/github.com/jhillyerd/enmime)][Pkg Docs] [![Build Status](https://travis-ci.org/jhillyerd/enmime.svg?branch=master)][Build Status] [![Go Report Card](https://goreportcard.com/badge/github.com/jhillyerd/enmime)][Go Report Card] [![Coverage Status](https://coveralls.io/repos/github/jhillyerd/enmime/badge.svg?branch=master)][Coverage Status] enmime is a MIME encoding and decoding library for Go, focused on generating and parsing MIME encoded emails. It is being developed in tandem with the [Inbucket] email service. enmime includes a fluent interface builder for generating MIME encoded messages, see the wiki for example [Builder Usage]. See our [Pkg Docs] for examples and API usage information. ## Development Status enmime is near production quality: it works but struggles to parse a small percentage of emails. It's possible the API will evolve slightly before the 1.0 release. See [CONTRIBUTING.md] for more information. ## About enmime is written in [Go][Golang]. enmime is open source software released under the MIT License. The latest version can be found at https://github.com/jhillyerd/enmime [Build Status]: https://travis-ci.org/jhillyerd/enmime [Builder Usage]: https://github.com/jhillyerd/enmime/wiki/Builder-Usage [Coverage Status]: https://coveralls.io/github/jhillyerd/enmime [CONTRIBUTING.md]: https://github.com/jhillyerd/enmime/blob/master/CONTRIBUTING.md [Inbucket]: http://www.inbucket.org/ [Golang]: http://golang.org/ [Go Report Card]: https://goreportcard.com/report/github.com/jhillyerd/enmime [Pkg Docs]: https://pkg.go.dev/github.com/jhillyerd/enmime enmime-0.9.3/boundary.go000066400000000000000000000163611417532643400151670ustar00rootroot00000000000000package enmime import ( "bufio" "bytes" stderrors "errors" "io" "io/ioutil" "unicode" "github.com/pkg/errors" ) // 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 var errNoBoundaryTerminator = stderrors.New("expected boundary not present") type boundaryReader struct { finished bool // No parts remain when finished partsRead int // Number of parts read thus far atPartStart bool // Whether the current part is at its beginning r *bufio.Reader // Source reader nlPrefix []byte // NL + MIME boundary prefix prefix []byte // MIME boundary prefix final []byte // Final boundary prefix buffer *bytes.Buffer // Content waiting to be read unbounded bool // Flag to throw errNoBoundaryTerminator } // newBoundaryReader returns an initialized boundaryReader func newBoundaryReader(reader *bufio.Reader, boundary string) *boundaryReader { fullBoundary := []byte("\n--" + boundary + "--") return &boundaryReader{ r: reader, nlPrefix: fullBoundary[:len(fullBoundary)-2], prefix: fullBoundary[1 : len(fullBoundary)-2], final: fullBoundary[1:], buffer: new(bytes.Buffer), } } // Read returns a buffer containing the content up until boundary // // Excerpt from io package on io.Reader implementations: // // type Reader interface { // Read(p []byte) (n int, err error) // } // // Read reads up to len(p) bytes into p. It returns the number of // bytes read (0 <= n <= len(p)) and any error encountered. Even // if Read returns n < len(p), it may use all of p as scratch space // during the call. If some data is available but not len(p) bytes, // Read conventionally returns what is available instead of waiting // for more. // // When Read encounters an error or end-of-file condition after // successfully reading n > 0 bytes, it returns the number of bytes // read. It may return the (non-nil) error from the same call or // return the error (and n == 0) from a subsequent call. An instance // of this general case is that a Reader returning a non-zero number // of bytes at the end of the input stream may return either err == EOF // or err == nil. The next Read should return 0, EOF. // // Callers should always process the n > 0 bytes returned before // considering the error err. Doing so correctly handles I/O errors // that happen after reading some bytes and also both of the allowed // EOF behaviors. func (b *boundaryReader) Read(dest []byte) (n int, err error) { if b.buffer.Len() >= len(dest) { // This read request can be satisfied entirely by the buffer. n, err = b.buffer.Read(dest) if b.atPartStart && n > 0 { b.atPartStart = false } return n, err } for i := 0; i < len(dest); i++ { var cs []byte cs, err = b.r.Peek(1) if err != nil && err != io.EOF { return 0, errors.WithStack(err) } // Ensure that we can switch on the first byte of 'cs' without panic. if len(cs) > 0 { padding := 1 check := false switch cs[0] { // Check for carriage return as potential CRLF boundary prefix. case '\r': padding = 2 check = true // Check for line feed as potential LF boundary prefix. case '\n': check = true default: if b.atPartStart { // If we're at the very beginning of the part (even before the headers), // check to see if there's a delimiter that immediately follows. padding = 0 check = true } } if check { var peek []byte peek, err = b.r.Peek(len(b.nlPrefix) + padding + 1) switch err { case nil: // Check the whitespace at the head of the peek to avoid checking for a boundary early. if bytes.HasPrefix(peek, []byte("\n\n")) || bytes.HasPrefix(peek, []byte("\n\r")) || bytes.HasPrefix(peek, []byte("\r\n\r")) || bytes.HasPrefix(peek, []byte("\r\n\n")) { break } // Check the peek buffer for a boundary delimiter or terminator. if b.isDelimiter(peek[padding:]) || b.isTerminator(peek[padding:]) { // We have found our boundary terminator, lets write out the final bytes // and return io.EOF to indicate that this section read is complete. n, err = b.buffer.Read(dest) switch err { case nil, io.EOF: if b.atPartStart && n > 0 { b.atPartStart = false } return n, io.EOF default: return 0, errors.WithStack(err) } } case io.EOF: // We have reached the end without finding a boundary, // so we flag the boundary reader to add an error to // the errors slice and write what we have to the buffer. b.unbounded = true default: continue } } } var next byte next, err = b.r.ReadByte() if err != nil { // EOF is not fatal, it just means that we have drained the reader. if errors.Is(err, io.EOF) { break } return 0, errors.WithStack(err) } if err = b.buffer.WriteByte(next); err != nil { return 0, errors.WithStack(err) } } // Read the contents of the buffer into the destination slice. n, err = b.buffer.Read(dest) if b.atPartStart && n > 0 { b.atPartStart = false } return n, err } // Next moves over the boundary to the next part, returns true if there is another part to be read. func (b *boundaryReader) Next() (bool, error) { if b.finished { return false, nil } if b.partsRead > 0 { // Exhaust the current part to prevent errors when moving to the next part. _, _ = io.Copy(ioutil.Discard, b) } for { var line []byte = nil var err error for { // Read whole line, handle extra long lines in cycle var segment []byte segment, err = b.r.ReadSlice('\n') if line == nil { line = segment } else { line = append(line, segment...) } if err == nil || err == io.EOF { break } else if err != bufio.ErrBufferFull || len(segment) == 0 { return false, errors.WithStack(err) } } if len(line) > 0 && (line[0] == '\r' || line[0] == '\n') { // Blank line continue } if b.isTerminator(line) { b.finished = true return false, nil } if err != io.EOF && b.isDelimiter(line) { // Start of a new part. b.partsRead++ b.atPartStart = true return true, nil } if err == io.EOF { // Intentionally not wrapping with stack. return false, io.EOF } if b.partsRead == 0 { // The first part didn't find the starting delimiter, burn off any preamble in front of // the boundary. continue } b.finished = true return false, errors.WithMessagef(errNoBoundaryTerminator, "expecting boundary %q, got %q", string(b.prefix), string(line)) } } // isDelimiter returns true for --BOUNDARY\r\n but not --BOUNDARY-- func (b *boundaryReader) isDelimiter(buf []byte) bool { idx := bytes.Index(buf, b.prefix) if idx == -1 { return false } // Fast forward to the end of the boundary prefix. buf = buf[idx+len(b.prefix):] if len(buf) > 0 { if unicode.IsSpace(rune(buf[0])) { return true } } return false } // isTerminator returns true for --BOUNDARY-- func (b *boundaryReader) isTerminator(buf []byte) bool { idx := bytes.Index(buf, b.final) return idx != -1 } enmime-0.9.3/boundary_test.go000066400000000000000000000321461417532643400162250ustar00rootroot00000000000000package enmime import ( "bufio" "bytes" "io" "io/ioutil" "strings" "testing" "github.com/pkg/errors" ) func TestBoundaryReader(t *testing.T) { var ttable = []struct { input, boundary, want string }{ { input: "good\r\n--STOPHERE\r\nafter", boundary: "STOPHERE", want: "good", }, { input: "good\r\n--STOPHERE\t\r\nafter", boundary: "STOPHERE", want: "good", }, { input: "good\r\n--STOPHERE--\r\nafter", boundary: "STOPHERE", want: "good", }, { input: "good\r\n--STOPHERE \t\r\nafter", boundary: "STOPHERE", want: "good", }, { input: "good\r\n--STOPHERE--\t \r\nafter", boundary: "STOPHERE", want: "good", }, { input: "good\r\n--STOPHEREA\r\n--STOPHERE--\r\nafter", boundary: "STOPHERE", want: "good\r\n--STOPHEREA", }, { input: "good\r\n--STOPHERE-A\r\n--STOPHERE--\r\nafter", boundary: "STOPHERE", want: "good\r\n--STOPHERE-A", }, { input: "good\n--STOPHERE\nafter", boundary: "STOPHERE", want: "good", }, { input: "good\n--STOPHERE--\nafter", boundary: "STOPHERE", want: "good", }, { input: "good\n--STOPHEREA\n--STOPHERE--\nafter", boundary: "STOPHERE", want: "good\n--STOPHEREA", }, { input: "good\n--STOPHERE-A\n--STOPHERE--\nafter", boundary: "STOPHERE", want: "good\n--STOPHERE-A", }, } for _, tt := range ttable { ir := bufio.NewReader(strings.NewReader(tt.input)) br := newBoundaryReader(ir, tt.boundary) output, err := ioutil.ReadAll(br) if err != nil { t.Fatalf("Got error: %v\ninput: %q", err, tt.input) } // Test the buffered data is correct got := string(output) if got != tt.want { t.Errorf("boundaryReader input: %q\ngot: %q, want: %q", tt.input, got, tt.want) } // Test the data remaining in reader is correct rest, err := ioutil.ReadAll(ir) if err != nil { t.Fatal(err) } got = string(rest) want := tt.input[len(tt.want):] if got != want { t.Errorf("Rest of reader:\ngot: %q, want: %q", got, want) } } } func TestBoundaryReaderBuffer(t *testing.T) { // Check that Read() can serve accurately from its buffer input := "good\r\n--STOPHERE\r\nafter" boundary := "STOPHERE" want := []byte("good") ir := bufio.NewReader(strings.NewReader(input)) br := newBoundaryReader(ir, boundary) d := make([]byte, 1) for i, wc := range want { n, err := br.Read(d) if err != nil { t.Fatal("Unexepcted error:", err) } if n != 1 { t.Error("Got", n, "bytes, want 1") } if d[0] != wc { t.Errorf("Got byte[%v] == %v, want: %v", i, d[0], wc) } } _, err := br.Read(d) if err != io.EOF { t.Error("Got", err, "wanted: EOF") } } func TestBoundaryReaderEOF(t *testing.T) { // Confirm we get an EOF at end input := "good\r\n--STOPHERE\r\nafter" boundary := "STOPHERE" want := "good" ir := bufio.NewReader(strings.NewReader(input)) br := newBoundaryReader(ir, boundary) output, err := ioutil.ReadAll(br) if err != nil { t.Fatal(err) } got := string(output) if got != want { t.Fatal("got:", got, "want:", want) } buf := make([]byte, 256) n, err := br.Read(buf) if err != io.EOF { t.Error("got:", err, "want: EOF") } if n != 0 { t.Error("read ", n, "bytes, want: 0") } } func TestBoundaryReaderParts(t *testing.T) { var ttable = []struct { input string boundary string parts []string }{ { input: "preamble\r\n--STOP\r\npart1\r\n--STOP\r\npart2\r\n--STOP--\r\n", boundary: "STOP", parts: []string{"part1", "part2"}, }, { input: "preamble\r\n--STOP \t\r\npart1\r\n--STOP\t \r\npart2\r\n--STOP-- \t\r\n", boundary: "STOP", parts: []string{"part1", "part2"}, }, { input: "\npreamble\n--STOP\npart1\n--STOP\npart2\n--STOP--\n", boundary: "STOP", parts: []string{"part1", "part2"}, }, { input: "\n--STOP\npart1\n--STOP\npart2\n--STOP--\n", boundary: "STOP", parts: []string{"part1", "part2"}, }, { input: "--STOP\npart1\n--STOP\npart2\n--STOP--\n", boundary: "STOP", parts: []string{"part1", "part2"}, }, { input: "--STOP\npart1\n--STOP\n--STOP--\n", boundary: "STOP", parts: []string{"part1", ""}, }, { input: "--STOP\n--STOP\npart2\n--STOP--\n", boundary: "STOP", parts: []string{"", "part2"}, }, { input: "--STOP\n--STOP\n--STOP--\n", boundary: "STOP", parts: []string{"", ""}, }, } for _, tt := range ttable { ir := bufio.NewReader(strings.NewReader(tt.input)) br := newBoundaryReader(ir, tt.boundary) for i, want := range tt.parts { next, err := br.Next() if err != nil { t.Fatalf("Error %q on part %v, input %q", err, i, tt.input) } if !next { t.Fatal("Next() = false, want: true") } output, err := ioutil.ReadAll(br) if err != nil { t.Fatal(err) } got := string(output) if got != want { t.Errorf("boundaryReader input: %q\ngot: %q, want: %q", tt.input, got, want) } } next, err := br.Next() if err != nil { t.Fatal(err) } if next { t.Fatal("Next() = true, want: false") } // How does it handle being called a second time? next, err = br.Next() if err != nil { t.Fatal(err) } if next { t.Fatal("Next() = true, want: false") } } } func TestBoundaryReaderPartialRead(t *testing.T) { // Make sure Next() still works after a partial read input := "\r\n--STOPHERE\r\n1111\r\n--STOPHERE\r\n2222\r\n--STOPHERE\r\n" boundary := "STOPHERE" wants := []string{"11", "2222"} ir := bufio.NewReader(strings.NewReader(input)) br := newBoundaryReader(ir, boundary) for i, want := range wants { next, err := br.Next() if err != nil { t.Fatalf("Error %q on part %v, input %q", err, i, input) } if !next { t.Fatal("Next() = false, want: true") } // Build a buffer the size of our wanted string b := make([]byte, len(want)) count, err := br.Read(b) if err != nil { t.Fatal(err) } if count != len(want) { t.Errorf("Read() size = %v, wanted %v", count, len(want)) } got := string(b[:count]) if got != want { t.Errorf("boundaryReader got: %q, want: %q", got, want) } } } func TestBoundaryReaderNoMatch(t *testing.T) { input := "\r\n--STOPHERE\r\n1111\r\n--STOPHERE\r\n2222\r\n--STOPHERE\r\n" boundary := "NOMATCH" ir := bufio.NewReader(strings.NewReader(input)) br := newBoundaryReader(ir, boundary) next, err := br.Next() if err != io.EOF { t.Fatalf("err = %v, want: io.EOF", err) } if next { t.Fatalf("Next() = true, want: false") } } func TestBoundaryReaderNoTerminator(t *testing.T) { input := "preamble\r\n--STOPHERE\r\n1111\r\n" boundary := "STOPHERE" ir := bufio.NewReader(strings.NewReader(input)) br := newBoundaryReader(ir, boundary) // First part should not error next, err := br.Next() if err != nil { t.Fatalf("Error %q on first part, input %q", err, input) } if !next { t.Fatal("Next() = false, want: true") } // There is no second part should, error should be EOF. want := "EOF" next, err = br.Next() if err == nil { t.Fatal("Error was nil, wanted:", want) } if !strings.Contains(err.Error(), want) { t.Fatalf("err = %v, want: %v", err, want) } if next { t.Fatalf("Next() = true, want: false") } } func TestBoundaryReaderBufferBoundaryAbut(t *testing.T) { // Verify operation when the boundary string abuts the end of the peek buffer prefix := []byte("preamble\r\n--STOPHERE\r\n") peekSuffix := []byte("\r\n--STOPHERE") afterPeek := []byte("\r\nanother part\r\n--STOPHERE--") buf := make([]byte, 0, len(prefix)+peekBufferSize+len(afterPeek)) boundary := "STOPHERE" // Setup buffer buf = append(buf, prefix...) padding := peekBufferSize - len(peekSuffix) for i := 0; i < padding; i++ { buf = append(buf, 'x') } buf = append(buf, peekSuffix...) buf = append(buf, afterPeek...) // Attempt to read ir := bufio.NewReader(bytes.NewBuffer(buf)) br := newBoundaryReader(ir, boundary) // Skip preamble, first part should not error next, err := br.Next() if err != nil { t.Fatalf("Error %q on first part", err) } if !next { t.Fatal("Next() = false, want: true") } output, err := ioutil.ReadAll(br) if err != nil { t.Fatalf("Got error: %v", err) } if len(output) != padding { t.Errorf("len(output) == %v, want %v", len(output), padding) } // Second part should not error next, err = br.Next() if err != nil { t.Fatalf("Error %q on second part", err) } if !next { t.Fatal("Next() = false, want: true") } output, err = ioutil.ReadAll(br) if err != nil { t.Fatalf("Got error: %v", err) } want := "another part" got := string(output) if got != want { t.Errorf("ReadAll() got: %q, want: %q", got, want) } } func TestBoundaryReaderBufferBoundaryCross(t *testing.T) { // Verify operation when the boundary string does not fit in the peek buffer prefix := []byte("preamble\r\n--STOPHERE\r\n") peekSuffix := []byte("\r\n--STOP") afterPeek := []byte("HERE\r\nanother part\r\n--STOPHERE--") buf := make([]byte, 0, len(prefix)+peekBufferSize+len(afterPeek)) boundary := "STOPHERE" // Setup buffer buf = append(buf, prefix...) padding := peekBufferSize - len(peekSuffix) for i := 0; i < padding; i++ { buf = append(buf, 'x') } buf = append(buf, peekSuffix...) buf = append(buf, afterPeek...) // Attempt to read ir := bufio.NewReader(bytes.NewBuffer(buf)) br := newBoundaryReader(ir, boundary) // Skip preamble, first part should not error next, err := br.Next() if err != nil { t.Fatalf("Error %q on first part", err) } if !next { t.Fatal("Next() = false, want: true") } output, err := ioutil.ReadAll(br) if err != nil { t.Fatalf("Got error: %v", err) } if len(output) != padding { t.Errorf("len(output) == %v, want %v", len(output), padding) } // Second part should not error next, err = br.Next() if err != nil { t.Fatalf("Error %q on second part", err) } if !next { t.Fatal("Next() = false, want: true") } output, err = ioutil.ReadAll(br) if err != nil { t.Fatalf("Got error: %v", err) } want := "another part" got := string(output) if got != want { t.Errorf("ReadAll() got: %q, want: %q", got, want) } } func TestBoundaryReaderReadErrors(t *testing.T) { // Destination byte slice is shorter than buffer length dest := make([]byte, 1) br := &boundaryReader{ buffer: bytes.NewBuffer([]byte{'1', '2', '3'}), atPartStart: true, } n, err := br.Read(dest) if n != 1 { t.Fatal("Read() did not read bytes equal to len(dest), failed") } if err != nil { t.Fatal("Read() should not have returned an error, failed") } if br.atPartStart { t.Fatal("Read() of non-zero length should have unset atStartPart boolean") } // Using bufio.Reader with a 0 length buffer will cause // Peek method to return a non io.EOF error. dest = make([]byte, 10) br.r = &bufio.Reader{} n, err = br.Read(dest) if n != 0 { t.Fatal("Read() should not have read any bytes, failed") } if errors.Cause(err) != bufio.ErrBufferFull { t.Fatal("Read() should have returned bufio.ErrBufferFull error, failed") } // Next method to return a non io.EOF error. next, err := br.Next() if next { t.Fatal("Next() should have returned false, failed") } if errors.Cause(err) != bufio.ErrBufferFull { t.Fatal("Read() should have returned bufio.ErrBufferFull error, failed") } } // TestBoundaryReaderLongLine checks that boundaryReader can read lines longer than the `peekBufferSize`. func TestBoundaryReaderLongLine(t *testing.T) { data := bytes.Repeat([]byte{1}, 7*1024) data[6*1024] = '\n' br := &boundaryReader{ r: bufio.NewReader(bytes.NewReader(data)), } next, err := br.Next() if next { t.Fatal("Next() should have returned false, failed") } if err != nil { t.Fatal("Next() should have returned no error, failed") } } // TestReadLenNotCap checks that the `boundaryReader` `io.Reader` implementation fills the provided // slice based on its length (as per the `io.Reader` documentation), and not its capacity. func TestReadLenNotCap(t *testing.T) { t.Parallel() input := "--STOP\nabcdefghijklm\n--STOP\nnopqrstuvwxyz\n--STOP--\n" boundary := "STOP" parts := []string{"abcdefghijklm", "nopqrstuvwxyz"} ir := bufio.NewReader(strings.NewReader(input)) br := newBoundaryReader(ir, boundary) for i, want := range parts { next, err := br.Next() if err != nil { t.Fatalf("Error %q on part %v, input %q", err, i, input) } if !next { t.Fatal("Next() = false, want: true") } var out []byte b := make([]byte, 6, 20) // Ensure the capacity is greater than the length. max := len(b) var c int for err == nil { c, err = br.Read(b) if c > max { t.Errorf("Per the docuemtation for io.Reader, should not have read more than %d bytes, but read %d", max, c) } out = append(out, b[0:c]...) } if err != io.EOF { t.Errorf("Expected %v, but got: %+v", io.EOF, err) } if want != string(out) { t.Errorf("Expected part to be read as %q, but got %q", want, out) } } } func BenchmarkBoundaryReader(b *testing.B) { const ( input = "content\r\n--BOUNDARY\r\n" boundary = "BOUNDARY" ) var err error for i := 0; i < b.N; i++ { ir := bufio.NewReader(strings.NewReader(input)) br := newBoundaryReader(ir, boundary) _, err = io.Copy(ioutil.Discard, br) if err != nil { b.Fatalf("Failed to read content: %+v", err) } } } enmime-0.9.3/builder.go000066400000000000000000000224111417532643400147630ustar00rootroot00000000000000package enmime import ( "bytes" "errors" "io/ioutil" "mime" "net/mail" "net/textproto" "os" "path/filepath" "reflect" "time" "github.com/jhillyerd/enmime/internal/stringutil" ) // MailBuilder facilitates the easy construction of a MIME message. Each manipulation method // returns a copy of the receiver struct. It can be considered immutable if the caller does not // modify the string and byte slices passed in. Immutability allows the headers or entire message // to be reused across multiple threads. type MailBuilder struct { to, cc, bcc []mail.Address from mail.Address replyTo mail.Address subject string date time.Time header textproto.MIMEHeader text, html []byte inlines, attachments []*Part err error } // Builder returns an empty MailBuilder struct. func Builder() MailBuilder { return MailBuilder{} } // Error returns the stored error from a file attachment/inline read or nil. func (p MailBuilder) Error() error { return p.err } // Date returns a copy of MailBuilder with the specified Date header. func (p MailBuilder) Date(date time.Time) MailBuilder { p.date = date return p } // From returns a copy of MailBuilder with the specified From header. func (p MailBuilder) From(name, addr string) MailBuilder { p.from = mail.Address{Name: name, Address: addr} return p } // Subject returns a copy of MailBuilder with the specified Subject header. func (p MailBuilder) Subject(subject string) MailBuilder { p.subject = subject return p } // To returns a copy of MailBuilder with this name & address appended to the To header. name may be // empty. func (p MailBuilder) To(name, addr string) MailBuilder { if len(addr) > 0 { p.to = append(p.to, mail.Address{Name: name, Address: addr}) } return p } // ToAddrs returns a copy of MailBuilder with the specified To addresses. func (p MailBuilder) ToAddrs(to []mail.Address) MailBuilder { p.to = to return p } // CC returns a copy of MailBuilder with this name & address appended to the CC header. name may be // empty. func (p MailBuilder) CC(name, addr string) MailBuilder { if len(addr) > 0 { p.cc = append(p.cc, mail.Address{Name: name, Address: addr}) } return p } // CCAddrs returns a copy of MailBuilder with the specified CC addresses. func (p MailBuilder) CCAddrs(cc []mail.Address) MailBuilder { p.cc = cc return p } // BCC returns a copy of MailBuilder with this name & address appended to the BCC list. name may be // empty. This method only has an effect if the Send method is used to transmit the message, there // is no effect on the parts returned by Build(). func (p MailBuilder) BCC(name, addr string) MailBuilder { if len(addr) > 0 { p.bcc = append(p.bcc, mail.Address{Name: name, Address: addr}) } return p } // BCCAddrs returns a copy of MailBuilder with the specified as the blind CC list. This method only // has an effect if the Send method is used to transmit the message, there is no effect on the parts // returned by Build(). func (p MailBuilder) BCCAddrs(bcc []mail.Address) MailBuilder { p.bcc = bcc return p } // ReplyTo returns a copy of MailBuilder with this name & address appended to the To header. name // may be empty. func (p MailBuilder) ReplyTo(name, addr string) MailBuilder { p.replyTo = mail.Address{Name: name, Address: addr} return p } // Header returns a copy of MailBuilder with the specified value added to the named header. func (p MailBuilder) Header(name, value string) MailBuilder { // Copy existing header map h := textproto.MIMEHeader{} for k, v := range p.header { h[k] = v } h.Add(name, value) p.header = h return p } // Text returns a copy of MailBuilder that will use the provided bytes for its text/plain Part. func (p MailBuilder) Text(body []byte) MailBuilder { p.text = body return p } // HTML returns a copy of MailBuilder that will use the provided bytes for its text/html Part. func (p MailBuilder) HTML(body []byte) MailBuilder { p.html = body return p } // AddAttachment returns a copy of MailBuilder that includes the specified attachment. func (p MailBuilder) AddAttachment(b []byte, contentType string, fileName string) MailBuilder { part := NewPart(contentType) part.Content = b part.FileName = fileName part.Disposition = cdAttachment p.attachments = append(p.attachments, part) return p } // AddFileAttachment returns a copy of MailBuilder that includes the specified attachment. // fileName, will be populated from the base name of path. Content type will be detected from the // path extension. func (p MailBuilder) AddFileAttachment(path string) MailBuilder { // Only allow first p.err value if p.err != nil { return p } f, err := os.Open(path) if err != nil { p.err = err return p } b, err := ioutil.ReadAll(f) if err != nil { p.err = err return p } name := filepath.Base(path) ctype := mime.TypeByExtension(filepath.Ext(name)) return p.AddAttachment(b, ctype, name) } // AddInline returns a copy of MailBuilder that includes the specified inline. fileName and // contentID may be left empty. func (p MailBuilder) AddInline( b []byte, contentType string, fileName string, contentID string, ) MailBuilder { part := NewPart(contentType) part.Content = b part.FileName = fileName part.Disposition = cdInline part.ContentID = contentID p.inlines = append(p.inlines, part) return p } // AddFileInline returns a copy of MailBuilder that includes the specified inline. fileName and // contentID will be populated from the base name of path. Content type will be detected from the // path extension. func (p MailBuilder) AddFileInline(path string) MailBuilder { // Only allow first p.err value if p.err != nil { return p } f, err := os.Open(path) if err != nil { p.err = err return p } b, err := ioutil.ReadAll(f) if err != nil { p.err = err return p } name := filepath.Base(path) ctype := mime.TypeByExtension(filepath.Ext(name)) return p.AddInline(b, ctype, name, name) } // Build performs some basic validations, then constructs a tree of Part structs from the configured // MailBuilder. It will set the Date header to now if it was not explicitly set. func (p MailBuilder) Build() (*Part, error) { if p.err != nil { return nil, p.err } // Validations if p.from.Address == "" { return nil, errors.New("from not set") } if len(p.to)+len(p.cc)+len(p.bcc) == 0 { return nil, errors.New(ErrorMissingRecipient) } // Fully loaded structure; the presence of text, html, inlines, and attachments will determine // how much is necessary: // // multipart/mixed // |- multipart/related // | |- multipart/alternative // | | |- text/plain // | | `- text/html // | `- inlines.. // `- attachments.. // // We build this tree starting at the leaves, re-rooting as needed. var root, part *Part if p.text != nil || p.html == nil { root = NewPart(ctTextPlain) root.Content = p.text root.Charset = utf8 } if p.html != nil { part = NewPart(ctTextHTML) part.Content = p.html part.Charset = utf8 if root == nil { root = part } else { root.NextSibling = part } } if p.text != nil && p.html != nil { // Wrap Text & HTML bodies part = root root = NewPart(ctMultipartAltern) root.AddChild(part) } if len(p.inlines) > 0 { part = root root = NewPart(ctMultipartRelated) root.AddChild(part) for _, ip := range p.inlines { // Copy inline Part to isolate mutations part = &Part{} *part = *ip part.Header = make(textproto.MIMEHeader) root.AddChild(part) } } if len(p.attachments) > 0 { part = root root = NewPart(ctMultipartMixed) root.AddChild(part) for _, ap := range p.attachments { // Copy attachment Part to isolate mutations part = &Part{} *part = *ap part.Header = make(textproto.MIMEHeader) root.AddChild(part) } } // Headers h := root.Header h.Set(hnMIMEVersion, "1.0") h.Set("From", p.from.String()) h.Set("Subject", p.subject) if len(p.to) > 0 { h.Set("To", stringutil.JoinAddress(p.to)) } if len(p.cc) > 0 { h.Set("Cc", stringutil.JoinAddress(p.cc)) } if p.replyTo.Address != "" { h.Set("Reply-To", p.replyTo.String()) } date := p.date if date.IsZero() { date = time.Now() } h.Set("Date", date.Format(time.RFC1123Z)) for k, v := range p.header { for _, s := range v { h.Add(k, s) } } return root, nil } // SendWithReversePath encodes the message and sends it via the specified Sender. func (p MailBuilder) SendWithReversePath(sender Sender, from string) error { buf := &bytes.Buffer{} root, err := p.Build() if err != nil { return err } err = root.Encode(buf) if err != nil { return err } recips := make([]string, 0, len(p.to)+len(p.cc)+len(p.bcc)) for _, a := range p.to { recips = append(recips, a.Address) } for _, a := range p.cc { recips = append(recips, a.Address) } for _, a := range p.bcc { recips = append(recips, a.Address) } return sender.Send(from, recips, buf.Bytes()) } // Send encodes the message and sends it via the specified Sender, using the address provided to // `From()` as the reverse-path. func (p MailBuilder) Send(sender Sender) error { return p.SendWithReversePath(sender, p.from.Address) } // Equals uses the reflect package to test two MailBuilder structs for equality, primarily for unit // tests. func (p MailBuilder) Equals(o MailBuilder) bool { return reflect.DeepEqual(p, o) } enmime-0.9.3/builder_test.go000066400000000000000000000661251417532643400160340ustar00rootroot00000000000000package enmime_test import ( "bytes" "net/mail" "path/filepath" "reflect" "strconv" "testing" "time" "github.com/jhillyerd/enmime" "github.com/jhillyerd/enmime/internal/test" ) type mockSender struct { from string to []string msg []byte } func (s *mockSender) Send(from string, to []string, msg []byte) error { s.from = from s.to = to s.msg = msg return nil } var addrSlice = []mail.Address{{Name: "name", Address: "addr"}} func TestBuilderEquals(t *testing.T) { a := enmime.Builder() b := enmime.Builder() if !a.Equals(b) { t.Error("New PartBuilders should be equal") } } func TestBuilderFrom(t *testing.T) { a := enmime.Builder().From("name", "same") b := enmime.Builder().From("name", "same") if !a.Equals(b) { t.Error("Same From(value) should be equal") } a = enmime.Builder().From("name", "foo") b = enmime.Builder().From("name", "bar") if a.Equals(b) { t.Error("Different From(value) should not be equal") } a = enmime.Builder().From("name", "foo") b = a.From("name", "bar") if a.Equals(b) { t.Error("From() should not mutate receiver, failed") } want := mail.Address{Name: "name", Address: "from@inbucket.org"} a = enmime.Builder().From(want.Name, want.Address).Subject("foo").ToAddrs(addrSlice) p, err := a.Build() if err != nil { t.Fatal(err) } got := p.Header.Get("From") if got != want.String() { t.Errorf("From: %q, want: %q", got, want) } } func TestBuilderSubject(t *testing.T) { a := enmime.Builder().Subject("same") b := enmime.Builder().Subject("same") if !a.Equals(b) { t.Error("Same Subject(value) should be equal") } a = enmime.Builder().Subject("foo") b = enmime.Builder().Subject("bar") if a.Equals(b) { t.Error("Different Subject(value) should not be equal") } a = enmime.Builder().Subject("foo") b = a.Subject("bar") if a.Equals(b) { t.Error("Subject() should not mutate receiver, failed") } want := "engaging subject" a = enmime.Builder().Subject(want).From("name", "foo").ToAddrs(addrSlice) p, err := a.Build() if err != nil { t.Fatal(err) } got := p.Header.Get("Subject") if got != want { t.Errorf("Subject: %q, want: %q", got, want) } } func TestBuilderDate(t *testing.T) { a := enmime.Builder().Date(time.Date(2017, 1, 1, 13, 14, 15, 16, time.UTC)) b := enmime.Builder().Date(time.Date(2017, 1, 1, 13, 14, 15, 16, time.UTC)) if !a.Equals(b) { t.Error("Same Date(value) should be equal") } a = enmime.Builder().Date(time.Date(2017, 1, 1, 13, 14, 15, 16, time.UTC)) b = enmime.Builder().Date(time.Date(2018, 1, 1, 13, 14, 15, 16, time.UTC)) if a.Equals(b) { t.Error("Different Date(value) should not be equal") } a = enmime.Builder().Date(time.Date(2017, 1, 1, 13, 14, 15, 16, time.UTC)) b = a.Date(time.Date(2018, 1, 1, 13, 14, 15, 16, time.UTC)) if a.Equals(b) { t.Error("Date() should not mutate receiver, failed") } input := time.Date(2017, 1, 1, 13, 14, 15, 16, time.UTC) want := "Sun, 01 Jan 2017 13:14:15 +0000" a = enmime.Builder().Date(input).Subject("hi").From("name", "foo").ToAddrs(addrSlice) p, err := a.Build() if err != nil { t.Fatal(err) } got := p.Header.Get("Date") if got != want { t.Errorf("Date: %q, want: %q", got, want) } } func TestBuilderTo(t *testing.T) { a := enmime.Builder().To("name", "same") b := enmime.Builder().To("name", "same") if !a.Equals(b) { t.Error("Same To(value) should be equal") } a = enmime.Builder().To("name", "foo") b = enmime.Builder().To("name", "bar") if a.Equals(b) { t.Error("Different To(value) should not be equal") } a = enmime.Builder().To("name", "foo") for i := 0; i < 1000; i++ { b = a.To("name", "bar"+strconv.Itoa(i)) if a.Equals(b) { t.Error("To() should not mutate receiver, failed") } a = b } a = enmime.Builder().From("name", "foo").Subject("foo") a = a.To("one", "one@inbucket.org") a = a.To("two", "two@inbucket.org") want := "\"one\" , \"two\" " p, err := a.Build() if err != nil { t.Fatal(err) } got := p.Header.Get("To") if !reflect.DeepEqual(got, want) { t.Errorf("To: %q, want: %q", got, want) } } func TestBuilderToAddrs(t *testing.T) { a := enmime.Builder().ToAddrs([]mail.Address{{Name: "name", Address: "same"}}) b := enmime.Builder().ToAddrs([]mail.Address{{Name: "name", Address: "same"}}) if !a.Equals(b) { t.Error("Same To(value) should be equal") } a = enmime.Builder().ToAddrs([]mail.Address{{Name: "name", Address: "foo"}}) b = enmime.Builder().ToAddrs([]mail.Address{{Name: "name", Address: "bar"}}) if a.Equals(b) { t.Error("Different To(value) should not be equal") } a = enmime.Builder().ToAddrs([]mail.Address{{Name: "name", Address: "foo"}}) b = a.ToAddrs([]mail.Address{{Name: "name", Address: "bar"}}) if a.Equals(b) { t.Error("To() should not mutate receiver, failed") } input := []mail.Address{ {Name: "one", Address: "one@inbucket.org"}, {Name: "two", Address: "two@inbucket.org"}, } want := "\"one\" , \"two\" " a = enmime.Builder().ToAddrs(input).From("name", "foo").Subject("foo") p, err := a.Build() if err != nil { t.Fatal(err) } got := p.Header.Get("To") if !reflect.DeepEqual(got, want) { t.Errorf("To: %q, want: %q", got, want) } } func TestBuilderCC(t *testing.T) { a := enmime.Builder().CC("name", "same") b := enmime.Builder().CC("name", "same") if !a.Equals(b) { t.Error("Same CC(value) should be equal") } a = enmime.Builder().CC("name", "foo") b = enmime.Builder().CC("name", "bar") if a.Equals(b) { t.Error("Different CC(value) should not be equal") } a = enmime.Builder().CC("name", "foo") b = a.CC("name", "bar") if a.Equals(b) { t.Error("CC() should not mutate receiver, failed") } a = enmime.Builder().From("name", "foo").Subject("foo") a = a.CC("one", "one@inbucket.org") a = a.CC("two", "two@inbucket.org") want := "\"one\" , \"two\" " p, err := a.Build() if err != nil { t.Fatal(err) } got := p.Header.Get("CC") if !reflect.DeepEqual(got, want) { t.Errorf("CC: %q, want: %q", got, want) } } func TestBuilderCCAddrs(t *testing.T) { a := enmime.Builder().CCAddrs([]mail.Address{{Name: "name", Address: "same"}}) b := enmime.Builder().CCAddrs([]mail.Address{{Name: "name", Address: "same"}}) if !a.Equals(b) { t.Error("Same CC(value) should be equal") } a = enmime.Builder().CCAddrs([]mail.Address{{Name: "name", Address: "foo"}}) b = enmime.Builder().CCAddrs([]mail.Address{{Name: "name", Address: "bar"}}) if a.Equals(b) { t.Error("Different CC(value) should not be equal") } a = enmime.Builder().CCAddrs([]mail.Address{{Name: "name", Address: "foo"}}) b = a.CCAddrs([]mail.Address{{Name: "name", Address: "bar"}}) if a.Equals(b) { t.Error("CC() should not mutate receiver, failed") } input := []mail.Address{ {Name: "one", Address: "one@inbucket.org"}, {Name: "two", Address: "two@inbucket.org"}, } want := "\"one\" , \"two\" " a = enmime.Builder().CCAddrs(input).From("name", "foo").Subject("foo") p, err := a.Build() if err != nil { t.Fatal(err) } got := p.Header.Get("Cc") if !reflect.DeepEqual(got, want) { t.Errorf("CC: %q, want: %q", got, want) } } func TestBuilderBCC(t *testing.T) { a := enmime.Builder().BCC("name", "same") b := enmime.Builder().BCC("name", "same") if !a.Equals(b) { t.Error("Same BCC(value) should be equal") } a = enmime.Builder().BCC("name", "foo") b = enmime.Builder().BCC("name", "bar") if a.Equals(b) { t.Error("Different BCC(value) should not be equal") } a = enmime.Builder().BCC("name", "foo") b = a.BCC("name", "bar") if a.Equals(b) { t.Error("BCC() should not mutate receiver, failed") } a = enmime.Builder().From("name", "foo").Subject("foo") a = a.BCC("one", "one@inbucket.org") a = a.BCC("two", "two@inbucket.org") want := "" p, err := a.Build() if err != nil { t.Fatal(err) } got := p.Header.Get("BCC") if !reflect.DeepEqual(got, want) { t.Errorf("BCC: %q, want: %q", got, want) } } func TestBuilderBCCAddrs(t *testing.T) { a := enmime.Builder().BCCAddrs([]mail.Address{{Name: "name", Address: "same"}}) b := enmime.Builder().BCCAddrs([]mail.Address{{Name: "name", Address: "same"}}) if !a.Equals(b) { t.Error("Same BCC(value) should be equal") } a = enmime.Builder().BCCAddrs([]mail.Address{{Name: "name", Address: "foo"}}) b = enmime.Builder().BCCAddrs([]mail.Address{{Name: "name", Address: "bar"}}) if a.Equals(b) { t.Error("Different BCC(value) should not be equal") } a = enmime.Builder().BCCAddrs([]mail.Address{{Name: "name", Address: "foo"}}) b = a.BCCAddrs([]mail.Address{{Name: "name", Address: "bar"}}) if a.Equals(b) { t.Error("BCC() should not mutate receiver, failed") } // BCC doesn't show up in headers input := []mail.Address{ {Name: "one", Address: "one@inbucket.org"}, {Name: "two", Address: "two@inbucket.org"}, } want := "" a = enmime.Builder().BCCAddrs(input).From("name", "foo").Subject("foo") p, err := a.Build() if err != nil { t.Fatal(err) } got := p.Header.Get("Bcc") if !reflect.DeepEqual(got, want) { t.Errorf("BCC: %q, want: %q", got, want) } } func TestBuilderReplyTo(t *testing.T) { a := enmime.Builder().ReplyTo("name", "same") b := enmime.Builder().ReplyTo("name", "same") if !a.Equals(b) { t.Error("Same ReplyTo(value) should be equal") } a = enmime.Builder().ReplyTo("name", "foo") b = enmime.Builder().ReplyTo("name", "bar") if a.Equals(b) { t.Error("Different ReplyTo(value) should not be equal") } a = enmime.Builder().ReplyTo("name", "foo") b = a.ReplyTo("name", "bar") if a.Equals(b) { t.Error("ReplyTo() should not mutate receiver, failed") } a = enmime.Builder().ToAddrs(addrSlice).From("name", "foo").Subject("foo") a = a.ReplyTo("one", "one@inbucket.org") want := "\"one\" " p, err := a.Build() if err != nil { t.Fatal(err) } got := p.Header.Get("Reply-To") if got != want { t.Errorf("Reply-To: %q, want: %q", got, want) } } func TestBuilderText(t *testing.T) { a := enmime.Builder().Text([]byte("same")) b := enmime.Builder().Text([]byte("same")) if !a.Equals(b) { t.Error("Same Text(value) should be equal") } a = enmime.Builder().Text([]byte("foo")) b = enmime.Builder().Text([]byte("bar")) if a.Equals(b) { t.Error("Different Text(value) should not be equal") } a = enmime.Builder().Text([]byte("foo")) b = a.Text([]byte("bar")) if a.Equals(b) { t.Error("Text() should not mutate receiver, failed") } want := "test text body" a = enmime.Builder().Text([]byte(want)).From("name", "foo").Subject("foo").ToAddrs(addrSlice) p, err := a.Build() if err != nil { t.Fatal(err) } got := string(p.Content) if got != want { t.Errorf("Content: %q, want: %q", got, want) } want = "text/plain" got = p.ContentType if got != want { t.Errorf("Content-Type: %q, want: %q", got, want) } want = "utf-8" got = p.Charset if got != want { t.Errorf("Charset: %q, want: %q", got, want) } } func TestBuilderHTML(t *testing.T) { a := enmime.Builder().HTML([]byte("same")) b := enmime.Builder().HTML([]byte("same")) if !a.Equals(b) { t.Error("Same HTML(value) should be equal") } a = enmime.Builder().HTML([]byte("foo")) b = enmime.Builder().HTML([]byte("bar")) if a.Equals(b) { t.Error("Different HTML(value) should not be equal") } a = enmime.Builder().HTML([]byte("foo")) b = a.HTML([]byte("bar")) if a.Equals(b) { t.Error("HTML() should not mutate receiver, failed") } want := "test html body" a = enmime.Builder().HTML([]byte(want)).From("name", "foo").Subject("foo").ToAddrs(addrSlice) p, err := a.Build() if err != nil { t.Fatal(err) } got := string(p.Content) if got != want { t.Errorf("Content: %q, want: %q", got, want) } want = "text/html" got = p.ContentType if got != want { t.Errorf("Content-Type: %q, want: %q", got, want) } want = "utf-8" got = p.Charset if got != want { t.Errorf("Charset: %q, want: %q", got, want) } } func TestBuilderMultiBody(t *testing.T) { text := "test text body" html := "test html body" a := enmime.Builder(). Text([]byte(text)). HTML([]byte(html)). From("name", "foo"). Subject("foo"). ToAddrs(addrSlice) root, err := a.Build() if err != nil { t.Fatal(err) } // Should be multipart p := root want := "multipart/alternative" got := p.ContentType if got != want { t.Errorf("Content-Type: %q, want: %q", got, want) } // Find text part p = root.DepthMatchFirst(func(p *enmime.Part) bool { return p.ContentType == "text/plain" }) if p == nil { t.Fatal("Did not find a text/plain part") } want = text got = string(p.Content) if got != want { t.Errorf("Content: %q, want: %q", got, want) } want = "utf-8" got = p.Charset if got != want { t.Errorf("Charset: %q, want: %q", got, want) } // Find HTML part p = root.DepthMatchFirst(func(p *enmime.Part) bool { return p.ContentType == "text/html" }) if p == nil { t.Fatal("Did not find a text/html part") } want = html got = string(p.Content) if got != want { t.Errorf("Content: %q, want: %q", got, want) } want = "utf-8" got = p.Charset if got != want { t.Errorf("Charset: %q, want: %q", got, want) } } func TestBuilderAddAttachment(t *testing.T) { a := enmime.Builder().AddAttachment([]byte("same"), "ct", "fn") b := enmime.Builder().AddAttachment([]byte("same"), "ct", "fn") if !a.Equals(b) { t.Error("Same AddAttachment(value) should be equal") } a = enmime.Builder().AddAttachment([]byte("foo"), "ct", "fn") b = enmime.Builder().AddAttachment([]byte("bar"), "ct", "fn") if a.Equals(b) { t.Error("Different AddAttachment(value) should not be equal") } a = enmime.Builder().AddAttachment([]byte("foo"), "ct", "fn") b = a.AddAttachment([]byte("bar"), "ct", "fn") b1 := b.AddAttachment([]byte("baz"), "ct", "fn") b2 := b.AddAttachment([]byte("bax"), "ct", "fn") if a.Equals(b) || b.Equals(b1) || b1.Equals(b2) { t.Error("AddAttachment() should not mutate receiver, failed") } want := "fake JPG data" name := "photo.jpg" disposition := "attachment" a = enmime.Builder(). Text([]byte("text")). HTML([]byte("html")). From("name", "foo"). Subject("foo"). ToAddrs(addrSlice). AddAttachment([]byte(want), "image/jpeg", name) root, err := a.Build() if err != nil { t.Fatal(err) } p := root.DepthMatchFirst(func(p *enmime.Part) bool { return p.FileName == name }) if p == nil { t.Fatalf("Did not find a %q part", name) } if p.Disposition != disposition { t.Errorf("Content disposition: %s, want: %s", p.Disposition, disposition) } got := string(p.Content) if got != want { t.Errorf("Content: %q, want: %q", got, want) } // Check structure wantTypes := []string{ "multipart/mixed", "multipart/alternative", "text/plain", "text/html", "image/jpeg", } gotParts := root.DepthMatchAll(func(p *enmime.Part) bool { return true }) gotTypes := make([]string, 0) for _, p := range gotParts { gotTypes = append(gotTypes, p.ContentType) } test.DiffStrings(t, gotTypes, wantTypes) } func TestBuilderAddFileAttachment(t *testing.T) { a := enmime.Builder().AddFileAttachment("zzzDOESNOTEXIST") if a.Error() == nil { t.Error("Expected an error, got nil") } want := a.Error() _, got := a.Build() if got != want { t.Errorf("Build should abort; got: %v, want: %v", got, want) } b := a.AddFileAttachment("zzzDOESNOTEXIST2") got = b.Error() if got != want { // Only the first error should be stored t.Errorf("Error redefined; got %v, wanted %v", got, want) } a = enmime.Builder().From("name", "from") _ = a.AddFileAttachment("zzzDOESNOTEXIST") if a.Error() != nil { t.Error("AddFileAttachment error mutated receiver") } a = enmime.Builder().AddFileAttachment("builder_test.go") b = enmime.Builder().AddFileAttachment("builder_test.go") if a.Error() != nil { t.Fatalf("Expected no error, got %v", a.Error()) } if b.Error() != nil { t.Fatalf("Expected no error, got %v", b.Error()) } if !a.Equals(b) { t.Error("Same AddFileAttachment(value) should be equal") } a = enmime.Builder().AddFileAttachment("builder_test.go") b = enmime.Builder().AddFileAttachment("builder.go") if a.Error() != nil { t.Fatalf("Expected no error, got %v", a.Error()) } if b.Error() != nil { t.Fatalf("Expected no error, got %v", b.Error()) } if a.Equals(b) { t.Error("Different AddFileAttachment(value) should not be equal") } a = enmime.Builder().AddFileAttachment("builder_test.go") b = a.AddFileAttachment("builder_test.go") b1 := b.AddFileAttachment("builder_test.go") b2 := b.AddFileAttachment("builder.go") if a.Equals(b) || b.Equals(b1) || b1.Equals(b2) { t.Error("AddFileAttachment() should not mutate receiver, failed") } name := "fake.png" ctype := "image/png" a = enmime.Builder(). Text([]byte("text")). HTML([]byte("html")). From("name", "foo"). Subject("foo"). ToAddrs(addrSlice). AddFileAttachment(filepath.Join("testdata", "attach", "fake.png")) root, err := a.Build() if err != nil { t.Fatal(err) } p := root.DepthMatchFirst(func(p *enmime.Part) bool { return p.FileName == name }) if p == nil { t.Fatalf("Did not find a %q part", name) } p = root.DepthMatchFirst(func(p *enmime.Part) bool { return p.ContentType == ctype }) if p == nil { t.Fatalf("Did not find a %q part", ctype) } } func TestBuilderAddInline(t *testing.T) { a := enmime.Builder().AddInline([]byte("same"), "ct", "fn", "cid") b := enmime.Builder().AddInline([]byte("same"), "ct", "fn", "cid") if !a.Equals(b) { t.Error("Same AddInline(value) should be equal") } a = enmime.Builder().AddInline([]byte("foo"), "ct", "fn", "cid") b = enmime.Builder().AddInline([]byte("bar"), "ct", "fn", "cid") if a.Equals(b) { t.Error("Different AddInline(value) should not be equal") } a = enmime.Builder().AddInline([]byte("foo"), "ct", "fn", "cid") b = a.AddInline([]byte("bar"), "ct", "fn", "cid") b1 := b.AddInline([]byte("baz"), "ct", "fn", "cid") b2 := b.AddInline([]byte("bax"), "ct", "fn", "cid") if a.Equals(b) || b.Equals(b1) || b1.Equals(b2) { t.Error("AddInline() should not mutate receiver, failed") } want := "fake JPG data" name := "photo.jpg" disposition := "inline" cid := "" a = enmime.Builder(). Text([]byte("text")). HTML([]byte("html")). From("name", "foo"). Subject("foo"). ToAddrs(addrSlice). AddInline([]byte(want), "image/jpeg", name, cid) root, err := a.Build() if err != nil { t.Fatal(err) } p := root.DepthMatchFirst(func(p *enmime.Part) bool { return p.ContentID == cid }) if p == nil { t.Fatalf("Did not find a %q part", cid) } if p.Disposition != disposition { t.Errorf("Content disposition: %s, want: %s", p.Disposition, disposition) } got := string(p.Content) if got != want { t.Errorf("Content: %q, want: %q", got, want) } // Check structure wantTypes := []string{ "multipart/related", "multipart/alternative", "text/plain", "text/html", "image/jpeg", } gotParts := root.DepthMatchAll(func(p *enmime.Part) bool { return true }) gotTypes := make([]string, 0) for _, p := range gotParts { gotTypes = append(gotTypes, p.ContentType) } test.DiffStrings(t, gotTypes, wantTypes) } func TestBuilderAddFileInline(t *testing.T) { a := enmime.Builder().AddFileInline("zzzDOESNOTEXIST") if a.Error() == nil { t.Error("Expected an error, got nil") } want := a.Error() _, got := a.Build() if got != want { t.Errorf("Build should abort; got: %v, want: %v", got, want) } b := a.AddFileInline("zzzDOESNOTEXIST2") got = b.Error() if got != want { // Only the first error should be stored t.Errorf("Error redefined; got %v, wanted %v", got, want) } a = enmime.Builder().From("name", "from") _ = a.AddFileInline("zzzDOESNOTEXIST") if a.Error() != nil { t.Error("AddFileInline error mutated receiver") } a = enmime.Builder().AddFileInline("builder_test.go") b = enmime.Builder().AddFileInline("builder_test.go") if a.Error() != nil { t.Fatalf("Expected no error, got %v", a.Error()) } if b.Error() != nil { t.Fatalf("Expected no error, got %v", b.Error()) } if !a.Equals(b) { t.Error("Same AddFileInline(value) should be equal") } a = enmime.Builder().AddFileInline("builder_test.go") b = enmime.Builder().AddFileInline("builder.go") if a.Error() != nil { t.Fatalf("Expected no error, got %v", a.Error()) } if b.Error() != nil { t.Fatalf("Expected no error, got %v", b.Error()) } if a.Equals(b) { t.Error("Different AddFileInline(value) should not be equal") } a = enmime.Builder().AddFileInline("builder_test.go") b = a.AddFileInline("builder_test.go") b1 := b.AddFileInline("builder_test.go") b2 := b.AddFileInline("builder.go") if a.Equals(b) || b.Equals(b1) || b1.Equals(b2) { t.Error("AddFileInline() should not mutate receiver, failed") } name := "fake.png" ctype := "image/png" a = enmime.Builder(). Text([]byte("text")). HTML([]byte("html")). From("name", "foo"). Subject("foo"). ToAddrs(addrSlice). AddFileInline(filepath.Join("testdata", "attach", "fake.png")) root, err := a.Build() if err != nil { t.Fatal(err) } p := root.DepthMatchFirst(func(p *enmime.Part) bool { return p.ContentID == name }) if p == nil { t.Fatalf("Did not find a %q part", name) } p = root.DepthMatchFirst(func(p *enmime.Part) bool { return p.ContentType == ctype }) if p == nil { t.Fatalf("Did not find a %q part", ctype) } } func TestValidation(t *testing.T) { _, err := enmime.Builder(). To("name", "address"). From("name", "address"). Subject("subject"). Build() if err != nil { t.Errorf("error %v, expected nil", err) } _, err = enmime.Builder(). CC("name", "address"). From("name", "address"). Subject("subject"). Build() if err != nil { t.Errorf("error %v, expected nil", err) } _, err = enmime.Builder(). BCC("name", "address"). From("name", "address"). Subject("subject"). Build() if err != nil { t.Errorf("error %v, expected nil", err) } _, err = enmime.Builder(). From("name", "address"). Subject("subject"). Build() if err == nil { t.Error("error nil, expected value") } _, err = enmime.Builder(). To("name", "address"). Subject("subject"). Build() if err == nil { t.Error("error nil, expected value") } } func TestBuilderFullStructure(t *testing.T) { a := enmime.Builder(). Text([]byte("text")). HTML([]byte("html")). From("name", "foo"). Subject("foo"). ToAddrs(addrSlice). AddAttachment([]byte("attach data"), "image/jpeg", "image.jpg"). AddInline([]byte("inline data"), "image/png", "image.png", "") root, err := a.Build() if err != nil { t.Fatal(err) } want := "1.0" got := root.Header.Get("MIME-Version") if got != want { t.Errorf("MIME-Version: %q, want: %q", got, want) } // Check structure via "parent > child" content types wantTypes := []string{ " > multipart/mixed", "multipart/mixed > multipart/related", "multipart/related > multipart/alternative", "multipart/alternative > text/plain", "multipart/alternative > text/html", "multipart/related > image/png", "multipart/mixed > image/jpeg", } gotParts := root.DepthMatchAll(func(p *enmime.Part) bool { return true }) gotTypes := make([]string, 0) for _, p := range gotParts { pct := "" if p.Parent != nil { pct = p.Parent.ContentType } gotTypes = append(gotTypes, pct+" > "+p.ContentType) } test.DiffStrings(t, gotTypes, wantTypes) } func TestHeader(t *testing.T) { a := enmime.Builder().Header("name", "same") b := enmime.Builder().Header("name", "same") if !a.Equals(b) { t.Error("Same Header(value) should be equal") } a = enmime.Builder().Header("name", "foo") b = enmime.Builder().Header("name", "bar") if a.Equals(b) { t.Error("Different Header(value) should not be equal") } a = enmime.Builder().Header("name", "foo") b = a.Header("name", "bar") if a.Equals(b) { t.Error("Header() should not mutate receiver, failed") } want := []string{"value one", "another value"} a = enmime.Builder().ToAddrs(addrSlice).From("name", "foo").Subject("foo") for _, s := range want { a = a.Header("X-Test", s) } p, err := a.Build() if err != nil { t.Fatal(err) } got := p.Header["X-Test"] test.DiffStrings(t, got, want) } func TestBuilderQPHeaders(t *testing.T) { msg := enmime.Builder(). To("Patrik Fältström", "paf@nada.kth.se"). To("Keld Jørn Simonsen", "keld@dkuug.dk"). From("Olle Järnefors", "ojarnef@admin.kth.se"). Subject("RFC 2047"). Date(time.Date(2017, 1, 1, 13, 14, 15, 16, time.UTC)) p, err := msg.Build() if err != nil { t.Fatal(err) } b := &bytes.Buffer{} p.Encode(b) if err != nil { t.Fatal(err) } test.DiffGolden(t, b.Bytes(), "testdata", "encode", "build-qp-addr-headers.golden") } func TestSend(t *testing.T) { sender := &mockSender{} from := "from@example.com" tos := []string{"to0@example.com", "to1@example.com"} ccs := []string{"cc0@example.com", "cc1@example.com"} bccs := []string{"bcc0@example.com", "bcc1@example.com"} text := []byte("test text body") html := []byte("test html body") a := enmime.Builder(). Text(text). HTML(html). From("name", from). Subject("foo"). To("to 0", tos[0]). To("to 1", tos[1]). CC("cc 0", ccs[0]). CC("cc 1", ccs[1]). BCC("bcc 0", bccs[0]). BCC("bcc 1", bccs[1]) err := a.Send(sender) if err != nil { t.Fatal(err) } if sender.from != from { t.Errorf("Got from %q, wanted %q", sender.from, from) } addrs := append([]string{}, tos...) addrs = append(addrs, ccs...) addrs = append(addrs, bccs...) test.DiffStrings(t, sender.to, addrs) if !bytes.Contains(sender.msg, text) { t.Errorf("msg bytes did not contain text body %q", text) } if !bytes.Contains(sender.msg, html) { t.Errorf("msg bytes did not contain html body %q", html) } } func TestSendWithReversePath(t *testing.T) { sender := &mockSender{} ret := "return@example.com" from := "from@example.com" to := "t0@example.com" text := []byte("test text body") a := enmime.Builder(). Text(text). From("name", from). Subject("foo"). To("to 0", to) err := a.SendWithReversePath(sender, ret) if err != nil { t.Fatal(err) } if sender.from != ret { // Builder's .From() should not be provided to Sender.Send(). t.Errorf("Got from %q, wanted %q", sender.from, ret) } test.DiffStrings(t, sender.to, []string{to}) if !bytes.Contains(sender.msg, text) { t.Errorf("msg bytes did not contain text body %q", text) } } func TestEmptyTo(t *testing.T) { from := "from@example.com" text := []byte("test text body") rcpt := "rcpt name" a := enmime.Builder(). Text(text). From("name", from). Subject("foo"). To(rcpt, "") _, err := a.Build() if err == nil { t.Fatal("Expected error, got nil") } if err.Error() != enmime.ErrorMissingRecipient { t.Fatalf("Unexpected error, wanted %q got %s", enmime.ErrorMissingRecipient, err) } } func TestEmptyBCC(t *testing.T) { from := "from@example.com" text := []byte("test text body") rcpt := "rcpt name" a := enmime.Builder(). Text(text). From("name", from). Subject("foo"). BCC(rcpt, "") _, err := a.Build() if err == nil { t.Fatal("Expected error, got nil") } if err.Error() != enmime.ErrorMissingRecipient { t.Fatalf("Unexpected error, wanted %q got %s", enmime.ErrorMissingRecipient, err) } } func TestEmptyCC(t *testing.T) { from := "from@example.com" text := []byte("test text body") rcpt := "rcpt name" a := enmime.Builder(). Text(text). From("name", from). Subject("foo"). CC(rcpt, "") _, err := a.Build() if err == nil { t.Fatal("Expected error, got nil") } if err.Error() != enmime.ErrorMissingRecipient { t.Fatalf("Unexpected error, wanted %q got %s", enmime.ErrorMissingRecipient, err) } } enmime-0.9.3/cmd/000077500000000000000000000000001417532643400135515ustar00rootroot00000000000000enmime-0.9.3/cmd/example_test.go000066400000000000000000000057331417532643400166020ustar00rootroot00000000000000package cmd_test import ( "log" "os" "strings" "github.com/jhillyerd/enmime" "github.com/jhillyerd/enmime/cmd" ) func Example() { mail := `From: James Hillyerd To: Greg Reader , Root Node Date: Sat, 04 Dec 2016 18:38:25 -0800 Subject: Example Message Content-Type: multipart/mixed; boundary="Enmime-Test-100" --Enmime-Test-100 Content-Type: text/plain Text section. --Enmime-Test-100 Content-Type: text/html HTML section. --Enmime-Test-100 Content-Transfer-Encoding: base64 Content-Disposition: inline; filename=favicon.png Content-Type: image/png; x-unix-mode=0644; name="favicon.png" Content-Id: <8B8481A2-25CA-4886-9B5A-8EB9115DD064@skynet> iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ bWFnZVJlYWR5ccllPAAAAlFJREFUeNqUU8tOFEEUPVVdNV3dPe8xYRBnjGhmBgKjKzCIiQvBoIaN bly5Z+PSv3Aj7DSiP2B0rwkLGVdGgxITSCRIJGSMEQWZR3eVt5sEFBgTb/dN1yvnnHtPNTPG4Pqd HgCMXnPRSZrpSuH8vUJu4DE4rYHDGAZDX62BZttHqTiIayM3gGiXQsgYLEvATaqxU+dy1U13YXap XptpNHY8iwn8KyIAzm1KBdtRZWErpI5lEWTXp5Z/vHpZ3/wyKKwYGGOdAYwR0EZwoezTYApBEIOb yELl/aE1/83cp40Pt5mxqCKrE4Ck+mVWKKcI5tA8BLEhRBKJLjez6a7MLq7XZtp+yyOawwCBtkiB VZDKzRk4NN7NQBMYPHiZDFhXY+p9ff7F961vVcnl4R5I2ykJ5XFN7Ab7Gc61VoipNBKF+PDyztu5 lfrSLT/wIwCxq0CAGtXHZTzqR2jtwQiXONma6hHpj9sLT7YaPxfTXuZdBGA02Wi7FS48YiTfj+i2 NhqtdhP5RC8mh2/Op7y0v6eAcWVLFT8D7kWX5S9mepp+C450MV6aWL1cGnvkxbwHtLW2B9AOkLeU d9KEDuh9fl/7CEj7YH5g+3r/lWfF9In7tPz6T4IIwBJOr1SJyIGQMZQbsh5P9uBq5VJtqHh2mo49 pdw5WFoEwKWqWHacaWOjQXWGcifKo6vj5RGS6zykI587XeUIQDqJSmAp+lE4qt19W5P9o8+Lma5D cjsC8JiT607lMVkdqQ0Vyh3lHhmh52tfNy78ajXv0rgYzv8nfwswANuk+7sD/Q0aAAAAAElFTkSu QmCC --Enmime-Test-100 Content-Transfer-Encoding: base64 Content-Type: text/html; name="test.html" Content-Disposition: attachment; filename=test.html PGh0bWw+Cg== --Enmime-Test-100-- ` // Convert MIME text to Envelope r := strings.NewReader(mail) env, err := enmime.ReadEnvelope(r) if err != nil { log.Fatal(err) return } err = cmd.EnvelopeToMarkdown(os.Stdout, env, "Example Message Output") if err != nil { log.Fatal(err) return } // Output: // Example Message Output // ====================== // // ## Header // Content-Type: multipart/mixed; boundary="Enmime-Test-100" // Date: Sat, 04 Dec 2016 18:38:25 -0800 // // ## Envelope // ### From // - James Hillyerd `` // // ### To // - Greg Reader `` // - Root Node `` // // ### Subject // Example Message // // ## Body Text // Text section. // // ## Body HTML // HTML section. // // ## Attachment List // - test.html (text/html) // // ## Inline List // - favicon.png (image/png) // Content-ID: 8B8481A2-25CA-4886-9B5A-8EB9115DD064@skynet // // ## Other Part List // // ## MIME Part Tree // multipart/mixed // |-- text/plain // |-- text/html // |-- image/png, disposition: inline, filename: "favicon.png" // `-- text/html, disposition: attachment, filename: "test.html" // } enmime-0.9.3/cmd/mime-dump/000077500000000000000000000000001417532643400154435ustar00rootroot00000000000000enmime-0.9.3/cmd/mime-dump/README.md000066400000000000000000000014031417532643400167200ustar00rootroot00000000000000mime-dump ========= mime-dump is a utility that aids in the debugging of enmime. To use it type `go build` in this directory, then pass it an email to parse: ./mime-dump ../test-data/mail/html-mime-inline.raw If all goes well, it will output a markdown formatted document describing the email... ---- html-mime-inline.raw ==================== Envelope -------- From: James Hillyerd To: greg@nobody.com Subject: MIME test 1 Body Text --------- Test of text section Body HTML --------- Test of HTML section Attachment List --------------- MIME Part Tree -------------- multipart/alternative |-- text/plain `-- multipart/related |-- text/html `-- image/png, disposition: inline, filename: "favicon.png" enmime-0.9.3/cmd/mime-dump/mime-dump.go000066400000000000000000000021451417532643400176660ustar00rootroot00000000000000// Package main outputs a markdown formatted document describing the provided email package main import ( "fmt" "io" "os" "path/filepath" "github.com/jhillyerd/enmime" "github.com/jhillyerd/enmime/cmd" ) type dumper struct { errOut, stdOut io.Writer exit exitFunc } type exitFunc func(int) func newDefaultDumper() *dumper { return &dumper{ errOut: os.Stderr, stdOut: os.Stdout, exit: os.Exit, } } func main() { d := newDefaultDumper() d.exit(d.dump(os.Args)) } func (d *dumper) dump(args []string) int { if len(args) < 2 { fmt.Fprintln(d.errOut, "Missing filename argument") return 1 } reader, err := os.Open(args[1]) if err != nil { fmt.Fprintln(d.errOut, "Failed to open file:", err) return 1 } // basename is used as the markdown title. basename := filepath.Base(args[1]) e, err := enmime.ReadEnvelope(reader) if err != nil { fmt.Fprintf(d.errOut, "Failed to read envelope:\n%+v\n", err) return 1 } if err = cmd.EnvelopeToMarkdown(d.stdOut, e, basename); err != nil { fmt.Fprintf(d.errOut, "Failed to render markdown:\n%+v\n", err) return 1 } return 0 } enmime-0.9.3/cmd/mime-dump/mime-dump_test.go000066400000000000000000000033151417532643400207250ustar00rootroot00000000000000package main import ( "bytes" "path/filepath" "strings" "testing" ) func TestNewDefaultDumper(t *testing.T) { if newDefaultDumper() == nil { t.Fatal("Dumper instance should not be nil") } } func TestNotEnoughArgs(t *testing.T) { b := &bytes.Buffer{} d := &dumper{ errOut: b, } exitCode := d.dump(nil) if exitCode != 1 { t.Fatal("Should have returned an exit code of 1, failed") } if b.String() != "Missing filename argument\n" { t.Fatal("Should be missing filename argument, failed") } } func TestFailedToOpenFile(t *testing.T) { b := &bytes.Buffer{} d := &dumper{ errOut: b, } exitCode := d.dump([]string{"", ""}) if exitCode != 1 { t.Fatal("Should have returned an exit code of 1, failed") } if !strings.HasPrefix(b.String(), "Failed to open file") { t.Fatal("Should have failed to open file, failed") } } func TestFailedToParseFile(t *testing.T) { b := &bytes.Buffer{} d := &dumper{ errOut: b, } exitCode := d.dump([]string{"", filepath.Join("..", "..", "testdata", "mail", "erroneous.raw")}) if exitCode != 1 { t.Fatal("Should have returned an exit code of 1, failed") } if !strings.HasPrefix(b.String(), "Failed to read envelope") { t.Fatal("Should have failed to parse file, but couldn't find error message") } } func TestSuccess(t *testing.T) { b := &bytes.Buffer{} s := &bytes.Buffer{} d := &dumper{ errOut: b, stdOut: s, } exitCode := d.dump([]string{"", filepath.Join("..", "..", "testdata", "mail", "attachment.raw")}) if exitCode != 0 { t.Fatal("Should have returned an exit code of 0, failed") } if b.Len() > 0 { t.Fatal("Should not have produced any errors, failed") } if !(s.Len() > 0) { t.Fatal("Should have printed markdown document, failed") } } enmime-0.9.3/cmd/mime-extractor/000077500000000000000000000000001417532643400165115ustar00rootroot00000000000000enmime-0.9.3/cmd/mime-extractor/mime-extractor.go000066400000000000000000000037401417532643400220040ustar00rootroot00000000000000// Package main extracts attachments from the provided email package main import ( "flag" "fmt" "io" "io/ioutil" "os" "path/filepath" "github.com/jhillyerd/enmime" "github.com/jhillyerd/enmime/cmd" ) var ( mimefile = flag.String("f", "", "mime(eml) file") outdir = flag.String("o", "", "output dir") ) type extractor struct { errOut, stdOut io.Writer exit exitFunc wd workingDir fileWrite attachmentWriter } type exitFunc func(int) type workingDir func() (string, error) type attachmentWriter func(string, []byte, os.FileMode) error func newDefaultExtractor() *extractor { return &extractor{ errOut: os.Stderr, stdOut: os.Stdout, exit: os.Exit, wd: os.Getwd, fileWrite: ioutil.WriteFile, } } func main() { flag.Parse() ex := newDefaultExtractor() ex.exit(ex.extract(*mimefile, *outdir)) } func (ex *extractor) extract(file, dir string) int { if file == "" { fmt.Fprintln(ex.errOut, "Missing filename argument") flag.Usage() return 1 } if dir == "" { dir, _ = ex.wd() } if err := os.MkdirAll(dir, os.ModePerm); err != nil { fmt.Fprintf(ex.errOut, "Mkdir %s failed.", dir) return 2 } reader, err := os.Open(file) if err != nil { fmt.Fprintln(ex.errOut, "Failed to open file:", err) return 1 } // basename is used as the markdown title basename := filepath.Base(file) e, err := enmime.ReadEnvelope(reader) if err != nil { fmt.Fprintln(ex.errOut, "During enmime.ReadEnvelope:", err) return 1 } if err = cmd.EnvelopeToMarkdown(ex.stdOut, e, basename); err != nil { fmt.Fprintln(ex.errOut, err) return 1 } // Write errOut attachments fmt.Fprintf(ex.errOut, "\nExtracting attachments into %s...", dir) for _, a := range e.Attachments { newFileName := filepath.Join(dir, a.FileName) err = ex.fileWrite(newFileName, a.Content, 0644) if err != nil { fmt.Fprintf(ex.stdOut, "Error writing file %q: %v\n", newFileName, err) break } } fmt.Fprintln(ex.errOut, " Done!") return 0 } enmime-0.9.3/cmd/mime-extractor/mime-extractor_test.go000066400000000000000000000062241417532643400230430ustar00rootroot00000000000000package main import ( "bytes" "fmt" "io/ioutil" "os" "path/filepath" "strings" "testing" ) func TestNewDefaultExtractor(t *testing.T) { if newDefaultExtractor() == nil { t.Fatal("Extractor instance should not be nil") } } func TestExtractEmptyFilename(t *testing.T) { b := &bytes.Buffer{} testExtractor := &extractor{ errOut: b, stdOut: ioutil.Discard, } exitCode := testExtractor.extract("", "") if exitCode != 1 { t.Fatal("Exit code should be 1, failed") } if !strings.Contains(b.String(), "Missing filename argument") { t.Fatal("Should not succeed with an empty filename, failed") } } func TestExtractEmptyOutputDirectory(t *testing.T) { b := &bytes.Buffer{} workingDirectoryFn := func() (string, error) { return "", nil } testExtractor := &extractor{ errOut: b, stdOut: ioutil.Discard, wd: workingDirectoryFn, } exitCode := testExtractor.extract("some.file", "") if exitCode != 2 { t.Fatal("Exit code should be 2, failed") } if !strings.Contains(b.String(), "Mkdir failed.") { t.Fatal("Should not succeed with an empty output directory, failed") } } func TestExtractFailedToOpenFile(t *testing.T) { b := &bytes.Buffer{} testExtractor := &extractor{ errOut: b, stdOut: ioutil.Discard, wd: os.Getwd, } exitCode := testExtractor.extract("some.file", "") if exitCode != 1 { t.Fatal("Exit code should be 1, failed") } if !strings.Contains(b.String(), "Failed to open file:") { t.Fatal("File should not exist, failed") } } func TestExtractFailedToParse(t *testing.T) { s := &bytes.Buffer{} testExtractor := &extractor{ errOut: s, stdOut: ioutil.Discard, wd: os.Getwd, } exitCode := testExtractor.extract(filepath.Join("..", "..", "testdata", "mail", "erroneous.raw"), "") if exitCode != 1 { t.Fatal("Exit code should be 1, failed") } if !strings.Contains(s.String(), "During enmime.ReadEnvelope") { t.Fatal("Should have failed to write the attachment, failed") } } func TestExtractAttachmentWriteFail(t *testing.T) { s := &bytes.Buffer{} fw := func(filename string, data []byte, perm os.FileMode) error { return fmt.Errorf("AttachmentWriteFail") } testExtractor := &extractor{ errOut: ioutil.Discard, stdOut: s, wd: os.Getwd, fileWrite: fw, } exitCode := testExtractor.extract(filepath.Join("..", "..", "testdata", "mail", "attachment.raw"), "") if exitCode != 0 { t.Fatal("Exit code should be 0, failed") } if !strings.HasSuffix(s.String(), "AttachmentWriteFail\n") { t.Fatal("Should have failed to write the attachment, failed") } } func TestExtractSuccess(t *testing.T) { b := &bytes.Buffer{} attachmentCount := 0 fw := func(filename string, data []byte, perm os.FileMode) error { attachmentCount++ return nil } testExtractor := &extractor{ errOut: b, stdOut: ioutil.Discard, wd: os.Getwd, fileWrite: fw, } exitCode := testExtractor.extract(filepath.Join("..", "..", "testdata", "mail", "attachment.raw"), "") if exitCode != 0 { t.Fatal("Exit code should be 0, failed") } if attachmentCount < 1 { t.Fatal("Should be one attachment, failed") } if !strings.Contains(b.String(), "Done!") { t.Fatal("Should have succeeded, failed") } } enmime-0.9.3/cmd/utils.go000066400000000000000000000100351417532643400152370ustar00rootroot00000000000000package cmd import ( "bufio" "fmt" "io" "net/mail" "sort" "strings" "github.com/jhillyerd/enmime" ) // AddressHeaders enumerates SMTP headers that contain email addresses var addressHeaders = []string{"From", "To", "Delivered-To", "Cc", "Bcc", "Reply-To"} // markdown adds some simple HTML tag like methods to a bufio.Writer type markdown struct { *bufio.Writer } func (md *markdown) H1(content string) { bar := strings.Repeat("=", len(content)) fmt.Fprintf(md, "%v\n%v\n\n", content, bar) } func (md *markdown) H2(content string) { fmt.Fprintf(md, "## %v\n", content) } func (md *markdown) H3(content string) { fmt.Fprintf(md, "### %v\n", content) } // Printf implements fmt.Printf for markdown func (md *markdown) Printf(format string, args ...interface{}) { fmt.Fprintf(md, format, args...) } // Println implements fmt.Println for markdown func (md *markdown) Println(args ...interface{}) { fmt.Fprintln(md, args...) } // EnvelopeToMarkdown renders the contents of an enmime.Envelope in Markdown format. Used by // mime-dump and mime-extractor commands. func EnvelopeToMarkdown(w io.Writer, e *enmime.Envelope, name string) error { md := &markdown{bufio.NewWriter(w)} md.H1(name) // Output a sorted list of headers, minus the ones displayed later md.H2("Header") if e.Root != nil && e.Root.Header != nil { keys := make([]string, 0, len(e.Root.Header)) for k := range e.Root.Header { switch strings.ToLower(k) { case "from", "to", "cc", "bcc", "reply-to", "subject": continue } keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { md.Printf(" %v: %v\n", k, e.GetHeader(k)) } } md.Println() md.H2("Envelope") for _, hkey := range addressHeaders { addrlist, err := e.AddressList(hkey) if err != nil { if err == mail.ErrHeaderNotPresent { continue } return err } md.H3(hkey) for _, addr := range addrlist { md.Printf("- %v `<%v>`\n", addr.Name, addr.Address) } md.Println() } md.H3("Subject") md.Println(e.GetHeader("Subject")) md.Println() md.H2("Body Text") md.Println(e.Text) md.Println() md.H2("Body HTML") md.Println(e.HTML) md.Println() md.H2("Attachment List") for _, a := range e.Attachments { md.Printf("- %v (%v)\n", a.FileName, a.ContentType) if a.ContentID != "" { md.Printf(" Content-ID: %s\n", a.ContentID) } } md.Println() md.H2("Inline List") for _, a := range e.Inlines { md.Printf("- %v (%v)\n", a.FileName, a.ContentType) if a.ContentID != "" { md.Printf(" Content-ID: %s\n", a.ContentID) } } md.Println() md.H2("Other Part List") for _, a := range e.OtherParts { md.Printf("- %v (%v)\n", a.FileName, a.ContentType) if a.ContentID != "" { md.Printf(" Content-ID: %s\n", a.ContentID) } } md.Println() md.H2("MIME Part Tree") if e.Root == nil { md.Println("Message was not MIME encoded") } else { FormatPart(md, e.Root, " ") } if len(e.Errors) > 0 { md.Println() md.H2("Errors") for _, perr := range e.Errors { md.Println("-", perr) } } return md.Flush() } // FormatPart pretty prints the Part tree func FormatPart(w io.Writer, p *enmime.Part, indent string) { if p == nil { return } sibling := p.NextSibling child := p.FirstChild // Compute indent strings myindent := indent + "`-- " childindent := indent + " " if sibling != nil { myindent = indent + "|-- " childindent = indent + "| " } if p.Parent == nil { // Root shouldn't be decorated, has no siblings myindent = indent childindent = indent } // Format and print this node ctype := "MISSING TYPE" if p.ContentType != "" { ctype = p.ContentType } disposition := "" if p.Disposition != "" { disposition = fmt.Sprintf(", disposition: %s", p.Disposition) } filename := "" if p.FileName != "" { filename = fmt.Sprintf(", filename: %q", p.FileName) } errors := "" if len(p.Errors) > 0 { errors = fmt.Sprintf(" (errors: %v)", len(p.Errors)) } fmt.Fprintf(w, "%s%s%s%s%s\n", myindent, ctype, disposition, filename, errors) // Recurse FormatPart(w, child, childindent) FormatPart(w, sibling, indent) } enmime-0.9.3/cmd/utils_test.go000066400000000000000000000063441417532643400163060ustar00rootroot00000000000000package cmd import ( "bufio" "bytes" "testing" "github.com/jhillyerd/enmime" ) func TestMarkdownH1(t *testing.T) { buf := new(bytes.Buffer) bw := bufio.NewWriter(buf) md := &markdown{bw} md.H1("Big Header") bw.Flush() want := "Big Header\n==========\n\n" got := buf.String() if got != want { t.Errorf("got: %q, wanted: %q", got, want) } } func TestMarkdownH2(t *testing.T) { buf := new(bytes.Buffer) bw := bufio.NewWriter(buf) md := &markdown{bw} md.H2("Big Header") bw.Flush() want := "## Big Header\n" got := buf.String() if got != want { t.Errorf("got: %q, wanted: %q", got, want) } } func TestMarkdownH3(t *testing.T) { buf := new(bytes.Buffer) bw := bufio.NewWriter(buf) md := &markdown{bw} md.H3("Big Header") bw.Flush() want := "### Big Header\n" got := buf.String() if got != want { t.Errorf("got: %q, wanted: %q", got, want) } } func TestMarkdownPrintf(t *testing.T) { buf := new(bytes.Buffer) bw := bufio.NewWriter(buf) md := &markdown{bw} md.Printf("%v %q", 123, "quoted") bw.Flush() want := "123 \"quoted\"" got := buf.String() if got != want { t.Errorf("got: %q, wanted: %q", got, want) } } func TestMarkdownPrintln(t *testing.T) { buf := new(bytes.Buffer) bw := bufio.NewWriter(buf) md := &markdown{bw} md.Println("words") bw.Flush() want := "words\n" got := buf.String() if got != want { t.Errorf("got: %q, wanted: %q", got, want) } } func TestFormatPartNil(t *testing.T) { buf := new(bytes.Buffer) FormatPart(buf, nil, "") got := buf.String() want := "" if got != want { t.Errorf("FormatPart(nil) == %q, want: %q", got, want) } } func TestFormatPartEmpty(t *testing.T) { buf := new(bytes.Buffer) FormatPart(buf, &enmime.Part{}, "") got := buf.String() want := "MISSING TYPE\n" if got != want { t.Errorf("FormatPart(nil) == %q, want: %q", got, want) } } func TestFormatPartMulti(t *testing.T) { buf := new(bytes.Buffer) // Build part tree root := &enmime.Part{ ContentType: "multipart/alternative", } lev1 := &enmime.Part{ ContentType: "text/plain", Parent: root, NextSibling: &enmime.Part{ ContentType: "text/html", Parent: root, NextSibling: &enmime.Part{ ContentType: "multipart/mixed", Parent: root, }, }, } root.FirstChild = lev1 lev2 := &enmime.Part{ ContentType: "image/png", Disposition: "inline", FileName: "test.png", Parent: lev1, NextSibling: &enmime.Part{ ContentType: "image/jpeg", Disposition: "attachment", FileName: "test.jpg", Parent: lev1, }, } lev1.NextSibling.NextSibling.FirstChild = lev2 // Setup an error lev1.Errors = []*enmime.Error{ {Name: "Test Error", Detail: "None", Severe: false}, } // Desired output lines lines := []string{ "multipart/alternative", "|-- text/plain (errors: 1)", "|-- text/html", "`-- multipart/mixed", " |-- image/png, disposition: inline, filename: \"test.png\"", " `-- image/jpeg, disposition: attachment, filename: \"test.jpg\"", } FormatPart(buf, root, "") for i, want := range lines { got, err := buf.ReadString('\n') if err != nil { t.Fatalf("Error on line %v: %v", i+1, err) } // Drop \n got = got[:len(got)-1] if got != want { t.Errorf("Line %v got: %q, want: %q", i+1, got, want) } } } enmime-0.9.3/detect.go000066400000000000000000000056341417532643400146150ustar00rootroot00000000000000package enmime import ( "net/textproto" "strings" "github.com/jhillyerd/enmime/mediatype" ) // detectMultipartMessage returns true if the message has a recognized multipart Content-Type header func detectMultipartMessage(root *Part) bool { // Parse top-level multipart ctype := root.Header.Get(hnContentType) mtype, _, _, err := mediatype.Parse(ctype) // According to rfc2046#section-5.1.7 all other multipart should // be treated as multipart/mixed return err == nil && strings.HasPrefix(mtype, ctMultipartPrefix) } // detectAttachmentHeader returns true, if the given header defines an attachment. First it checks // if the Content-Disposition header defines either an attachment part or an inline part with at // least one parameter. If this test is false, the Content-Type header is checked for attachment, // but not inline. Email clients use inline for their text bodies. // // Valid Attachment-Headers: // // - Content-Disposition: attachment; filename="frog.jpg" // - Content-Disposition: inline; filename="frog.jpg" // - Content-Type: attachment; filename="frog.jpg" func detectAttachmentHeader(header textproto.MIMEHeader) bool { mtype, params, _, _ := mediatype.Parse(header.Get(hnContentDisposition)) if strings.ToLower(mtype) == cdAttachment || (strings.ToLower(mtype) == cdInline && len(params) > 0) { return true } mtype, _, _, _ = mediatype.Parse(header.Get(hnContentType)) return strings.ToLower(mtype) == cdAttachment } // detectTextHeader returns true, if the the MIME headers define a valid 'text/plain' or 'text/html' // part. If the emptyContentTypeIsPlain argument is set to true, a missing Content-Type header will // result in a positive plain part detection. func detectTextHeader(header textproto.MIMEHeader, emptyContentTypeIsText bool) bool { ctype := header.Get(hnContentType) if ctype == "" && emptyContentTypeIsText { return true } if mtype, _, _, err := mediatype.Parse(ctype); err == nil { switch mtype { case ctTextPlain, ctTextHTML: return true } } return false } // detectBinaryBody returns true if the mail header defines a binary body. func detectBinaryBody(root *Part) bool { if detectTextHeader(root.Header, true) { // It is text/plain, but an attachment. // Content-Type: text/plain; name="test.csv" // Content-Disposition: attachment; filename="test.csv" // Check for attachment only, or inline body is marked // as attachment, too. mtype, _, _, _ := mediatype.Parse(root.Header.Get(hnContentDisposition)) return strings.ToLower(mtype) == cdAttachment } isBin := detectAttachmentHeader(root.Header) if !isBin { // This must be an attachment, if the Content-Type is not // 'text/plain' or 'text/html'. // Example: // Content-Type: application/pdf; name="doc.pdf" mtype, _, _, _ := mediatype.Parse(root.Header.Get(hnContentType)) mtype = strings.ToLower(mtype) if mtype != ctTextPlain && mtype != ctTextHTML { return true } } return isBin } enmime-0.9.3/detect_test.go000066400000000000000000000105071417532643400156470ustar00rootroot00000000000000package enmime import ( "net/textproto" "os" "path/filepath" "testing" ) func TestDetectSinglePart(t *testing.T) { r, _ := os.Open(filepath.Join("testdata", "mail", "non-mime.raw")) msg, err := ReadParts(r) if err != nil { t.Fatal(err) } if detectMultipartMessage(msg) { t.Error("Failed to identify non-multipart message") } } func TestDetectMultiPart(t *testing.T) { r, _ := os.Open(filepath.Join("testdata", "mail", "html-mime-inline.raw")) msg, err := ReadParts(r) if err != nil { t.Fatal(err) } if !detectMultipartMessage(msg) { t.Error("Failed to identify multipart MIME message") } } func TestDetectUnknownMultiPart(t *testing.T) { r, _ := os.Open(filepath.Join("testdata", "mail", "unknown-part-type.raw")) msg, err := ReadParts(r) if err != nil { t.Fatal(err) } if !detectMultipartMessage(msg) { t.Error("Failed to identify multipart MIME message of unknown type") } } func TestDetectBinaryBody(t *testing.T) { ttable := []struct { filename string disposition string }{ {filename: "attachment-only.raw", disposition: "attachment"}, {filename: "attachment-only-inline.raw", disposition: "inline"}, {filename: "attachment-only-no-disposition.raw", disposition: "none"}, {filename: "attachment-only-text-attachment.raw", disposition: "attachment"}, } for _, tt := range ttable { r, _ := os.Open(filepath.Join("testdata", "mail", tt.filename)) root, err := ReadParts(r) if err != nil { t.Fatal(err) } if !detectBinaryBody(root) { t.Errorf("Failed to identify binary body %s in %q", tt.disposition, tt.filename) } } } func TestDetectAttachmentHeader(t *testing.T) { var htests = []struct { want bool header textproto.MIMEHeader }{ { want: true, header: textproto.MIMEHeader{ "Content-Disposition": []string{"attachment; filename=\"test.jpg\""}}, }, { want: true, header: textproto.MIMEHeader{ "Content-Disposition": []string{"ATTACHMENT; filename=\"test.jpg\""}}, }, { want: true, header: textproto.MIMEHeader{ "Content-Type": []string{"image/jpg; name=\"test.jpg\""}, "Content-Disposition": []string{"attachment; filename=\"test.jpg\""}, }, }, { want: true, header: textproto.MIMEHeader{ "Content-Type": []string{"attachment; filename=\"test.jpg\""}}, }, { want: false, header: textproto.MIMEHeader{ "Content-Disposition": []string{"inline"}}, }, { want: false, header: textproto.MIMEHeader{ "Content-Disposition": []string{"inline; broken"}}, }, { want: true, header: textproto.MIMEHeader{ "Content-Disposition": []string{"attachment; broken"}}, }, { want: true, header: textproto.MIMEHeader{ "Content-Disposition": []string{"inline; filename=\"frog.jpg\""}}, }, { want: false, header: textproto.MIMEHeader{ "Content-Disposition": []string{"non-attachment; filename=\"frog.jpg\""}}, }, { want: false, header: textproto.MIMEHeader{}, }, } for _, s := range htests { got := detectAttachmentHeader(s.header) if got != s.want { t.Errorf("detectAttachmentHeader(%v) == %v, want: %v", s.header, got, s.want) } } } func TestDetectTextHeader(t *testing.T) { var htests = []struct { want bool header textproto.MIMEHeader emptyIsPlain bool }{ { want: true, header: textproto.MIMEHeader{"Content-Type": []string{"text/plain"}}, emptyIsPlain: true, }, { want: true, header: textproto.MIMEHeader{"Content-Type": []string{"text/html"}}, emptyIsPlain: true, }, { want: true, header: textproto.MIMEHeader{"Content-Type": []string{"text/html; charset=utf-8"}}, emptyIsPlain: true, }, { want: true, header: textproto.MIMEHeader{}, emptyIsPlain: true, }, { want: false, header: textproto.MIMEHeader{}, emptyIsPlain: false, }, { want: false, header: textproto.MIMEHeader{"Content-Type": []string{"image/jpeg;"}}, emptyIsPlain: true, }, { want: false, header: textproto.MIMEHeader{"Content-Type": []string{"application/octet-stream"}}, emptyIsPlain: true, }, } for _, s := range htests { got := detectTextHeader(s.header, s.emptyIsPlain) if got != s.want { t.Errorf("detectTextHeader(%v, %v) == %v, want: %v", s.header, s.emptyIsPlain, got, s.want) } } } enmime-0.9.3/encode.go000066400000000000000000000126641417532643400146030ustar00rootroot00000000000000package enmime import ( "bufio" "encoding/base64" "io" "mime" "mime/quotedprintable" "net/textproto" "sort" "time" "github.com/jhillyerd/enmime/internal/coding" "github.com/jhillyerd/enmime/internal/stringutil" ) // b64Percent determines the percent of non-ASCII characters enmime will tolerate before switching // from quoted-printable to base64 encoding. const b64Percent = 20 type transferEncoding byte const ( te7Bit transferEncoding = iota teQuoted teBase64 ) var crnl = []byte{'\r', '\n'} // Encode writes this Part and all its children to the specified writer in MIME format. func (p *Part) Encode(writer io.Writer) error { if p.Header == nil { p.Header = make(textproto.MIMEHeader) } cte := p.setupMIMEHeaders() // Encode this part. b := bufio.NewWriter(writer) p.encodeHeader(b) if len(p.Content) > 0 { b.Write(crnl) if err := p.encodeContent(b, cte); err != nil { return err } } if p.FirstChild == nil { return b.Flush() } // Encode children. endMarker := []byte("\r\n--" + p.Boundary + "--") marker := endMarker[:len(endMarker)-2] c := p.FirstChild for c != nil { b.Write(marker) b.Write(crnl) if err := c.Encode(b); err != nil { return err } c = c.NextSibling } b.Write(endMarker) b.Write(crnl) return b.Flush() } // setupMIMEHeaders determines content transfer encoding, generates a boundary string if required, // then sets the Content-Type (type, charset, filename, boundary) and Content-Disposition headers. func (p *Part) setupMIMEHeaders() transferEncoding { // Determine content transfer encoding. // If we are encoding a part that previously had content-transfer-encoding set, unset it so // the correct encoding detection can be done below. p.Header.Del(hnContentEncoding) cte := te7Bit if len(p.Content) > 0 { cte = teBase64 if p.TextContent() { cte = selectTransferEncoding(p.Content, false) if p.Charset == "" { p.Charset = utf8 } } // RFC 2045: 7bit is assumed if CTE header not present. switch cte { case teBase64: p.Header.Set(hnContentEncoding, cteBase64) case teQuoted: p.Header.Set(hnContentEncoding, cteQuotedPrintable) } } // Setup headers. if p.FirstChild != nil && p.Boundary == "" { // Multipart, generate random boundary marker. p.Boundary = "enmime-" + stringutil.UUID() } if p.ContentID != "" { p.Header.Set(hnContentID, coding.ToIDHeader(p.ContentID)) } fileName := p.FileName switch selectTransferEncoding([]byte(p.FileName), true) { case teBase64: fileName = mime.BEncoding.Encode(utf8, p.FileName) case teQuoted: fileName = mime.QEncoding.Encode(utf8, p.FileName) } if p.ContentType != "" { // Build content type header. param := make(map[string]string) for k, v := range p.ContentTypeParams { param[k] = v } setParamValue(param, hpCharset, p.Charset) setParamValue(param, hpName, fileName) setParamValue(param, hpBoundary, p.Boundary) if mt := mime.FormatMediaType(p.ContentType, param); mt != "" { p.ContentType = mt } p.Header.Set(hnContentType, p.ContentType) } if p.Disposition != "" { // Build disposition header. param := make(map[string]string) setParamValue(param, hpFilename, fileName) if !p.FileModDate.IsZero() { setParamValue(param, hpModDate, p.FileModDate.Format(time.RFC822)) } if mt := mime.FormatMediaType(p.Disposition, param); mt != "" { p.Disposition = mt } p.Header.Set(hnContentDisposition, p.Disposition) } return cte } // encodeHeader writes out a sorted list of headers. func (p *Part) encodeHeader(b *bufio.Writer) { keys := make([]string, 0, len(p.Header)) for k := range p.Header { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { for _, v := range p.Header[k] { encv := v switch selectTransferEncoding([]byte(v), true) { case teBase64: encv = mime.BEncoding.Encode(utf8, v) case teQuoted: encv = mime.QEncoding.Encode(utf8, v) } // _ used to prevent early wrapping wb := stringutil.Wrap(76, k, ":_", encv, "\r\n") wb[len(k)+1] = ' ' b.Write(wb) } } } // encodeContent writes out the content in the selected encoding. func (p *Part) encodeContent(b *bufio.Writer, cte transferEncoding) (err error) { switch cte { case teBase64: enc := base64.StdEncoding text := make([]byte, enc.EncodedLen(len(p.Content))) base64.StdEncoding.Encode(text, p.Content) // Wrap lines. lineLen := 76 for len(text) > 0 { if lineLen > len(text) { lineLen = len(text) } if _, err = b.Write(text[:lineLen]); err != nil { return err } b.Write(crnl) text = text[lineLen:] } case teQuoted: qp := quotedprintable.NewWriter(b) if _, err = qp.Write(p.Content); err != nil { return err } err = qp.Close() default: _, err = b.Write(p.Content) } return err } // selectTransferEncoding scans content for non-ASCII characters and selects 'b' or 'q' encoding. func selectTransferEncoding(content []byte, quoteLineBreaks bool) transferEncoding { if len(content) == 0 { return te7Bit } // Binary chars remaining before we choose b64 encoding. threshold := b64Percent * len(content) / 100 bincount := 0 for _, b := range content { if (b < ' ' || '~' < b) && b != '\t' { if !quoteLineBreaks && (b == '\r' || b == '\n') { continue } bincount++ if bincount >= threshold { return teBase64 } } } if bincount == 0 { return te7Bit } return teQuoted } // setParamValue will ignore empty values func setParamValue(p map[string]string, k, v string) { if v != "" { p[k] = v } } enmime-0.9.3/encode_test.go000066400000000000000000000170301417532643400156320ustar00rootroot00000000000000package enmime_test import ( "bytes" "testing" "time" "github.com/jhillyerd/enmime" "github.com/jhillyerd/enmime/internal/test" ) func TestEncodePartEmpty(t *testing.T) { p := &enmime.Part{} b := &bytes.Buffer{} err := p.Encode(b) if err != nil { t.Fatal(err) } test.DiffGolden(t, b.Bytes(), "testdata", "encode", "part-empty.golden") } func TestEncodePartHeaderOnly(t *testing.T) { p := enmime.NewPart("text/plain") b := &bytes.Buffer{} err := p.Encode(b) if err != nil { t.Fatal(err) } test.DiffGolden(t, b.Bytes(), "testdata", "encode", "part-header-only.golden") } func TestEncodePartHeaderOnlyDefaultTransferEncoding(t *testing.T) { p := enmime.NewPart("text/plain") p.Header.Add("X-Empty-Header", "") b := &bytes.Buffer{} err := p.Encode(b) if err != nil { t.Fatal(err) } test.DiffGolden(t, b.Bytes(), "testdata", "encode", "part-header-only-default-encoding.golden") } func TestEncodePartDefaultHeaders(t *testing.T) { p := enmime.NewPart("application/zip") p.Boundary = "enmime-abcdefg0123456789" p.Charset = "binary" p.ContentID = "mycontentid" p.ContentTypeParams["param1"] = "myparameter1" p.ContentTypeParams["param2"] = "myparameter2" p.Disposition = "attachment" p.FileName = "stuff.zip" p.FileModDate, _ = time.Parse(time.RFC822, "01 Feb 03 04:05 GMT") p.Content = []byte("ZIPZIPZIP") b := &bytes.Buffer{} err := p.Encode(b) if err != nil { t.Fatal(err) } test.DiffGolden(t, b.Bytes(), "testdata", "encode", "part-default-headers.golden") } func TestEncodePartQuotedHeaders(t *testing.T) { p := enmime.NewPart("application/zip") p.Boundary = "enmime-abcdefg0123456789" p.Charset = "binary" p.ContentID = "mycontentid" p.ContentTypeParams["param1"] = "myparameter1" p.ContentTypeParams["param2"] = "myparameter2" p.Disposition = "attachment" p.FileName = `árvíztűrő "x" tükörfúrógép.zip` p.FileModDate, _ = time.Parse(time.RFC822, "01 Feb 03 04:05 GMT") p.Content = []byte("ZIPZIPZIP") b := &bytes.Buffer{} err := p.Encode(b) if err != nil { t.Fatal(err) } test.DiffGolden(t, b.Bytes(), "testdata", "encode", "part-quoted-headers.golden") } func TestEncodePartQuotedPrintableHeaders(t *testing.T) { p := enmime.NewPart("application/zip") p.Boundary = "enmime-abcdefg0123456789" p.Charset = "binary" p.ContentID = "mycontentid" p.ContentTypeParams["param1"] = "myparameter1" p.ContentTypeParams["param2"] = "myparameter2" p.Disposition = "attachment" p.FileName = `árvíztűrő "x" tükörfúrógép.zip` p.FileModDate, _ = time.Parse(time.RFC822, "01 Feb 03 04:05 GMT") p.Header.Add("X-QP-Header", "Just enough to need qp ☆") p.Content = []byte("ZIPZIPZIP") b := &bytes.Buffer{} err := p.Encode(b) if err != nil { t.Fatal(err) } test.DiffGolden(t, b.Bytes(), "testdata", "encode", "part-quoted-printable-headers.golden") } func TestEncodePartBinaryHeader(t *testing.T) { p := enmime.NewPart("text/plain") p.Header.Set("Subject", "¡Hola, señor!") p.Header.Set("X-Data", string([]byte{ 0x3, 0x17, 0xe1, 0x7e, 0xe8, 0xeb, 0xa2, 0x96, 0x9d, 0x95, 0xa7, 0x67, 0x82, 0x9, 0xdf, 0x8e, 0xc, 0x2c, 0x6a, 0x2b, 0x9b, 0xbe, 0x79, 0xa4, 0x69, 0xd8, 0xae, 0x86, 0xd7, 0xab, 0xa8, 0x72, 0x52, 0x15, 0xfb, 0x80, 0x8e, 0x47, 0xe1, 0xae, 0xaa, 0x5e, 0xa2, 0xb2, 0xc0, 0x90, 0x59, 0xe3, 0x35, 0xf8, 0x60, 0xb7, 0xb1, 0x63, 0x77, 0xd7, 0x5f, 0x92, 0x58, 0xa8, 0x75, })) p.Content = []byte("This is a test of a plain text part.\r\n\r\nAnother line.\r\n") b := &bytes.Buffer{} err := p.Encode(b) if err != nil { t.Fatal(err) } test.DiffGolden(t, b.Bytes(), "testdata", "encode", "part-bin-header.golden") } func TestEncodePartContentOnly(t *testing.T) { p := &enmime.Part{} p.Content = []byte("No header, only content.") b := &bytes.Buffer{} err := p.Encode(b) if err != nil { t.Fatal(err) } test.DiffGolden(t, b.Bytes(), "testdata", "encode", "part-content-only.golden") } func TestEncodePartContentOnlyQP(t *testing.T) { p := &enmime.Part{} p.Content = []byte("☆ No header, only content.") b := &bytes.Buffer{} err := p.Encode(b) if err != nil { t.Fatal(err) } test.DiffGolden(t, b.Bytes(), "testdata", "encode", "part-content-only-qp.golden") } func TestEncodePartPlain(t *testing.T) { p := enmime.NewPart("text/plain") p.Content = []byte("This is a test of a plain text part.\r\n\r\nAnother line.\r\n") b := &bytes.Buffer{} err := p.Encode(b) if err != nil { t.Fatal(err) } test.DiffGolden(t, b.Bytes(), "testdata", "encode", "part-plain.golden") } func TestEncodePartWithChildren(t *testing.T) { p := enmime.NewPart("multipart/alternative") p.Boundary = "enmime-1234567890-parent" p.Content = []byte("Bro, do you even MIME?") root := p p = enmime.NewPart("text/html") p.Content = []byte("
HTML part
") root.FirstChild = p p = enmime.NewPart("text/plain") p.Content = []byte("Plain text part") root.FirstChild.NextSibling = p b := &bytes.Buffer{} err := root.Encode(b) if err != nil { t.Fatal(err) } test.DiffGolden(t, b.Bytes(), "testdata", "encode", "part-with-children.golden") } func TestEncodePartNoContentWithChildren(t *testing.T) { p := enmime.NewPart("multipart/alternative") p.Boundary = "enmime-1234567890-parent" root := p p = enmime.NewPart("text/html") p.Content = []byte("
HTML part
") root.FirstChild = p p = enmime.NewPart("text/plain") p.Content = []byte("Plain text part") root.FirstChild.NextSibling = p b := &bytes.Buffer{} err := root.Encode(b) if err != nil { t.Fatal(err) } test.DiffGolden(t, b.Bytes(), "testdata", "encode", "nocontent-with-children.golden") } func TestEncodePartContentQuotable(t *testing.T) { p := enmime.NewPart("text/plain") p.Content = []byte("¡Hola, señor! Welcome to MIME") b := &bytes.Buffer{} err := p.Encode(b) if err != nil { t.Fatal(err) } test.DiffGolden(t, b.Bytes(), "testdata", "encode", "part-quoted-content.golden") } func TestEncodePartWithExistingEncodingHeader(t *testing.T) { p := enmime.NewPart("text/plain") p.Header.Add("Content-Transfer-Encoding", "quoted-printable") p.Content = []byte("Hello=") b := &bytes.Buffer{} err := p.Encode(b) if err != nil { t.Fatal(err) } test.DiffGolden(t, b.Bytes(), "testdata", "encode", "part-quotable-content.golden") } func TestEncodePartContentBinary(t *testing.T) { c := make([]byte, 2000) for i := range c { c[i] = byte(i % 256) } p := enmime.NewPart("image/jpeg") p.Content = c b := &bytes.Buffer{} err := p.Encode(b) if err != nil { t.Fatal(err) } test.DiffGolden(t, b.Bytes(), "testdata", "encode", "part-bin-content.golden") } func TestEncodeFileModDate(t *testing.T) { p := enmime.NewPart("text/plain") p.Content = []byte("¡Hola, señor! Welcome to MIME") p.Disposition = "inline" p.FileModDate, _ = time.Parse(time.RFC822, "01 Feb 03 04:05 GMT") b := &bytes.Buffer{} err := p.Encode(b) if err != nil { t.Fatal(err) } test.DiffGolden(t, b.Bytes(), "testdata", "encode", "part-file-mod-date.golden") } func TestEncodePartContentNonAsciiText(t *testing.T) { p := enmime.NewPart("text/plain") threshold := 20 cases := []int{ threshold - 1, threshold, threshold + 1, } for _, numNonAscii := range cases { nonAscii := bytes.Repeat([]byte{byte(0x10)}, numNonAscii) ascii := bytes.Repeat([]byte{0x41}, 100-numNonAscii) p.Content = append(nonAscii, ascii[:]...) b := &bytes.Buffer{} err := p.Encode(b) if err != nil { t.Fatal(err) } if numNonAscii < threshold { test.DiffStrings(t, []string{p.Header.Get("Content-Transfer-Encoding")}, []string{"quoted-printable"}) } else { test.DiffStrings(t, []string{p.Header.Get("Content-Transfer-Encoding")}, []string{"base64"}) } } } enmime-0.9.3/enmime.go000066400000000000000000000055751417532643400146230ustar00rootroot00000000000000// Package enmime implements a MIME encoding and decoding library. It's built on top of Go's // included mime/multipart support where possible, but is geared towards parsing MIME encoded // emails. // // Overview // // The enmime API has two conceptual layers. The lower layer is a tree of Part structs, // representing each component of a decoded MIME message. The upper layer, called an Envelope // provides an intuitive way to interact with a MIME message. // // Part Tree // // Calling ReadParts causes enmime to parse the body of a MIME message into a tree of Part objects, // each of which is aware of its content type, filename and headers. The content of a Part is // available as a slice of bytes via the Content field. // // If the part was encoded in quoted-printable or base64, it is decoded prior to being placed in // Content. If the Part contains text in a character set other than utf-8, enmime will attempt to // convert it to utf-8. // // To locate a particular Part, pass a custom PartMatcher function into the BreadthMatchFirst() or // DepthMatchFirst() methods to search the Part tree. BreadthMatchAll() and DepthMatchAll() will // collect all Parts matching your criteria. // // Envelope // // ReadEnvelope returns an Envelope struct. Behind the scenes a Part tree is constructed, and then // sorted into the correct fields of the Envelope. // // The Envelope contains both the plain text and HTML portions of the email. If there was no plain // text Part available, the HTML Part will be down-converted using the html2text library[1]. The // root of the Part tree, as well as slices of the inline and attachment Parts are also available. // // Headers // // Every MIME Part has its own headers, accessible via the Part.Header field. The raw headers for // an Envelope are available in Root.Header. Envelope also provides helper methods to fetch // headers: GetHeader(key) will return the RFC 2047 decoded value of the specified header. // AddressList(key) will convert the specified address header into a slice of net/mail.Address // values. // // Errors // // enmime attempts to be tolerant of poorly encoded MIME messages. In situations where parsing is // not possible, the ReadEnvelope and ReadParts functions will return a hard error. If enmime is // able to continue parsing the message, it will add an entry to the Errors slice on the relevant // Part. After parsing is complete, all Part errors will be appended to the Envelope Errors slice. // The Error* constants can be used to identify a specific class of error. // // Please note that enmime parses messages into memory, so it is not likely to perform well with // multi-gigabyte attachments. // // enmime is open source software released under the MIT License. The latest version can be found // at https://github.com/jhillyerd/enmime // // [1]: https://github.com/jaytaylor/html2text package enmime // import "github.com/jhillyerd/enmime" enmime-0.9.3/envelope.go000066400000000000000000000254241417532643400151610ustar00rootroot00000000000000package enmime import ( "fmt" "io" "mime" "net/mail" "net/textproto" "strings" "github.com/jaytaylor/html2text" "github.com/jhillyerd/enmime/internal/coding" "github.com/jhillyerd/enmime/mediatype" "github.com/pkg/errors" ) // Envelope is a simplified wrapper for MIME email messages. type Envelope struct { Text string // The plain text portion of the message HTML string // The HTML portion of the message Root *Part // The top-level Part Attachments []*Part // All parts having a Content-Disposition of attachment Inlines []*Part // All parts having a Content-Disposition of inline // All non-text parts that were not placed in Attachments or Inlines, such as multipart/related // content. OtherParts []*Part Errors []*Error // Errors encountered while parsing header *textproto.MIMEHeader // Header from original message } // GetHeaderKeys returns a list of header keys seen in this message. Get // individual headers with `GetHeader(name)` func (e *Envelope) GetHeaderKeys() (headers []string) { if e.header == nil { return } for key := range *e.header { headers = append(headers, key) } return headers } // GetHeader processes the specified header for RFC 2047 encoded words and returns the result as a // UTF-8 string func (e *Envelope) GetHeader(name string) string { if e.header == nil { return "" } return coding.DecodeExtHeader(e.header.Get(name)) } // GetHeaderValues processes the specified header for RFC 2047 encoded words and returns all existing // values as a list of UTF-8 strings func (e *Envelope) GetHeaderValues(name string) []string { if e.header == nil { return []string{} } rawValues := (*e.header)[textproto.CanonicalMIMEHeaderKey(name)] var values []string for _, v := range rawValues { values = append(values, coding.DecodeExtHeader(v)) } return values } // SetHeader sets given header name to the given value. // If the header exists already, all existing values are replaced. func (e *Envelope) SetHeader(name string, value []string) error { if name == "" { return fmt.Errorf("provide non-empty header name") } for i, v := range value { if i == 0 { e.header.Set(name, mime.BEncoding.Encode("utf-8", v)) continue } e.header.Add(name, mime.BEncoding.Encode("utf-8", v)) } return nil } // AddHeader appends given header value to header name without changing existing values. // If the header does not exist already, it will be created. func (e *Envelope) AddHeader(name string, value string) error { if name == "" { return fmt.Errorf("provide non-empty header name") } e.header.Add(name, mime.BEncoding.Encode("utf-8", value)) return nil } // DeleteHeader deletes given header. func (e *Envelope) DeleteHeader(name string) error { if name == "" { return fmt.Errorf("provide non-empty header name") } e.header.Del(name) return nil } // AddressList returns a mail.Address slice with RFC 2047 encoded names converted to UTF-8 func (e *Envelope) AddressList(key string) ([]*mail.Address, error) { if e.header == nil { return nil, fmt.Errorf("no headers available") } if !AddressHeaders[strings.ToLower(key)] { return nil, fmt.Errorf("%s is not an address header", key) } str := decodeToUTF8Base64Header(e.header.Get(key)) // These statements are handy for debugging ParseAddressList errors // fmt.Println("in: ", m.header.Get(key)) // fmt.Println("out: ", str) ret, err := mail.ParseAddressList(str) if err != nil { switch err.Error() { case "mail: expected comma": return mail.ParseAddressList(ensureCommaDelimitedAddresses(str)) case "mail: no address": return nil, mail.ErrHeaderNotPresent } return nil, err } return ret, nil } // Clone returns a clone of the current Envelope func (e *Envelope) Clone() *Envelope { if e == nil { return nil } newEnvelope := &Envelope{ e.Text, e.HTML, e.Root.Clone(nil), e.Attachments, e.Inlines, e.OtherParts, e.Errors, e.header, } return newEnvelope } // ReadEnvelope is a wrapper around ReadParts and EnvelopeFromPart. It parses the content of the // provided reader into an Envelope, downconverting HTML to plain text if needed, and sorting the // attachments, inlines and other parts into their respective slices. Errors are collected from all // Parts and placed into the Envelope.Errors slice. func ReadEnvelope(r io.Reader) (*Envelope, error) { // Read MIME parts from reader root, err := ReadParts(r) if err != nil { return nil, errors.WithMessage(err, "Failed to ReadParts") } return EnvelopeFromPart(root) } // EnvelopeFromPart uses the provided Part tree to build an Envelope, downconverting HTML to plain // text if needed, and sorting the attachments, inlines and other parts into their respective // slices. Errors are collected from all Parts and placed into the Envelopes Errors slice. func EnvelopeFromPart(root *Part) (*Envelope, error) { e := &Envelope{ Root: root, header: &root.Header, } if detectMultipartMessage(root) { // Multi-part message (message with attachments, etc) if err := parseMultiPartBody(root, e); err != nil { return nil, err } } else { if detectBinaryBody(root) { // Attachment only, no text if root.Disposition == cdInline { e.Inlines = append(e.Inlines, root) } else { e.Attachments = append(e.Attachments, root) } } else { // Only text, no attachments if err := parseTextOnlyBody(root, e); err != nil { return nil, err } } } // Down-convert HTML to text if necessary if e.Text == "" && e.HTML != "" { // We always warn when this happens e.Root.addWarning( ErrorPlainTextFromHTML, "Message did not contain a text/plain part") var err error if e.Text, err = html2text.FromString(e.HTML); err != nil { e.Text = "" // Down-conversion shouldn't fail p := e.Root.BreadthMatchFirst(matchHTMLBodyPart) p.addError(ErrorPlainTextFromHTML, "Failed to downconvert HTML: %v", err) } } // Copy part errors into Envelope. if e.Root != nil { _ = e.Root.DepthMatchAll(func(part *Part) bool { // Using DepthMatchAll to traverse all parts, don't care about result. for i := range part.Errors { // Range index is needed to get the correct address, because range value points to // a locally scoped variable. e.Errors = append(e.Errors, part.Errors[i]) } return false }) } return e, nil } // parseTextOnlyBody parses a plain text message in root that has MIME-like headers, but // only contains a single part - no boundaries, etc. The result is placed in e. func parseTextOnlyBody(root *Part, e *Envelope) error { // Determine character set var charset string var isHTML bool if ctype := root.Header.Get(hnContentType); ctype != "" { if mediatype, mparams, _, err := mediatype.Parse(ctype); err == nil { isHTML = (mediatype == ctTextHTML) if mparams[hpCharset] != "" { charset = mparams[hpCharset] } } } // Read transcoded text if isHTML { rawHTML := string(root.Content) // Note: Empty e.Text will trigger html2text conversion e.HTML = rawHTML if charset == "" { // Search for charset in HTML metadata if charset = coding.FindCharsetInHTML(rawHTML); charset != "" { // Found charset in HTML if convHTML, err := coding.ConvertToUTF8String(charset, root.Content); err == nil { // Successful conversion e.HTML = convHTML } else { // Conversion failed root.addWarning(ErrorCharsetConversion, err.Error()) } } // Converted from charset in HTML return nil } } else { e.Text = string(root.Content) } return nil } // parseMultiPartBody parses a multipart message in root. The result is placed in e. func parseMultiPartBody(root *Part, e *Envelope) error { // Parse top-level multipart ctype := root.Header.Get(hnContentType) mediatype, params, _, err := mediatype.Parse(ctype) if err != nil { return fmt.Errorf("unable to parse media type: %v", err) } if !strings.HasPrefix(mediatype, ctMultipartPrefix) { return fmt.Errorf("unknown mediatype: %v", mediatype) } boundary := params[hpBoundary] if boundary == "" { return fmt.Errorf("unable to locate boundary param in Content-Type header") } // Locate text body if mediatype == ctMultipartAltern { p := root.BreadthMatchFirst(func(p *Part) bool { return p.ContentType == ctTextPlain && p.Disposition != cdAttachment }) if p != nil { e.Text = string(p.Content) } } else { // multipart is of a mixed type parts := root.DepthMatchAll(func(p *Part) bool { return p.ContentType == ctTextPlain && p.Disposition != cdAttachment }) for i, p := range parts { if i > 0 { e.Text += "\n--\n" } e.Text += string(p.Content) } } // Locate HTML body p := root.DepthMatchFirst(matchHTMLBodyPart) if p != nil { e.HTML += string(p.Content) } // Locate attachments e.Attachments = root.BreadthMatchAll(func(p *Part) bool { return p.Disposition == cdAttachment || p.ContentType == ctAppOctetStream }) // Locate inlines e.Inlines = root.BreadthMatchAll(func(p *Part) bool { return p.Disposition == cdInline && !strings.HasPrefix(p.ContentType, ctMultipartPrefix) }) // Locate others parts not considered in attachments or inlines e.OtherParts = root.BreadthMatchAll(func(p *Part) bool { if strings.HasPrefix(p.ContentType, ctMultipartPrefix) { return false } if p.Disposition != "" { return false } if p.ContentType == ctAppOctetStream { return false } return p.ContentType != ctTextPlain && p.ContentType != ctTextHTML }) return nil } // Used by Part matchers to locate the HTML body. Not inlined because it's used in multiple places. func matchHTMLBodyPart(p *Part) bool { return p.ContentType == ctTextHTML && p.Disposition != cdAttachment } // Used by AddressList to ensure that address lists are properly delimited func ensureCommaDelimitedAddresses(s string) string { // This normalizes the whitespace, but may interfere with CFWS (comments with folding whitespace) // RFC-5322 3.4.0: // because some legacy implementations interpret the comment, // comments generally SHOULD NOT be used in address fields // to avoid confusing such implementations. s = strings.Join(strings.Fields(s), " ") inQuotes := false inDomain := false escapeSequence := false sb := strings.Builder{} for _, r := range s { if escapeSequence { escapeSequence = false sb.WriteRune(r) continue } if r == '"' { inQuotes = !inQuotes sb.WriteRune(r) continue } if inQuotes { if r == '\\' { escapeSequence = true sb.WriteRune(r) continue } } else { if r == '@' { inDomain = true sb.WriteRune(r) continue } if inDomain { if r == ';' { sb.WriteRune(r) break } if r == ',' { inDomain = false sb.WriteRune(r) continue } if r == ' ' { inDomain = false sb.WriteRune(',') sb.WriteRune(r) continue } } } sb.WriteRune(r) } return sb.String() } enmime-0.9.3/envelope_test.go000066400000000000000000001051041417532643400162120ustar00rootroot00000000000000package enmime_test import ( "bytes" "sort" "strings" "testing" "github.com/go-test/deep" "github.com/jhillyerd/enmime" "github.com/jhillyerd/enmime/internal/test" ) func TestParseHeaderOnly(t *testing.T) { want := "" msg := test.OpenTestData("mail", "header-only.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse non-MIME:", err) } if !strings.Contains(e.Text, want) { t.Errorf("Expected %q to contain %q", e.Text, want) } if e.HTML != "" { t.Errorf("Expected no HTML body, got %q", e.HTML) } if e.Root == nil { t.Errorf("Expected a root part") } if len(e.Root.Header) != 7 { t.Errorf("Expected 7 headers, got %d", len(e.Root.Header)) } } func TestParseNonMime(t *testing.T) { want := "This is a test mailing" msg := test.OpenTestData("mail", "non-mime.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse non-MIME:", err) } if !strings.Contains(e.Text, want) { t.Errorf("Expected %q to contain %q", e.Text, want) } if e.HTML != "" { t.Errorf("Expected no HTML body, got %q", e.HTML) } } func TestParseNonMimeHTML(t *testing.T) { msg := test.OpenTestData("mail", "non-mime-html.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse non-MIME:", err) } if len(e.Errors) == 1 { want := enmime.ErrorPlainTextFromHTML got := e.Errors[0].Name if got != want { t.Errorf("e.Errors[0] got: %v, want: %v", got, want) } } else { t.Errorf("len(e.Errors) got: %v, want: 1", len(e.Errors)) } want := "This is *a* *test* mailing" if !strings.Contains(e.Text, want) { t.Errorf("Expected %q to contain %q", e.Text, want) } want = "This" if !strings.Contains(e.HTML, want) { t.Errorf("Expected %q to contain %q", e.HTML, want) } } func TestParseMimeTree(t *testing.T) { msg := test.OpenTestData("mail", "attachment.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } if e.Root == nil { t.Error("Message should have a root node") } } func TestParseInlineText(t *testing.T) { msg := test.OpenTestData("mail", "html-mime-inline.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } want := "Test of text section" if e.Text != want { t.Error("got:", e.Text, "want:", want) } } func TestParseInlineBadCharsetText(t *testing.T) { msg := test.OpenTestData("mail", "html-mime-bad-charset-inline.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } want := "Test of text section" if e.Text != want { t.Error("got:", e.Text, "want:", want) } } func TestParseInlineBadUknownCharsetText(t *testing.T) { msg := test.OpenTestData("mail", "html-mime-bad-unknown-charset-inline.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } want := "Test of text section" if e.Text != want { t.Error("got:", e.Text, "want:", want) } } func TestParseMultiAlernativeText(t *testing.T) { msg := test.OpenTestData("mail", "mime-alternative.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } want := "Section one\n" if e.Text != want { t.Error("Text parts should not concatenate, got:", e.Text, "want:", want) } } func TestParseMultiMixedText(t *testing.T) { msg := test.OpenTestData("mail", "mime-mixed.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } want := "Section one\n\n--\nSection two" if e.Text != want { t.Error("Text parts should concatenate, got:", e.Text, "want:", want) } } func TestParseMultiMixedRelatedHtml(t *testing.T) { msg := test.OpenTestData("mail", "mime-mixed-related.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } want := "

html1

" if e.HTML != want { t.Error("Text parts should concatenate, got:", e.HTML, "want:", want) } } func TestParseMultiSignedText(t *testing.T) { msg := test.OpenTestData("mail", "mime-signed.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } want := "Section one\n\n--\nSection two" if e.Text != want { t.Error("Text parts should concatenate, got:", e.Text, "want:", want) } } func TestParseQuotedPrintable(t *testing.T) { msg := test.OpenTestData("mail", "quoted-printable.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } want := "Phasellus sit amet arcu" if !strings.Contains(e.Text, want) { t.Errorf("Text: %q should contain: %q", e.Text, want) } } func TestParseQuotedPrintableMime(t *testing.T) { msg := test.OpenTestData("mail", "quoted-printable-mime.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } want := "Nullam venenatis ante" if !strings.Contains(e.Text, want) { t.Errorf("Text: %q should contain: %q", e.Text, want) } } func TestParseInlineHTML(t *testing.T) { msg := test.OpenTestData("mail", "html-mime-inline.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } want := "" if !strings.Contains(e.HTML, want) { t.Errorf("HTML: %q should contain: %q", e.Text, want) } want = "Test of HTML section" if !strings.Contains(e.HTML, want) { t.Errorf("HTML: %q should contain: %q", e.Text, want) } } func TestParseAttachment(t *testing.T) { msg := test.OpenTestData("mail", "attachment.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } want := "A text section" if !strings.Contains(e.Text, want) { t.Errorf("Text: %q should contain: %q", e.Text, want) } if e.HTML != "" { t.Error("mime.HTML should be empty, attachment is not for display, got:", e.HTML) } if len(e.Inlines) > 0 { t.Error("Should have no inlines, got:", len(e.Inlines)) } if len(e.Attachments) != 1 { t.Fatal("Should have a single attachment, got:", len(e.Attachments)) } want = "test.html" got := e.Attachments[0].FileName if got != want { t.Error("FileName got:", got, "want:", want) } want = "" test.ContentContainsString(t, e.Attachments[0].Content, want) } func TestParseAttachmentOctet(t *testing.T) { msg := test.OpenTestData("mail", "attachment-octet.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } want := "A text section" if !strings.Contains(e.Text, want) { t.Errorf("Text: %q should contain: %q", e.Text, want) } if e.HTML != "" { t.Error("mime.HTML should be empty, attachment is not for display, got:", e.HTML) } if len(e.Inlines) > 0 { t.Error("Should have no inlines, got:", len(e.Inlines)) } if len(e.Attachments) != 1 { t.Fatal("Should have a single attachment, got:", len(e.Attachments)) } want = "ATTACHMENT.EXE" got := e.Attachments[0].FileName if got != want { t.Error("FileName got:", got, "want:", want) } wantBytes := []byte{ 0x3, 0x17, 0xe1, 0x7e, 0xe8, 0xeb, 0xa2, 0x96, 0x9d, 0x95, 0xa7, 0x67, 0x82, 0x9, 0xdf, 0x8e, 0xc, 0x2c, 0x6a, 0x2b, 0x9b, 0xbe, 0x79, 0xa4, 0x69, 0xd8, 0xae, 0x86, 0xd7, 0xab, 0xa8, 0x72, 0x52, 0x15, 0xfb, 0x80, 0x8e, 0x47, 0xe1, 0xae, 0xaa, 0x5e, 0xa2, 0xb2, 0xc0, 0x90, 0x59, 0xe3, 0x35, 0xf8, 0x60, 0xb7, 0xb1, 0x63, 0x77, 0xd7, 0x5f, 0x92, 0x58, 0xa8, 0x75, } if !bytes.Equal(e.Attachments[0].Content, wantBytes) { t.Error("Attachment should have correct content") } } func TestParseAttachmentApplication(t *testing.T) { msg := test.OpenTestData("mail", "attachment-application.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } if len(e.Inlines) > 0 { t.Error("Should have no inlines, got:", len(e.Inlines)) } if len(e.Attachments) != 1 { t.Fatal("Should have a single attachment, got:", len(e.Attachments)) } want := "some.doc" got := e.Attachments[0].FileName if got != want { t.Error("FileName got:", got, "want:", want) } } func TestParseOtherParts(t *testing.T) { msg := test.OpenTestData("mail", "other-parts.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } want := "A text section" if !strings.Contains(e.Text, want) { t.Errorf("Text: %q should contain: %q", e.Text, want) } if e.HTML != "" { t.Error("mime.HTML should be empty, attachment is not for display, got:", e.HTML) } if len(e.Inlines) > 0 { t.Error("Should have no inlines, got:", len(e.Inlines)) } if len(e.Attachments) > 0 { t.Error("Should have no attachments, got:", len(e.Attachments)) } if len(e.OtherParts) != 1 { t.Fatal("Should have one other part, got:", len(e.OtherParts)) } want = "B05.gif" got := e.OtherParts[0].FileName if got != want { t.Error("FileName got:", got, "want:", want) } wantBytes := []byte{ 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0xf, 0x0, 0xf, 0x0, 0xa2, 0x5, 0x0, 0xde, 0xeb, 0xf3, 0x5b, 0xb0, 0xec, 0x0, 0x89, 0xe3, 0xa3, 0xd0, 0xed, 0x0, 0x46, 0x74, 0xdd, 0xed, 0xfa, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x21, 0xf9, 0x4, 0x1, 0x0, 0x0, 0x5, 0x0, 0x2c, 0x0, 0x0, 0x0, 0x0, 0xf, 0x0, 0xf, 0x0, 0x0, 0x3, 0x40, 0x58, 0x25, 0xa4, 0x4b, 0xb0, 0x39, 0x1, 0x46, 0xa3, 0x23, 0x5b, 0x47, 0x46, 0x68, 0x9d, 0x20, 0x6, 0x9f, 0xd2, 0x95, 0x45, 0x44, 0x8, 0xe8, 0x29, 0x39, 0x69, 0xeb, 0xbd, 0xc, 0x41, 0x4a, 0xae, 0x82, 0xcd, 0x1c, 0x9f, 0xce, 0xaf, 0x1f, 0xc3, 0x34, 0x18, 0xc2, 0x42, 0xb8, 0x80, 0xf1, 0x18, 0x84, 0xc0, 0x9e, 0xd0, 0xe8, 0xf2, 0x1, 0xb5, 0x19, 0xad, 0x41, 0x53, 0x33, 0x9b, 0x0, 0x0, 0x3b, } if !bytes.Equal(e.OtherParts[0].Content, wantBytes) { t.Error("Other part should have correct content") } } func TestParseInline(t *testing.T) { msg := test.OpenTestData("mail", "html-mime-inline.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } want := "Test of text section" if !strings.Contains(e.Text, want) { t.Errorf("Text: %q should contain: %q", e.Text, want) } want = ">Test of HTML section<" if !strings.Contains(e.HTML, want) { t.Errorf("HTML: %q should contain %q", e.HTML, want) } if len(e.Inlines) != 1 { t.Fatal("Should have one inline, got:", len(e.Inlines)) } if len(e.Attachments) > 0 { t.Error("Should have no attachments, got:", len(e.Attachments)) } want = "favicon.png" got := e.Inlines[0].FileName if got != want { t.Error("FileName got:", got, "want:", want) } if !bytes.HasPrefix(e.Inlines[0].Content, []byte{0x89, 'P', 'N', 'G'}) { t.Error("Inline should have correct content") } } func TestParseOtherPartsRelated(t *testing.T) { msg := test.OpenTestData("mail", "other-multi-related.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } want := "Plain text." if !strings.Contains(e.Text, want) { t.Errorf("Text: %q should contain: %q", e.Text, want) } want = "HTML text." if !strings.Contains(e.HTML, want) { t.Errorf("HTML: %q should contain %q", e.HTML, want) } if len(e.Attachments) > 0 { t.Error("Should have no attachments, got:", len(e.Attachments)) } if len(e.Inlines) > 0 { t.Error("Should have no inlines, got:", len(e.Inlines)) } if len(e.OtherParts) != 2 { t.Fatal("Should have two other parts, got:", len(e.Inlines)) } want = "image001.png" got := e.OtherParts[0].FileName if got != want { t.Error("FileName got:", got, "want:", want) } want = "image001.png@01D3BA12.F6C6AEB0" got = e.OtherParts[0].ContentID if got != want { t.Error("ContentID got:", got, "want:", want) } if !bytes.HasPrefix(e.OtherParts[0].Content, []byte{0x89, 'P', 'N', 'G'}) { t.Error("Other part should have correct content") } want = "image002.png" got = e.OtherParts[1].FileName if got != want { t.Error("FileName got:", got, "want:", want) } want = "image002.png@01D3BA12.F6C6AEB0" got = e.OtherParts[1].ContentID if got != want { t.Error("ContentID got:", got, "want:", want) } if !bytes.HasPrefix(e.OtherParts[1].Content, []byte{0x89, 'P', 'N', 'G'}) { t.Error("Other part should have correct content") } } func TestParseHTMLOnlyInline(t *testing.T) { msg := test.OpenTestData("mail", "html-only-inline.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } if len(e.Errors) == 1 { want := enmime.ErrorPlainTextFromHTML got := e.Errors[0].Name if got != want { t.Errorf("e.Errors[0] got: %v, want: %v", got, want) } } else { t.Errorf("len(e.Errors) got: %v, want: 1", len(e.Errors)) } want := "Test of HTML section" if !strings.Contains(e.Text, want) { t.Errorf("Downconverted Text: %q should contain: %q", e.Text, want) } want = ">Test of HTML section<" if !strings.Contains(e.HTML, want) { t.Errorf("HTML: %q should contain %q", e.HTML, want) } if len(e.Inlines) != 1 { t.Error("Should one inline, got:", len(e.Inlines)) } if len(e.Attachments) > 0 { t.Fatal("Should have no attachments, got:", len(e.Attachments)) } want = "favicon.png" got := e.Inlines[0].FileName if got != want { t.Error("FileName got:", got, "want:", want) } if !bytes.HasPrefix(e.Inlines[0].Content, []byte{0x89, 'P', 'N', 'G'}) { t.Error("Inline should have correct content") } } func TestParseInlineMultipart(t *testing.T) { msg := test.OpenTestData("mail", "inlinemultipart.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } if len(e.Errors) != 0 { t.Errorf("len(e.Errors) got: %v, want: 0", len(e.Errors)) } want := "Simple text." if !strings.Contains(e.Text, want) { t.Errorf("Downconverted Text: %q should contain: %q", e.Text, want) } if len(e.Inlines) != 1 { t.Error("Should have one inline, got:", len(e.Inlines)) } if len(e.Attachments) != 1 { t.Fatal("Should have one attachments, got:", len(e.Attachments)) } want = "test.txt" got := e.Inlines[0].FileName if got != want { t.Error("FileName got:", got, "want:", want) } if !bytes.HasPrefix(e.Inlines[0].Content, []byte("Text")) { t.Error("Inline should have correct content") } } func TestParseNestedHeaders(t *testing.T) { msg := test.OpenTestData("mail", "html-mime-inline.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } if len(e.Inlines) != 1 { t.Error("Should one inline, got:", len(e.Inlines)) } want := "favicon.png" got := e.Inlines[0].FileName if got != want { t.Error("FileName got:", got, "want:", want) } want = "<8B8481A2-25CA-4886-9B5A-8EB9115DD064@skynet>" got = e.Inlines[0].Header.Get("Content-Id") if got != want { t.Errorf("Content-Id header was: %q, want: %q", got, want) } } func TestParseHTMLOnlyCharsetInHeaderOnly(t *testing.T) { msg := test.OpenTestData("mail", "non-mime-html-charset-header-only.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse non-MIME:", err) } if !strings.ContainsRune(e.HTML, 0xfc) { t.Error("HTML body should contained German ü") } if !strings.Contains(e.HTML, "Müller") { t.Error("HTML body should contained 'Müller'") } } func TestEnvelopeGetHeader(t *testing.T) { // Test empty header e := &enmime.Envelope{} want := "" got := e.GetHeader("Subject") if got != want { t.Errorf("Subject was: %q, want: %q", got, want) } // Even non-MIME messages should support encoded-words in headers // Also, encoded addresses should be suppored r := test.OpenTestData("mail", "qp-ascii-header.raw") e, err := enmime.ReadEnvelope(r) if err != nil { t.Fatal("Failed to parse non-MIME:", err) } want = "Test QP Subject!" got = e.GetHeader("Subject") if got != want { t.Errorf("Subject was: %q, want: %q", got, want) } // Test UTF-8 subject line r = test.OpenTestData("mail", "qp-utf8-header.raw") e, err = enmime.ReadEnvelope(r) if err != nil { t.Fatal("Failed to parse MIME:", err) } want = "MIME UTF8 Test \u00a2 More Text" got = e.GetHeader("Subject") if got != want { t.Errorf("Subject was: %q, want: %q", got, want) } } func TestEnvelopeGetHeaderKeys(t *testing.T) { // Test empty header e := &enmime.Envelope{} got := e.GetHeaderKeys() if got != nil { t.Errorf("Headers was: %q, want: nil", got) } // Even non-MIME messages should support encoded-words in headers // Also, encoded addresses should be suppored r := test.OpenTestData("mail", "qp-ascii-header.raw") e, err := enmime.ReadEnvelope(r) if err != nil { t.Fatal("Failed to parse non-MIME:", err) } want := []string{"Date", "From", "Subject", "To", "X-Mailer"} got = e.GetHeaderKeys() sort.Strings(got) test.DiffStrings(t, got, want) // Test UTF-8 subject line r = test.OpenTestData("mail", "qp-utf8-header.raw") e, err = enmime.ReadEnvelope(r) if err != nil { t.Fatal("Failed to parse MIME:", err) } want = []string{"Content-Type", "Date", "From", "Message-Id", "Mime-Version", "Sender", "Subject", "To", "User-Agent"} got = e.GetHeaderKeys() sort.Strings(got) test.DiffStrings(t, got, want) } func TestEnvelopeGetHeaderValues(t *testing.T) { r := test.OpenTestData("mail", "ctype-bug.raw") e, err := enmime.ReadEnvelope(r) if err != nil { t.Fatal("Failed to parse MIME:", err) } // test Received headers want := []string{ "by 10.76.55.35 with SMTP id o3csp106612oap; Fri, 10 Jul 2015 13:12:34 -0700 (PDT)", "from mail135-10.atl141.mandrillapp.com (mail135-10.atl141.mandrillapp.com. [198.2.135.10]) by mx.google.com with ESMTPS id k184si6630505ywf.180.2015.07.10.13.12.34 for (version=TLSv1.2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128); Fri, 10 Jul 2015 13:12:34 -0700 (PDT)", "from pmta03.mandrill.prod.atl01.rsglab.com (127.0.0.1) by mail135-10.atl141.mandrillapp.com id hk0jj41sau80 for ; Fri, 10 Jul 2015 20:12:33 +0000 (envelope-from )", "from [67.214.212.122] by mandrillapp.com id 163e4a0faf244a2da6b0121cc7af1fe9; Fri, 10 Jul 2015 20:12:33 +0000", } got := e.GetHeaderValues("received") diff := deep.Equal(got, want) if diff != nil { t.Errorf("Got: %+v, want: %+v", got, want) } } func TestEnvelopeSetHeader(t *testing.T) { r := test.OpenTestData("mail", "qp-utf8-header.raw") e, err := enmime.ReadEnvelope(r) if err != nil { t.Fatal("Failed to parse MIME:", err) } // replace existing header want := "André Pirard " e.SetHeader("To", []string{want}) got := e.GetHeader("To") if got != want { t.Errorf("Got: %q, want: %q", got, want) } // replace existing header with multiple values wantSlice := []string{"Mirosław Marczak ", "James Hillyerd "} e.SetHeader("To", wantSlice) gotSlice := e.GetHeaderValues("to") diff := deep.Equal(gotSlice, wantSlice) if diff != nil { t.Errorf("Got: %+v, want: %+v", gotSlice, wantSlice) } // replace non-existing header want = "foobar" e.SetHeader("X-Foo-Bar", []string{want}) got = e.GetHeader("X-Foo-Bar") if got != want { t.Errorf("Got: %q, want: %q", got, want) } } func TestEnvelopeAddHeader(t *testing.T) { r := test.OpenTestData("mail", "qp-utf8-header.raw") e, err := enmime.ReadEnvelope(r) if err != nil { t.Fatal("Failed to parse MIME:", err) } // add to existing header to := "James Hillyerd " wantSlice := []string{"Mirosław Marczak ", "James Hillyerd "} e.AddHeader("To", to) gotSlice := e.GetHeaderValues("To") diff := deep.Equal(gotSlice, wantSlice) if diff != nil { t.Errorf("Got: %+v, want: %+v", gotSlice, wantSlice) } // add to non-existing header want := "foobar" e.AddHeader("X-Foo-Bar", want) got := e.GetHeader("X-Foo-Bar") if got != want { t.Errorf("Got: %q, want: %q", got, want) } } func TestEnvelopeDeleteHeader(t *testing.T) { r := test.OpenTestData("mail", "qp-utf8-header.raw") e, err := enmime.ReadEnvelope(r) if err != nil { t.Fatal("Failed to parse MIME:", err) } // delete user-agent header e.DeleteHeader("User-Agent") got := e.GetHeader("User-Agent") want := "" if got != want { t.Errorf("Got: %q, want: %q", got, want) } } func TestEnvelopeAddressList(t *testing.T) { // Test empty header e := &enmime.Envelope{} _, err := e.AddressList("To") if err == nil { t.Error("AddressList(\"To\") should have returned err, got nil") } r := test.OpenTestData("mail", "qp-utf8-header.raw") e, err = enmime.ReadEnvelope(r) if err != nil { t.Fatal("Failed to parse MIME:", err) } _, err = e.AddressList("BCC") if err == nil { t.Error("AddressList(\"BCC\") should have returned err, got nil") } _, err = e.AddressList("Subject") if err == nil { t.Error("AddressList(\"Subject\") should have returned err, got nil") } toAddresses, err := e.AddressList("To") if err != nil { t.Fatal("Failed to parse To list:", err) } if len(toAddresses) != 1 { t.Fatalf("len(toAddresses) == %v, want: %v", len(toAddresses), 1) } // Confirm address name was decoded properly want := "Mirosław Marczak" got := toAddresses[0].Name if got != want { t.Errorf("To was: %q, want: %q", got, want) } senderAddresses, err := e.AddressList("Sender") if err != nil { t.Fatal("Failed to parse Sender list:", err) } if len(senderAddresses) != 1 { t.Fatalf("len(senderAddresses) == %v, want: %v", len(senderAddresses), 1) } // Confirm address name was decoded properly want = "André Pirard" got = senderAddresses[0].Name if got != want { t.Errorf("Sender was: %q, want: %q", got, want) } } func TestDetectCharacterSetInHTML(t *testing.T) { msg := test.OpenTestData("mail", "non-mime-missing-charset.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse non-MIME:", err) } if strings.ContainsRune(e.HTML, 0x80) { t.Error("HTML body should not have contained a Windows CP1250 Euro Symbol") } if !strings.ContainsRune(e.HTML, 0x20ac) { t.Error("HTML body should have contained a Unicode Euro Symbol") } } func TestAttachmentOnly(t *testing.T) { var aTests = []struct { filename string attachmentsLen int inlinesLen int }{ {filename: "attachment-only.raw", attachmentsLen: 1, inlinesLen: 0}, {filename: "attachment-only-inline.raw", attachmentsLen: 0, inlinesLen: 1}, } for _, a := range aTests { // Mail with disposition attachment msg := test.OpenTestData("mail", a.filename) e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } if len(e.Attachments) != a.attachmentsLen { t.Fatal("len(Attachments) got:", len(e.Attachments), "want:", a.attachmentsLen) } if a.attachmentsLen > 0 { got := e.Attachments[0].Content if !bytes.HasPrefix(got, []byte{0x89, 'P', 'N', 'G'}) { t.Errorf("Content should be PNG image, got: %v", got) } } if len(e.Inlines) != a.inlinesLen { t.Fatal("len(Inlines) got:", len(e.Inlines), "want:", a.inlinesLen) } if a.inlinesLen > 0 { got := e.Inlines[0].Content if !bytes.HasPrefix(got, []byte{0x89, 'P', 'N', 'G'}) { t.Errorf("Content should be PNG image, got: %v", got) } } // Check, if root header is set if len(e.Root.Header) < 1 { t.Errorf("No root header defined, but must be set from binary only part.") } // Check, that the root part has content if len(e.Root.Content) == 0 { t.Errorf("Root part of envelope has no content.") } } } func TestDuplicateParamsInMime(t *testing.T) { msg := test.OpenTestData("mail", "mime-duplicate-param.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } if e.Attachments[0].FileName != "Invoice_302232133150612.pdf" { t.Fatal("Mail should have a part with filename Invoice_302232133150612.pdf") } } func TestUnquotedSpecialCharParamsInMime(t *testing.T) { msg := test.OpenTestData("mail", "mime-unquoted-tspecials-param.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } if e.Attachments[0].FileName != "Invoice_(302232133150612).pdf" { t.Fatal("Mail should have a part with filename Invoice_(302232133150612).pdf") } } func TestBadAddressHeaderInMime(t *testing.T) { msg := test.OpenTestData("mail", "malformed-multiple-address-header-values.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } froms, err := e.AddressList("From") if err != nil { t.Log(err) } if len(froms) < 1 { t.Fatal("From header should have at least one entry") } } func TestBadContentTypeInMime(t *testing.T) { msg := test.OpenTestData("mail", "mime-bad-content-type.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } if e.Attachments[0].FileName != "Invoice_302232133150612.pdf" { t.Fatal("Mail should have a part with filename Invoice_302232133150612.pdf") } } func TestBadContentTransferEncodingInMime(t *testing.T) { msg := test.OpenTestData("mail", "mime-bad-content-transfer-encoding.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } var expectedErrorPresent bool for _, v := range e.Errors { if v.Name == enmime.ErrorMalformedBase64 && v.Severe { expectedErrorPresent = true } } if !expectedErrorPresent { t.Fatal("Mail should have a severe malformed base64 error") } } func TestBadMime8bitFilename(t *testing.T) { msg := test.OpenTestData("mail", "mime-bad-8bit-filename.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } if strings.TrimSpace(e.Text) != "Text part" { t.Fatal("Text part not parsed correctly") } if len(e.Attachments) != 1 { t.Fatal("Wrong number of attachments") } if e.Attachments[0].FileName != "管理.doc" { t.Fatal("Wrong attachment name") } } func TestBlankMediaName(t *testing.T) { msg := test.OpenTestData("mail", "mime-blank-media-name.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } if e.Attachments[0].FileName != "Invoice_302232133150612.pdf" { t.Fatal("Mail should have a part with filename Invoice_302232133150612.pdf") } } func TestEnvelopeHeaders(t *testing.T) { headers := map[string]string{ "Received-Spf": "pass (google.com: domain of bounce-md_30112948.55a02731.v1-163e4a0faf244a2da6b0121cc7af1fe9@mandrill.papertrailapp.com designates 198.2.135.10 as permitted sender) client-ip=198.2.135.10;", "To": "", "Domainkey-Signature": "a=rsa-sha1; c=nofws; q=dns; s=mandrill; d=papertrailapp.com; b=Cv9EE+3+CO+puDhpfQOsuwuP6YqJQBA/Z6OofPTXqWf/Asr/edsi7aoXIE+forQ/q8DjhhMMuMiD bQ1tlRXMFckw08GjqU7RN+ouwJEMXOpzxUgp6OwrITvddwhddEg6H3uYRva5pNJqonDDykshHyjA EVeAdcY4tjYQrcRxw/0=;", "Dkim-Signature": "v=1; a=rsa-sha1; c=relaxed/relaxed; s=mandrill; d=papertrailapp.com; h=From:Subject:To:Message-Id:Date:MIME-Version:Content-Type; i=support@papertrailapp.com; bh=2tw/BU7QN7gmFr2K2wnVpETYxbU=; b=T+PzWzjbOoKO3jNANsmqsnbM+gnbgT9EQBP8DOSno75iHQ9AuU6xcDCPctvJt50Exr6aTs9qJmEG baCa39danDRIx5zXsdaSy34+SKfDODdgmwEEfKFeULQGPwF1g73tXeX4k0kwt+bm6f0baWbaLwR1 RdhUd42jEMossTKuD9w= v=1; a=rsa-sha256; c=relaxed/relaxed; d=mandrillapp.com; i=@mandrillapp.com; q=dns/txt; s=mandrill; t=1436559153; h=From : Subject : To : Message-Id : Date : MIME-Version : Content-Type : From : Subject : Date : X-Mandrill-User : List-Unsubscribe; bh=eW2QM8XcfLCwIBTvTJaT619pYOD3YrxBvxC9cZ2gxe0=; b=quxFFNbO04KKNNB8yMd9Zch6wogobVbNFlpGIOQI/jA9FuhdZvMxQwwZ2jeno7c17v2eXY Vp3c1vwvVERCboNaPwwxrKkrhqMxM8rb15n8xM3v0IplkQ3vs9G5agiTT1qqxErsrS6xAqmj UNUPKEXuSjr24HqmQzxPry0aIgHdI=", "Message-Id": "<55a02731af510_7b0b33f2c7821d@pt02w01.papertrailapp.com.tmail>", "X-Report-Abuse": "Please forward a copy of this message, including all headers, to abuse@mandrill.com You can also report abuse here: http://mandrillapp.com/contact/abuse?id=30112948.163e4a0faf244a2da6b0121cc7af1fe9", "Mime-Version": "1.0", "Return-Path": " ", "Authentication-Results": "mx.google.com; spf=pass (google.com: domain of bounce-md_30112948.55a02731.v1-163e4a0faf244a2da6b0121cc7af1fe9@mandrill.papertrailapp.com designates 198.2.135.10 as permitted sender) smtp.mail=bounce-md_30112948.55a02731.v1-163e4a0faf244a2da6b0121cc7af1fe9@mandrill.papertrailapp.com; dkim=pass header.i=@papertrailapp.com; dkim=pass header.i=@mandrillapp.com", "From": "Papertrail ", "Subject": "Welcome to Papertrail", "Content-Type": `multipart/alternative; boundary="_av-rPFkvS5QROAYLq2cQTUr1w"`, "X-Mandrill-User": "md_30112948", "Delivered-To": "deepak@redsift.io", "Received": "by 10.76.55.35 with SMTP id o3csp106612oap; Fri, 10 Jul 2015 13:12:34 -0700 (PDT) from mail135-10.atl141.mandrillapp.com (mail135-10.atl141.mandrillapp.com. [198.2.135.10]) by mx.google.com with ESMTPS id k184si6630505ywf.180.2015.07.10.13.12.34 for (version=TLSv1.2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128); Fri, 10 Jul 2015 13:12:34 -0700 (PDT) from pmta03.mandrill.prod.atl01.rsglab.com (127.0.0.1) by mail135-10.atl141.mandrillapp.com id hk0jj41sau80 for ; Fri, 10 Jul 2015 20:12:33 +0000 (envelope-from ) from [67.214.212.122] by mandrillapp.com id 163e4a0faf244a2da6b0121cc7af1fe9; Fri, 10 Jul 2015 20:12:33 +0000", "X-Received": "by 10.170.119.147 with SMTP id l141mr25507408ykb.89.1436559154116; Fri, 10 Jul 2015 13:12:34 -0700 (PDT)", "Date": "Fri, 10 Jul 2015 20:12:33 +0000", } msg := test.OpenTestData("mail", "ctype-bug.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } if len(e.Root.Header) != len(headers) { t.Errorf("Failed to extract expected headers. Got %v headers, expected %v", len(e.Root.Header), len(headers)) } for k := range headers { if e.Root.Header[k] == nil { t.Errorf("Header named %q was missing, want it to exist", k) } } for k, v := range e.Root.Header { if _, ok := headers[k]; !ok { t.Errorf("Got header named %q, did not expect it to exist", k) continue } for _, val := range v { if !strings.Contains(headers[k], val) { t.Errorf("Got header %q with value %q, wanted value contained in:\n%q", k, val, headers[k]) } } } } func TestInlineTextBody(t *testing.T) { headers := map[string]string{ "To": "", "Message-Id": "", "Mime-Version": "1.0", "From": "Chris Garrett ", "Subject": "Text body only with disposition inline", "Content-Type": `text/html; charset="UTF-8"`, "Content-Disposition": "inline", "Content-Transfer-Encoding": "quoted-printable", "Date": "Wed, 8 Feb 2017 03:23:13 -0500", } msg := test.OpenTestData("mail", "attachment-only-inline-quoted-printable.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } want := "Just some html content" if e.Text != want { t.Errorf("Got Text with value %s, wanted value:\n%s", e.Text, want) } if !strings.Contains(e.HTML, want) { t.Errorf("Expected %q to contain %q", e.HTML, want) } if len(e.Root.Header) != len(headers) { t.Errorf("Failed to extract expected headers. Got %v headers, expected %v", len(e.Root.Header), len(headers)) } for k := range headers { if e.Root.Header[k] == nil { t.Errorf("Header named %q was missing, want it to exist", k) } } for k, v := range e.Root.Header { if _, ok := headers[k]; !ok { t.Errorf("Got header named %q, did not expect it to exist", k) continue } for _, val := range v { if !strings.Contains(headers[k], val) { t.Errorf("Got header %q with value %q, wanted value contained in:\n%q", k, val, headers[k]) } } } } func TestBinaryOnlyBodyHeaders(t *testing.T) { headers := map[string]string{ "To": "bob@test.com", "From": "alice@test.com", "Subject": "Test", "Message-Id": "<56A0AA5F.4020203@test.com>", "Date": "Thu, 21 Jan 2016 10:52:31 +0100", "Mime-Version": "1.0", "Content-Type": `image/jpeg; name="favicon.jpg"`, "Content-Transfer-Encoding": "base64", "Content-Disposition": `attachment; filename="favicon.jpg"`, } msg := test.OpenTestData("mail", "attachment-only.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } if len(e.Root.Header) != len(headers) { t.Errorf("Failed to extract expected headers. Got %v headers, expected %v", len(e.Root.Header), len(headers)) } for k := range headers { if e.Root.Header[k] == nil { t.Errorf("Header named %q was missing, want it to exist", k) } } for k, v := range e.Root.Header { if _, ok := headers[k]; !ok { t.Errorf("Got header named %q, did not expect it to exist", k) continue } for _, val := range v { if !strings.Contains(headers[k], val) { t.Errorf("Got header %q with value %q, wanted value contained in:\n%q", k, val, headers[k]) } } } } func TestEnvelopeEpilogue(t *testing.T) { msg := test.OpenTestData("mail", "epilogue-sample.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } got := string(e.Root.Epilogue) want := "Potentially malicious content\n" if got != want { t.Errorf("Epilogue == %q, want: %q", got, want) } } func TestCloneEnvelope(t *testing.T) { msg := test.OpenTestData("mail", "other-multi-related.raw") e, err := enmime.ReadEnvelope(msg) if err != nil { t.Fatal("Failed to parse MIME:", err) } clone := e.Clone() test.CompareEnvelope(t, clone, e) } func TestNilHeaderValues(t *testing.T) { e := &enmime.Envelope{} values := e.GetHeaderValues("") if len(values) > 0 { t.Fatal("No header values should be available, failed") } } func TestSetHeaderEmptyName(t *testing.T) { e := &enmime.Envelope{} err := e.SetHeader("", nil) if err == nil || err.Error() != "provide non-empty header name" { t.Fatal("Cannot set a header without a name, failed") } } func TestAddHeaderEmptyName(t *testing.T) { e := &enmime.Envelope{} err := e.AddHeader("", "") if err == nil || err.Error() != "provide non-empty header name" { t.Fatal("Cannot add a header without a name, failed") } } func TestDeleteHeaderEmptyName(t *testing.T) { e := &enmime.Envelope{} err := e.DeleteHeader("") if err == nil || err.Error() != "provide non-empty header name" { t.Fatal("Cannot add a header without a name, failed") } } func TestNilEnvelopeClone(t *testing.T) { var e *enmime.Envelope if e.Clone() != nil { t.Fatal("Clone of nil envelope is not nil, failed") } } enmime-0.9.3/error.go000066400000000000000000000036171417532643400144750ustar00rootroot00000000000000package enmime import ( "fmt" ) const ( // ErrorMalformedBase64 name. ErrorMalformedBase64 = "Malformed Base64" // ErrorMalformedHeader name. ErrorMalformedHeader = "Malformed Header" // ErrorMissingBoundary name. ErrorMissingBoundary = "Missing Boundary" // ErrorMissingContentType name. ErrorMissingContentType = "Missing Content-Type" // ErrorCharsetConversion name. ErrorCharsetConversion = "Character Set Conversion" // ErrorContentEncoding name. ErrorContentEncoding = "Content Encoding" // ErrorPlainTextFromHTML name. ErrorPlainTextFromHTML = "Plain Text from HTML" // ErrorCharsetDeclaration name. ErrorCharsetDeclaration = "Character Set Declaration Mismatch" // ErrorMissingRecipient name. ErrorMissingRecipient = "no recipients (to, cc, bcc) set" ) // Error describes an error encountered while parsing. type Error struct { Name string // The name or type of error encountered, from Error consts. Detail string // Additional detail about the cause of the error, if available. Severe bool // Indicates that a portion of the message was lost during parsing. } // Error formats the enmime.Error as a string. func (e *Error) Error() string { sev := "W" if e.Severe { sev = "E" } return fmt.Sprintf("[%s] %s: %s", sev, e.Name, e.Detail) } // String formats the enmime.Error as a string. DEPRECATED; use Error() instead. func (e *Error) String() string { return e.Error() } // addWarning builds a severe Error and appends to the Part error slice. func (p *Part) addError(name string, detailFmt string, args ...interface{}) { p.Errors = append( p.Errors, &Error{ name, fmt.Sprintf(detailFmt, args...), true, }) } // addWarning builds a non-severe Error and appends to the Part error slice. func (p *Part) addWarning(name string, detailFmt string, args ...interface{}) { p.Errors = append( p.Errors, &Error{ name, fmt.Sprintf(detailFmt, args...), false, }) } enmime-0.9.3/error_test.go000066400000000000000000000062021417532643400155250ustar00rootroot00000000000000package enmime import ( "os" "path/filepath" "testing" ) func TestErrorStringConversion(t *testing.T) { e := &Error{ Name: "WarnName", Detail: "Warn Details", Severe: false, } want := "[W] WarnName: Warn Details" got := e.Error() if got != want { t.Error("got:", got, "want:", want) } e = &Error{ Name: "ErrorName", Detail: "Error Details", Severe: true, } // Using deprecated String() method here for test coverage want = "[E] ErrorName: Error Details" got = e.String() if got != want { t.Error("got:", got, "want:", want) } } func TestErrorAddError(t *testing.T) { p := &Part{} p.addError(ErrorMalformedHeader, "1 %v %q", 2, "three") if len(p.Errors) != 1 { t.Fatal("len(p.Errors) ==", len(p.Errors), ", want: 1") } e := p.Errors[0] if e.Name != ErrorMalformedHeader { t.Errorf("e.Name == %q, want: %q", e.Name, ErrorMalformedHeader) } if !e.Severe { t.Errorf("e.Severe == %v, want: true", e.Severe) } want := "1 2 \"three\"" if e.Detail != want { t.Errorf("e.Detail == %q, want: %q", e.Detail, want) } } func TestErrorAddWarning(t *testing.T) { p := &Part{} p.addWarning(ErrorMalformedHeader, "1 %v %q", 2, "three") if len(p.Errors) != 1 { t.Fatal("len(p.Errors) ==", len(p.Errors), ", want: 1") } e := p.Errors[0] if e.Name != ErrorMalformedHeader { t.Errorf("e.Name == %q, want: %q", e.Name, ErrorMalformedHeader) } if e.Severe { t.Errorf("e.Severe == %v, want: false", e.Severe) } want := "1 2 \"three\"" if e.Detail != want { t.Errorf("e.Detail == %q, want: %q", e.Detail, want) } } func TestErrorEnvelopeWarnings(t *testing.T) { // To pass each file below must error one or more times with the specified errorName, and no // other errorNames. var files = []struct { filename string perror string }{ {"bad-final-boundary.raw", ErrorMissingBoundary}, {"bad-header-wrap.raw", ErrorMalformedHeader}, {"html-only-inline.raw", ErrorPlainTextFromHTML}, {"missing-content-type2.raw", ErrorMissingContentType}, {"empty-header.raw", ErrorMissingContentType}, {"unk-encoding-part.raw", ErrorContentEncoding}, {"unk-charset-html-only.raw", ErrorCharsetConversion}, {"unk-charset-part.raw", ErrorCharsetConversion}, {"malformed-base64-attach.raw", ErrorMalformedBase64}, {"incorrect-charset.raw", ErrorCharsetDeclaration}, } for _, tt := range files { t.Run(tt.filename, func(t *testing.T) { r, _ := os.Open(filepath.Join("testdata", "low-quality", tt.filename)) e, err := ReadEnvelope(r) if err != nil { t.Fatalf("Failed to parse MIME: %+v", err) } if len(e.Errors) == 0 { t.Error("Got 0 warnings, expected at least one for:", tt.filename) } satisfied := false for _, perr := range e.Errors { if perr.Name == tt.perror { satisfied = true if perr.Severe { t.Errorf("Expected Severe to be false, got true for %q", perr.Name) } } } if !satisfied { var errorList string for _, perr := range e.Errors { errorList += perr.Error() errorList += "\n" } t.Errorf( "File %q should have error of type %q, got these instead:\n%s", tt.filename, tt.perror, errorList) } }) } } enmime-0.9.3/example_test.go000066400000000000000000000146431417532643400160370ustar00rootroot00000000000000package enmime_test import ( "fmt" "net/smtp" "os" "sort" "strings" "github.com/jhillyerd/enmime" ) // ExampleBuilder illustrates how to build and send a MIME encoded message. func ExampleBuilder() { // Create an SMTP Sender which relies on Go's built-in net/smtp package. Advanced users // may provide their own Sender, or mock it in unit tests. smtpHost := "smtp.relay.host:25" smtpAuth := smtp.PlainAuth("", "user", "pw", "host") sender := enmime.NewSMTP(smtpHost, smtpAuth) // MailBuilder is (mostly) immutable, each method below returns a new MailBuilder without // modifying the original. master := enmime.Builder(). From("Do Not Reply", "noreply@inbucket.org"). Subject("Inbucket Newsletter"). Text([]byte("Text body")). HTML([]byte("

HTML body

")) // master is immutable, causing each msg below to have a single recipient. msg := master.To("Esteemed Customer", "user1@inbucket.org") msg.Send(sender) msg = master.To("Another Customer", "user2@inbucket.org") msg.Send(sender) } func ExampleReadEnvelope() { // Open a sample message file. r, err := os.Open("testdata/mail/qp-utf8-header.raw") if err != nil { fmt.Print(err) return } // Parse message body with enmime. env, err := enmime.ReadEnvelope(r) if err != nil { fmt.Print(err) return } // Headers can be retrieved via Envelope.GetHeader(name). fmt.Printf("From: %v\n", env.GetHeader("From")) // Address-type headers can be parsed into a list of decoded mail.Address structs. alist, _ := env.AddressList("To") for _, addr := range alist { fmt.Printf("To: %s <%s>\n", addr.Name, addr.Address) } // enmime can decode quoted-printable headers. fmt.Printf("Subject: %v\n", env.GetHeader("Subject")) // The plain text body is available as mime.Text. fmt.Printf("Text Body: %v chars\n", len(env.Text)) // The HTML body is stored in mime.HTML. fmt.Printf("HTML Body: %v chars\n", len(env.HTML)) // mime.Inlines is a slice of inlined attacments. fmt.Printf("Inlines: %v\n", len(env.Inlines)) // mime.Attachments contains the non-inline attachments. fmt.Printf("Attachments: %v\n", len(env.Attachments)) // Output: // From: James Hillyerd , André Pirard // To: Mirosław Marczak // Subject: MIME UTF8 Test ¢ More Text // Text Body: 1300 chars // HTML Body: 1736 chars // Inlines: 0 // Attachments: 0 } // ExampleEnvelope demonstrates the relationship between Envelope and Parts. func ExampleEnvelope() { // Create sample message in memory raw := `From: user@inbucket.org Subject: Example message Content-Type: multipart/alternative; boundary=Enmime-100 --Enmime-100 Content-Type: text/plain X-Comment: part1 hello! --Enmime-100 Content-Type: text/html X-Comment: part2 hello! --Enmime-100 Content-Type: text/plain Content-Disposition: attachment; filename=hi.txt X-Comment: part3 hello again! --Enmime-100-- ` // Parse message body with enmime.ReadEnvelope r := strings.NewReader(raw) env, err := enmime.ReadEnvelope(r) if err != nil { fmt.Print(err) return } // The root Part contains the message header, which is also available via the // Envelope.GetHeader() method. fmt.Printf("Root Part Subject: %q\n", env.Root.Header.Get("Subject")) fmt.Printf("Envelope Subject: %q\n", env.GetHeader("Subject")) fmt.Println() // The text from part1 is consumed and placed into the Envelope.Text field. fmt.Printf("Text Content: %q\n", env.Text) // But part1 is also available as a child of the root Part. Only the headers may be accessed, // because the content has been consumed. part1 := env.Root.FirstChild fmt.Printf("Part 1 X-Comment: %q\n", part1.Header.Get("X-Comment")) fmt.Println() // The HTML from part2 is consumed and placed into the Envelope.HTML field. fmt.Printf("HTML Content: %q\n", env.HTML) // And part2 is available as the second child of the root Part. Only the headers may be // accessed, because the content has been consumed. part2 := env.Root.FirstChild.NextSibling fmt.Printf("Part 2 X-Comment: %q\n", part2.Header.Get("X-Comment")) fmt.Println() // Because part3 has a disposition of attachment, it is added to the Envelope.Attachments // slice fmt.Printf("Attachment 1 X-Comment: %q\n", env.Attachments[0].Header.Get("X-Comment")) // And is still available as the third child of the root Part part3 := env.Root.FirstChild.NextSibling.NextSibling fmt.Printf("Part 3 X-Comment: %q\n", part3.Header.Get("X-Comment")) // The content of Attachments, Inlines and OtherParts are available as a slice of bytes fmt.Printf("Part 3 Content: %q\n", part3.Content) // part3 contained a malformed header line, enmime has attached an Error to it p3error := part3.Errors[0] fmt.Println(p3error.Error()) fmt.Println() // All Part errors are collected and placed into Envelope.Errors fmt.Println("Envelope errors:") for _, e := range env.Errors { fmt.Println(e.Error()) } // Output: // Root Part Subject: "Example message" // Envelope Subject: "Example message" // // Text Content: "hello!" // Part 1 X-Comment: "part1" // // HTML Content: "hello!" // Part 2 X-Comment: "part2" // // Attachment 1 X-Comment: "part3" // Part 3 X-Comment: "part3" // Part 3 Content: "hello again!" // [W] Malformed Header: Continued line "filename=hi.txt" was not indented // // Envelope errors: // [W] Malformed Header: Continued line "filename=hi.txt" was not indented } func ExampleEnvelope_GetHeaderKeys() { // Open a sample message file. r, err := os.Open("testdata/mail/qp-utf8-header.raw") if err != nil { fmt.Print(err) return } // Parse message with enmime. env, err := enmime.ReadEnvelope(r) if err != nil { fmt.Print(err) return } // A list of headers is retrieved via Envelope.GetHeaderKeys(). headers := env.GetHeaderKeys() sort.Strings(headers) // Print each header, key and value. for _, header := range headers { fmt.Printf("%s: %v\n", header, env.GetHeader(header)) } // Output: // Content-Type: multipart/alternative; boundary="------------020203040006070307010003" // Date: Fri, 19 Oct 2012 12:22:49 -0700 // From: James Hillyerd , André Pirard // Message-Id: <5081A889.3020108@jamehi03lx.noa.com> // Mime-Version: 1.0 // Sender: André Pirard // Subject: MIME UTF8 Test ¢ More Text // To: Mirosław Marczak // User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:16.0) Gecko/20121010 Thunderbird/16.0.1 } enmime-0.9.3/go.mod000066400000000000000000000011521417532643400141130ustar00rootroot00000000000000module github.com/jhillyerd/enmime require ( github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a github.com/go-test/deep v1.0.7 github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28 github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 github.com/mattn/go-runewidth v0.0.12 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pkg/errors v0.9.1 github.com/rivo/uniseg v0.2.0 // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect golang.org/x/net v0.0.0-20210501142056-aec3718b3fa0 // indirect golang.org/x/text v0.3.6 ) go 1.13 enmime-0.9.3/go.sum000066400000000000000000000053571417532643400141530ustar00rootroot00000000000000github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8= github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M= github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28 h1:gBeyun7mySAKWg7Fb0GOcv0upX9bdaZScs8QcRo8mEY= github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14= github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 h1:g0fAGBisHaEQ0TRq1iBvemFRf+8AEWEmBESSiWB3Vsc= github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= golang.org/x/net v0.0.0-20210501142056-aec3718b3fa0 h1:6QqBc2UURz4Sbr4IE15uXM8CTQlHnRdtKuogDhwnu2Y= golang.org/x/net v0.0.0-20210501142056-aec3718b3fa0/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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= enmime-0.9.3/header.go000066400000000000000000000146031417532643400145710ustar00rootroot00000000000000package enmime import ( "bufio" "bytes" "fmt" "mime" "net/textproto" "strings" "github.com/jhillyerd/enmime/internal/coding" "github.com/jhillyerd/enmime/mediatype" "github.com/pkg/errors" ) const ( // Standard MIME content dispositions cdAttachment = "attachment" cdInline = "inline" // Standard MIME content types ctAppPrefix = "application/" ctAppOctetStream = "application/octet-stream" ctMultipartAltern = "multipart/alternative" ctMultipartMixed = "multipart/mixed" ctMultipartPrefix = "multipart/" ctMultipartRelated = "multipart/related" ctTextPrefix = "text/" ctTextPlain = "text/plain" ctTextHTML = "text/html" // Standard Transfer encodings cte7Bit = "7bit" cte8Bit = "8bit" cteBase64 = "base64" cteBinary = "binary" cteQuotedPrintable = "quoted-printable" // Standard MIME header names hnContentDisposition = "Content-Disposition" hnContentEncoding = "Content-Transfer-Encoding" hnContentID = "Content-ID" hnContentType = "Content-Type" hnMIMEVersion = "MIME-Version" // Standard MIME header parameters hpBoundary = "boundary" hpCharset = "charset" hpFile = "file" hpFilename = "filename" hpName = "name" hpModDate = "modification-date" utf8 = "utf-8" ) // AddressHeaders is the set of SMTP headers that contain email addresses, used by // Envelope.AddressList(). Key characters must be all lowercase. var AddressHeaders = map[string]bool{ "bcc": true, "cc": true, "delivered-to": true, "from": true, "reply-to": true, "to": true, "sender": true, "resent-bcc": true, "resent-cc": true, "resent-from": true, "resent-reply-to": true, "resent-to": true, "resent-sender": true, } // Terminology from RFC 2047: // encoded-word: the entire =?charset?encoding?encoded-text?= string // charset: the character set portion of the encoded word // encoding: the character encoding type used for the encoded-text // encoded-text: the text we are decoding // ParseMediaType is a more tolerant implementation of Go's mime.ParseMediaType function. // // Tolerances accounted for: // * Missing ';' between content-type and media parameters // * Repeating media parameters // * Unquoted values in media parameters containing 'tspecials' characters func ParseMediaType(ctype string) (mtype string, params map[string]string, invalidParams []string, err error) { // Export of internal function. return mediatype.Parse(ctype) } // readHeader reads a block of SMTP or MIME headers and returns a textproto.MIMEHeader. // Header parse warnings & errors will be added to p.Errors, io errors will be returned directly. func readHeader(r *bufio.Reader, p *Part) (textproto.MIMEHeader, error) { // buf holds the massaged output for textproto.Reader.ReadMIMEHeader() buf := &bytes.Buffer{} tp := textproto.NewReader(r) firstHeader := true for { // Pull out each line of the headers as a temporary slice s s, err := tp.ReadLineBytes() if err != nil { buf.Write([]byte{'\r', '\n'}) break } firstColon := bytes.IndexByte(s, ':') firstSpace := bytes.IndexAny(s, " \t\n\r") if firstSpace == 0 { // Starts with space: continuation buf.WriteByte(' ') buf.Write(textproto.TrimBytes(s)) continue } if firstColon == 0 { // Can't parse line starting with colon: skip p.addError(ErrorMalformedHeader, "Header line %q started with a colon", s) continue } if firstColon > 0 { // Contains a colon, treat as a new header line if !firstHeader { // New Header line, end the previous buf.Write([]byte{'\r', '\n'}) } // Behavior change in net/textproto package in Golang 1.12.10 and 1.13.1: // A space preceding the first colon in a header line is no longer handled // automatically due to CVE-2019-16276 which takes advantage of this // particular violation of RFC-7230 to exploit HTTP/1.1 if bytes.Contains(s[:firstColon+1], []byte{' ', ':'}) { s = bytes.Replace(s, []byte{' ', ':'}, []byte{':'}, 1) } s = textproto.TrimBytes(s) buf.Write(s) firstHeader = false } else { // No colon: potential non-indented continuation if len(s) > 0 { // Attempt to detect and repair a non-indented continuation of previous line buf.WriteByte(' ') buf.Write(s) p.addWarning(ErrorMalformedHeader, "Continued line %q was not indented", s) } else { // Empty line, finish header parsing buf.Write([]byte{'\r', '\n'}) break } } } buf.Write([]byte{'\r', '\n'}) tr := textproto.NewReader(bufio.NewReader(buf)) header, err := tr.ReadMIMEHeader() return header, errors.WithStack(err) } // decodeToUTF8Base64Header decodes a MIME header per RFC 2047, reencoding to =?utf-8b? func decodeToUTF8Base64Header(input string) string { if !strings.Contains(input, "=?") { // Don't scan if there is nothing to do here return input } // The standard lib performs an incremental inspection of this string, where the // "skipSpace" method only strings.trimLeft for spaces and tabs. Here we have a // hard dependency on space existing and not on next expected rune. // // For resolving #112 with the least change, I will implement the // "quoted display-name" detector, which will resolve the case specific // issue stated in #112, but only in the case of a quoted display-name // followed, without whitespace, by addr-spec. tokens := strings.FieldsFunc(quotedDisplayName(input), whiteSpaceRune) output := make([]string, len(tokens)) for i, token := range tokens { if len(token) > 4 && strings.Contains(token, "=?") { // Stash parenthesis, they should not be encoded prefix := "" suffix := "" if token[0] == '(' { prefix = "(" token = token[1:] } if token[len(token)-1] == ')' { suffix = ")" token = token[:len(token)-1] } // Base64 encode token output[i] = prefix + mime.BEncoding.Encode("UTF-8", coding.DecodeExtHeader(token)) + suffix } else { output[i] = token } } // Return space separated tokens return strings.Join(output, " ") } func quotedDisplayName(s string) string { if !strings.HasPrefix(s, "\"") { return s } idx := strings.LastIndex(s, "\"") return fmt.Sprintf("%s %s", s[:idx+1], s[idx+1:]) } // Detects a RFC-822 linear-white-space, passed to strings.FieldsFunc. func whiteSpaceRune(r rune) bool { return r == ' ' || r == '\t' || r == '\r' || r == '\n' } enmime-0.9.3/header_test.go000066400000000000000000000225101417532643400156240ustar00rootroot00000000000000package enmime import ( "bufio" "net/textproto" "strings" "testing" ) // Test re-encoding to base64 func TestDecodeToUTF8Base64Header(t *testing.T) { var testTable = []struct { in, want string }{ {"no encoding", "no encoding"}, {"=?utf-8?q?abcABC_=24_=c2=a2_=e2=82=ac?=", "=?UTF-8?b?YWJjQUJDICQgwqIg4oKs?="}, {"=?iso-8859-1?q?#=a3_c=a9_r=ae_u=b5?=", "=?UTF-8?b?I8KjIGPCqSBywq4gdcK1?="}, {"=?big5?q?=a1=5d_=a1=61_=a1=71?=", "=?UTF-8?b?77yIIO+9myDjgIg=?="}, // Must respect separate tokens {"=?UTF-8?Q?Miros=C5=82aw?= ", "=?UTF-8?b?TWlyb3PFgmF3?= "}, {"First Last (=?iso-8859-1?q?#=a3_c=a9_r=ae_u=b5?=)", "First Last (=?UTF-8?b?I8KjIGPCqSBywq4gdcK1?=)"}, // Quoted display name without space before angle-addr spec, Issue #112 {"\"=?UTF-8?b?TWlyb3PFgmF3?=\"", "=?UTF-8?b?Ik1pcm9zxYJhdyI=?= "}, } for _, tt := range testTable { got := decodeToUTF8Base64Header(tt.in) if got != tt.want { t.Errorf("DecodeHeader(%q) == %q, want: %q", tt.in, got, tt.want) } } } func TestReadHeader(t *testing.T) { // These values will surround the test table input string. prefix := "From: hooman\n \n being\n" suffix := "Subject: hi\n\nPart body\n" data := make([]byte, 16*1024) for i := 0; i < len(data); i++ { data[i] = 'x' } sdata := string(data) var ttable = []struct { label, input, hname, want string correct bool extras []string }{ { label: "basic crlf", input: "Foo: bar\r\n", hname: "Foo", want: "bar", correct: true, }, { label: "basic lf", input: "To: anybody\n", hname: "To", want: "anybody", correct: true, }, { label: "hyphenated", input: "Content-Language: en\r\n", hname: "Content-Language", want: "en", correct: true, }, { label: "numeric", input: "Privilege: 127\n", hname: "Privilege", want: "127", correct: true, }, { label: "space before colon", input: "SID : 0\r\n", hname: "SID", want: "0", correct: true, }, { label: "space in name", input: "Audio Mode : None\r\n", hname: "Audio Mode", want: "None", correct: true, }, { label: "sdata", input: "Cookie: " + sdata + "\r\n", hname: "Cookie", want: sdata, correct: true, }, { label: "missing name", input: ": line1=foo\r\n", hname: "", want: "", correct: false, }, { label: "blank line in continuation", input: "X-Continuation: line1=foo\r\n" + " \r\n" + " line2=bar\r\n", hname: "X-Continuation", want: "line1=foo line2=bar", correct: true, }, { label: "lf-space continuation", input: "Content-Type: text/plain;\n charset=us-ascii\n", hname: "Content-Type", want: "text/plain; charset=us-ascii", correct: true, }, { label: "lf-tab continuation", input: "X-Tabbed-Continuation: line1=foo;\n\tline2=bar\n", hname: "X-Tabbed-Continuation", want: "line1=foo; line2=bar", correct: true, }, { label: "equals in name", input: "name=value:text\n", hname: "name=value", want: "text", correct: true, }, { label: "no space before continuation", input: "X-Bad-Continuation: line1=foo;\nline2=bar\n", hname: "X-Bad-Continuation", want: "line1=foo; line2=bar", correct: false, }, { label: "not really a continuation", input: "X-Not-Continuation: line1=foo;\nline2: bar\n", hname: "X-Not-Continuation", want: "line1=foo;", correct: true, extras: []string{"line2"}, }, { label: "continuation with header style", input: "X-Continuation: line1=foo;\n not-a-header 15 X-Not-Header: bar\n", hname: "X-Continuation", want: "line1=foo; not-a-header 15 X-Not-Header: bar", correct: true, }, { label: "multiline continuation with header style, few spaces", input: "X-Continuation-DKIM-like: line1=foo;\n" + " h=Subject:From:Reply-To:To:Date:Message-ID: List-ID:List-Unsubscribe:\n" + " Content-Type:MIME-Version;\n", hname: "X-Continuation-DKIM-like", want: "line1=foo;" + " h=Subject:From:Reply-To:To:Date:Message-ID: List-ID:List-Unsubscribe:" + " Content-Type:MIME-Version;", correct: true, }, { label: "multiline continuation, few colons", input: "Authentication-Results: mx.google.com;\n" + " spf=pass (google.com: sender)\n" + " dkim=pass header.i=@1;\n" + " dkim=pass header.i=@2\n", hname: "Authentication-Results", want: "mx.google.com;" + " spf=pass (google.com: sender)" + " dkim=pass header.i=@1;" + " dkim=pass header.i=@2", correct: true, }, { label: "continuation containing early name-colon", input: "DKIM-Signature: a=rsa-sha256; v=1; q=dns/txt;\r\n" + " s=krs; t=1603674005; h=Content-Transfer-Encoding: Mime-Version:\r\n" + " Content-Type: Subject: From: To: Message-Id: Sender: Date;\r\n", hname: "DKIM-Signature", want: "a=rsa-sha256; v=1; q=dns/txt;" + " s=krs; t=1603674005; h=Content-Transfer-Encoding: Mime-Version:" + " Content-Type: Subject: From: To: Message-Id: Sender: Date;", correct: true, }, } for _, tt := range ttable { t.Run(tt.label, func(t *testing.T) { if lastc := tt.input[len(tt.input)-1]; lastc != '\r' && lastc != '\n' { t.Fatalf("Malformed test case, %q input does not end with a CR or LF", tt.label) } // Reader we will share with readHeader() r := bufio.NewReader(strings.NewReader(prefix + tt.input + suffix)) p := &Part{} header, err := readHeader(r, p) if err != nil { t.Fatal(err) } // Check exepcted prefix header. got := header.Get("From") want := "hooman being" if got != want { t.Errorf("Prefix (From) header mangled\ngot: %q, want: %q", got, want) } // Check exepcted suffix header. got = header.Get("Subject") want = "hi" if got != want { t.Errorf("Suffix (Subject) header mangled\ngot: %q, want: %q", got, want) } // Check exepcted header from ttable. got = header.Get(tt.hname) if got != tt.want { t.Errorf( "Stripped %q header value mismatch\ngot : %q,\nwant: %q", tt.hname, got, tt.want) } // Check error count. wantErrs := 0 if !tt.correct { wantErrs = 1 } gotErrs := len(p.Errors) if gotErrs != wantErrs { t.Errorf("Got %v p.Errors, want %v", gotErrs, wantErrs) } // Check for extra headers by removing expected ones. delete(header, "From") delete(header, "Subject") delete(header, textproto.CanonicalMIMEHeaderKey(tt.hname)) for _, hname := range tt.extras { delete(header, textproto.CanonicalMIMEHeaderKey(hname)) } for hname := range header { t.Errorf("Found unexpected header %q after parsing", hname) } // Output input if any check failed. if t.Failed() { t.Errorf("input: %q", tt.input) } // readHeader should have consumed the two header lines, and the blank line, but not the // body want = "Part body" line, isPrefix, err := r.ReadLine() got = string(line) if err != nil { t.Fatal(err) } if isPrefix { t.Fatal("isPrefix was true, wanted false") } if got != want { t.Errorf("Line got: %q, want: %q", got, want) } }) } } func TestCommaDelimitedAddressLists(t *testing.T) { testData := []struct { have string want string }{ { have: `"Joe @ Company" `, want: `"Joe @ Company" , `, }, { have: `Joe Company `, want: `Joe Company , `, }, { have: `Joe Company:Joey John ;`, want: `Joe Company:Joey , John ;`, }, { have: `Joe Company:Joey John ; Jimmy John `, want: `Joe Company:Joey , John ;`, }, { have: `Joe Company John Company `, want: `Joe Company , John Company `, }, { have: `Joe Company ,John Company `, want: `Joe Company ,John Company `, }, { have: `joe@company.com other@company.com`, want: `joe@company.com, other@company.com`, }, { have: `Jimmy John joe@company.com other@company.com`, want: `Jimmy John , joe@company.com, other@company.com`, }, { have: `Jimmy John joe@company.com John Company `, want: `Jimmy John , joe@company.com, John Company `, }, { have: ` "Giant; \"Big\" Box" `, want: `, "Giant; \"Big\" Box" `, }, { have: `A Group:Ed Jones ,joe@where.test,John ;`, want: `A Group:Ed Jones ,joe@where.test,John ;`, }, { have: `A Group:Ed Jones joe@where.test John ;`, want: `A Group:Ed Jones , joe@where.test, John ;`, }, } for i := range testData { v := ensureCommaDelimitedAddresses(testData[i].have) if testData[i].want != v { t.Fatalf("Expected %s, but got %s", testData[i].want, v) } } } enmime-0.9.3/inspect.go000066400000000000000000000035551417532643400150120ustar00rootroot00000000000000package enmime import ( "bufio" "bytes" "io" "net/textproto" "github.com/jhillyerd/enmime/internal/coding" "github.com/pkg/errors" ) var defaultHeadersList = []string{ "From", "To", "Sender", "CC", "BCC", "Subject", "Date", } // DecodeHeaders returns a limited selection of mime headers for use by user agents // Default header list: // "Date", "Subject", "Sender", "From", "To", "CC" and "BCC" // // Additional headers provided will be formatted canonically: // h, err := enmime.DecodeHeaders(b, "content-type", "user-agent") func DecodeHeaders(b []byte, addtlHeaders ...string) (textproto.MIMEHeader, error) { b = ensureHeaderBoundary(b) tr := textproto.NewReader(bufio.NewReader(bytes.NewReader(b))) headers, err := tr.ReadMIMEHeader() switch errors.Cause(err) { case nil, io.EOF: // carry on, io.EOF is expected default: return nil, err } headerList := defaultHeadersList headerList = append(headerList, addtlHeaders...) res := map[string][]string{} for _, header := range headerList { h := textproto.CanonicalMIMEHeaderKey(header) res[h] = make([]string, 0, len(headers[h])) for _, value := range headers[h] { res[h] = append(res[h], coding.RFC2047Decode(value)) } } return res, nil } // ensureHeaderBoundary scans through an rfc822 document to ensure the boundary between headers and body exists func ensureHeaderBoundary(b []byte) []byte { slice := bytes.SplitAfter(b, []byte{'\r', '\n'}) dest := make([]byte, 0, len(b)+2) headers := true for _, v := range slice { if headers && (bytes.Contains(v, []byte{':'}) || bytes.HasPrefix(v, []byte{' '}) || bytes.HasPrefix(v, []byte{'\t'})) { dest = append(dest, v...) continue } if headers { headers = false if !bytes.Equal(v, []byte{'\r', '\n'}) { dest = append(dest, append([]byte{'\r', '\n'}, v...)...) continue } } dest = append(dest, v...) } return dest } enmime-0.9.3/inspect_test.go000066400000000000000000000056141417532643400160470ustar00rootroot00000000000000package enmime_test import ( "bytes" "io" "io/ioutil" "net/mail" "net/textproto" "strings" "testing" "github.com/jhillyerd/enmime" "github.com/jhillyerd/enmime/internal/test" ) func TestDecodeHeaders(t *testing.T) { t.Run("rfc2047 sample", func(t *testing.T) { r := test.OpenTestData("mail", "qp-utf8-header.raw") b, err := ioutil.ReadAll(r) if err != nil { t.Errorf("%+v", err) } h, err := enmime.DecodeHeaders(b) if err != nil { t.Errorf("%+v", err) } if !strings.Contains(h.Get("To"), "Mirosław Marczak") { t.Errorf("Error decoding RFC2047 header value") } }) t.Run("no break between headers and content", func(t *testing.T) { r := test.OpenTestData("mail", "qp-utf8-header-no-break.raw") b, err := ioutil.ReadAll(r) if err != nil { t.Errorf("%+v", err) } h, err := enmime.DecodeHeaders(b) if err != nil { t.Errorf("%+v", err) } if !strings.Contains(h.Get("To"), "Mirosław Marczak") { t.Errorf("Error decoding RFC2047 header value") } }) t.Run("textproto header read error", func(t *testing.T) { r := test.OpenTestData("low-quality", "bad-header-start.raw") b, err := ioutil.ReadAll(r) if err != nil { t.Errorf("%+v", err) } _, err = enmime.DecodeHeaders(b) switch err.(type) { case textproto.ProtocolError: // carry on default: t.Fatalf("Did return expected error: %T:%+v", err, err) } }) t.Run("rfc2047 recursive sample", func(t *testing.T) { r := test.OpenTestData("mail", "qp-utf8-header-recursed.raw") b, err := ioutil.ReadAll(r) if err != nil { t.Errorf("%+v", err) } h, err := enmime.DecodeHeaders(b) if err != nil { t.Errorf("%+v", err) } if !strings.Contains(h.Get("From"), "WirelessCaller (203) 402-5984 WirelessCaller (203) 402-5984 WirelessCaller (203) 402-5984") { t.Errorf("Error decoding recursive RFC2047 header value") } }) } func BenchmarkHumanHeadersOnly(b *testing.B) { r := test.OpenTestData("mail", "qp-utf8-header.raw") eml, err := ioutil.ReadAll(r) if err != nil { b.Fatal(err) } for i := 0; i < b.N; i++ { h, err := enmime.DecodeHeaders(eml) if err != nil { b.Fatal(err) } _, err = mail.ParseAddressList(h.Get("From")) if err != nil { b.Fatal(err) } _, err = mail.ParseAddressList(h.Get("To")) if err != nil { b.Fatal(err) } _ = h.Get("Subject") } } func BenchmarkReadEnvelope(b *testing.B) { r := test.OpenTestData("mail", "qp-utf8-header.raw") eml, err := ioutil.ReadAll(r) if err != nil { b.Fatal(err) } reusedReader := bytes.NewReader(eml) for i := 0; i < b.N; i++ { env, err := enmime.ReadEnvelope(reusedReader) if err != nil { b.Fatal(err) } _, err = env.AddressList("From") if err != nil { b.Fatal(err) } _, err = env.AddressList("To") if err != nil { b.Fatal(err) } env.GetHeader("Subject") // reset reader for next run _, err = reusedReader.Seek(0, io.SeekStart) if err != nil { b.Fatal(err) } } } enmime-0.9.3/internal/000077500000000000000000000000001417532643400146225ustar00rootroot00000000000000enmime-0.9.3/internal/coding/000077500000000000000000000000001417532643400160655ustar00rootroot00000000000000enmime-0.9.3/internal/coding/base64.go000066400000000000000000000035701417532643400175050ustar00rootroot00000000000000package coding import ( "fmt" "io" ) // base64CleanerTable notes byte values that should be stripped (-2), stripped w/ error (-1). var base64CleanerTable = []int8{ -1, -1, -1, -1, -1, -1, -1, -1, -1, -2, -2, -1, -1, -2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, } // Base64Cleaner improves the tolerance of in Go's built-in base64 decoder by stripping out // characters that would cause decoding to fail. type Base64Cleaner struct { // Report of non-whitespace characters detected while cleaning base64 data. Errors []error r io.Reader buffer [1024]byte } // Enforce io.Reader interface. var _ io.Reader = &Base64Cleaner{} // NewBase64Cleaner returns a Base64Cleaner object for the specified reader. Base64Cleaner // implements the io.Reader interface. func NewBase64Cleaner(r io.Reader) *Base64Cleaner { return &Base64Cleaner{ Errors: make([]error, 0), r: r, } } // Read method for io.Reader interface. func (bc *Base64Cleaner) Read(p []byte) (n int, err error) { // Size our buf to smallest of len(p) or len(bc.buffer). size := len(bc.buffer) if size > len(p) { size = len(p) } buf := bc.buffer[:size] bn, err := bc.r.Read(buf) for i := 0; i < bn; i++ { switch base64CleanerTable[buf[i]&0x7f] { case -2: // Strip these silently: tab, \n, \r, space, equals sign. case -1: // Strip these, but warn the client. bc.Errors = append(bc.Errors, fmt.Errorf("unexpected %q in base64 stream", buf[i])) default: p[n] = buf[i] n++ } } return } enmime-0.9.3/internal/coding/base64_test.go000066400000000000000000000025601417532643400205420ustar00rootroot00000000000000package coding_test import ( "io" "strings" "testing" "github.com/jhillyerd/enmime/internal/coding" ) func TestBase64Cleaner(t *testing.T) { buf := make([]byte, 1024) testCases := []struct { input, want string }{ {"", ""}, {"\tA B\r\nC", "ABC"}, {"XYZ===", "XYZ"}, } for _, tc := range testCases { t.Run(tc.want, func(t *testing.T) { cleaner := coding.NewBase64Cleaner(strings.NewReader(tc.input)) n, err := cleaner.Read(buf) if err != nil && err != io.EOF { t.Fatal(err) } for _, e := range cleaner.Errors { t.Error(e) } got := string(buf[:n]) if got != tc.want { t.Error("got:", got, "want:", tc.want) } }) } } // TestBase64CleanerErrors sends invalid characters and tests error messages func TestBase64CleanerErrors(t *testing.T) { buf := make([]byte, 1024) testCases := []struct { input, want string }{ {"a!", "a"}, {"@b", "b"}, {"#c", "c"}, {"d$d", "dd"}, {"ee\b", "ee"}, } for _, tc := range testCases { t.Run(tc.want, func(t *testing.T) { cleaner := coding.NewBase64Cleaner(strings.NewReader(tc.input)) n, err := cleaner.Read(buf) if err != nil && err != io.EOF { t.Fatal(err) } if len(cleaner.Errors) != 1 { t.Errorf("got %d Errors, wanted 1", len(cleaner.Errors)) } got := string(buf[:n]) if got != tc.want { t.Error("got:", got, "want:", tc.want) } }) } } enmime-0.9.3/internal/coding/charsets.go000066400000000000000000000426021417532643400202340ustar00rootroot00000000000000package coding import ( "bytes" "fmt" "io" "io/ioutil" "regexp" "strings" "github.com/cention-sany/utf7" "golang.org/x/text/encoding" "golang.org/x/text/encoding/charmap" "golang.org/x/text/encoding/japanese" "golang.org/x/text/encoding/korean" "golang.org/x/text/encoding/simplifiedchinese" "golang.org/x/text/encoding/traditionalchinese" "golang.org/x/text/encoding/unicode" "golang.org/x/text/transform" ) const utf8 = "utf-8" // encodings is based on golang.org/x/net/html/charset/table.go var encodings = map[string]struct { e encoding.Encoding name string }{ "unicode-1-1-utf-8": {encoding.Nop, utf8}, "utf-8": {encoding.Nop, utf8}, "utf8": {encoding.Nop, utf8}, "utf-7": {utf7.UTF7, "utf-7"}, "utf7": {utf7.UTF7, "utf-7"}, "866": {charmap.CodePage866, "ibm866"}, "cp866": {charmap.CodePage866, "ibm866"}, "csibm866": {charmap.CodePage866, "ibm866"}, "ibm866": {charmap.CodePage866, "ibm866"}, "csisolatin2": {charmap.ISO8859_2, "iso-8859-2"}, "iso-8859-2": {charmap.ISO8859_2, "iso-8859-2"}, "iso-ir-101": {charmap.ISO8859_2, "iso-8859-2"}, "iso8859-2": {charmap.ISO8859_2, "iso-8859-2"}, "iso88592": {charmap.ISO8859_2, "iso-8859-2"}, "iso_8859-2": {charmap.ISO8859_2, "iso-8859-2"}, "iso_8859-2:1987": {charmap.ISO8859_2, "iso-8859-2"}, "l2": {charmap.ISO8859_2, "iso-8859-2"}, "latin2": {charmap.ISO8859_2, "iso-8859-2"}, "csisolatin3": {charmap.ISO8859_3, "iso-8859-3"}, "iso-8859-3": {charmap.ISO8859_3, "iso-8859-3"}, "iso-ir-109": {charmap.ISO8859_3, "iso-8859-3"}, "iso8859-3": {charmap.ISO8859_3, "iso-8859-3"}, "iso88593": {charmap.ISO8859_3, "iso-8859-3"}, "iso_8859-3": {charmap.ISO8859_3, "iso-8859-3"}, "iso_8859-3:1988": {charmap.ISO8859_3, "iso-8859-3"}, "l3": {charmap.ISO8859_3, "iso-8859-3"}, "latin3": {charmap.ISO8859_3, "iso-8859-3"}, "csisolatin4": {charmap.ISO8859_4, "iso-8859-4"}, "iso-8859-4": {charmap.ISO8859_4, "iso-8859-4"}, "iso-ir-110": {charmap.ISO8859_4, "iso-8859-4"}, "iso8859-4": {charmap.ISO8859_4, "iso-8859-4"}, "iso88594": {charmap.ISO8859_4, "iso-8859-4"}, "iso_8859-4": {charmap.ISO8859_4, "iso-8859-4"}, "iso_8859-4:1988": {charmap.ISO8859_4, "iso-8859-4"}, "l4": {charmap.ISO8859_4, "iso-8859-4"}, "latin4": {charmap.ISO8859_4, "iso-8859-4"}, "csisolatincyrillic": {charmap.ISO8859_5, "iso-8859-5"}, "cyrillic": {charmap.ISO8859_5, "iso-8859-5"}, "iso-8859-5": {charmap.ISO8859_5, "iso-8859-5"}, "iso-ir-144": {charmap.ISO8859_5, "iso-8859-5"}, "iso8859-5": {charmap.ISO8859_5, "iso-8859-5"}, "iso88595": {charmap.ISO8859_5, "iso-8859-5"}, "iso_8859-5": {charmap.ISO8859_5, "iso-8859-5"}, "iso_8859-5:1988": {charmap.ISO8859_5, "iso-8859-5"}, "arabic": {charmap.ISO8859_6, "iso-8859-6"}, "asmo-708": {charmap.ISO8859_6, "iso-8859-6"}, "csiso88596e": {charmap.ISO8859_6, "iso-8859-6"}, "csiso88596i": {charmap.ISO8859_6, "iso-8859-6"}, "csisolatinarabic": {charmap.ISO8859_6, "iso-8859-6"}, "ecma-114": {charmap.ISO8859_6, "iso-8859-6"}, "iso-8859-6": {charmap.ISO8859_6, "iso-8859-6"}, "iso-8859-6-e": {charmap.ISO8859_6, "iso-8859-6"}, "iso-8859-6-i": {charmap.ISO8859_6, "iso-8859-6"}, "iso-ir-127": {charmap.ISO8859_6, "iso-8859-6"}, "iso8859-6": {charmap.ISO8859_6, "iso-8859-6"}, "iso88596": {charmap.ISO8859_6, "iso-8859-6"}, "iso_8859-6": {charmap.ISO8859_6, "iso-8859-6"}, "iso_8859-6:1987": {charmap.ISO8859_6, "iso-8859-6"}, "csisolatingreek": {charmap.ISO8859_7, "iso-8859-7"}, "ecma-118": {charmap.ISO8859_7, "iso-8859-7"}, "elot_928": {charmap.ISO8859_7, "iso-8859-7"}, "greek": {charmap.ISO8859_7, "iso-8859-7"}, "greek8": {charmap.ISO8859_7, "iso-8859-7"}, "iso-8859-7": {charmap.ISO8859_7, "iso-8859-7"}, "iso-ir-126": {charmap.ISO8859_7, "iso-8859-7"}, "iso8859-7": {charmap.ISO8859_7, "iso-8859-7"}, "iso88597": {charmap.ISO8859_7, "iso-8859-7"}, "iso_8859-7": {charmap.ISO8859_7, "iso-8859-7"}, "iso_8859-7:1987": {charmap.ISO8859_7, "iso-8859-7"}, "sun_eu_greek": {charmap.ISO8859_7, "iso-8859-7"}, "csiso88598e": {charmap.ISO8859_8, "iso-8859-8"}, "csisolatinhebrew": {charmap.ISO8859_8, "iso-8859-8"}, "hebrew": {charmap.ISO8859_8, "iso-8859-8"}, "iso-8859-8": {charmap.ISO8859_8, "iso-8859-8"}, "iso-8859-8-e": {charmap.ISO8859_8, "iso-8859-8"}, "iso-ir-138": {charmap.ISO8859_8, "iso-8859-8"}, "iso8859-8": {charmap.ISO8859_8, "iso-8859-8"}, "iso88598": {charmap.ISO8859_8, "iso-8859-8"}, "iso_8859-8": {charmap.ISO8859_8, "iso-8859-8"}, "iso_8859-8:1988": {charmap.ISO8859_8, "iso-8859-8"}, "visual": {charmap.ISO8859_8, "iso-8859-8"}, "csiso88598i": {charmap.ISO8859_8, "iso-8859-8-i"}, "iso-8859-8-i": {charmap.ISO8859_8, "iso-8859-8-i"}, "logical": {charmap.ISO8859_8, "iso-8859-8-i"}, "csisolatin6": {charmap.ISO8859_10, "iso-8859-10"}, "iso-8859-10": {charmap.ISO8859_10, "iso-8859-10"}, "iso-ir-157": {charmap.ISO8859_10, "iso-8859-10"}, "iso8859-10": {charmap.ISO8859_10, "iso-8859-10"}, "iso885910": {charmap.ISO8859_10, "iso-8859-10"}, "l6": {charmap.ISO8859_10, "iso-8859-10"}, "latin6": {charmap.ISO8859_10, "iso-8859-10"}, "iso-8859-13": {charmap.ISO8859_13, "iso-8859-13"}, "iso8859-13": {charmap.ISO8859_13, "iso-8859-13"}, "iso885913": {charmap.ISO8859_13, "iso-8859-13"}, "iso-8859-14": {charmap.ISO8859_14, "iso-8859-14"}, "iso8859-14": {charmap.ISO8859_14, "iso-8859-14"}, "iso885914": {charmap.ISO8859_14, "iso-8859-14"}, "csisolatin9": {charmap.ISO8859_15, "iso-8859-15"}, "iso-8859-15": {charmap.ISO8859_15, "iso-8859-15"}, "iso8859-15": {charmap.ISO8859_15, "iso-8859-15"}, "iso885915": {charmap.ISO8859_15, "iso-8859-15"}, "iso_8859-15": {charmap.ISO8859_15, "iso-8859-15"}, "l9": {charmap.ISO8859_15, "iso-8859-15"}, "iso-8859-16": {charmap.ISO8859_16, "iso-8859-16"}, "cskoi8r": {charmap.KOI8R, "koi8-r"}, "koi": {charmap.KOI8R, "koi8-r"}, "koi8": {charmap.KOI8R, "koi8-r"}, "koi8-r": {charmap.KOI8R, "koi8-r"}, "koi8_r": {charmap.KOI8R, "koi8-r"}, "koi8-u": {charmap.KOI8U, "koi8-u"}, "csmacintosh": {charmap.Macintosh, "macintosh"}, "mac": {charmap.Macintosh, "macintosh"}, "macintosh": {charmap.Macintosh, "macintosh"}, "x-mac-roman": {charmap.Macintosh, "macintosh"}, "dos-874": {charmap.Windows874, "windows-874"}, "iso-8859-11": {charmap.Windows874, "windows-874"}, "iso8859-11": {charmap.Windows874, "windows-874"}, "iso885911": {charmap.Windows874, "windows-874"}, "tis-620": {charmap.Windows874, "windows-874"}, "windows-874": {charmap.Windows874, "windows-874"}, "cp1250": {charmap.Windows1250, "windows-1250"}, "windows-1250": {charmap.Windows1250, "windows-1250"}, "x-cp1250": {charmap.Windows1250, "windows-1250"}, "cp1251": {charmap.Windows1251, "windows-1251"}, "windows-1251": {charmap.Windows1251, "windows-1251"}, "x-cp1251": {charmap.Windows1251, "windows-1251"}, "ansi_x3.4-1968": {charmap.Windows1252, "windows-1252"}, "ascii": {charmap.Windows1252, "windows-1252"}, "cp1252": {charmap.Windows1252, "windows-1252"}, "cp819": {charmap.Windows1252, "windows-1252"}, "csisolatin1": {charmap.Windows1252, "windows-1252"}, "ibm819": {charmap.Windows1252, "windows-1252"}, "iso-8859-1": {charmap.ISO8859_1, "iso-8859-1"}, "iso-ir-100": {charmap.Windows1252, "windows-1252"}, "iso8859-1": {charmap.ISO8859_1, "iso-8859-1"}, "iso8859_1": {charmap.ISO8859_1, "iso-8859-1"}, "iso88591": {charmap.ISO8859_1, "iso-8859-1"}, "iso_8859-1": {charmap.ISO8859_1, "iso-8859-1"}, "iso_8859-1:1987": {charmap.ISO8859_1, "iso-8859-1"}, "l1": {charmap.Windows1252, "windows-1252"}, "latin1": {charmap.Windows1252, "windows-1252"}, "us-ascii": {charmap.Windows1252, "windows-1252"}, "windows-1252": {charmap.Windows1252, "windows-1252"}, "x-cp1252": {charmap.Windows1252, "windows-1252"}, "cp1253": {charmap.Windows1253, "windows-1253"}, "windows-1253": {charmap.Windows1253, "windows-1253"}, "x-cp1253": {charmap.Windows1253, "windows-1253"}, "cp1254": {charmap.Windows1254, "windows-1254"}, "csisolatin5": {charmap.Windows1254, "windows-1254"}, "iso-8859-9": {charmap.Windows1254, "windows-1254"}, "iso-ir-148": {charmap.Windows1254, "windows-1254"}, "iso8859-9": {charmap.Windows1254, "windows-1254"}, "iso88599": {charmap.Windows1254, "windows-1254"}, "iso_8859-9": {charmap.Windows1254, "windows-1254"}, "iso_8859-9:1989": {charmap.Windows1254, "windows-1254"}, "l5": {charmap.Windows1254, "windows-1254"}, "latin5": {charmap.Windows1254, "windows-1254"}, "windows-1254": {charmap.Windows1254, "windows-1254"}, "x-cp1254": {charmap.Windows1254, "windows-1254"}, "cp1255": {charmap.Windows1255, "windows-1255"}, "windows-1255": {charmap.Windows1255, "windows-1255"}, "x-cp1255": {charmap.Windows1255, "windows-1255"}, "cp1256": {charmap.Windows1256, "windows-1256"}, "windows-1256": {charmap.Windows1256, "windows-1256"}, "x-cp1256": {charmap.Windows1256, "windows-1256"}, "cp1257": {charmap.Windows1257, "windows-1257"}, "windows-1257": {charmap.Windows1257, "windows-1257"}, "x-cp1257": {charmap.Windows1257, "windows-1257"}, "cp1258": {charmap.Windows1258, "windows-1258"}, "windows-1258": {charmap.Windows1258, "windows-1258"}, "x-cp1258": {charmap.Windows1258, "windows-1258"}, "x-mac-cyrillic": {charmap.MacintoshCyrillic, "x-mac-cyrillic"}, "x-mac-ukrainian": {charmap.MacintoshCyrillic, "x-mac-cyrillic"}, "chinese": {simplifiedchinese.GBK, "gbk"}, "csgb2312": {simplifiedchinese.GBK, "gbk"}, "csiso58gb231280": {simplifiedchinese.GBK, "gbk"}, "gb2312": {simplifiedchinese.GBK, "gbk"}, "gb_2312": {simplifiedchinese.GBK, "gbk"}, "gb_2312-80": {simplifiedchinese.GBK, "gbk"}, "gbk": {simplifiedchinese.GBK, "gbk"}, "iso-ir-58": {simplifiedchinese.GBK, "gbk"}, "x-gbk": {simplifiedchinese.GBK, "gbk"}, "gb18030": {simplifiedchinese.GB18030, "gb18030"}, "hz-gb-2312": {simplifiedchinese.HZGB2312, "hz-gb-2312"}, "big5": {traditionalchinese.Big5, "big5"}, "big5-hkscs": {traditionalchinese.Big5, "big5"}, "cn-big5": {traditionalchinese.Big5, "big5"}, "csbig5": {traditionalchinese.Big5, "big5"}, "x-x-big5": {traditionalchinese.Big5, "big5"}, "cseucpkdfmtjapanese": {japanese.EUCJP, "euc-jp"}, "euc-jp": {japanese.EUCJP, "euc-jp"}, "x-euc-jp": {japanese.EUCJP, "euc-jp"}, "csiso2022jp": {japanese.ISO2022JP, "iso-2022-jp"}, "iso-2022-jp": {japanese.ISO2022JP, "iso-2022-jp"}, "csshiftjis": {japanese.ShiftJIS, "shift_jis"}, "ms_kanji": {japanese.ShiftJIS, "shift_jis"}, "shift-jis": {japanese.ShiftJIS, "shift_jis"}, "shift_jis": {japanese.ShiftJIS, "shift_jis"}, "sjis": {japanese.ShiftJIS, "shift_jis"}, "windows-31j": {japanese.ShiftJIS, "shift_jis"}, "x-sjis": {japanese.ShiftJIS, "shift_jis"}, "cseuckr": {korean.EUCKR, "euc-kr"}, "csksc56011987": {korean.EUCKR, "euc-kr"}, "euc-kr": {korean.EUCKR, "euc-kr"}, "iso-ir-149": {korean.EUCKR, "euc-kr"}, "korean": {korean.EUCKR, "euc-kr"}, "ks_c_5601-1987": {korean.EUCKR, "euc-kr"}, "ks_c_5601-1989": {korean.EUCKR, "euc-kr"}, "ksc5601": {korean.EUCKR, "euc-kr"}, "ksc_5601": {korean.EUCKR, "euc-kr"}, "windows-949": {korean.EUCKR, "euc-kr"}, "csiso2022kr": {encoding.Replacement, "replacement"}, "iso-2022-kr": {encoding.Replacement, "replacement"}, "iso-2022-cn": {encoding.Replacement, "replacement"}, "iso-2022-cn-ext": {encoding.Replacement, "replacement"}, "utf-16be": {unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM), "utf-16be"}, "utf-16": {unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM), "utf-16le"}, "utf-16le": {unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM), "utf-16le"}, "x-user-defined": {charmap.XUserDefined, "x-user-defined"}, "iso646-us": {charmap.Windows1252, "windows-1252"}, // ISO646 isn't us-ascii but 1991 version is. "iso: western": {charmap.Windows1252, "windows-1252"}, // same as iso-8859-1 "we8iso8859p1": {charmap.Windows1252, "windows-1252"}, // same as iso-8859-1 "cp936": {simplifiedchinese.GBK, "gbk"}, // same as gb2312 "cp850": {charmap.CodePage850, "cp850"}, "cp-850": {charmap.CodePage850, "cp850"}, "ibm850": {charmap.CodePage850, "cp850"}, "136": {traditionalchinese.Big5, "big5"}, // same as chinese big5 "cp932": {japanese.ShiftJIS, "shift_jis"}, "8859-1": {charmap.Windows1252, "windows-1252"}, "8859_1": {charmap.Windows1252, "windows-1252"}, "8859-2": {charmap.ISO8859_2, "iso-8859-2"}, "8859_2": {charmap.ISO8859_2, "iso-8859-2"}, "8859-3": {charmap.ISO8859_3, "iso-8859-3"}, "8859_3": {charmap.ISO8859_3, "iso-8859-3"}, "8859-4": {charmap.ISO8859_4, "iso-8859-4"}, "8859_4": {charmap.ISO8859_4, "iso-8859-4"}, "8859-5": {charmap.ISO8859_5, "iso-8859-5"}, "8859_5": {charmap.ISO8859_5, "iso-8859-5"}, "8859-6": {charmap.ISO8859_6, "iso-8859-6"}, "8859_6": {charmap.ISO8859_6, "iso-8859-6"}, "8859-7": {charmap.ISO8859_7, "iso-8859-7"}, "8859_7": {charmap.ISO8859_7, "iso-8859-7"}, "8859-8": {charmap.ISO8859_8, "iso-8859-8"}, "8859_8": {charmap.ISO8859_8, "iso-8859-8"}, "8859-10": {charmap.ISO8859_10, "iso-8859-10"}, "8859_10": {charmap.ISO8859_10, "iso-8859-10"}, "8859-13": {charmap.ISO8859_13, "iso-8859-13"}, "8859_13": {charmap.ISO8859_13, "iso-8859-13"}, "8859-14": {charmap.ISO8859_14, "iso-8859-14"}, "8859_14": {charmap.ISO8859_14, "iso-8859-14"}, "8859-15": {charmap.ISO8859_15, "iso-8859-15"}, "8859_15": {charmap.ISO8859_15, "iso-8859-15"}, "8859-16": {charmap.ISO8859_16, "iso-8859-16"}, "8859_16": {charmap.ISO8859_16, "iso-8859-16"}, "utf8mb4": {encoding.Nop, "utf-8"}, // emojis, but golang can handle it directly "238": {charmap.Windows1250, "windows-1250"}, } var metaTagCharsetRegexp = regexp.MustCompile( `(?i)[a-zA-Z0-9_.:-]+)\s*"?`) var metaTagCharsetIndex int func init() { // Find the submatch index for charset in metaTagCharsetRegexp for i, name := range metaTagCharsetRegexp.SubexpNames() { if name == "charset" { metaTagCharsetIndex = i break } } } // ConvertToUTF8String uses the provided charset to decode a slice of bytes into a normal // UTF-8 string. func ConvertToUTF8String(charset string, textBytes []byte) (string, error) { csentry, ok := encodings[strings.ToLower(charset)] if !ok { return "", fmt.Errorf("unsupported charset %q", charset) } input := bytes.NewReader(textBytes) reader := transform.NewReader(input, csentry.e.NewDecoder()) output, err := ioutil.ReadAll(reader) if err != nil { return "", err } return string(output), nil } // NewCharsetReader generates charset-conversion readers, converting from the provided charset into // UTF-8. CharsetReader is a factory signature defined by Go's mime.WordDecoder. // // This function is similar to: https://godoc.org/golang.org/x/net/html/charset#NewReaderLabel func NewCharsetReader(charset string, input io.Reader) (io.Reader, error) { if strings.ToLower(charset) == utf8 { return input, nil } csentry, ok := encodings[strings.ToLower(charset)] if !ok { return nil, fmt.Errorf("unsupported charset %q", charset) } return transform.NewReader(input, csentry.e.NewDecoder()), nil } // FindCharsetInHTML looks for charset in the HTML meta tag (v4.01 and v5). func FindCharsetInHTML(html string) string { charsetMatches := metaTagCharsetRegexp.FindAllStringSubmatch(html, -1) if len(charsetMatches) > 0 { return charsetMatches[0][metaTagCharsetIndex] } return "" } enmime-0.9.3/internal/coding/charsets_test.go000066400000000000000000000052441417532643400212740ustar00rootroot00000000000000package coding_test import ( "bytes" "io/ioutil" "strings" "testing" "github.com/jhillyerd/enmime/internal/coding" ) // Test an invalid character set with the CharsetReader func TestInvalidCharsetReader(t *testing.T) { inputReader := strings.NewReader("unused") outputReader, err := coding.NewCharsetReader("INVALIDcharsetZZZ", inputReader) if outputReader != nil { t.Error("outputReader should be nil") } if err == nil { t.Error("err should not be nil") } } // Test some different character sets with the CharsetReader func TestCharsetReader(t *testing.T) { var testTable = []struct { charset string input []byte want string }{ {"utf-8", []byte("abcABC\u2014"), "abcABC\u2014"}, {"windows-1250", []byte{'a', 'Z', 0x96}, "aZ\u2013"}, {"big5", []byte{0xa1, 0x5d, 0xa1, 0x61, 0xa1, 0x71}, "\uff08\uff5b\u3008"}, {"utf-7", []byte("Hello, World+ACE- 1 +- 1 +AD0- 2"), "Hello, World! 1 + 1 = 2"}, } for _, tt := range testTable { inputReader := bytes.NewReader(tt.input) outputReader, err := coding.NewCharsetReader(tt.charset, inputReader) if err != nil { t.Error("err should be nil, got:", err) } result, err := ioutil.ReadAll(outputReader) if err != nil { t.Error("err should be nil, got:", err) } got := string(result) if got != tt.want { t.Errorf("NewCharsetReader(%q, %q) = %q, want: %q", tt.charset, tt.input, got, tt.want) } } } // Search for character set info inside of HTML func TestFindCharsetInHTML(t *testing.T) { var ttable = []struct { input, want string }{ {``, "UTF-8"}, {``, "us-ascii"}, {``, "big5"}, {``, "us-ascii"}, {``, "windows-1250"}, {``, ""}, } for _, tt := range ttable { got := coding.FindCharsetInHTML(tt.input) if got != tt.want { t.Errorf("Got: %q, want: %q, for: %q", got, tt.want, tt.input) } } } func TestConvertToUTF8String(t *testing.T) { var testTable = []struct { charset string input []byte want string }{ {"utf-8", []byte("abcABC\u2014"), "abcABC\u2014"}, {"windows-1250", []byte{'a', 'Z', 0x96}, "aZ\u2013"}, {"big5", []byte{0xa1, 0x5d, 0xa1, 0x61, 0xa1, 0x71}, "\uff08\uff5b\u3008"}, } // Success Conditions for _, v := range testTable { s, err := coding.ConvertToUTF8String(v.charset, v.input) if err != nil { t.Error("UTF-8 conversion failed") } if s != v.want { t.Errorf("Got %s, but wanted %s", s, v.want) } } // Fail for unsupported charset _, err := coding.ConvertToUTF8String("123", []byte("there is no 123 charset")) if err == nil { t.Error("Charset 123 should not exist") } } enmime-0.9.3/internal/coding/headerext.go000066400000000000000000000052231417532643400203670ustar00rootroot00000000000000package coding import ( "fmt" "io" "mime" "strings" ) // DecodeExtHeader decodes a single line (per RFC 2047, aka Message Header Extensions) using Golang's // mime.WordDecoder. func DecodeExtHeader(input string) string { if !strings.Contains(input, "=?") { // Don't scan if there is nothing to do here return input } dec := new(mime.WordDecoder) dec.CharsetReader = NewCharsetReader header, err := dec.DecodeHeader(input) if err != nil { return input } return header } // RFC2047Decode returns a decoded string if the input uses RFC2047 encoding, otherwise it will // return the input. // // RFC2047 Example: `=?UTF-8?B?bmFtZT0iw7DCn8KUwoo=?=` func RFC2047Decode(s string) string { // Convert CR/LF to spaces. s = strings.Map(func(r rune) rune { if r == '\n' || r == '\r' { return ' ' } return r }, s) var err error decoded := false for { s, err = rfc2047Recurse(s) switch err { case nil: decoded = true continue default: if decoded { keyValuePair := strings.SplitAfter(s, "=") if len(keyValuePair) < 2 { return s } // Add quotes as needed. if !strings.HasPrefix(keyValuePair[1], "\"") { keyValuePair[1] = fmt.Sprintf("\"%s", keyValuePair[1]) } if !strings.HasSuffix(keyValuePair[1], "\"") { keyValuePair[1] = fmt.Sprintf("%s\"", keyValuePair[1]) } return strings.Join(keyValuePair, "") } return s } } } // rfc2047Recurse is called for if the value contains content encoded in RFC2047 format and decodes // it. func rfc2047Recurse(s string) (string, error) { us := strings.ToUpper(s) if !strings.Contains(us, "?Q?") && !strings.Contains(us, "?B?") { return s, io.EOF } var val string if val = DecodeExtHeader(s); val == s { if val = DecodeExtHeader(fixRFC2047String(val)); val == s { return val, io.EOF } } return val, nil } // fixRFC2047String removes the following characters from charset and encoding segments of an // RFC2047 string: '\n', '\r' and ' ' func fixRFC2047String(s string) string { inString := false isWithinTerminatingEqualSigns := false questionMarkCount := 0 sb := &strings.Builder{} for _, v := range s { switch v { case '=': if questionMarkCount == 3 { inString = false } else { isWithinTerminatingEqualSigns = true } sb.WriteRune(v) case '?': if isWithinTerminatingEqualSigns { inString = true } else { questionMarkCount++ } isWithinTerminatingEqualSigns = false sb.WriteRune(v) case '\n', '\r', ' ': if !inString { sb.WriteRune(v) } isWithinTerminatingEqualSigns = false default: isWithinTerminatingEqualSigns = false sb.WriteRune(v) } } return sb.String() } enmime-0.9.3/internal/coding/headerext_test.go000066400000000000000000000075261417532643400214360ustar00rootroot00000000000000package coding import ( "testing" ) func TestDecodePlainPassthrough(t *testing.T) { var ttable = []string{ "Test", "Testing One two 3 4", } for _, in := range ttable { t.Run(in, func(t *testing.T) { got := DecodeExtHeader(in) if got != in { t.Errorf("DecodeExtHeader(%q) == %q, want: %q", in, got, in) } }) } } func TestDecodeFailurePassthrough(t *testing.T) { var ttable = []struct { label, in string }{ { label: "Newline detection & abort", in: "=?US\nASCII?Q?Keith_Moore?=", }, { label: "Carriage return detection & abort", in: "=?US-ASCII?\r?Keith_Moore?=", }, { label: "Invalid termination", in: "=?US-ASCII?Q?Keith_Moore?!", }, } for _, tt := range ttable { t.Run(tt.in, func(t *testing.T) { got := DecodeExtHeader(tt.in) if got != tt.in { t.Errorf("DecodeExtHeader(%q) == %q, want: %q", tt.in, got, tt.in) } }) } } func TestDecodeAsciiB64(t *testing.T) { var ttable = []struct { in, want string }{ // Simple ASCII quoted-printable encoded word {"=?US-ASCII?B?SGVsbG8gV29ybGQ=?=", "Hello World"}, // Abutting a MIME header comment is legal {"(=?US-ASCII?B?SGVsbG8gV29ybGQ=?=)", "(Hello World)"}, // The entire header does not need to be encoded {"(Prefix =?US-ASCII?B?SGVsbG8gV29ybGQ=?=)", "(Prefix Hello World)"}, } for _, tt := range ttable { t.Run(tt.in, func(t *testing.T) { got := DecodeExtHeader(tt.in) if got != tt.want { t.Errorf("DecodeExtHeader(%q) == %q, want: %q", tt.in, got, tt.want) } }) } } func TestDecodeAsciiQ(t *testing.T) { var ttable = []struct { in, want string }{ // Simple ASCII QP encoded word {"=?US-ASCII?Q?Keith_Moore?=", "Keith Moore"}, // Abutting a MIME header comment is legal {"(=?US-ASCII?Q?Keith_Moore?=)", "(Keith Moore)"}, // The entire header does not need to be encoded {"(Keith =?US-ASCII?Q?Moore?=)", "(Keith Moore)"}, } for _, tt := range ttable { t.Run(tt.in, func(t *testing.T) { got := DecodeExtHeader(tt.in) if got != tt.want { t.Errorf("DecodeExtHeader(%q) == %q, want: %q", tt.in, got, tt.want) } }) } } // Spacing rules from RFC 2047 func TestDecodeSpacing(t *testing.T) { var ttable = []struct { in, want string }{ {"(=?ISO-8859-1?Q?a?=)", "(a)"}, {"(=?ISO-8859-1?Q?a?= b)", "(a b)"}, {"(=?ISO-8859-1?Q?a?= =?ISO-8859-1?Q?b?=)", "(ab)"}, {"(=?ISO-8859-1?Q?a?= =?ISO-8859-1?Q?b?=)", "(ab)"}, {"(=?ISO-8859-1?Q?a?=\r\n =?ISO-8859-1?Q?b?=)", "(ab)"}, {"(=?ISO-8859-1?Q?a_b?=)", "(a b)"}, {"(=?ISO-8859-1?Q?a?= =?ISO-8859-2?Q?_b?=)", "(a b)"}, } for _, tt := range ttable { t.Run(tt.in, func(t *testing.T) { got := DecodeExtHeader(tt.in) if got != tt.want { t.Errorf("DecodeExtHeader(%q) == %q, want: %q", tt.in, got, tt.want) } }) } } // Test some different character sets func TestDecodeCharsets(t *testing.T) { var ttable = []struct { in, want string }{ {"=?utf-8?q?abcABC_=24_=c2=a2_=e2=82=ac?=", "abcABC $ \u00a2 \u20ac"}, {"=?iso-8859-1?q?#=a3_c=a9_r=ae_u=b5?=", "#\u00a3 c\u00a9 r\u00ae u\u00b5"}, {"=?big5?q?=a1=5d_=a1=61_=a1=71?=", "\uff08 \uff5b \u3008"}, } for _, tt := range ttable { t.Run(tt.in, func(t *testing.T) { got := DecodeExtHeader(tt.in) if got != tt.want { t.Errorf("DecodeExtHeader(%q) == %q, want: %q", tt.in, got, tt.want) } }) } } func TestRfc2047Decode(t *testing.T) { var ttable = []struct { label, in, want string }{ {"pass through", "plain text", "plain text"}, {"quoted", "=?US-ASCII?q?Hello=20World?=", "Hello World"}, {"base64", "=?US-ASCII?b?SGVsbG8gV29ybGQ=?=", "Hello World"}, {"nested qp+b64", "=?utf-8?b?PT9VUy1BU0NJST9xP0hlbGxvPTIwV29ybGQ/PQ==?=", "Hello World"}, } for _, tt := range ttable { t.Run(tt.label, func(t *testing.T) { got := RFC2047Decode(tt.in) if got != tt.want { t.Errorf("RFC2047Decode(%q) == %q, want: %q", tt.in, got, tt.want) } }) } } enmime-0.9.3/internal/coding/idheader.go000066400000000000000000000011401417532643400201550ustar00rootroot00000000000000package coding import ( "net/url" "strings" ) // FromIDHeader decodes a Content-ID or Message-ID header value (RFC 2392) into a utf-8 string. // Example: "" becomes "foo?bar baz". func FromIDHeader(v string) string { if v == "" { return v } v = strings.TrimLeft(v, "<") v = strings.TrimRight(v, ">") if r, err := url.QueryUnescape(v); err == nil { v = r } return v } // ToIDHeader encodes a Content-ID or Message-ID header value (RFC 2392) from a utf-8 string. func ToIDHeader(v string) string { v = url.QueryEscape(v) return "<" + strings.Replace(v, "%40", "@", -1) + ">" } enmime-0.9.3/internal/coding/idheader_test.go000066400000000000000000000016431417532643400212240ustar00rootroot00000000000000package coding_test import ( "testing" "github.com/jhillyerd/enmime/internal/coding" ) func TestFromIDHeader(t *testing.T) { testCases := []struct { input, want string }{ {"", ""}, {"<>", ""}, {"<%🤯>", "%🤯"}, {"", "foo@inbucket.org"}, {"", "foo%bar"}, {"foo+bar", "foo bar"}, } for _, tc := range testCases { t.Run(tc.input, func(t *testing.T) { got := coding.FromIDHeader(tc.input) if got != tc.want { t.Errorf("got %q, want %q", got, tc.want) } }) } } func TestToIDHeader(t *testing.T) { testCases := []struct { input, want string }{ {"", "<>"}, {"foo@inbucket.org", ""}, {"foo%bar", ""}, {"foo bar", ""}, } for _, tc := range testCases { t.Run(tc.input, func(t *testing.T) { got := coding.ToIDHeader(tc.input) if got != tc.want { t.Errorf("got %q, want %q", got, tc.want) } }) } } enmime-0.9.3/internal/coding/quotedprint.go000066400000000000000000000070101417532643400207700ustar00rootroot00000000000000package coding import ( "bufio" "fmt" "io" ) // QPCleaner scans quoted printable content for invalid characters and encodes them so that // Go's quoted-printable decoder does not abort with an error. type QPCleaner struct { in *bufio.Reader overflow []byte lineLen int } // MaxQPLineLen is the maximum line length we allow before inserting `=\r\n`. Prevents buffer // overflows in mime/quotedprintable.Reader. const MaxQPLineLen = 1024 var ( _ io.Reader = &QPCleaner{} // Assert QPCleaner implements io.Reader. escapedEquals = []byte("=3D") // QP encoded value of an equals sign. lineBreak = []byte("=\r\n") ) // NewQPCleaner returns a QPCleaner for the specified reader. func NewQPCleaner(r io.Reader) *QPCleaner { return &QPCleaner{ in: bufio.NewReader(r), overflow: nil, lineLen: 0, } } // Read method for io.Reader interface. func (qp *QPCleaner) Read(dest []byte) (n int, err error) { destLen := len(dest) if len(qp.overflow) > 0 { // Copy bytes that didn't fit into dest buffer during previous read. n = copy(dest, qp.overflow) qp.overflow = qp.overflow[n:] } // writeByte outputs a single byte, space for which will have already been ensured by the loop // condition. Updates counters. writeByte := func(in byte) { dest[n] = in n++ qp.lineLen++ } // writeBytes outputs multiple bytes, storing overflow for next read. Updates counters. writeBytes := func(in []byte) { nc := copy(dest[n:], in) if nc < len(in) { // Stash unwritten bytes into overflow. qp.overflow = append(qp.overflow, []byte(in[nc:])...) } n += nc qp.lineLen += len(in) } // ensureLineLen ensures there is room to write `requested` bytes, preventing a line break being // inserted in the middle of the escaped string. The requested count is in addition to the // byte that was already reserved for this loop iteration. ensureLineLen := func(requested int) { if qp.lineLen+requested >= MaxQPLineLen { writeBytes(lineBreak) qp.lineLen = 0 } } // Loop over bytes in qp.in ByteReader while there is space in dest. for n < destLen { var b byte b, err = qp.in.ReadByte() if err != nil { return n, err } if qp.lineLen >= MaxQPLineLen { writeBytes(lineBreak) qp.lineLen = 0 if n == destLen { break } } switch { // Pass valid hex bytes through, otherwise escapes the equals symbol. case b == '=': ensureLineLen(2) var hexBytes []byte hexBytes, err = qp.in.Peek(2) if err != nil && err != io.EOF { return 0, err } if validHexBytes(hexBytes) { dest[n] = b n++ } else { writeBytes(escapedEquals) } // Valid special character. case b == '\t': writeByte(b) // Valid special characters that reset line length. case b == '\r' || b == '\n': writeByte(b) qp.lineLen = 0 // Invalid characters, render as quoted-printable. case b < ' ' || '~' < b: ensureLineLen(2) writeBytes([]byte(fmt.Sprintf("=%02X", b))) // Acceptable characters. default: writeByte(b) } } return n, err } func validHexByte(b byte) bool { return '0' <= b && b <= '9' || 'A' <= b && b <= 'F' || 'a' <= b && b <= 'f' } // validHexBytes returns true if this byte sequence represents a valid quoted-printable escape // sequence or line break, minus the initial equals sign. func validHexBytes(v []byte) bool { if len(v) > 0 && v[0] == '\n' { // Soft line break. return true } if len(v) < 2 { return false } if v[0] == '\r' && v[1] == '\n' { // Soft line break. return true } return validHexByte(v[0]) && validHexByte(v[1]) } enmime-0.9.3/internal/coding/quotedprint_test.go000066400000000000000000000105331417532643400220330ustar00rootroot00000000000000package coding_test import ( "bytes" "errors" "fmt" "io" "io/ioutil" "strings" "testing" "github.com/jhillyerd/enmime/internal/coding" ) func TestQPCleaner(t *testing.T) { ttable := []struct { input string want string }{ {"", ""}, {"abcDEF_", "abcDEF_"}, {"=5bSlack=5d", "=5bSlack=5d"}, {"low: ,high:~", "low: ,high:~"}, {"\r\n\t", "\r\n\t"}, {"pédagogues", "p=C3=A9dagogues"}, {"Stuffs’s", "Stuffs=E2=80=99s"}, {"=", "=3D"}, {"=a", "=3Da"}, } for _, tc := range ttable { // Run cleaner cleaner := coding.NewQPCleaner(strings.NewReader(tc.input)) buf := new(bytes.Buffer) _, err := buf.ReadFrom(cleaner) if err != nil { t.Fatal(err) } got := buf.String() if got != tc.want { t.Errorf("Got: %q, want: %q", got, tc.want) } } } // TestQPCleanerOverflow attempts to confuse the cleaner by issuing smaller subsequent reads. func TestQPCleanerOverflow(t *testing.T) { input := bytes.Repeat([]byte("pédagogues =\r\n"), 1000) want := bytes.Repeat([]byte("p=C3=A9dagogues =\r\n"), 1000) inbuf := bytes.NewBuffer(input) qp := coding.NewQPCleaner(inbuf) offset := 0 for len := 1000; len > 0; len -= 100 { p := make([]byte, len) n, err := qp.Read(p) if err != nil { t.Fatal(err) } if n < 1 { t.Fatalf("Read(p) = %v, wanted >0, at want[%v]", n, offset) } for i := 0; i < n; i++ { if p[i] != want[offset] { t.Errorf("p[%v] = %q, want: %q (want[%v])", i, p[i], want[offset], offset) } offset++ } } } // TestQPCleanerSmallDest repeatedly calls Read with a small destination buffer. func TestQPCleanerSmallDest(t *testing.T) { input := bytes.Repeat([]byte("pédagogues =z =\r\n"), 100) want := bytes.Repeat([]byte("p=C3=A9dagogues =3Dz =\r\n"), 100) for bufSize := 5; bufSize > 0; bufSize-- { t.Run(fmt.Sprintf("%v byte buffer", bufSize), func(t *testing.T) { inbuf := bytes.NewBuffer(input) qp := coding.NewQPCleaner(inbuf) offset := 0 p := make([]byte, bufSize) for { n, err := qp.Read(p) if err != nil && err != io.EOF { t.Fatal(err) } if n < 1 && offset < len(want) { t.Fatalf("Read(p) = %v, wanted >0, at want[%v]", n, offset) } for i := 0; i < n; i++ { if p[i] != want[offset] { t.Errorf("p[%v] = %q, want: %q (want[%v])", i, p[i], want[offset], offset) } offset++ } if err == io.EOF { break } } }) } } // TestQPCleanerLineBreak verifies QPCleaner breaks long lines correctly. func TestQPCleanerLineBreak(t *testing.T) { input := bytes.Repeat([]byte("pédagogues =z "), 10000) inbuf := bytes.NewBuffer(input) qp := coding.NewQPCleaner(inbuf) output, err := ioutil.ReadAll(qp) if err != nil { t.Fatal(err) } want := 1024 // Desired wrapping point. tolerance := 3 if len(output) < want { t.Fatalf("wanted minimum output len %v, got %v", want, len(output)) } // Examine each line of output long enough to wrap. for i := 0; len(output) > want; i++ { got := bytes.Index(output, []byte("=\r\n")) // Wrapping a few characters early is OK, but not late. if got > want || want-got > tolerance { t.Errorf("iteration %v: got line break at %v, wanted %v +/- %v", i, got, want, tolerance) } if got == 0 { break } output = output[got+3:] // Extend past =\r\n } } func TestQPCleanerLineBreakBufferFull(t *testing.T) { input := bytes.Repeat([]byte("abc"), 10000) inbuf := bytes.NewBuffer(input) qp := coding.NewQPCleaner(inbuf) dest := make([]byte, 1025) n, err := qp.Read(dest) if err != nil { t.Fatal(err) } if n != 1025 { t.Errorf("Unexpected result length: %d", n) } } var ErrPeek = errors.New("enmime test peek error") type peekBreakReader string // Read always returns a ErrPeek func (r peekBreakReader) Read(p []byte) (int, error) { return copy(p, r), ErrPeek } func TestQPPeekError(t *testing.T) { qp := coding.NewQPCleaner(peekBreakReader("=a")) buf := make([]byte, 100) _, err := qp.Read(buf) if err != ErrPeek { t.Errorf("Got: %q, want: %q", err, ErrPeek) } } var result int func BenchmarkQPCleaner(b *testing.B) { b.StopTimer() input := bytes.Repeat([]byte("pédagogues\t =zz =\r\n"), b.N) b.SetBytes(int64(len(input))) inbuf := bytes.NewBuffer(input) qp := coding.NewQPCleaner(inbuf) p := make([]byte, 1024) b.StartTimer() for { n, err := qp.Read(p) result += n if err == io.EOF { break } if err != nil { b.Fatalf("Read(): %v", err) } } } enmime-0.9.3/internal/stringutil/000077500000000000000000000000001417532643400170265ustar00rootroot00000000000000enmime-0.9.3/internal/stringutil/addr.go000066400000000000000000000006201417532643400202650ustar00rootroot00000000000000package stringutil import ( "bytes" "net/mail" ) // JoinAddress formats a slice of Address structs such that they can be used in a To or Cc header. func JoinAddress(addrs []mail.Address) string { if len(addrs) == 0 { return "" } buf := &bytes.Buffer{} for i, a := range addrs { if i > 0 { _, _ = buf.WriteString(", ") } _, _ = buf.WriteString(a.String()) } return buf.String() } enmime-0.9.3/internal/stringutil/addr_test.go000066400000000000000000000021271417532643400213300ustar00rootroot00000000000000package stringutil_test import ( "net/mail" "testing" "github.com/jhillyerd/enmime/internal/stringutil" ) func TestJoinAddressEmpty(t *testing.T) { got := stringutil.JoinAddress(make([]mail.Address, 0)) if got != "" { t.Errorf("Empty list got: %q, wanted empty string", got) } } func TestJoinAddressSingle(t *testing.T) { input := []mail.Address{ {Name: "", Address: "one@bar.com"}, } want := "" got := stringutil.JoinAddress(input) if got != want { t.Errorf("got: %q, want: %q", got, want) } input = []mail.Address{ {Name: "one name", Address: "one@bar.com"}, } want = `"one name" ` got = stringutil.JoinAddress(input) if got != want { t.Errorf("got: %q, want: %q", got, want) } } func TestJoinAddressMany(t *testing.T) { input := []mail.Address{ {Name: "one", Address: "one@bar.com"}, {Name: "", Address: "two@foo.com"}, {Name: "three", Address: "three@baz.com"}, } want := `"one" , , "three" ` got := stringutil.JoinAddress(input) if got != want { t.Errorf("got: %q, want: %q", got, want) } } enmime-0.9.3/internal/stringutil/split.go000066400000000000000000000015031417532643400205070ustar00rootroot00000000000000package stringutil const escape = '\\' // SplitQuoted splits a string, ignoring separators present inside of quoted runs. Separators // cannot be escaped outside of quoted runs, the escaping will be ignored. // // Quotes are preserved in result, but the separators are removed. func SplitQuoted(s string, sep rune, quote rune) []string { a := make([]string, 0, 8) quoted := false escaped := false p := 0 for i, c := range s { if c == escape { // Escape can escape itself. escaped = !escaped continue } if c == quote { quoted = !quoted continue } escaped = false if !quoted && c == sep { a = append(a, s[p:i]) p = i + 1 } } if quoted && quote != 0 { // s contained an unterminated quoted-run, re-split without quoting. return SplitQuoted(s, sep, rune(0)) } return append(a, s[p:]) } enmime-0.9.3/internal/stringutil/split_test.go000066400000000000000000000032001417532643400215420ustar00rootroot00000000000000package stringutil import ( "testing" ) func TestSplitQuoted(t *testing.T) { testCases := []struct { input string want []string }{ // All tests split on ; and treat " as quoting character. { input: ``, want: []string{``}, }, { input: `;`, want: []string{``, ``}, }, { input: `"`, want: []string{`"`}, }, { input: `a;b`, want: []string{`a`, `b`}, }, { input: `a;b;`, want: []string{`a`, `b`, ``}, }, { input: `a;b;c`, want: []string{`a`, `b`, `c`}, }, { // Separators are ignored within quoted-runs. input: `a;"b;c";d`, want: []string{`a`, `"b;c"`, `d`}, }, { // Unterminated quoted-run will cause quotes to be ignored from the start of the string. input: `"a;b;c;d`, want: []string{`"a`, `b`, `c`, `d`}, }, { // Unterminated quoted-run will cause quotes to be ignored from the start of the string. input: `"a;b";"c;d`, want: []string{`"a`, `b"`, `"c`, `d`}, }, { // Quotes must be escaped via RFC2047 encoding, not just a backslash. // b through c below must not be treated as a single quoted-run. input: `a;"b\";\"c";d`, want: []string{`a`, `"b\"`, `\"c"`, `d`}, }, { input: `a;b\;c`, want: []string{`a`, `b\`, `c`}, }, } for _, tc := range testCases { t.Run(tc.input, func(t *testing.T) { got := SplitQuoted(tc.input, ';', '"') t.Logf("\ngot : %q\nwant: %q\n", got, tc.want) if len(got) != len(tc.want) { t.Errorf("got len %v, want len %v", len(got), len(tc.want)) return } for i, g := range got { if g != tc.want[i] { t.Errorf("Element %v differs", i) } } }) } } enmime-0.9.3/internal/stringutil/uuid.go000066400000000000000000000010771417532643400203300ustar00rootroot00000000000000package stringutil import ( "fmt" "math/rand" "sync" "time" ) var uuidRand = rand.New(rand.NewSource(time.Now().UnixNano())) var uuidMutex = &sync.Mutex{} // UUID generates a random UUID according to RFC 4122. func UUID() string { uuid := make([]byte, 16) uuidMutex.Lock() _, _ = uuidRand.Read(uuid) uuidMutex.Unlock() // variant bits; see section 4.1.1 uuid[8] = uuid[8]&^0xc0 | 0x80 // version 4 (pseudo-random); see section 4.1.3 uuid[6] = uuid[6]&^0xf0 | 0x40 return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:]) } enmime-0.9.3/internal/stringutil/uuid_test.go000066400000000000000000000004761417532643400213710ustar00rootroot00000000000000package stringutil_test import ( "testing" "github.com/jhillyerd/enmime/internal/stringutil" ) func TestUUID(t *testing.T) { id1 := stringutil.UUID() id2 := stringutil.UUID() if id1 == id2 { t.Errorf("Random UUID should not equal another random UUID") t.Logf("id1: %q", id1) t.Logf("id2: %q", id2) } } enmime-0.9.3/internal/stringutil/wrap.go000066400000000000000000000015231417532643400203270ustar00rootroot00000000000000package stringutil // Wrap builds a byte slice from strs, wrapping on word boundaries before max chars func Wrap(max int, strs ...string) []byte { input := make([]byte, 0) output := make([]byte, 0) for _, s := range strs { input = append(input, []byte(s)...) } if len(input) < max { // Doesn't need to be wrapped return input } ls := -1 // Last seen space index lw := -1 // Last written byte index ll := 0 // Length of current line for i := 0; i < len(input); i++ { ll++ switch input[i] { case ' ', '\t': ls = i } if ll >= max { if ls >= 0 { output = append(output, input[lw+1:ls]...) output = append(output, '\r', '\n', ' ') lw = ls // Jump over the space we broke on ll = 1 // Count leading space above // Rewind i = lw + 1 ls = -1 } } } return append(output, input[lw+1:]...) } enmime-0.9.3/internal/stringutil/wrap_test.go000066400000000000000000000026641417532643400213750ustar00rootroot00000000000000package stringutil_test import ( "testing" "github.com/jhillyerd/enmime/internal/stringutil" ) func TestWrapEmpty(t *testing.T) { b := stringutil.Wrap(80, "") got := string(b) if got != "" { t.Errorf(`got: %q, want: ""`, got) } } func TestWrapIdentityShort(t *testing.T) { want := "short string" b := stringutil.Wrap(15, want) got := string(b) if got != want { t.Errorf("got: %q, want: %q", got, want) } } func TestWrapIdentityLong(t *testing.T) { want := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" b := stringutil.Wrap(5, want) got := string(b) if got != want { t.Errorf("got: %q, want: %q", got, want) } } func TestWrap(t *testing.T) { testCases := []struct { input, want string }{ { "one two three", "one\r\n two\r\n three", }, { "a bb ccc dddd eeeee ffffff", "a bb\r\n ccc\r\n dddd\r\n eeeee\r\n ffffff", }, { "aaaaaa bbbbb cccc ddd ee f", "aaaaaa\r\n bbbbb\r\n cccc\r\n ddd\r\n ee f", }, { "1 3 5 1 3 5 1 3 5", "1 3 5\r\n 1 3 5\r\n 1 3 5", }, { "55555 55555 55555", "55555\r\n 55555\r\n 55555", }, { "666666 666666 666666", "666666\r\n 666666\r\n 666666", }, { "7777777 7777777 7777777", "7777777\r\n 7777777\r\n 7777777", }, } for _, tc := range testCases { t.Run(tc.input, func(t *testing.T) { b := stringutil.Wrap(6, tc.input) got := string(b) if got != tc.want { t.Errorf("got: %q, want: %q", got, tc.want) } }) } } enmime-0.9.3/internal/test/000077500000000000000000000000001417532643400156015ustar00rootroot00000000000000enmime-0.9.3/internal/test/golden.go000066400000000000000000000071711417532643400174060ustar00rootroot00000000000000package test import ( "bytes" "flag" "fmt" "io/ioutil" "os" "path/filepath" "strings" "testing" ) var update = flag.Bool("update", false, "Update .golden files") type section struct { ctype byte s []string } // Inspired by https://github.com/paulgb/simplediff func diff(before, after []string) []section { beforeMap := make(map[string][]int) for i, s := range before { beforeMap[s] = append(beforeMap[s], i) } overlap := make([]int, len(before)) // Track start/len of largest overlapping match in old/new var startBefore, startAfter, subLen int for iafter, s := range after { o := make([]int, len(before)) for _, ibefore := range beforeMap[s] { idx := 1 if ibefore > 0 && overlap[ibefore-1] > 0 { idx = overlap[ibefore-1] + 1 } o[ibefore] = idx if idx > subLen { // largest substring so far, store indices subLen = o[ibefore] startBefore = ibefore - subLen + 1 startAfter = iafter - subLen + 1 } } overlap = o } if subLen == 0 { // No common substring, issue - and + r := make([]section, 0) if len(before) > 0 { r = append(r, section{'-', before}) } if len(after) > 0 { r = append(r, section{'+', after}) } return r } // common substring unchanged, recurse on before/after substring r := diff(before[0:startBefore], after[0:startAfter]) r = append(r, section{' ', after[startAfter : startAfter+subLen]}) r = append(r, diff(before[startBefore+subLen:], after[startAfter+subLen:])...) return r } // DiffStrings does a entry by entry comparison of got and want. func DiffStrings(t *testing.T, got []string, want []string) { t.Helper() if len(got) == 0 && len(want) == 0 { return } sections := diff(want, got) if len(sections) == 1 && sections[0].ctype == ' ' { // Equal return } t.Error("diff -want +got:") for _, s := range sections { if s.ctype == ' ' && len(s.s) > 5 { // Omit excess unchanged lines for i := 0; i < 2; i++ { t.Logf("|%c%s", s.ctype, s.s[i]) } t.Log("...") for i := len(s.s) - 2; i < len(s.s); i++ { t.Logf("|%c%s", s.ctype, s.s[i]) } continue } for _, l := range s.s { t.Logf("|%c%s", s.ctype, l) } } } // DiffLines does a line by line comparison of got and want. func DiffLines(t *testing.T, got []byte, want []byte) { t.Helper() if !bytes.Equal(got, want) { b := bytes.NewBufferString("diff -want +got:\n") glines := strings.Split(string(got), "\n") wlines := strings.Split(string(want), "\n") sections := diff(wlines, glines) for _, s := range sections { if s.ctype == ' ' && len(s.s) > 5 { // Omit excess unchanged lines for i := 0; i < 2; i++ { fmt.Fprintf(b, "|%c%s\n", s.ctype, s.s[i]) } b.WriteString("...\n") for i := len(s.s) - 2; i < len(s.s); i++ { fmt.Fprintf(b, "|%c%s\n", s.ctype, s.s[i]) } continue } for _, l := range s.s { fmt.Fprintf(b, "|%c%s\n", s.ctype, l) } } t.Error(b.String()) } } // DiffGolden does a line by comparison of got to the golden file specified by path. If the update // flag is true, differing golden files will be updated with lines in got. func DiffGolden(t *testing.T, got []byte, path ...string) { t.Helper() pathstr := filepath.Join(path...) f, err := os.Open(pathstr) if err != nil { t.Fatal(err) } golden, err := ioutil.ReadAll(f) if err != nil { t.Fatal(err) } if !bytes.Equal(got, golden) { if *update { // Update golden file if err := ioutil.WriteFile(pathstr, got, 0666); err != nil { t.Fatal(err) } } else { t.Errorf("Test output did not match %s\nTo update golden file, run: go test -update", pathstr) // Fail test with differences DiffLines(t, got, golden) } } } enmime-0.9.3/internal/test/golden_test.go000066400000000000000000000065631417532643400204510ustar00rootroot00000000000000package test import "testing" func TestEqualStrings(t *testing.T) { got := make([]string, 0) want := make([]string, 0) mockt := &testing.T{} DiffStrings(mockt, got, want) if mockt.Failed() { t.Error("Two empty slices should succeed") } got = []string{"foo"} want = []string{"foo"} mockt = &testing.T{} DiffStrings(mockt, got, want) if mockt.Failed() { t.Error("Two equal single line slices should succeed") } got = []string{"foo", "bar", "baz", "enmime", "1", "2", "3", "4", "5", ""} want = []string{"foo", "bar", "baz", "enmime", "1", "2", "3", "4", "5", ""} mockt = &testing.T{} DiffStrings(mockt, got, want) if mockt.Failed() { t.Error("Two equal multiline slices should succeed") } } func TestDifferingStrings(t *testing.T) { got := []string{"foo"} want := []string{"bar"} mockt := &testing.T{} DiffStrings(mockt, got, want) if !mockt.Failed() { t.Error("Two differing single line slices should fail") } got = []string{"foo", "bar", "baz", "enmime", "1", "2", "3", "4", "5", ""} want = []string{"foo", "bar", "baz", "inbucket", "1", "2", "3", "4", "5", ""} mockt = &testing.T{} DiffStrings(mockt, got, want) if !mockt.Failed() { t.Error("Two differing multiline slices should fail") } } func TestEqualLines(t *testing.T) { got := make([]byte, 0) want := make([]byte, 0) mockt := &testing.T{} DiffLines(mockt, got, want) if mockt.Failed() { t.Error("Two empty slices should succeed") } got = []byte("foo\n") want = []byte("foo\n") mockt = &testing.T{} DiffLines(mockt, got, want) if mockt.Failed() { t.Error("Two equal single line slices should succeed") } got = []byte("foo\nbar\nbaz\nenmime\n1\n2\n3\n4\n5\n") want = []byte("foo\nbar\nbaz\nenmime\n1\n2\n3\n4\n5\n") mockt = &testing.T{} DiffLines(mockt, got, want) if mockt.Failed() { t.Error("Two equal multiline slices should succeed") } } func TestDifferingLines(t *testing.T) { got := []byte("foo") want := []byte("bar") mockt := &testing.T{} DiffLines(mockt, got, want) if !mockt.Failed() { t.Error("Two differing single line slices should fail") } got = []byte("foo\n") want = []byte("bar\n") mockt = &testing.T{} DiffLines(mockt, got, want) if !mockt.Failed() { t.Error("Two differing single line slices should fail") } got = []byte("foo\nbar\nbaz\nenmime\n1\n2\n3\n4\n5\n") want = []byte("foo\nbar\nbaz\ninbucket\n1\n2\n3\n4\n5\n") mockt = &testing.T{} DiffLines(mockt, got, want) if !mockt.Failed() { t.Error("Two differing multiline slices should fail") } // Test missing EOL got = []byte("foo\nenmime") want = []byte("foo\ninbucket") mockt = &testing.T{} DiffLines(mockt, got, want) if !mockt.Failed() { t.Error("Two differing no-EOL slices should fail") } } func TestGoldenMissing(t *testing.T) { mockt := &testing.T{} done := make(chan struct{}) go func() { // t.Fatal exits current goroutine, calling deferrals defer close(done) DiffGolden(mockt, []byte{}, "zzzDOESNTEXIST") }() <-done if !mockt.Failed() { t.Error("Missing golden file should fail test") } } func TestGolden(t *testing.T) { mockt := &testing.T{} DiffGolden(mockt, []byte("one\n"), "testdata", "test.golden") if !mockt.Failed() { t.Error("Differing bytes in golden file should fail test") } mockt = &testing.T{} DiffGolden(mockt, []byte("one\ntwo\nthree\n"), "testdata", "test.golden") if mockt.Failed() { t.Error("Same bytes in golden file should not fail test") } } enmime-0.9.3/internal/test/testdata/000077500000000000000000000000001417532643400174125ustar00rootroot00000000000000enmime-0.9.3/internal/test/testdata/test.golden000066400000000000000000000000161417532643400215600ustar00rootroot00000000000000one two three enmime-0.9.3/internal/test/testing.go000066400000000000000000000117151417532643400176120ustar00rootroot00000000000000package test import ( "bytes" "io" "os" "path/filepath" "strings" "testing" "github.com/jhillyerd/enmime" ) // PartExists indicates to ComparePart that this part is expect to exist var PartExists = &enmime.Part{} // OpenTestData is a utility function to open a file in testdata for reading, it will panic if there // is an error. func OpenTestData(subdir, filename string) io.Reader { // Open test part for parsing raw, err := os.Open(filepath.Join("testdata", subdir, filename)) if err != nil { // err already contains full path to file panic(err) } return raw } // ComparePart test helper compares the attributes of two parts, returning true if they are equal. // t.Errorf() will be called for each field that is not equal. The presence of child and siblings // will be checked, but not the attributes of them. Header, Errors and unexported fields are // ignored. func ComparePart(t *testing.T, got *enmime.Part, want *enmime.Part) (equal bool) { t.Helper() if got == nil && want != nil { t.Error("Part == nil, want not nil") return } if got != nil && want == nil { t.Error("Part != nil, want nil") return } equal = true if got == nil && want == nil { return } if (got.Parent == nil) != (want.Parent == nil) { equal = false gs := "nil" ws := "nil" if got.Parent != nil { gs = "present" } if want.Parent != nil { ws = "present" } t.Errorf("Part.Parent == %q, want: %q", gs, ws) } if (got.FirstChild == nil) != (want.FirstChild == nil) { equal = false gs := "nil" ws := "nil" if got.FirstChild != nil { gs = "present" } if want.FirstChild != nil { ws = "present" } t.Errorf("Part.FirstChild == %q, want: %q", gs, ws) } if (got.NextSibling == nil) != (want.NextSibling == nil) { equal = false gs := "nil" ws := "nil" if got.NextSibling != nil { gs = "present" } if want.NextSibling != nil { ws = "present" } t.Errorf("Part.NextSibling == %q, want: %q", gs, ws) } if got.ContentType != want.ContentType { equal = false t.Errorf("Part.ContentType == %q, want: %q", got.ContentType, want.ContentType) } if got.Disposition != want.Disposition { equal = false t.Errorf("Part.Disposition == %q, want: %q", got.Disposition, want.Disposition) } if got.FileName != want.FileName { equal = false t.Errorf("Part.FileName == %q, want: %q", got.FileName, want.FileName) } if got.Charset != want.Charset { equal = false t.Errorf("Part.Charset == %q, want: %q", got.Charset, want.Charset) } if got.PartID != want.PartID { equal = false t.Errorf("Part.PartID == %q, want: %q", got.PartID, want.PartID) } return } // ContentContainsString checks if the provided readers content contains the specified substring func ContentContainsString(t *testing.T, b []byte, substr string) { t.Helper() got := string(b) if !strings.Contains(got, substr) { t.Errorf("content == %q, should contain: %q", got, substr) } } // ContentEqualsString checks if the provided readers content is the specified string func ContentEqualsString(t *testing.T, b []byte, str string) { t.Helper() got := string(b) if got != str { t.Errorf("content == %q, want: %q", got, str) } } // ContentEqualsBytes checks if the provided readers content is the specified []byte func ContentEqualsBytes(t *testing.T, b []byte, want []byte) { t.Helper() if !bytes.Equal(b, want) { t.Errorf("content:\n%v, want:\n%v", b, want) } } // CompareEnvelope test helper compares the attributes of two envelopes, returning true if they are equal. // t.Errorf() will be called for each field that is not equal. The presence of child and siblings // will be checked, but not the attributes of them. Unexported fields are // ignored. func CompareEnvelope(t *testing.T, got *enmime.Envelope, want *enmime.Envelope) (equal bool) { t.Helper() if got == nil && want != nil { t.Error("Envelope == nil, want not nil") return } if got != nil && want == nil { t.Error("Envelope != nil, want nil") return } equal = true if got == nil && want == nil { return } if !ComparePart(t, got.Root, want.Root) { equal = false t.Error("Envelope.Root mismatch between envelopes") } if got.Text != want.Text { equal = false t.Errorf("Envelope.Text == %q, want: %q", got.Text, want.Text) } if got.HTML != want.HTML { equal = false t.Errorf("Envelope.HTML == %q, want: %q", got.HTML, want.HTML) } if len(got.Attachments) != len(want.Attachments) { equal = false t.Errorf("Envelope.Attachments has %q elements, want: %q", len(got.Attachments), len(want.Attachments)) } if len(got.Inlines) != len(want.Inlines) { equal = false t.Errorf("Envelope.Inlines has %q elements, want: %q", len(got.Inlines), len(want.Inlines)) } if len(got.OtherParts) != len(want.OtherParts) { equal = false t.Errorf("Envelope.OtherParts has %q elements, want: %q", len(got.OtherParts), len(want.OtherParts)) } if len(got.Errors) != len(want.Errors) { equal = false t.Errorf("Envelope.Errors has %q elements, want: %q", len(got.Errors), len(want.Errors)) } return } enmime-0.9.3/internal/test/testing_test.go000066400000000000000000000160101417532643400206420ustar00rootroot00000000000000package test import ( "os" "path/filepath" "testing" "github.com/jhillyerd/enmime" ) func TestHelperComparePartsEqual(t *testing.T) { testCases := []struct { name string part *enmime.Part }{ {"nil", nil}, {"empty", &enmime.Part{}}, {"Parent", &enmime.Part{Parent: &enmime.Part{}}}, {"FirstChild", &enmime.Part{FirstChild: &enmime.Part{}}}, {"NextSibling", &enmime.Part{NextSibling: &enmime.Part{}}}, {"ContentType", &enmime.Part{ContentType: "such/wow"}}, {"Disposition", &enmime.Part{Disposition: "irritable"}}, {"FileName", &enmime.Part{FileName: "readme.txt"}}, {"Charset", &enmime.Part{Charset: "utf-7.999"}}, {"PartID", &enmime.Part{PartID: "0.1"}}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { mockt := &testing.T{} if !ComparePart(mockt, tc.part, tc.part) { t.Errorf("Got false while comparing a Part %v to itself: %+v", tc.name, tc.part) } if mockt.Failed() { t.Errorf("Helper failed test for %q, should have been successful", tc.name) } }) } } // TestHelperComparePartsInequal tests compareParts with differing Parts func TestHelperComparePartsInequal(t *testing.T) { testCases := []struct { name string a, b *enmime.Part }{ { name: "nil", a: nil, b: &enmime.Part{}, }, { name: "Parent", a: &enmime.Part{}, b: &enmime.Part{Parent: &enmime.Part{}}, }, { name: "FirstChild", a: &enmime.Part{}, b: &enmime.Part{FirstChild: &enmime.Part{}}, }, { name: "NextSibling", a: &enmime.Part{}, b: &enmime.Part{NextSibling: &enmime.Part{}}, }, { name: "ContentType", a: &enmime.Part{ContentType: "text/plain"}, b: &enmime.Part{ContentType: "text/html"}, }, { name: "Disposition", a: &enmime.Part{Disposition: "happy"}, b: &enmime.Part{Disposition: "sad"}, }, { name: "FileName", a: &enmime.Part{FileName: "foo.gif"}, b: &enmime.Part{FileName: "bar.jpg"}, }, { name: "Charset", a: &enmime.Part{Charset: "foo"}, b: &enmime.Part{Charset: "bar"}, }, { name: "PartID", a: &enmime.Part{PartID: "0"}, b: &enmime.Part{PartID: "1.1"}, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { mockt := &testing.T{} if ComparePart(mockt, tc.a, tc.b) { t.Errorf( "Got true while comparing inequal Parts (%v):\n"+ "Part A: %+v\nPart B: %+v", tc.name, tc.a, tc.b) } if tc.name != "" && !mockt.Failed() { t.Errorf("Mock test succeeded for %s, should have failed", tc.name) } }) } } // TestOpenTestDataPanic verifies that this function will panic as predicted func TestOpenTestDataPanic(t *testing.T) { defer func() { if r := recover(); r == nil { t.Error("OpenTestData did not panic") } }() _ = OpenTestData("invalidDir", "invalidFile") } // TestOpenTestData ensures that the returned io.Reader has the correct underlying type, that // the file descriptor referenced is a directory and that we have permission to read it func TestOpenTestData(t *testing.T) { // this will open a handle to the "testdata" directory r := OpenTestData("", "") if r == nil { t.Error("The returned io.Reader should not be nil") } osFilePtr, ok := r.(*os.File) if !ok { t.Errorf("Underlying type should be *os.File, but got %T instead", r) } info, err := osFilePtr.Stat() if err != nil { t.Error("We should have read permission for \"testdata\" directory") } if !info.IsDir() { t.Error("File descriptor labeled \"testdata\" should be a directory") } } // TestContentContainsString checks if the string contains a provided sub-string func TestContentContainsString(t *testing.T) { // Success ContentContainsString(t, []byte("someString"), "some") // Failure ContentContainsString(&testing.T{}, []byte("someString"), "nope") } // TestContentEqualsString checks if the strings are equal func TestContentEqualsString(t *testing.T) { // Success ContentEqualsString(t, []byte("someString"), "someString") // Failure ContentEqualsString(&testing.T{}, []byte("someString"), "nope") } // TestContentEqualsBytes checks if the slices of bytes are equal func TestContentEqualsBytes(t *testing.T) { // Success ContentEqualsBytes(t, []byte("someString"), []byte("someString")) // Failure ContentEqualsBytes(&testing.T{}, []byte("someString"), []byte("nope")) } // TestCompareEnvelope checks all publicly accessible members of an envelope for differences func TestCompareEnvelope(t *testing.T) { fileA, err := os.Open(filepath.Join("..", "..", "testdata", "mail", "attachment.raw")) if err != nil { t.Error(err) } envelopeA, err := enmime.ReadEnvelope(fileA) if err != nil { t.Error(err) } // Success success := CompareEnvelope(t, envelopeA, envelopeA) if !success { t.Error("Same file should have identical envelopes") } // Success on "want" and "got" nil success = CompareEnvelope(t, nil, nil) if !success { t.Error("Comparing nil to nil should result in true") } // Fail on "got" nil success = CompareEnvelope(&testing.T{}, nil, envelopeA) if success { t.Error("Got is nil, envelopeA should not be the same") } // Fail on "want" nil success = CompareEnvelope(&testing.T{}, envelopeA, nil) if success { t.Error("Want is nil, envelopeA should not be the same") } // Fail on root Part mismatch nil envelopeB := *envelopeA envelopeB.Root = nil success = CompareEnvelope(&testing.T{}, envelopeA, &envelopeB) if success { t.Error("Envelope Root parts should not be the same") } envelopeB.Root = envelopeA.Root // Fail on Text mismatch envelopeB.Text = "mismatch" success = CompareEnvelope(&testing.T{}, envelopeA, &envelopeB) if success { t.Error("Envelope Text parts should not be the same") } envelopeB.Text = envelopeA.Text // Fail on HTML mismatch envelopeB.HTML = "mismatch" success = CompareEnvelope(&testing.T{}, envelopeA, &envelopeB) if success { t.Error("Envelope HTML parts should not be the same") } envelopeB.HTML = envelopeA.HTML // Fail on Attachment count mismatch envelopeB.Attachments = append(envelopeB.Attachments, &enmime.Part{}) success = CompareEnvelope(&testing.T{}, envelopeA, &envelopeB) if success { t.Error("Envelope Attachment slices should not be the same") } envelopeB.Attachments = envelopeA.Attachments // Fail on Inlines count mismatch envelopeB.Inlines = append(envelopeB.Inlines, &enmime.Part{}) success = CompareEnvelope(&testing.T{}, envelopeA, &envelopeB) if success { t.Error("Envelope Inlines slices should not be the same") } envelopeB.Inlines = envelopeA.Inlines // Fail on OtherParts count mismatch envelopeB.OtherParts = append(envelopeB.OtherParts, &enmime.Part{}) success = CompareEnvelope(&testing.T{}, envelopeA, &envelopeB) if success { t.Error("Envelope OtherParts slices should not be the same") } envelopeB.OtherParts = envelopeA.OtherParts // Fail on Errors count mismatch envelopeB.Errors = append(envelopeB.Errors, &enmime.Error{}) success = CompareEnvelope(&testing.T{}, envelopeA, &envelopeB) if success { t.Error("Envelope Errors slices should not be the same") } envelopeB.Errors = envelopeA.Errors } enmime-0.9.3/match.go000066400000000000000000000043521417532643400144350ustar00rootroot00000000000000package enmime import ( "container/list" ) // PartMatcher is a function type that you must implement to search for Parts using the // BreadthMatch* functions. Implementators should inspect the provided Part and return true if it // matches your criteria. type PartMatcher func(part *Part) bool // BreadthMatchFirst performs a breadth first search of the Part tree and returns the first part // that causes the given matcher to return true func (p *Part) BreadthMatchFirst(matcher PartMatcher) *Part { q := list.New() q.PushBack(p) // Push children onto queue and attempt to match in that order for q.Len() > 0 { e := q.Front() p := e.Value.(*Part) if matcher(p) { return p } q.Remove(e) c := p.FirstChild for c != nil { q.PushBack(c) c = c.NextSibling } } return nil } // BreadthMatchAll performs a breadth first search of the Part tree and returns all parts that cause // the given matcher to return true func (p *Part) BreadthMatchAll(matcher PartMatcher) []*Part { q := list.New() q.PushBack(p) matches := make([]*Part, 0, 10) // Push children onto queue and attempt to match in that order for q.Len() > 0 { e := q.Front() p := e.Value.(*Part) if matcher(p) { matches = append(matches, p) } q.Remove(e) c := p.FirstChild for c != nil { q.PushBack(c) c = c.NextSibling } } return matches } // DepthMatchFirst performs a depth first search of the Part tree and returns the first part that // causes the given matcher to return true func (p *Part) DepthMatchFirst(matcher PartMatcher) *Part { root := p for { if matcher(p) { return p } c := p.FirstChild if c != nil { p = c } else { for p.NextSibling == nil { if p == root { return nil } p = p.Parent } p = p.NextSibling } } } // DepthMatchAll performs a depth first search of the Part tree and returns all parts that causes // the given matcher to return true func (p *Part) DepthMatchAll(matcher PartMatcher) []*Part { root := p matches := make([]*Part, 0, 10) for { if matcher(p) { matches = append(matches, p) } c := p.FirstChild if c != nil { p = c } else { for p.NextSibling == nil { if p == root { return matches } p = p.Parent } p = p.NextSibling } } } enmime-0.9.3/match_test.go000066400000000000000000000125451417532643400154770ustar00rootroot00000000000000package enmime import ( "testing" ) func TestBreadthMatchFirst(t *testing.T) { // Setup test MIME tree: // root // ├── a1 // │   ├── b1 // │   └── b2 // ├── a2 // └── a3 root := &Part{ContentType: "multipart/alternative", FileName: "root"} a1 := &Part{ContentType: "multipart/related", Parent: root, FileName: "a1"} a2 := &Part{ContentType: "text/plain", Parent: root, FileName: "a2"} a3 := &Part{ContentType: "text/html", Parent: root, FileName: "a3"} b1 := &Part{ContentType: "text/plain", Parent: a1, FileName: "b1"} b2 := &Part{ContentType: "text/html", Parent: a1, FileName: "b2"} root.FirstChild = a1 a1.NextSibling = a2 a2.NextSibling = a3 a1.FirstChild = b1 b1.NextSibling = b2 p := root.BreadthMatchFirst(func(pt *Part) bool { return pt.ContentType == "text/plain" }) if p == nil { t.Fatal("BreadthMatchFirst should have returned a result for text/plain") } if p != a2 { t.Error("BreadthMatchFirst should have returned a2, got:", p.FileName) } p = root.BreadthMatchFirst(func(pt *Part) bool { return pt.ContentType == "text/html" }) if p == nil { t.Fatal("BreadthMatchFirst should have returned a result for text/html") } if p != a3 { t.Error("BreadthMatchFirst should have returned a3, got:", p.FileName) } } func TestBreadthMatchAll(t *testing.T) { // Setup test MIME tree: // root // ├── a1 // │   ├── b1 // │   └── b2 // ├── a2 // └── a3 root := &Part{ContentType: "multipart/alternative", FileName: "root"} a1 := &Part{ContentType: "multipart/related", Parent: root, FileName: "a1"} a2 := &Part{ContentType: "text/plain", Parent: root, FileName: "a2"} a3 := &Part{ContentType: "text/html", Parent: root, FileName: "a3"} b1 := &Part{ContentType: "text/plain", Parent: a1, FileName: "b1"} b2 := &Part{ContentType: "text/html", Parent: a1, FileName: "b2"} root.FirstChild = a1 a1.NextSibling = a2 a2.NextSibling = a3 a1.FirstChild = b1 b1.NextSibling = b2 ps := root.BreadthMatchAll(func(pt *Part) bool { return pt.ContentType == "text/plain" }) if len(ps) != 2 { t.Fatal("BreadthMatchAll should have returned two matches, got:", len(ps)) } if ps[0] != a2 { t.Error("BreadthMatchAll should have returned a2, got:", ps[0].FileName) } if ps[1] != b1 { t.Error("BreadthMatchAll should have returned b1, got:", ps[1].FileName) } ps = root.BreadthMatchAll(func(pt *Part) bool { return pt.ContentType == "text/html" }) if len(ps) != 2 { t.Fatal("BreadthMatchAll should have returned two matches, got:", len(ps)) } if ps[0] != a3 { t.Error("BreadthMatchAll should have returned a3, got:", ps[0].FileName) } if ps[1] != b2 { t.Error("BreadthMatchAll should have returned b2, got:", ps[1].FileName) } } func TestDepthMatchFirst(t *testing.T) { // Setup test MIME tree: // root // ├── a1 // │   ├── b1 // │   └── b2 // ├── a2 // └── a3 root := &Part{ContentType: "multipart/alternative", FileName: "root"} a1 := &Part{ContentType: "multipart/related", Parent: root, FileName: "a1"} a2 := &Part{ContentType: "text/plain", Parent: root, FileName: "a2"} a3 := &Part{ContentType: "text/html", Parent: root, FileName: "a3"} b1 := &Part{ContentType: "text/plain", Parent: a1, FileName: "b1"} b2 := &Part{ContentType: "text/html", Parent: a1, FileName: "b2"} root.FirstChild = a1 a1.NextSibling = a2 a2.NextSibling = a3 a1.FirstChild = b1 b1.NextSibling = b2 p := root.DepthMatchFirst(func(pt *Part) bool { return pt.ContentType == "text/plain" }) if p == nil { t.Fatal("DepthMatchFirst should have returned a result for text/plain") } if p != b1 { t.Error("DepthMatchFirst should have returned b1, got:", p.FileName) } p = root.DepthMatchFirst(func(pt *Part) bool { return pt.ContentType == "text/html" }) if p != b2 { t.Error("DepthMatchFirst should have returned b2, got:", p.FileName) } } func TestDepthMatchAll(t *testing.T) { // Setup test MIME tree: // root // ├── a1 // │   ├── b1 // │   └── b2 // ├── a2 // └── a3 root := &Part{ContentType: "multipart/alternative", FileName: "root"} a1 := &Part{ContentType: "multipart/related", Parent: root, FileName: "a1"} a2 := &Part{ContentType: "text/plain", Parent: root, FileName: "a2"} a3 := &Part{ContentType: "text/html", Parent: root, FileName: "a3"} b1 := &Part{ContentType: "text/plain", Parent: a1, FileName: "b1"} b2 := &Part{ContentType: "text/html", Parent: a1, FileName: "b2"} root.FirstChild = a1 a1.NextSibling = a2 a2.NextSibling = a3 a1.FirstChild = b1 b1.NextSibling = b2 ps := root.DepthMatchAll(func(pt *Part) bool { return pt.ContentType == "text/plain" }) if len(ps) != 2 { t.Fatal("DepthMatchAll should have returned two matches, got:", len(ps)) } if ps[0] != b1 { t.Error("DepthMatchAll should have returned b1, got:", ps[0].FileName) } if ps[1] != a2 { t.Error("DepthMatchAll should have returned a2, got:", ps[1].FileName) } ps = root.DepthMatchAll(func(pt *Part) bool { return pt.ContentType == "text/html" }) if len(ps) != 2 { t.Fatal("DepthMatchAll should have returned two matches, got:", len(ps)) } if ps[0] != b2 { t.Error("DepthMatchAll should have returned b2, got:", ps[0].FileName) } if ps[1] != a3 { t.Error("DepthMatchAll should have returned a3, got:", ps[1].FileName) } } enmime-0.9.3/mediatype/000077500000000000000000000000001417532643400147675ustar00rootroot00000000000000enmime-0.9.3/mediatype/mediatype.go000066400000000000000000000302311417532643400172760ustar00rootroot00000000000000package mediatype import ( "fmt" "mime" "strings" _utf8 "unicode/utf8" "github.com/jhillyerd/enmime/internal/coding" "github.com/jhillyerd/enmime/internal/stringutil" "github.com/pkg/errors" ) const ( // Standard MIME content types ctAppPrefix = "application/" ctAppOctetStream = "application/octet-stream" ctMultipartAltern = "multipart/alternative" ctMultipartMixed = "multipart/mixed" ctMultipartPrefix = "multipart/" ctMultipartRelated = "multipart/related" ctTextPrefix = "text/" ctTextPlain = "text/plain" ctTextHTML = "text/html" // Used as a placeholder in case of malformed Content-Type headers ctPlaceholder = "x-not-a-mime-type/x-not-a-mime-type" // Used as a placeholder param value in case of malformed // Content-Type/Content-Disposition parameters that lack values. // E.g.: Content-Type: text/html;iso-8859-1 pvPlaceholder = "not-a-param-value" utf8 = "utf-8" ) // Parse is a more tolerant implementation of Go's mime.ParseMediaType function. // // Tolerances accounted for: // * Missing ';' between content-type and media parameters // * Repeating media parameters // * Unquoted values in media parameters containing 'tspecials' characters // * Newline characters func Parse(ctype string) (mtype string, params map[string]string, invalidParams []string, err error) { mtype, params, err = mime.ParseMediaType( fixNewlines(fixUnescapedQuotes(fixUnquotedSpecials(fixMangledMediaType(ctype, ';'))))) if err != nil { if err.Error() == "mime: no media type" { return "", nil, nil, nil } return "", nil, nil, errors.WithStack(err) } if mtype == ctPlaceholder { mtype = "" } for name, value := range params { if value != pvPlaceholder { continue } invalidParams = append(invalidParams, name) delete(params, name) } return mtype, params, invalidParams, err } // fixMangledMediaType is used to insert ; separators into media type strings that lack them, and // remove repeated parameters. func fixMangledMediaType(mtype string, sep rune) string { strsep := string([]rune{sep}) if mtype == "" { return "" } parts := stringutil.SplitQuoted(mtype, sep, '"') mtype = "" if strings.Contains(parts[0], "=") { // A parameter pair at this position indicates we are missing a content-type. parts[0] = fmt.Sprintf("%s%s %s", ctAppOctetStream, strsep, parts[0]) parts = strings.Split(strings.Join(parts, strsep), strsep) } for i, p := range parts { switch i { case 0: if p == "" { // The content type is completely missing. Put in a placeholder. p = ctPlaceholder } // Check for missing token after slash. if strings.HasSuffix(p, "/") { switch p { case ctTextPrefix: p = ctTextPlain case ctAppPrefix: p = ctAppOctetStream case ctMultipartPrefix: p = ctMultipartMixed default: // Safe default p = ctAppOctetStream } } // Remove extra ctype parts if strings.Count(p, "/") > 1 { ps := strings.SplitN(p, "/", 3) p = strings.Join(ps[0:2], "/") } default: if len(p) == 0 { // Ignore trailing separators. continue } if len(strings.TrimSpace(p)) == 0 { // Ignore empty parameters. continue } if !strings.Contains(p, "=") { p = p + "=" + pvPlaceholder } // RFC-2047 encoded attribute name. p = coding.RFC2047Decode(p) pair := strings.SplitAfter(p, "=") if strings.Contains(mtype, strings.TrimSpace(pair[0])) { // Ignore repeated parameters. continue } if strings.ContainsAny(pair[0], "()<>@,;:\"\\/[]?") { // Attribute is a strict token and cannot be a quoted-string. If any of the above // characters are present in a token it must be quoted and is therefor an invalid // attribute. Discard the pair. continue } } mtype += p // Only terminate with semicolon if not the last parameter and if it doesn't already have a // semicolon. if i != len(parts)-1 && !strings.HasSuffix(mtype, ";") { // Remove whitespace between parameter=value and ; mtype = strings.TrimRight(mtype, " \t") mtype += ";" } } mtype = strings.TrimSuffix(mtype, ";") return mtype } // consumeParam takes the the parameter part of a Content-Type header, returns a clean version of // the first parameter (quoted as necessary), and the remainder of the parameter part of the // Content-Type header. // // Given this this header: // `Content-Type: text/calendar; charset=utf-8; method=text/calendar` // `consumeParams` should be given this part: // ` charset=utf-8; method=text/calendar` // And returns (first pass): // `consumed = "charset=utf-8;"` // `rest = " method=text/calendar"` // Capture the `consumed` value (to build a clean Content-Type header value) and pass the value of // `rest` back to `consumeParam`. That second call will return: // `consumed = " method=\"text/calendar\""` // `rest = ""` // Again, use the value of `consumed` to build a clean Content-Type header value. Given that `rest` // is empty, all of the parameters have been consumed successfully. // // If `consumed` is returned empty and `rest` is not empty, then the value of `rest` does not // begin with a parsable parameter. This does not necessarily indicate a problem. For example, // if there is trailing whitespace, it would be returned here. func consumeParam(s string) (consumed, rest string) { i := strings.IndexByte(s, '=') if i < 0 { return "", s } // Write out parameter name. param := strings.Builder{} param.WriteString(s[:i+1]) s = s[i+1:] value := strings.Builder{} valueQuotedOriginally := false valueQuoteAdded := false valueQuoteNeeded := false rfc2047Needed := false var r rune findValueStart: for i, r = range s { switch r { case ' ', '\t': // Do not preserve leading whitespace. case '"': valueQuotedOriginally = true valueQuoteAdded = true valueQuoteNeeded = true param.WriteRune(r) break findValueStart case ';': if value.Len() == 0 { // Value was empty, return immediately. param.WriteString(`"";`) return param.String(), s[i+1:] } break findValueStart default: if r > 127 { rfc2047Needed = true } valueQuotedOriginally = false valueQuoteAdded = false value.WriteRune(r) break findValueStart } } quoteIfUnquoted := func() { if !valueQuoteNeeded { if !valueQuoteAdded { param.WriteByte('"') valueQuoteAdded = true } valueQuoteNeeded = true } } if len(s)-i < 1 { // Parameter value starts at the end of the string, make empty // quoted string to play nice with mime.ParseMediaType. param.WriteString(`""`) } else { // The beginning of the value is not at the end of the string. for _, v := range []byte{'(', ')', '<', '>', '@', ',', ':', '/', '[', ']', '?', '='} { if s[0] == v { quoteIfUnquoted() break } } _, runeLength := _utf8.DecodeRuneInString(s[i:]) s = s[i+runeLength:] escaped := false findValueEnd: for i, r = range s { if escaped { value.WriteRune(r) escaped = false continue } switch r { case ';': if valueQuotedOriginally { // We're in a quoted string, so whitespace is allowed. value.WriteRune(r) break } // Otherwise, we've reached the end of an unquoted value. rest = s[i:] break findValueEnd case ' ', '\t': if valueQuotedOriginally { // We're in a quoted string, so whitespace is allowed. value.WriteRune(r) break } // This string contains whitespace, must be quoted. quoteIfUnquoted() value.WriteRune(r) case '"': if valueQuotedOriginally { // We're in a quoted value. This is the end of that value. rest = s[i:] break findValueEnd } quoteIfUnquoted() value.WriteByte('\\') value.WriteRune(r) case '\\': if i < len(s)-1 { // If next char is present, escape it with backslash. value.WriteRune(r) escaped = true quoteIfUnquoted() } case '(', ')', '<', '>', '@', ',', ':', '/', '[', ']', '?', '=': quoteIfUnquoted() fallthrough default: if r > 127 { rfc2047Needed = true } value.WriteRune(r) } } } if value.Len() > 0 { // Convert whole value to RFC2047 if it contains forbidden characters (ASCII > 127) val := value.String() if rfc2047Needed { val = mime.BEncoding.Encode(utf8, val) // RFC 2047 must be quoted quoteIfUnquoted() } // Write the value param.WriteString(val) } // Add final quote if required if valueQuoteNeeded { param.WriteByte('"') } // Write last parsed char if any if rest != "" { if rest[0] != '"' { // When last char is quote, valueQuotedOriginally is surely true and the quote was already written. // Otherwise output the character (; for example) param.WriteByte(rest[0]) } // Focus the rest of the string rest = rest[1:] } return param.String(), rest } // fixUnquotedSpecials as defined in RFC 2045, section 5.1: // https://tools.ietf.org/html/rfc2045#section-5.1 func fixUnquotedSpecials(s string) string { idx := strings.IndexByte(s, ';') if idx < 0 || idx == len(s) { // No parameters return s } clean := strings.Builder{} clean.WriteString(s[:idx+1]) s = s[idx+1:] for len(s) > 0 { var consumed string consumed, s = consumeParam(s) if len(consumed) == 0 { clean.WriteString(s) return clean.String() } clean.WriteString(consumed) } return clean.String() } // fixUnescapedQuotes inspects for unescaped quotes inside of a quoted string and escapes them // // Input: application/rtf; charset=iso-8859-1; name=""V047411.rtf".rtf" // Output: application/rtf; charset=iso-8859-1; name="\"V047411.rtf\".rtf" func fixUnescapedQuotes(hvalue string) string { params := strings.SplitAfter(hvalue, ";") sb := &strings.Builder{} for i := 0; i < len(params); i++ { // Inspect for "=" byte. eq := strings.IndexByte(params[i], '=') if eq < 0 { // No "=", must be the content-type or a comment. sb.WriteString(params[i]) continue } sb.WriteString(params[i][:eq]) param := params[i][eq:] startingQuote := strings.IndexByte(param, '"') closingQuote := strings.LastIndexByte(param, '"') // Opportunity to exit early if there are no quotes. if startingQuote < 0 && closingQuote < 0 { // This value is not quoted, write the value and carry on. sb.WriteString(param) continue } // Check if only one quote was found in the string. if closingQuote == startingQuote { // Append the next chunk of params here in case of a semicolon mid string. if len(params) > i+1 { param = fmt.Sprintf("%s%s", param, params[i+1]) } closingQuote = strings.LastIndexByte(param, '"') i++ if closingQuote == startingQuote { sb.WriteString("=\"\"") return sb.String() } } // Write the k/v separator back in along with everything up until the first quote. sb.WriteByte('=') sb.WriteByte('"') // Starting quote sb.WriteString(param[1:startingQuote]) // Get the value, less the outer quotes. rest := param[closingQuote+1:] // If there is stuff after the last quote then we should escape the first quote. if len(rest) > 0 && rest != ";" { sb.WriteString("\\\"") } param = param[startingQuote+1 : closingQuote] escaped := false for strIdx := range []byte(param) { switch param[strIdx] { case '"': // We are inside of a quoted string, so lets escape this guy if it isn't already escaped. if !escaped { sb.WriteByte('\\') escaped = false } sb.WriteByte(param[strIdx]) case '\\': // Something is getting escaped, a quote is the only char that needs // this, so lets assume the following char is a double-quote. escaped = true sb.WriteByte('\\') default: escaped = false sb.WriteByte(param[strIdx]) } } // If there is stuff after the last quote then we should escape the last quote, apply the // rest and terminate with a quote. switch rest { case ";": sb.WriteByte('"') sb.WriteString(rest) case "": sb.WriteByte('"') default: sb.WriteByte('\\') sb.WriteByte('"') sb.WriteString(rest) sb.WriteByte('"') } } return sb.String() } // fixNewlines replaces \n with a space and removes \r func fixNewlines(value string) string { value = strings.ReplaceAll(value, "\n", " ") value = strings.ReplaceAll(value, "\r", "") return value } enmime-0.9.3/mediatype/mediatype_test.go000066400000000000000000000345111417532643400203420ustar00rootroot00000000000000package mediatype import ( "testing" ) func TestFixMangledMediaType(t *testing.T) { testCases := []struct { input string sep rune want string }{ { input: "", sep: ';', want: "", }, { input: `text/HTML; charset=UTF-8; format=flowed; content-transfer-encoding: 7bit=`, sep: ';', want: "text/HTML; charset=UTF-8; format=flowed", }, { input: "text/html;charset=", sep: ';', want: "text/html;charset=", }, { input: "text/;charset=", sep: ';', want: "text/plain;charset=", }, { input: "multipart/;charset=", sep: ';', want: "multipart/mixed;charset=", }, { input: "text/plain;", sep: ';', want: "text/plain", }, { // Removes empty parameters. input: `image/png; name="abc.png"; =""`, sep: ';', want: `image/png; name="abc.png"`, }, { input: "application/octet-stream;=?UTF-8?B?bmFtZT0iw7DCn8KUwoo=?=You've got a new voice miss call.msg", sep: ';', want: "application/octet-stream;name=\"ð\u009f\u0094\u008aYou've got a new voice miss call.msg\"", }, { input: `application/; name="Voice message from =?UTF-8?B?4piOICsxIDI1MS0yNDUtODA0NC5tc2c=?=";`, sep: ';', want: `application/octet-stream; name="Voice message from ☎ +1 251-245-8044.msg"`, }, { input: `application/pdf name="file.pdf"`, sep: ' ', want: `application/pdf;name="file.pdf"`, }, { // Removes duplicate parameters. input: `one/two; name="file.two"; name="file.two"`, sep: ';', want: `one/two; name="file.two"`, }, { // Removes duplicate parameters. input: `one/nosp;name="file.two"; name="file.two"`, sep: ';', want: `one/nosp;name="file.two"`, }, { // Removes duplicate parameters. input: `one/; name="file.two"; name="file.two"`, sep: ';', want: `application/octet-stream; name="file.two"`, }, { input: `application/octet-stream; =?UTF-8?B?bmFtZT3DsMKfwpTCii5tc2c=?=`, sep: ' ', want: "application/octet-stream;name=\"ð\u009f\u0094\u008a.msg\"", }, { // Removes duplicate parameters. input: `one/two name="file.two" name="file.two"`, sep: ' ', want: `one/two;name="file.two"`, }, { input: `; name="file.two"`, sep: ';', want: ctPlaceholder + `; name="file.two"`, }, { input: `charset=binary; name="logoleft.jpg"`, sep: ';', want: `application/octet-stream; charset=binary; name="logoleft.jpg"`, }, { input: `one/two;iso-8859-1`, sep: ';', want: `one/two;iso-8859-1=` + pvPlaceholder, }, { input: `one/two; name="file.two"; iso-8859-1`, sep: ';', want: `one/two; name="file.two"; iso-8859-1=` + pvPlaceholder, }, { input: `one/two; ; name="file.two"`, sep: ';', want: `one/two; name="file.two"`, }, // remove extra content type parts { input: `application/pdf/.pdf; name=1337.pdf`, sep: ';', want: `application/pdf; name=1337.pdf`, }, { input: `application/pdf/pdf/pdf; name=1337.pdf`, sep: ';', want: `application/pdf; name=1337.pdf`, }, } for _, tc := range testCases { t.Run(tc.input, func(t *testing.T) { got := fixMangledMediaType(tc.input, tc.sep) if got != tc.want { t.Errorf("got %q, want %q", got, tc.want) } }) } } func TestFixUnquotedSpecials(t *testing.T) { testCases := []struct { input, want string }{ { input: "", want: "", }, { input: "application/octet-stream", want: "application/octet-stream", }, { input: "application/octet-stream;", want: "application/octet-stream;", }, { input: `application/octet-stream; param1="value1"`, want: `application/octet-stream; param1="value1"`, }, { input: `application/octet-stream; param1="value1"\`, want: `application/octet-stream; param1="value1"\`, }, { input: "application/octet-stream; param1=value1", want: "application/octet-stream; param1=value1", }, { input: `application/octet-stream; param1=value1\`, want: "application/octet-stream; param1=value1", }, { input: `application/octet-stream; param1=value1\"`, want: `application/octet-stream; param1="value1\""`, }, { input: `application/octet-stream; param1=value"1"`, want: `application/octet-stream; param1="value\"1\""`, }, { input: `application/octet-stream; param1="value\"1\""`, want: `application/octet-stream; param1="value\"1\""`, }, { // Do not preserve unqoted whitespace. input: "application/octet-stream; param1= value1", want: "application/octet-stream; param1=value1", }, { // Do not preserve unqoted whitespace. input: "application/octet-stream; param1=\tvalue1", want: "application/octet-stream; param1=value1", }, { input: `application/octet-stream; param1="value1;"`, want: `application/octet-stream; param1="value1;"`, }, { input: `application/octet-stream; param1="value1;2.txt"`, want: `application/octet-stream; param1="value1;2.txt"`, }, { input: `application/octet-stream; param1="value 1"`, want: `application/octet-stream; param1="value 1"`, }, { // Preserve quoted whitespace. input: `application/octet-stream; param1=" value 1"`, want: `application/octet-stream; param1=" value 1"`, }, { // Preserve quoted whitespace. input: "application/octet-stream; param1=\"\tvalue 1\"", want: "application/octet-stream; param1=\"\tvalue 1\"", }, { // Preserve quoted whitespace. input: "application/octet-stream; param1=\"value\t1\"", want: "application/octet-stream; param1=\"value\t1\"", }, { input: `application/octet-stream; param1="value(1).pdf"`, want: `application/octet-stream; param1="value(1).pdf"`, }, { input: `application/octet-stream; param1=value(1).pdf`, want: `application/octet-stream; param1="value(1).pdf"`, }, { input: `application/octet-stream; param1=value(1).pdf; param2=value(2).pdf`, want: `application/octet-stream; param1="value(1).pdf"; param2="value(2).pdf"`, }, { input: "application/octet-stream; param1=value(1).pdf;\tparam2=value2.pdf;", want: "application/octet-stream; param1=\"value(1).pdf\";\tparam2=value2.pdf;", }, { input: `application/octet-stream; param1=value(1).pdf;param2=value2.pdf;`, want: `application/octet-stream; param1="value(1).pdf";param2=value2.pdf;`, }, { input: `application/octet-stream; param1=value/1`, want: `application/octet-stream; param1="value/1"`, }, { input: `multipart/alternative; boundary=?UOAwFjScLp1is-162467503201177404728935166502-`, want: `multipart/alternative; boundary="?UOAwFjScLp1is-162467503201177404728935166502-"`, }, { input: `text/HTML; charset="UTF-8Return-Path: bounce-810_HTML-1070564-43@example.com`, want: `text/HTML; charset="UTF-8Return-Path: bounce-810_HTML-1070564-43@example.com"`, }, { input: `text/html;charset=`, want: `text/html;charset=""`, }, { input: `text/html; charset=; format=flowed`, want: `text/html; charset=""; format=flowed`, }, { input: `text/html;charset="`, want: `text/html;charset=""`, }, { // Check unquoted 8bit is encoded input: `application/msword;name=管理.doc`, want: `application/msword;name="=?utf-8?b?566h55CGLmRvYw==?="`, }, { // Check mix of ascii and unquoted 8bit is encoded input: `application/msword;name=15管理.doc`, want: `application/msword;name="=?utf-8?b?MTXnrqHnkIYuZG9j?="`, }, { // Check quoted 8bit is encoded input: `application/msword;name="15管理.doc"`, want: `application/msword;name="=?utf-8?b?MTXnrqHnkIYuZG9j?="`, }, { // Check quoted 8bit with missing closing quote is encoded input: `application/msword;name="15管理.doc`, want: `application/msword;name="=?utf-8?b?MTXnrqHnkIYuZG9j?="`, }, { // Trailing quote without starting quote is considered as part of param text for simplicity input: `application/msword;name=15管理.doc"`, want: `application/msword;name="=?utf-8?b?MTXnrqHnkIYuZG9jXCI=?="`, }, { // Invalid UTF-8 sequence does not cause any fatal error input: "application/msword;name=\xe2\x28\xa1.doc", want: `application/msword;name="=?utf-8?b?77+9KO+/vS5kb2M=?="`, }, { // Value with spaces is surrounded with quotes. input: `text/plain; name=Untitled document.txt`, want: `text/plain; name="Untitled document.txt"`, }, { // Value with spaces is surrounded with quotes. input: `text/plain; name=Untitled document.txt; disposition=inline`, want: `text/plain; name="Untitled document.txt"; disposition=inline`, }, } for _, tc := range testCases { t.Run(tc.input, func(t *testing.T) { got := fixUnquotedSpecials(tc.input) if got != tc.want { t.Errorf("\ngot : %s\nwant : %s\ninput: %s", got, tc.want, tc.input) } }) } } func TestFixUnEscapedQuotes(t *testing.T) { testCases := []struct { input, want string }{ { input: `application/rtf; charset=iso-8859-1; name=""V047411.rtf".rtf"`, want: `application/rtf; charset=iso-8859-1; name="\"V047411.rtf\".rtf"`, }, { input: `application/octet-stream; param1="`, want: `application/octet-stream; param1=""`, }, { input: `application/octet-stream; param1="\""`, want: `application/octet-stream; param1="\""`, }, { input: `application/rtf; charset=iso-8859-1; name=b"V047411.rtf".rtf`, want: `application/rtf; charset=iso-8859-1; name="b\"V047411.rtf\".rtf"`, }, { input: `application/rtf; charset=iso-8859-1; name="V047411.rtf".rtf`, want: `application/rtf; charset=iso-8859-1; name="\"V047411.rtf\".rtf"`, }, { input: `application/rtf; charset=iso-8859-1; name="V047411.rtf;".rtf`, want: `application/rtf; charset=iso-8859-1; name="\"V047411.rtf;\".rtf"`, }, { input: `application/rtf; charset=utf-8; name="žába.jpg"`, want: `application/rtf; charset=utf-8; name="žába.jpg"`, }, { input: `application/rtf; charset=utf-8; name=""žába".jpg"`, want: `application/rtf; charset=utf-8; name="\"žába\".jpg"`, }, } for _, tc := range testCases { t.Run(tc.input, func(t *testing.T) { got := fixUnescapedQuotes(tc.input) if got != tc.want { t.Errorf("\ngot: %s\nwant: %s", got, tc.want) } }) } } func TestParseMediaType(t *testing.T) { testCases := []struct { label string // Test case label. input string // Content type to parse. mtype string // Expected media type returned. params map[string]string // Expected params returned. }{ { label: "basic filename", input: "text/html; name=index.html", mtype: "text/html", params: map[string]string{"name": "index.html"}, }, { label: "quoted filename", input: `text/html; name="index.html"`, mtype: "text/html", params: map[string]string{"name": "index.html"}, }, { label: "basic filename trailing separator", input: "text/html; name=index.html;", mtype: "text/html", params: map[string]string{"name": "index.html"}, }, { label: "quoted filename trailing separator", input: `text/html; name="index.html";`, mtype: "text/html", params: map[string]string{"name": "index.html"}, }, { label: "unclosed quoted filename", input: `text/html; name="index.html`, mtype: "text/html", params: map[string]string{"name": "index.html"}, }, { label: "quoted filename with separator", input: `text/html; name="index;a.html"`, mtype: "text/html", params: map[string]string{"name": "index;a.html"}, }, { label: "quoted separator mid-string", input: `text/html; name="index;a.html"; hash=8675309`, mtype: "text/html", params: map[string]string{"name": "index;a.html", "hash": "8675309"}, }, { label: "with prefix whitespace", input: `text/plain; charset= "UTF-8"; format=flowed`, mtype: "text/plain", params: map[string]string{"charset": "UTF-8", "format": "flowed"}, }, { label: "with double prefix whitespace", input: `text/plain; charset = "UTF-8"; format=flowed`, mtype: "text/plain", params: map[string]string{"charset": "UTF-8", "format": "flowed"}, }, { label: "with postfix whitespace", input: `text/plain; charset="UTF-8" ; format=flowed`, mtype: "text/plain", params: map[string]string{"charset": "UTF-8", "format": "flowed"}, }, { label: "with whitespace tab", input: "text/plain; charset=\"UTF-8\"\t; format=flowed", mtype: "text/plain", params: map[string]string{"charset": "UTF-8", "format": "flowed"}, }, { label: "with newline and tab", input: "text/plain; charset=\"UTF-8\"\n\t; format=flowed", mtype: "text/plain", params: map[string]string{"charset": "UTF-8", "format": "flowed"}, }, { label: "with newline and space", input: "application/pdf; name=foo\n ; format=flowed", mtype: "application/pdf", params: map[string]string{"name": "foo", "format": "flowed"}, }, { label: "with more spaces", input: "application/pdf; name=foo ; format=flowed", mtype: "application/pdf", params: map[string]string{"name": "foo", "format": "flowed"}, }, { label: "with more tabs", input: "application/pdf; name=foo \t\t; format=flowed", mtype: "application/pdf", params: map[string]string{"name": "foo", "format": "flowed"}, }, { label: "with more newlines", input: "application/pdf; name=foo \n\n; format=flowed", mtype: "application/pdf", params: map[string]string{"name": "foo", "format": "flowed"}, }, { label: "unquoted with spaces", input: "application/pdf; x-unix-mode=0644; name=File name with spaces.pdf", mtype: "application/pdf", params: map[string]string{"x-unix-mode": "0644", "name": "File name with spaces.pdf"}, }, { label: "Outlook-Logo with newlines", input: `application/octet-stream; name="=?utf-8?B?T3V0bG9vay1Mb2dvCgpEZXNj?="`, mtype: "application/octet-stream", params: map[string]string{"name": "Outlook-Logo Desc"}, }, } for _, tc := range testCases { t.Run(tc.label, func(t *testing.T) { mtype, params, _, err := Parse(tc.input) if err != nil { t.Errorf("got err %v, want nil", err) return } if mtype != tc.mtype { t.Errorf("mtype got %q, want %q", mtype, tc.mtype) } for k, v := range tc.params { if params[k] != v { t.Errorf("params[%q] got %q, want %q", k, params[k], v) } // Delete param to allow check for unexpected below. delete(params, k) } for pname := range params { t.Errorf("Found unexpected param: %q=%q", pname, params[pname]) } }) } } enmime-0.9.3/part.go000066400000000000000000000320501417532643400143030ustar00rootroot00000000000000package enmime import ( "bufio" "bytes" "encoding/base64" "io" "io/ioutil" "mime/quotedprintable" "net/textproto" "strconv" "strings" "time" "github.com/gogs/chardet" "github.com/jhillyerd/enmime/internal/coding" "github.com/jhillyerd/enmime/mediatype" "github.com/pkg/errors" ) const ( minCharsetConfidence = 85 minCharsetRuneLength = 100 ) // Part represents a node in the MIME multipart tree. The Content-Type, Disposition and File Name // are parsed out of the header for easier access. type Part struct { PartID string // PartID labels this parts position within the tree. Parent *Part // Parent of this part (can be nil.) FirstChild *Part // FirstChild is the top most child of this part. NextSibling *Part // NextSibling of this part. Header textproto.MIMEHeader // Header for this Part. Boundary string // Boundary marker used within this part. ContentID string // ContentID header for cid URL scheme. ContentType string // ContentType header without parameters. ContentTypeParams map[string]string // Params, added to ContentType header. Disposition string // Content-Disposition header without parameters. FileName string // The file-name from disposition or type header. FileModDate time.Time // The modification date of the file. Charset string // The content charset encoding, may differ from charset in header. OrigCharset string // The original content charset when a different charset was detected. Errors []*Error // Errors encountered while parsing this part. Content []byte // Content after decoding, UTF-8 conversion if applicable. Epilogue []byte // Epilogue contains data following the closing boundary marker. } // NewPart creates a new Part object. func NewPart(contentType string) *Part { return &Part{ Header: make(textproto.MIMEHeader), ContentType: contentType, ContentTypeParams: make(map[string]string), } } // AddChild adds a child part to either FirstChild or the end of the children NextSibling chain. // The child may have siblings and children attached. This method will set the Parent field on // child and all its siblings. Safe to call on nil. func (p *Part) AddChild(child *Part) { if p == child { // Prevent paradox. return } if p != nil { if p.FirstChild == nil { // Make it the first child. p.FirstChild = child } else { // Append to sibling chain. current := p.FirstChild for current.NextSibling != nil { current = current.NextSibling } if current == child { // Prevent infinite loop. return } current.NextSibling = child } } // Update all new first-level children Parent pointers. for c := child; c != nil; c = c.NextSibling { if c == c.NextSibling { // Prevent infinite loop. return } c.Parent = p } } // TextContent indicates whether the content is text based on its content type. This value // determines what content transfer encoding scheme to use. func (p *Part) TextContent() bool { if p.ContentType == "" { // RFC 2045: no CT is equivalent to "text/plain; charset=us-ascii" return true } return strings.HasPrefix(p.ContentType, "text/") || strings.HasPrefix(p.ContentType, ctMultipartPrefix) } // setupHeaders reads the header, then populates the MIME header values for this Part. func (p *Part) setupHeaders(r *bufio.Reader, defaultContentType string) error { header, err := readHeader(r, p) if err != nil { return err } p.Header = header ctype := header.Get(hnContentType) if ctype == "" { if defaultContentType == "" { p.addWarning(ErrorMissingContentType, "MIME parts should have a Content-Type header") return nil } ctype = defaultContentType } // Parse Content-Type header. mtype, mparams, minvalidParams, err := mediatype.Parse(ctype) if err != nil { return err } for i := range minvalidParams { p.addWarning( ErrorMalformedHeader, "Content-Type header has malformed parameter %q", minvalidParams[i]) } p.ContentType = mtype // Set disposition, filename, charset if available. p.setupContentHeaders(mparams) p.Boundary = mparams[hpBoundary] p.ContentID = coding.FromIDHeader(header.Get(hnContentID)) return nil } // setupContentHeaders uses Content-Type media params and Content-Disposition headers to populate // the disposition, filename, and charset fields. func (p *Part) setupContentHeaders(mediaParams map[string]string) { // Determine content disposition, filename, character set. disposition, dparams, _, err := mediatype.Parse(p.Header.Get(hnContentDisposition)) if err == nil { // Disposition is optional p.Disposition = disposition p.FileName = coding.DecodeExtHeader(dparams[hpFilename]) } if p.FileName == "" && mediaParams[hpName] != "" { p.FileName = coding.DecodeExtHeader(mediaParams[hpName]) } if p.FileName == "" && mediaParams[hpFile] != "" { p.FileName = coding.DecodeExtHeader(mediaParams[hpFile]) } if p.Charset == "" { p.Charset = mediaParams[hpCharset] } if p.FileModDate.IsZero() { p.FileModDate, _ = time.Parse(time.RFC822, mediaParams[hpModDate]) } } // convertFromDetectedCharset attempts to detect the character set for the given part, and returns // an io.Reader that will convert from that charset to UTF-8. If the charset cannot be detected, // this method adds a warning to the part and automatically falls back to using // `convertFromStatedCharset` and returns the reader from that method. func (p *Part) convertFromDetectedCharset(r io.Reader) (io.Reader, error) { // Attempt to detect character set from part content. var cd *chardet.Detector switch p.ContentType { case "text/html": cd = chardet.NewHtmlDetector() default: cd = chardet.NewTextDetector() } buf, err := ioutil.ReadAll(r) if err != nil { return nil, errors.WithStack(err) } cs, err := cd.DetectBest(buf) switch err { case nil: // Carry on default: p.addWarning(ErrorCharsetDeclaration, "charset could not be detected: %v", err) } // Restore r. r = bytes.NewReader(buf) if cs == nil || cs.Confidence < minCharsetConfidence || len(bytes.Runes(buf)) < minCharsetRuneLength { // Low confidence or not enough characters, use declared character set. return p.convertFromStatedCharset(r), nil } // Confidence exceeded our threshold, use detected character set. if p.Charset != "" && !strings.EqualFold(cs.Charset, p.Charset) { p.addWarning(ErrorCharsetDeclaration, "declared charset %q, detected %q, confidence %d", p.Charset, cs.Charset, cs.Confidence) } if reader, err := coding.NewCharsetReader(cs.Charset, r); err == nil { r = reader p.OrigCharset = p.Charset p.Charset = cs.Charset } return r, nil } // convertFromStatedCharset returns a reader that will convert from the charset specified for the // current `*Part` to UTF-8. In case of error, or an unhandled character set, a warning will be // added to the `*Part` and the original io.Reader will be returned. func (p *Part) convertFromStatedCharset(r io.Reader) io.Reader { if p.Charset == "" { // US-ASCII. Just read. return r } reader, err := coding.NewCharsetReader(p.Charset, r) if err != nil { // Failed to get a conversion reader. p.addWarning(ErrorCharsetConversion, "failed to get reader for charset %q: %v", p.Charset, err) } else { return reader } // Try to parse charset again here to see if we can salvage some badly formed // ones like charset="charset=utf-8". charsetp := strings.Split(p.Charset, "=") if strings.EqualFold(charsetp[0], "charset") && len(charsetp) > 1 || strings.EqualFold(charsetp[0], "iso") && len(charsetp) > 1 { p.Charset = charsetp[1] reader, err = coding.NewCharsetReader(p.Charset, r) if err != nil { // Failed to get a conversion reader. p.addWarning(ErrorCharsetConversion, "failed to get reader for charset %q: %v", p.Charset, err) } else { return reader } } return r } // decodeContent performs transport decoding (base64, quoted-printable) and charset decoding, // placing the result into Part.Content. IO errors will be returned immediately; other errors // and warnings will be added to Part.Errors. func (p *Part) decodeContent(r io.Reader) error { // contentReader will point to the end of the content decoding pipeline. contentReader := r // b64cleaner aggregates errors, must maintain a reference to it to get them later. var b64cleaner *coding.Base64Cleaner // Build content decoding reader. encoding := p.Header.Get(hnContentEncoding) validEncoding := true switch strings.ToLower(encoding) { case cteQuotedPrintable: contentReader = coding.NewQPCleaner(contentReader) contentReader = quotedprintable.NewReader(contentReader) case cteBase64: b64cleaner = coding.NewBase64Cleaner(contentReader) contentReader = base64.NewDecoder(base64.RawStdEncoding, b64cleaner) case cte8Bit, cte7Bit, cteBinary, "": // No decoding required. default: // Unknown encoding. validEncoding = false p.addWarning( ErrorContentEncoding, "Unrecognized Content-Transfer-Encoding type %q", encoding) } // Build charset decoding reader. if validEncoding && strings.HasPrefix(p.ContentType, "text/") { var err error contentReader, err = p.convertFromDetectedCharset(contentReader) if err != nil { return p.base64CorruptInputCheck(err) } } // Decode and store content. content, err := ioutil.ReadAll(contentReader) if err != nil { return p.base64CorruptInputCheck(errors.WithStack(err)) } p.Content = content // Collect base64 errors. if b64cleaner != nil { for _, err := range b64cleaner.Errors { p.addWarning(ErrorMalformedBase64, err.Error()) } } // Set empty content-type error. if p.ContentType == "" { p.addWarning( ErrorMissingContentType, "content-type is empty for part id: %s", p.PartID) } return nil } // base64CorruptInputCheck will avoid fatal failure on corrupt base64 input // // This is a switch on errors.Cause(err).(type) for base64.CorruptInputError func (p *Part) base64CorruptInputCheck(err error) error { switch errors.Cause(err).(type) { case base64.CorruptInputError: p.Content = nil p.Errors = append(p.Errors, &Error{ Name: ErrorMalformedBase64, Detail: err.Error(), Severe: true, }) return nil default: return err } } // Clone returns a clone of the current Part. func (p *Part) Clone(parent *Part) *Part { if p == nil { return nil } newPart := &Part{ PartID: p.PartID, Header: p.Header, Parent: parent, Boundary: p.Boundary, ContentID: p.ContentID, ContentType: p.ContentType, Disposition: p.Disposition, FileName: p.FileName, Charset: p.Charset, Errors: p.Errors, Content: p.Content, Epilogue: p.Epilogue, } newPart.FirstChild = p.FirstChild.Clone(newPart) newPart.NextSibling = p.NextSibling.Clone(parent) return newPart } // ReadParts reads a MIME document from the provided reader and parses it into tree of Part objects. func ReadParts(r io.Reader) (*Part, error) { br := bufio.NewReader(r) root := &Part{PartID: "0"} // Read header; top-level default CT is text/plain us-ascii according to RFC 822. err := root.setupHeaders(br, `text/plain; charset="us-ascii"`) if err != nil { return nil, err } if strings.HasPrefix(root.ContentType, ctMultipartPrefix) { // Content is multipart, parse it. err = parseParts(root, br) if err != nil { return nil, err } } else { // Content is text or data, decode it. if err := root.decodeContent(br); err != nil { return nil, err } } return root, nil } // parseParts recursively parses a MIME multipart document and sets each Parts PartID. func parseParts(parent *Part, reader *bufio.Reader) error { firstRecursion := parent.Parent == nil // Loop over MIME boundaries. br := newBoundaryReader(reader, parent.Boundary) for indexPartID := 1; true; indexPartID++ { next, err := br.Next() if err != nil && errors.Cause(err) != io.EOF { return err } if br.unbounded { parent.addWarning(ErrorMissingBoundary, "Boundary %q was not closed correctly", parent.Boundary) } if !next { break } p := &Part{} // Set this Part's PartID, indicating its position within the MIME Part tree. if firstRecursion { p.PartID = strconv.Itoa(indexPartID) } else { p.PartID = parent.PartID + "." + strconv.Itoa(indexPartID) } // Look for part header. bbr := bufio.NewReader(br) if err = p.setupHeaders(bbr, ""); err != nil { return err } // Insert this Part into the MIME tree. parent.AddChild(p) if p.Boundary == "" { // Content is text or data, decode it. if err = p.decodeContent(bbr); err != nil { return err } } else { // Content is another multipart. err = parseParts(p, bbr) if err != nil { return err } } } // Store any content following the closing boundary marker into the epilogue. epilogue, err := ioutil.ReadAll(reader) if err != nil { return errors.WithStack(err) } parent.Epilogue = epilogue // If a Part is "multipart/" Content-Type, it will have .0 appended to its PartID // i.e. it is the root of its MIME Part subtree. if !firstRecursion { parent.PartID += ".0" } return nil } enmime-0.9.3/part_test.go000066400000000000000000000605131417532643400153470ustar00rootroot00000000000000package enmime_test import ( "testing" "github.com/jhillyerd/enmime" "github.com/jhillyerd/enmime/internal/test" ) func TestPlainTextPart(t *testing.T) { var want, got string var wantp *enmime.Part r := test.OpenTestData("parts", "textplain.raw") p, err := enmime.ReadParts(r) if err != nil { t.Fatalf("Unexpected parse error: %+v", err) } if p == nil { t.Fatal("Root node should not be nil") } wantp = &enmime.Part{ ContentType: "text/plain", Charset: "us-ascii", PartID: "0", } test.ComparePart(t, p, wantp) want = "7bit" got = p.Header.Get("Content-Transfer-Encoding") if got != want { t.Errorf("Content-Transfer-Encoding got: %q, want: %q", got, want) } want = "Test of text/plain section" test.ContentContainsString(t, p.Content, want) } func TestAddChildInfiniteLoops(t *testing.T) { // Part adds itself parentPart := &enmime.Part{ ContentType: "text/plain", Charset: "us-ascii", PartID: "0", } parentPart.AddChild(parentPart) // Part adds its own FirstChild childPart := &enmime.Part{ ContentType: "text/plain", Charset: "us-ascii", PartID: "1", } parentPart.FirstChild = childPart parentPart.AddChild(childPart) parentPart.FirstChild = nil // Part adds a child that is its own NextSibling childPart.NextSibling = childPart parentPart.AddChild(childPart) } func TestQuotedPrintablePart(t *testing.T) { var want, got string var wantp *enmime.Part r := test.OpenTestData("parts", "quoted-printable.raw") p, err := enmime.ReadParts(r) if err != nil { t.Fatalf("Unexpected parse error: %+v", err) } if p == nil { t.Fatal("Root node should not be nil") } wantp = &enmime.Part{ ContentType: "text/plain", Charset: "us-ascii", PartID: "0", } test.ComparePart(t, p, wantp) want = "quoted-printable" got = p.Header.Get("Content-Transfer-Encoding") if got != want { t.Errorf("Content-Transfer-Encoding got: %q, want: %q", got, want) } want = "Start=ABC=Finish" test.ContentEqualsString(t, p.Content, want) } func TestQuotedPrintableInvalidPart(t *testing.T) { var want, got string var wantp *enmime.Part r := test.OpenTestData("parts", "quoted-printable-invalid.raw") p, err := enmime.ReadParts(r) if err != nil { t.Fatalf("Unexpected parse error: %+v", err) } if p == nil { t.Fatal("Root node should not be nil") } wantp = &enmime.Part{ ContentType: "text/plain", Charset: "utf-8", Disposition: "inline", PartID: "0", } test.ComparePart(t, p, wantp) want = "quoted-printable" got = p.Header.Get("Content-Transfer-Encoding") if got != want { t.Errorf("Content-Transfer-Encoding got: %q, want: %q", got, want) } want = "Stuffs’s Weekly Summary" test.ContentContainsString(t, p.Content, want) } func TestMultiAlternParts(t *testing.T) { var want string var wantp *enmime.Part r := test.OpenTestData("parts", "multialtern.raw") p, err := enmime.ReadParts(r) // Examine root if err != nil { t.Fatalf("Unexpected parse error: %+v", err) } if p == nil { t.Fatal("Root node should not be nil") } wantp = &enmime.Part{ FirstChild: test.PartExists, ContentType: "multipart/alternative", PartID: "0", } test.ComparePart(t, p, wantp) test.ContentEqualsString(t, p.Content, "") // Examine first child p = p.FirstChild wantp = &enmime.Part{ Parent: test.PartExists, NextSibling: test.PartExists, ContentType: "text/plain", Charset: "us-ascii", PartID: "1", } test.ComparePart(t, p, wantp) want = "A text section" test.ContentContainsString(t, p.Content, want) // Examine sibling p = p.NextSibling wantp = &enmime.Part{ Parent: test.PartExists, ContentType: "text/html", Charset: "us-ascii", PartID: "2", } test.ComparePart(t, p, wantp) want = "An HTML section" test.ContentContainsString(t, p.Content, want) } // TestRootMissingContentType expects a default content type to be set for the root if not specified func TestRootMissingContentType(t *testing.T) { var want string r := test.OpenTestData("parts", "missing-ctype-root.raw") p, err := enmime.ReadParts(r) // Examine root if err != nil { t.Fatalf("Unexpected parse error: %+v", err) } if p == nil { t.Fatal("Root node should not be nil") } want = "text/plain" if p.ContentType != want { t.Errorf("Content-Type got: %q, want: %q", p.ContentType, want) } want = "us-ascii" if p.Charset != want { t.Errorf("Charset got: %q, want: %q", p.Charset, want) } } func TestPartMissingContentType(t *testing.T) { var want string var wantp *enmime.Part r := test.OpenTestData("parts", "missing-ctype.raw") p, err := enmime.ReadParts(r) // Examine root if err != nil { t.Fatalf("Unexpected parse error: %+v", err) } if p == nil { t.Fatal("Root node should not be nil") } wantp = &enmime.Part{ FirstChild: test.PartExists, ContentType: "multipart/alternative", PartID: "0", } test.ComparePart(t, p, wantp) test.ContentEqualsString(t, p.Content, "") // Examine first child p = p.FirstChild wantp = &enmime.Part{ Parent: test.PartExists, NextSibling: test.PartExists, // No ContentType PartID: "1", } test.ComparePart(t, p, wantp) want = "A text section" test.ContentContainsString(t, p.Content, want) // Examine sibling p = p.NextSibling wantp = &enmime.Part{ Parent: test.PartExists, ContentType: "text/html", Charset: "us-ascii", PartID: "2", } test.ComparePart(t, p, wantp) want = "An HTML section" test.ContentContainsString(t, p.Content, want) } func TestPartEmptyHeader(t *testing.T) { var want string var wantp *enmime.Part r := test.OpenTestData("parts", "empty-header.raw") p, err := enmime.ReadParts(r) // Examine root if err != nil { t.Fatalf("Unexpected parse error: %+v", err) } if p == nil { t.Fatal("Root node should not be nil") } wantp = &enmime.Part{ FirstChild: test.PartExists, ContentType: "multipart/alternative", PartID: "0", } test.ComparePart(t, p, wantp) test.ContentEqualsString(t, p.Content, "") // Examine first child p = p.FirstChild wantp = &enmime.Part{ Parent: test.PartExists, NextSibling: test.PartExists, // No ContentType PartID: "1", } test.ComparePart(t, p, wantp) want = "A text section" test.ContentContainsString(t, p.Content, want) // Examine sibling p = p.NextSibling wantp = &enmime.Part{ Parent: test.PartExists, ContentType: "text/html", Charset: "us-ascii", PartID: "2", } test.ComparePart(t, p, wantp) want = "An HTML section" test.ContentContainsString(t, p.Content, want) } func TestPartHeaders(t *testing.T) { r := test.OpenTestData("parts", "header-only.raw") p, err := enmime.ReadParts(r) if err != nil { t.Fatal(err) } want := "text/html" if p.ContentType != want { t.Errorf("ContentType %q, want %q", p.ContentType, want) } want = "file.html" if p.FileName != want { t.Errorf("FileName %q, want %q", p.FileName, want) } want = "utf-8" if p.Charset != want { t.Errorf("Charset %q, want %q", p.Charset, want) } want = "inline" if p.Disposition != want { t.Errorf("Disposition %q, want %q", p.Disposition, want) } want = "part123456@inbucket.org" if p.ContentID != want { t.Errorf("ContentID %q, want %q", p.ContentID, want) } } func TestMultiMixedParts(t *testing.T) { var want string var wantp *enmime.Part r := test.OpenTestData("parts", "multimixed.raw") p, err := enmime.ReadParts(r) // Examine root if err != nil { t.Fatalf("Unexpected parse error: %+v", err) } if p == nil { t.Fatal("Root node should not be nil") } wantp = &enmime.Part{ FirstChild: test.PartExists, ContentType: "multipart/mixed", PartID: "0", } test.ComparePart(t, p, wantp) test.ContentEqualsString(t, p.Content, "") // Examine first child p = p.FirstChild wantp = &enmime.Part{ Parent: test.PartExists, NextSibling: test.PartExists, ContentType: "text/plain", Charset: "us-ascii", PartID: "1", } test.ComparePart(t, p, wantp) want = "Section one" test.ContentContainsString(t, p.Content, want) // Examine sibling p = p.NextSibling wantp = &enmime.Part{ Parent: test.PartExists, ContentType: "text/plain", Charset: "us-ascii", PartID: "2", } test.ComparePart(t, p, wantp) want = "Section two" test.ContentContainsString(t, p.Content, want) } func TestMultiOtherParts(t *testing.T) { var want string var wantp *enmime.Part r := test.OpenTestData("parts", "multiother.raw") p, err := enmime.ReadParts(r) // Examine root if err != nil { t.Fatalf("Unexpected parse error: %+v", err) } if p == nil { t.Fatal("Root node should not be nil") } wantp = &enmime.Part{ FirstChild: test.PartExists, ContentType: "multipart/x-enmime", PartID: "0", } test.ComparePart(t, p, wantp) test.ContentEqualsString(t, p.Content, "") // Examine first child p = p.FirstChild wantp = &enmime.Part{ Parent: test.PartExists, NextSibling: test.PartExists, ContentType: "text/plain", Charset: "us-ascii", PartID: "1", } test.ComparePart(t, p, wantp) want = "Section one" test.ContentContainsString(t, p.Content, want) // Examine sibling p = p.NextSibling wantp = &enmime.Part{ Parent: test.PartExists, ContentType: "text/plain", Charset: "us-ascii", PartID: "2", } test.ComparePart(t, p, wantp) want = "Section two" test.ContentContainsString(t, p.Content, want) } func TestNestedAlternParts(t *testing.T) { var want string var wantp *enmime.Part r := test.OpenTestData("parts", "nestedmulti.raw") p, err := enmime.ReadParts(r) // Examine root if err != nil { t.Fatalf("Unexpected parse error: %+v", err) } if p == nil { t.Fatal("Root node should not be nil") } wantp = &enmime.Part{ ContentType: "multipart/alternative", FirstChild: test.PartExists, PartID: "0", } test.ComparePart(t, p, wantp) test.ContentEqualsString(t, p.Content, "") // Examine first child p = p.FirstChild wantp = &enmime.Part{ Parent: test.PartExists, NextSibling: test.PartExists, ContentType: "text/plain", Charset: "us-ascii", PartID: "1", } test.ComparePart(t, p, wantp) want = "A text section" test.ContentContainsString(t, p.Content, want) // Examine sibling p = p.NextSibling wantp = &enmime.Part{ Parent: test.PartExists, FirstChild: test.PartExists, ContentType: "multipart/related", PartID: "2.0", } test.ComparePart(t, p, wantp) test.ContentEqualsString(t, p.Content, "") // First nested p = p.FirstChild wantp = &enmime.Part{ Parent: test.PartExists, NextSibling: test.PartExists, ContentType: "text/html", Charset: "us-ascii", PartID: "2.1", } test.ComparePart(t, p, wantp) want = "An HTML section" test.ContentContainsString(t, p.Content, want) // Second nested p = p.NextSibling wantp = &enmime.Part{ Parent: test.PartExists, NextSibling: test.PartExists, ContentType: "text/plain", Disposition: "inline", FileName: "attach.txt", PartID: "2.2", } test.ComparePart(t, p, wantp) want = "An inline text attachment" test.ContentContainsString(t, p.Content, want) // Third nested p = p.NextSibling wantp = &enmime.Part{ Parent: test.PartExists, ContentType: "text/plain", Disposition: "inline", FileName: "attach2.txt", PartID: "2.3", } test.ComparePart(t, p, wantp) want = "Another inline text attachment" test.ContentContainsString(t, p.Content, want) } func TestPartSimilarBoundary(t *testing.T) { var want string var wantp *enmime.Part r := test.OpenTestData("parts", "similar-boundary.raw") p, err := enmime.ReadParts(r) // Examine root if err != nil { t.Fatalf("Unexpected parse error: %+v", err) } if p == nil { t.Fatal("Root node should not be nil") } wantp = &enmime.Part{ ContentType: "multipart/mixed", FirstChild: test.PartExists, PartID: "0", } test.ComparePart(t, p, wantp) test.ContentEqualsString(t, p.Content, "") // Examine first child p = p.FirstChild wantp = &enmime.Part{ Parent: test.PartExists, NextSibling: test.PartExists, ContentType: "text/plain", Charset: "us-ascii", PartID: "1", } test.ComparePart(t, p, wantp) want = "Section one" test.ContentContainsString(t, p.Content, want) // Examine sibling p = p.NextSibling wantp = &enmime.Part{ Parent: test.PartExists, FirstChild: test.PartExists, ContentType: "multipart/alternative", PartID: "2.0", } test.ComparePart(t, p, wantp) test.ContentEqualsString(t, p.Content, "") // First nested p = p.FirstChild wantp = &enmime.Part{ Parent: test.PartExists, NextSibling: test.PartExists, ContentType: "text/plain", Charset: "us-ascii", PartID: "2.1", } test.ComparePart(t, p, wantp) want = "A text section" test.ContentContainsString(t, p.Content, want) // Second nested p = p.NextSibling wantp = &enmime.Part{ Parent: test.PartExists, ContentType: "text/html", Charset: "us-ascii", PartID: "2.2", } test.ComparePart(t, p, wantp) want = "An HTML section" test.ContentContainsString(t, p.Content, want) } // Check we don't UTF-8 decode attachments func TestBinaryDecode(t *testing.T) { var want string var wantp *enmime.Part r := test.OpenTestData("parts", "bin-attach.raw") p, err := enmime.ReadParts(r) // Examine root if err != nil { t.Fatalf("Unexpected parse error: %+v", err) } if p == nil { t.Fatal("Root node should not be nil") } wantp = &enmime.Part{ FirstChild: test.PartExists, ContentType: "multipart/mixed", PartID: "0", } test.ComparePart(t, p, wantp) test.ContentEqualsString(t, p.Content, "") // Examine first child p = p.FirstChild wantp = &enmime.Part{ Parent: test.PartExists, NextSibling: test.PartExists, ContentType: "text/plain", Charset: "us-ascii", PartID: "1", } test.ComparePart(t, p, wantp) want = "A text section" test.ContentContainsString(t, p.Content, want) // Examine sibling p = p.NextSibling wantp = &enmime.Part{ Parent: test.PartExists, ContentType: "application/octet-stream", Charset: "us-ascii", Disposition: "attachment", FileName: "test.bin", PartID: "2", } test.ComparePart(t, p, wantp) wantBytes := []byte{ 0x50, 0x4B, 0x03, 0x04, 0x14, 0x00, 0x08, 0x00, 0x08, 0x00, 0xC2, 0x02, 0x29, 0x4A, 0x00, 0x00} test.ContentEqualsBytes(t, p.Content, wantBytes) } func TestMultiBase64Parts(t *testing.T) { var want string var wantp *enmime.Part r := test.OpenTestData("parts", "multibase64.raw") p, err := enmime.ReadParts(r) // Examine root if err != nil { t.Fatalf("Unexpected parse error: %+v", err) } if p == nil { t.Fatal("Root node should not be nil") } wantp = &enmime.Part{ FirstChild: test.PartExists, ContentType: "multipart/mixed", PartID: "0", } test.ComparePart(t, p, wantp) test.ContentEqualsString(t, p.Content, "") // Examine first child p = p.FirstChild wantp = &enmime.Part{ Parent: test.PartExists, NextSibling: test.PartExists, ContentType: "text/plain", Charset: "us-ascii", PartID: "1", } test.ComparePart(t, p, wantp) want = "A text section" test.ContentContainsString(t, p.Content, want) // Examine sibling p = p.NextSibling wantp = &enmime.Part{ Parent: test.PartExists, ContentType: "text/html", Disposition: "attachment", FileName: "test.html", PartID: "2", } test.ComparePart(t, p, wantp) want = "" test.ContentContainsString(t, p.Content, want) } func TestBadBoundaryTerm(t *testing.T) { var want string var wantp *enmime.Part r := test.OpenTestData("parts", "badboundary.raw") p, err := enmime.ReadParts(r) // Examine root if err != nil { t.Fatalf("Unexpected parse error: %+v", err) } if p == nil { t.Fatal("Root node should not be nil") } wantp = &enmime.Part{ FirstChild: test.PartExists, ContentType: "multipart/alternative", PartID: "0", } test.ComparePart(t, p, wantp) // Examine first child p = p.FirstChild wantp = &enmime.Part{ Parent: test.PartExists, NextSibling: test.PartExists, ContentType: "text/plain", Charset: "us-ascii", PartID: "1", } test.ComparePart(t, p, wantp) // Examine sibling p = p.NextSibling wantp = &enmime.Part{ Parent: test.PartExists, NextSibling: test.PartExists, ContentType: "text/html", Charset: "us-ascii", PartID: "2", } test.ComparePart(t, p, wantp) want = "An HTML section" test.ContentContainsString(t, p.Content, want) } func TestClonePart(t *testing.T) { r := test.OpenTestData("parts", "multiother.raw") p, err := enmime.ReadParts(r) // Examine root if err != nil { t.Fatalf("Unexpected parse error: %+v", err) } if p == nil { t.Fatal("Root node should not be nil") } clone := p.Clone(nil) test.ComparePart(t, clone, p) } func TestBarrenContentType(t *testing.T) { r := test.OpenTestData("parts", "barren-content-type.raw") p, err := enmime.ReadParts(r) if err != nil { t.Fatal(err) } wantp := &enmime.Part{ PartID: "0", Disposition: "attachment", } test.ComparePart(t, p, wantp) expected := enmime.ErrorMissingContentType satisfied := false for _, perr := range p.Errors { if perr.Name == expected { satisfied = true if perr.Severe { t.Errorf("Expected Severe to be false, got true for %q", perr.Name) } } } if !satisfied { t.Errorf( "Did not find expected error on part. Expected %q, but had: %v", expected, p.Errors) } } func TestEmptyContentTypeBadContent(t *testing.T) { r := test.OpenTestData("parts", "empty-ctype-bad-content.raw") p, err := enmime.ReadParts(r) if err != nil { t.Fatal(err) } wantp := &enmime.Part{ PartID: "1", Parent: test.PartExists, Disposition: "", } test.ComparePart(t, p.FirstChild, wantp) expected := enmime.ErrorMissingContentType satisfied := false for _, perr := range p.FirstChild.Errors { if perr.Name == expected { satisfied = true if perr.Severe { t.Errorf("Expected Severe to be false, got true for %q", perr.Name) } } } if !satisfied { t.Errorf( "Did not find expected error on part. Expected %q, but had: %v", expected, p.Errors) } } func TestMalformedContentTypeParams(t *testing.T) { r := test.OpenTestData("parts", "malformed-content-type-params.raw") p, err := enmime.ReadParts(r) if err != nil { t.Fatalf("%+v", err) } wantp := &enmime.Part{ PartID: "0", ContentType: "text/html", } test.ComparePart(t, p, wantp) expected := enmime.ErrorMalformedHeader satisfied := false for _, perr := range p.Errors { if perr.Name == expected { satisfied = true if perr.Severe { t.Errorf("Expected Severe to be false, got true for %q", perr.Name) } } } if !satisfied { t.Errorf( "Did not find expected error on part. Expected %q, but had: %v", expected, p.Errors) } } func TestContentTypeParamUnquotedSpecial(t *testing.T) { r := test.OpenTestData("parts", "unquoted-ctype-param-special.raw") p, err := enmime.ReadParts(r) if err != nil { t.Fatalf("%+v", err) } wantp := &enmime.Part{ PartID: "0", ContentType: "text/calendar", Disposition: "attachment", FileName: "calendar.ics", } test.ComparePart(t, p, wantp) } func TestNoClosingBoundary(t *testing.T) { r := test.OpenTestData("parts", "multimixed-no-closing-boundary.raw") p, err := enmime.ReadParts(r) if err != nil { t.Errorf("%+v", err) } if p == nil { t.Fatal("Expected part but got nil") } wantp := &enmime.Part{ Parent: test.PartExists, PartID: "1", ContentType: "text/html", Charset: "UTF-8", } test.ComparePart(t, p.FirstChild, wantp) expected := "Missing Boundary" hasCorrectError := false for _, v := range p.Errors { if v.Severe { t.Errorf("Expected Severe to be false, got true for %q", v.Name) } if v.Name == expected { hasCorrectError = true } } if !hasCorrectError { t.Fatalf("Did not find expected error on part. Expected %q but got %v", expected, p.Errors) } } func TestContentTypeParamMissingClosingQuote(t *testing.T) { r := test.OpenTestData("parts", "missing-closing-param-quote.raw") p, err := enmime.ReadParts(r) if err != nil { t.Fatalf("%+v", err) } wantp := &enmime.Part{ PartID: "0", ContentType: "text/html", Charset: "UTF-8Return-Path: bounce-810_HTML-769869545-477063-1070564-43@bounce.email.oflce57578375.com", } test.ComparePart(t, p, wantp) expected := enmime.ErrorCharsetConversion satisfied := false for _, perr := range p.Errors { if perr.Name == expected { satisfied = true if perr.Severe { t.Errorf("Expected Severe to be false, got true for %q", perr.Name) } } } if !satisfied { t.Errorf( "Did not find expected error on part. Expected %q, but had: %v", expected, p.Errors) } } func TestChardetFailure(t *testing.T) { const expectedContent = "GIF89ad\x00\x04\x00\x80\x00\x00\x00f\xccf\xff\x99!\xf9\x04\x00\x00\x00\x00\x00,\x00\x00\x00\x00d\x00\x04\x00\x00\x02\x1a\x8c\x8f\xa9\xcb\xed\x0f\xa3\x9c\xb4\xda\xeb\x80\u07bc\xfb\x0f\x86\xe2H\x96扦\xea*\x16\x00;" t.Run("text part", func(t *testing.T) { r := test.OpenTestData("parts", "chardet-fail.raw") p, err := enmime.ReadParts(r) if err != nil { t.Fatal(err) } wantp := &enmime.Part{ PartID: "0", ContentType: "text/plain", ContentID: "part3.E34FF3C4.059DAD00@example.com", FileName: "rzkly.txt", } test.ComparePart(t, p, wantp) expected := enmime.ErrorCharsetDeclaration satisfied := false for _, perr := range p.Errors { if perr.Name == expected { satisfied = true if perr.Severe { t.Errorf("Expected Severe to be false, got true for %q", perr.Name) } } } if !satisfied { t.Errorf( "Did not find expected error on part. Expected %q, but had: %v", expected, p.Errors) } test.ContentEqualsString(t, p.Content, expectedContent) }) t.Run("non-text part", func(t *testing.T) { r := test.OpenTestData("parts", "chardet-fail-non-txt.raw") p, err := enmime.ReadParts(r) if err != nil { t.Fatal(err) } if len(p.Errors) > 0 { t.Errorf("Errors encountered while processing part: %v", p.Errors) } wantp := &enmime.Part{ PartID: "0", ContentType: "image/gif", ContentID: "part3.E34FF3C4.059DAD00@example.com", FileName: "rzkly.gif", } test.ComparePart(t, p, wantp) test.ContentEqualsString(t, p.Content, expectedContent) }) t.Run("not enough characters part", func(t *testing.T) { r := test.OpenTestData("parts", "chardet-fail-not-long-enough.raw") p, err := enmime.ReadParts(r) if err != nil { t.Fatal(err) } if len(p.Errors) > 0 { t.Errorf("Errors encountered while processing part: %v", p.Errors) } wantp := &enmime.Part{ PartID: "0", ContentType: "text/plain", Charset: "UTF-8", } test.ComparePart(t, p, wantp) test.ContentEqualsString(t, p.Content, "和弟弟\r\n") }) } func TestChardetSuccess(t *testing.T) { // Testdata in these tests licensed under CC0: Public Domain t.Run("big-5 data in us-ascii part", func(t *testing.T) { r := test.OpenTestData("parts", "chardet-success-big-5.raw") p, err := enmime.ReadParts(r) if err != nil { t.Fatal(err) } expectedErr := enmime.Error{ Name: "Character Set Declaration Mismatch", Detail: "declared charset \"us-ascii\", detected \"Big5\", confidence 100", Severe: false, } foundExpectedErr := false if len(p.Errors) > 0 { for _, v := range p.Errors { if *v == expectedErr { foundExpectedErr = true } else { t.Errorf("Error encountered while processing part: %v", v) } } } if !foundExpectedErr { t.Errorf("Expected to find %v warning", expectedErr) } wantp := &enmime.Part{ PartID: "0", ContentType: "text/plain", Charset: "Big5", } test.ComparePart(t, p, wantp) }) t.Run("iso-8859-1 data in us-ascii part", func(t *testing.T) { r := test.OpenTestData("parts", "chardet-success-iso-8859-1.raw") p, err := enmime.ReadParts(r) if err != nil { t.Fatal(err) } expectedErr := enmime.Error{ Name: "Character Set Declaration Mismatch", Detail: "declared charset \"us-ascii\", detected \"ISO-8859-1\", confidence 90", Severe: false, } foundExpectedErr := false if len(p.Errors) > 0 { for _, v := range p.Errors { if *v == expectedErr { foundExpectedErr = true } else { t.Errorf("Error encountered while processing part: %v", v) } } } if !foundExpectedErr { t.Errorf("Expected to find %v warning", expectedErr) } wantp := &enmime.Part{ PartID: "0", ContentType: "text/plain", Charset: "ISO-8859-1", } test.ComparePart(t, p, wantp) }) } enmime-0.9.3/sender.go000066400000000000000000000023001417532643400146100ustar00rootroot00000000000000package enmime import "net/smtp" // Sender provides a method for enmime to send an email. type Sender interface { // Sends the provided msg to the specified recipients, providing the specified reverse-path to // the mail server to use for delivery error reporting. // // The message headers should usually include fields such as "From", "To", "Subject", and "Cc". // Sending "Bcc" messages is accomplished by including an email address in the recipients // parameter but not including it in the message headers. Send(reversePath string, recipients []string, msg []byte) error } // SMTPSender is a Sender backed by Go's built-in net/smtp.SendMail function. type SMTPSender struct { addr string auth smtp.Auth } var _ Sender = &SMTPSender{} // NewSMTP creates a new SMTPSender, which uses net/smtp.SendMail, and accepts the same // authentication parameters. If no authentication is required, `auth` may be nil. func NewSMTP(addr string, auth smtp.Auth) *SMTPSender { return &SMTPSender{addr, auth} } // Send a message using net/smtp.SendMail. func (s *SMTPSender) Send(reversePath string, recipients []string, msg []byte) error { return smtp.SendMail(s.addr, s.auth, reversePath, recipients, msg) } enmime-0.9.3/sender_test.go000066400000000000000000000014021417532643400156510ustar00rootroot00000000000000package enmime_test import ( "net/smtp" "strings" "testing" "github.com/jhillyerd/enmime" ) func TestSMTPSend(t *testing.T) { from := "user@example.com" auth := smtp.PlainAuth("", from, "password", "mail.example.com") s := enmime.NewSMTP("0.0.0.0", auth) // Satisfy requirements of the Send method and use an intentionally malformed From Address to // elicit an expected error from smtp.SendMail, which can be type-checked and verified. err := s.Send(from+"\rinvalid", []string{"to@example.com"}, []byte("message content")) if err == nil { t.Fatal("Send() returned nil error, wanted one.") } if !strings.Contains(err.Error(), "smtp: A line must not contain CR or LF") { t.Fatalf("Send() did not return expected error, failed: %s", err.Error()) } } enmime-0.9.3/shell.nix000066400000000000000000000003131417532643400146320ustar00rootroot00000000000000with import {}; stdenv.mkDerivation rec { name = "env"; env = buildEnv { name = name; paths = buildInputs; }; buildInputs = [ go golint ]; hardeningDisable = [ "fortify" ]; } enmime-0.9.3/testdata/000077500000000000000000000000001417532643400146175ustar00rootroot00000000000000enmime-0.9.3/testdata/attach/000077500000000000000000000000001417532643400160635ustar00rootroot00000000000000enmime-0.9.3/testdata/attach/fake.png000066400000000000000000000000211417532643400174700ustar00rootroot00000000000000Not really a PNG enmime-0.9.3/testdata/encode/000077500000000000000000000000001417532643400160545ustar00rootroot00000000000000enmime-0.9.3/testdata/encode/build-qp-addr-headers.golden000066400000000000000000000004511417532643400233040ustar00rootroot00000000000000Content-Type: text/plain; charset=utf-8 Date: Sun, 01 Jan 2017 13:14:15 +0000 From: =?utf-8?q?Olle_J=C3=A4rnefors?= Mime-Version: 1.0 Subject: RFC 2047 To: =?utf-8?q?Patrik_F=C3=A4ltstr=C3=B6m?= , =?utf-8?q?Keld_J=C3=B8rn_Simonsen?= enmime-0.9.3/testdata/encode/nocontent-with-children.golden000066400000000000000000000004341417532643400240150ustar00rootroot00000000000000Content-Type: multipart/alternative; boundary=enmime-1234567890-parent --enmime-1234567890-parent Content-Type: text/html; charset=utf-8
HTML part
--enmime-1234567890-parent Content-Type: text/plain; charset=utf-8 Plain text part --enmime-1234567890-parent-- enmime-0.9.3/testdata/encode/part-bin-content.golden000066400000000000000000000053631417532643400224410ustar00rootroot00000000000000Content-Transfer-Encoding: base64 Content-Type: image/jpeg AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4 OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3Bx cnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmq q6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj 5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/wABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhsc HR4fICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVGR0hJSktMTU5PUFFSU1RV VldYWVpbXF1eX2BhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ent8fX5/gIGCg4SFhoeIiYqLjI2O j5CRkpOUlZaXmJmam5ydnp+goaKjpKWmp6ipqqusra6vsLGys7S1tre4ubq7vL2+v8DBwsPExcbH yMnKy8zNzs/Q0dLT1NXW19jZ2tvc3d7f4OHi4+Tl5ufo6err7O3u7/Dx8vP09fb3+Pn6+/z9/v8A AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2Nzg5 Ojs8PT4/QEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaW1xdXl9gYWJjZGVmZ2hpamtsbW5vcHFy c3R1dnd4eXp7fH1+f4CBgoOEhYaHiImKi4yNjo+QkZKTlJWWl5iZmpucnZ6foKGio6Slpqeoqaqr rK2ur7CxsrO0tba3uLm6u7y9vr/AwcLDxMXGx8jJysvMzc7P0NHS09TV1tfY2drb3N3e3+Dh4uPk 5ebn6Onq6+zt7u/w8fLz9PX29/j5+vv8/f7/AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwd Hh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVW V1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6P kJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfI ycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/wAB AgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6 Ozw9Pj9AQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVpbXF1eX2BhYmNkZWZnaGlqa2xtbm9wcXJz dHV2d3h5ent8fX5/gIGCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJmam5ydnp+goaKjpKWmp6ipqqus ra6vsLGys7S1tre4ubq7vL2+v8DBwsPExcbHyMnKy8zNzs/Q0dLT1NXW19jZ2tvc3d7f4OHi4+Tl 5ufo6err7O3u7/Dx8vP09fb3+Pn6+/z9/v8AAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0e HyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZX WFlaW1xdXl9gYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXp7fH1+f4CBgoOEhYaHiImKi4yNjo+Q kZKTlJWWl5iZmpucnZ6foKGio6SlpqeoqaqrrK2ur7CxsrO0tba3uLm6u7y9vr/AwcLDxMXGx8jJ ysvMzc7P0NHS09TV1tfY2drb3N3e3+Dh4uPk5ebn6Onq6+zt7u/w8fLz9PX29/j5+vv8/f7/AAEC AwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7 PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0 dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6yt rq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm 5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/wABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4f ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVGR0hJSktMTU5PUFFSU1RVVldY WVpbXF1eX2BhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ent8fX5/gIGCg4SFhoeIiYqLjI2Oj5CR kpOUlZaXmJmam5ydnp+goaKjpKWmp6ipqqusra6vsLGys7S1tre4ubq7vL2+v8DBwsPExcbHyMnK y8zNzs8= enmime-0.9.3/testdata/encode/part-bin-header.golden000066400000000000000000000004061417532643400222100ustar00rootroot00000000000000Content-Type: text/plain; charset=utf-8 Subject: =?utf-8?b?wqFIb2xhLCBzZcOxb3Ih?= X-Data: =?utf-8?b?AxfhfujropadladnggnfjgwsaiubvnmkadiuhterqHJSFfuAjkfhrqpeorLA?= =?utf-8?b?kFnjNfhgt7Fjd9dfkliodQ==?= This is a test of a plain text part. Another line. enmime-0.9.3/testdata/encode/part-content-only-qp.golden000066400000000000000000000001211417532643400232530ustar00rootroot00000000000000Content-Transfer-Encoding: quoted-printable =E2=98=86 No header, only content.enmime-0.9.3/testdata/encode/part-content-only.golden000066400000000000000000000000321417532643400226360ustar00rootroot00000000000000 No header, only content.enmime-0.9.3/testdata/encode/part-default-headers.golden000066400000000000000000000004741417532643400232540ustar00rootroot00000000000000Content-Disposition: attachment; filename=stuff.zip; modification-date="01 Feb 03 04:05 GMT" Content-Id: Content-Transfer-Encoding: base64 Content-Type: application/zip; boundary=enmime-abcdefg0123456789; charset=binary; name=stuff.zip; param1=myparameter1; param2=myparameter2 WklQWklQWklQ enmime-0.9.3/testdata/encode/part-empty.golden000066400000000000000000000000001417532643400213360ustar00rootroot00000000000000enmime-0.9.3/testdata/encode/part-file-mod-date.golden000066400000000000000000000003051417532643400226170ustar00rootroot00000000000000Content-Disposition: inline; modification-date="01 Feb 03 04:05 GMT" Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=utf-8 =C2=A1Hola, se=C3=B1or! Welcome to MIMEenmime-0.9.3/testdata/encode/part-header-only-default-encoding.golden000066400000000000000000000000541417532643400256260ustar00rootroot00000000000000Content-Type: text/plain X-Empty-Header: enmime-0.9.3/testdata/encode/part-header-only.golden000066400000000000000000000000321417532643400224140ustar00rootroot00000000000000Content-Type: text/plain enmime-0.9.3/testdata/encode/part-plain.golden000066400000000000000000000001421417532643400213120ustar00rootroot00000000000000Content-Type: text/plain; charset=utf-8 This is a test of a plain text part. Another line. enmime-0.9.3/testdata/encode/part-quotable-content.golden000066400000000000000000000000611417532643400234730ustar00rootroot00000000000000Content-Type: text/plain; charset=utf-8 Hello=enmime-0.9.3/testdata/encode/part-quoted-content.golden000066400000000000000000000001771417532643400231700ustar00rootroot00000000000000Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=utf-8 =C2=A1Hola, se=C3=B1or! Welcome to MIMEenmime-0.9.3/testdata/encode/part-quoted-headers.golden000066400000000000000000000006641417532643400231320ustar00rootroot00000000000000Content-Disposition: attachment; filename="=?utf-8?b?w6FydsOtenTFsXLFkSAieCIgdMO8a8O2cmbDunLDs2fDqXAuemlw?="; modification-date="01 Feb 03 04:05 GMT" Content-Id: Content-Transfer-Encoding: base64 Content-Type: application/zip; boundary=enmime-abcdefg0123456789; charset=binary; name="=?utf-8?b?w6FydsOtenTFsXLFkSAieCIgdMO8a8O2cmbDunLDs2fDqXAuemlw?="; param1=myparameter1; param2=myparameter2 WklQWklQWklQ enmime-0.9.3/testdata/encode/part-quoted-printable-headers.golden000066400000000000000000000007571417532643400251130ustar00rootroot00000000000000Content-Disposition: attachment; filename="=?utf-8?b?w6FydsOtenTFsXLFkSAieCIgdMO8a8O2cmbDunLDs2fDqXAuemlw?="; modification-date="01 Feb 03 04:05 GMT" Content-Id: Content-Transfer-Encoding: base64 Content-Type: application/zip; boundary=enmime-abcdefg0123456789; charset=binary; name="=?utf-8?b?w6FydsOtenTFsXLFkSAieCIgdMO8a8O2cmbDunLDs2fDqXAuemlw?="; param1=myparameter1; param2=myparameter2 X-Qp-Header: =?utf-8?q?Just_enough_to_need_qp_=E2=98=86?= WklQWklQWklQ enmime-0.9.3/testdata/encode/part-with-children.golden000066400000000000000000000005051417532643400227530ustar00rootroot00000000000000Content-Type: multipart/alternative; boundary=enmime-1234567890-parent; charset=utf-8 Bro, do you even MIME? --enmime-1234567890-parent Content-Type: text/html; charset=utf-8
HTML part
--enmime-1234567890-parent Content-Type: text/plain; charset=utf-8 Plain text part --enmime-1234567890-parent-- enmime-0.9.3/testdata/low-quality/000077500000000000000000000000001417532643400171065ustar00rootroot00000000000000enmime-0.9.3/testdata/low-quality/bad-final-boundary.raw000066400000000000000000000011061417532643400232550ustar00rootroot00000000000000From: James Hillyerd Subject: Attachment Date: Thu, 18 Oct 2012 22:48:39 -0700 Message-Id: <07B7061D-2676-487E-942E-C341CE4D13DC@makita.skynet> To: greg@inbucket Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="Enmime-Test-100" --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii A text section --Enmime-Test-100 Content-Transfer-Encoding: base64 Content-Type: application/x-zip-compressed; name="foo_9E5D72.zip" Content-Disposition: attachment; filename="foo_9E5D72.zip" PGh0bWw+Cg== --Enmime-Test-100 enmime-0.9.3/testdata/low-quality/bad-header-start.raw000066400000000000000000000006361417532643400227350ustar00rootroot00000000000000 From: James Hillyerd Subject: plain text Date: Thu, 18 Oct 2012 22:48:39 -0700 Message-Id: <07B7061D-2676-487E-942E-C341CE4D13DC@makita.skynet> To: greg@inbucket Content-Type: multipart/alternative; boundary="Enmime-Test-100" --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii A text section --Enmime-Test-100 An HTML section --Enmime-Test-100-- enmime-0.9.3/testdata/low-quality/bad-header-wrap.raw000066400000000000000000000011461417532643400225460ustar00rootroot00000000000000From: James Hillyerd Subject: Attachment Date: Thu, 18 Oct 2012 22:48:39 -0700 Message-Id: <07B7061D-2676-487E-942E-C341CE4D13DC@makita.skynet> To: greg@inbucket Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="Enmime-Test-100" --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii A text section --Enmime-Test-100 Content-Transfer-Encoding: base64 Content-Type: application/x-zip-compressed; x-unix-mode=0600; name="7DDA4_foo_9E5D72.zip" Content-Disposition: attachment; filename="7DDA4_foo_9E5D72.zip" PGh0bWw+Cg== --Enmime-Test-100-- enmime-0.9.3/testdata/low-quality/empty-header.raw000066400000000000000000000006351417532643400222110ustar00rootroot00000000000000From: James Hillyerd Subject: plain text Date: Thu, 18 Oct 2012 22:48:39 -0700 Message-Id: <07B7061D-2676-487E-942E-C341CE4D13DC@makita.skynet> To: greg@inbucket Content-Type: multipart/alternative; boundary="Enmime-Test-100" --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii A text section --Enmime-Test-100 An HTML section --Enmime-Test-100-- enmime-0.9.3/testdata/low-quality/html-only-inline.raw000066400000000000000000000045161417532643400230260ustar00rootroot00000000000000From: James Hillyerd Content-Type: multipart/alternative; boundary="Apple-Mail=_E091454E-BCFA-43B4-99C0-678AEC9868D6" Subject: MIME test 1 Date: Sat, 13 Oct 2012 15:33:07 -0700 Message-Id: <4E2E5A48-1A2C-4450-8663-D41B451DA93E@makita.skynet> To: greg@nobody.com Mime-Version: 1.0 (Apple Message framework v1283) X-Mailer: Apple Mail (2.1283) --Apple-Mail=_E091454E-BCFA-43B4-99C0-678AEC9868D6 Content-Type: multipart/related; type="text/html"; boundary="Apple-Mail=_D2ABE25A-F0FE-404E-94EE-D98BD23448D5" --Apple-Mail=_D2ABE25A-F0FE-404E-94EE-D98BD23448D5 Content-Transfer-Encoding: 7bit Content-Type: text/html; charset=us-ascii Test of HTML section --Apple-Mail=_D2ABE25A-F0FE-404E-94EE-D98BD23448D5 Content-Transfer-Encoding: base64 Content-Disposition: inline; filename=favicon.png Content-Type: image/png; x-unix-mode=0644; name="favicon.png" Content-Id: <8B8481A2-25CA-4886-9B5A-8EB9115DD064@skynet> iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ bWFnZVJlYWR5ccllPAAAAlFJREFUeNqUU8tOFEEUPVVdNV3dPe8xYRBnjGhmBgKjKzCIiQvBoIaN bly5Z+PSv3Aj7DSiP2B0rwkLGVdGgxITSCRIJGSMEQWZR3eVt5sEFBgTb/dN1yvnnHtPNTPG4Pqd HgCMXnPRSZrpSuH8vUJu4DE4rYHDGAZDX62BZttHqTiIayM3gGiXQsgYLEvATaqxU+dy1U13YXap XptpNHY8iwn8KyIAzm1KBdtRZWErpI5lEWTXp5Z/vHpZ3/wyKKwYGGOdAYwR0EZwoezTYApBEIOb yELl/aE1/83cp40Pt5mxqCKrE4Ck+mVWKKcI5tA8BLEhRBKJLjez6a7MLq7XZtp+yyOawwCBtkiB VZDKzRk4NN7NQBMYPHiZDFhXY+p9ff7F961vVcnl4R5I2ykJ5XFN7Ab7Gc61VoipNBKF+PDyztu5 lfrSLT/wIwCxq0CAGtXHZTzqR2jtwQiXONma6hHpj9sLT7YaPxfTXuZdBGA02Wi7FS48YiTfj+i2 NhqtdhP5RC8mh2/Op7y0v6eAcWVLFT8D7kWX5S9mepp+C450MV6aWL1cGnvkxbwHtLW2B9AOkLeU d9KEDuh9fl/7CEj7YH5g+3r/lWfF9In7tPz6T4IIwBJOr1SJyIGQMZQbsh5P9uBq5VJtqHh2mo49 pdw5WFoEwKWqWHacaWOjQXWGcifKo6vj5RGS6zykI587XeUIQDqJSmAp+lE4qt19W5P9o8+Lma5D cjsC8JiT607lMVkdqQ0Vyh3lHhmh52tfNy78ajXv0rgYzv8nfwswANuk+7sD/Q0aAAAAAElFTkSu QmCC --Apple-Mail=_D2ABE25A-F0FE-404E-94EE-D98BD23448D5-- --Apple-Mail=_E091454E-BCFA-43B4-99C0-678AEC9868D6-- enmime-0.9.3/testdata/low-quality/incorrect-charset.raw000066400000000000000000000025421417532643400232430ustar00rootroot00000000000000From: James Hillyerd Subject: plain text Date: Thu, 18 Oct 2012 22:48:39 -0700 Message-Id: <07B7061D-2676-487E-942E-C341CE4D13DC@makita.skynet> To: greg@inbucket Content-Type: multipart/alternative; boundary="Enmime-Test-100" --Enmime-Test-100 Content-Transfer-Encoding: 8bit Content-Type: text/plain; charset=us-ascii Using Unicode/UTF-8, you can write in emails and source code things such as Mathematics and sciences: ∮ E⋅da = Q, n → ∞, ∑ f(i) = ∏ g(i), ⎧⎡⎛┌─────┐⎞⎤⎫ ⎪⎢⎜│a²+b³ ⎟⎥⎪ ∀x∈ℝ: ⌈x⌉ = −⌊−x⌋, α ∧ ¬β = ¬(¬α ∨ β), ⎪⎢⎜│───── ⎟⎥⎪ ⎪⎢⎜⎷ c₈ ⎟⎥⎪ ℕ ⊆ ℕ₀ ⊂ ℤ ⊂ ℚ ⊂ ℝ ⊂ ℂ, ⎨⎢⎜ ⎟⎥⎬ ⎪⎢⎜ ∞ ⎟⎥⎪ ⊥ < a ≠ b ≡ c ≤ d ≪ ⊤ ⇒ (⟦A⟧ ⇔ ⟪B⟫), ⎪⎢⎜ ⎲ ⎟⎥⎪ ⎪⎢⎜ ⎳aⁱ-bⁱ⎟⎥⎪ 2H₂ + O₂ ⇌ 2H₂O, R = 4.7 kΩ, ⌀ 200 mm ⎩⎣⎝i=1 ⎠⎦⎭ Linguistics and dictionaries: ði ıntəˈnæʃənəl fəˈnɛtık əsoʊsiˈeıʃn Y [ˈʏpsilɔn], Yen [jɛn], Yoga [ˈjoːgɑ] --Enmime-Test-100-- enmime-0.9.3/testdata/low-quality/malformed-base64-attach.raw000066400000000000000000000011021417532643400241050ustar00rootroot00000000000000From: James Hillyerd Subject: Attachment Date: Thu, 18 Oct 2012 22:48:39 -0700 Message-Id: <07B7061D-2676-487E-942E-C341CE4D13DC@makita.skynet> To: greg@inbucket Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="Enmime-Test-100" --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii A text section --Enmime-Test-100 Content-Type: application/msword; name="some.doc" Content-Transfer-Encoding: base64 Content-Disposition: attachment; filename="some.doc" PGh0*bWw+Cg== --Enmime-Test-100-- enmime-0.9.3/testdata/low-quality/missing-content-type.raw000066400000000000000000000003141417532643400237170ustar00rootroot00000000000000From: James Hillyerd Subject: plain text Date: Thu, 18 Oct 2012 22:48:39 -0700 Message-Id: <07B7061D-2676-487E-942E-C341CE4D13DC@makita.skynet> To: greg@inbucket A plain text email enmime-0.9.3/testdata/low-quality/missing-content-type2.raw000066400000000000000000000006751417532643400240130ustar00rootroot00000000000000From: James Hillyerd Subject: plain text Date: Thu, 18 Oct 2012 22:48:39 -0700 Message-Id: <07B7061D-2676-487E-942E-C341CE4D13DC@makita.skynet> To: greg@inbucket Content-Type: multipart/alternative; boundary="Enmime-Test-100" --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii A text section --Enmime-Test-100 Content-Transfer-Encoding: 7bit An HTML section --Enmime-Test-100-- enmime-0.9.3/testdata/low-quality/unk-charset-html-only.raw000066400000000000000000000004231417532643400237650ustar00rootroot00000000000000From: James Hillyerd Subject: Attachment Date: Thu, 18 Oct 2012 22:48:39 -0700 Message-Id: <07B7061D-2676-487E-942E-C341CE4D13DC@makita.skynet> To: greg@inbucket Mime-Version: 1.0 Content-Type: text/html

Hello World!

enmime-0.9.3/testdata/low-quality/unk-charset-part.raw000066400000000000000000000011061417532643400230070ustar00rootroot00000000000000From: James Hillyerd Subject: Attachment Date: Thu, 18 Oct 2012 22:48:39 -0700 Message-Id: <07B7061D-2676-487E-942E-C341CE4D13DC@makita.skynet> To: greg@inbucket Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="Enmime-Test-100" --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=fakescii A text section --Enmime-Test-100 Content-Transfer-Encoding: base64 Content-Type: application/x-zip-compressed; name="foo_9E5D72.zip" Content-Disposition: attachment; filename="foo_9E5D72.zip" PGh0bWw+Cg== --Enmime-Test-100-- enmime-0.9.3/testdata/low-quality/unk-encoding-part.raw000066400000000000000000000011101417532643400231370ustar00rootroot00000000000000From: James Hillyerd Subject: Attachment Date: Thu, 18 Oct 2012 22:48:39 -0700 Message-Id: <07B7061D-2676-487E-942E-C341CE4D13DC@makita.skynet> To: greg@inbucket Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="Enmime-Test-100" --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii A text section --Enmime-Test-100 Content-Transfer-Encoding: base9000 Content-Type: application/x-zip-compressed; name="foo_9E5D72.zip" Content-Disposition: attachment; filename="foo_9E5D72.zip" PGh0bWw+Cg== --Enmime-Test-100-- enmime-0.9.3/testdata/mail/000077500000000000000000000000001417532643400155415ustar00rootroot00000000000000enmime-0.9.3/testdata/mail/attachment-application.raw000066400000000000000000000011011417532643400226760ustar00rootroot00000000000000From: James Hillyerd Subject: Attachment Date: Thu, 18 Oct 2012 22:48:39 -0700 Message-Id: <07B7061D-2676-487E-942E-C341CE4D13DC@makita.skynet> To: greg@inbucket Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="Enmime-Test-100" --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii A text section --Enmime-Test-100 Content-Type: application/msword; name="some.doc" Content-Transfer-Encoding: base64 Content-Disposition: attachment; filename="some.doc" PGh0bWw+Cg== --Enmime-Test-100-- enmime-0.9.3/testdata/mail/attachment-octet.raw000066400000000000000000000011741417532643400215230ustar00rootroot00000000000000From: me@here.com To: you@there.com Subject: In One Ear MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="MyBoundaryString" This is a MIME message. If the next few lines look like gibberish, then your mail reader sucks. If you are using a MIME reader, then you aren't even seeing this. --MyBoundaryString Content-Type: text/plain Content-Transfer-Encoding: 7bit A text section --MyBoundaryString Content-Type: application/octet-stream; file="ATTACHMENT.EXE" Content-Transfer-Encoding: base64 AxfhfujropadladnggnfjgwsaiubvnmkadiuhterqHJSFfuAjkfhrqpeorLAkFn jNfhgt7Fjd9dfkliodf== --MyBoundaryString-- This text is ignored.enmime-0.9.3/testdata/mail/attachment-only-inline-quoted-printable.raw000066400000000000000000000012071417532643400261140ustar00rootroot00000000000000MIME-Version: 1.0 Message-ID: Date: Wed, 8 Feb 2017 03:23:13 -0500 Content-Type: text/html; charset="UTF-8" Content-Disposition: inline Content-Transfer-Encoding: quoted-printable To: From: Chris Garrett Subject: Text body only with disposition inline Just some html content enmime-0.9.3/testdata/mail/attachment-only-inline.raw000066400000000000000000000023241417532643400226400ustar00rootroot00000000000000To: bob@test.com From: alice@test.com Subject: Test Message-ID: <56A0AA5F.4020203@test.com> Date: Thu, 21 Jan 2016 10:52:31 +0100 MIME-Version: 1.0 Content-Type: image/jpeg; name="favicon.jpg" Content-Transfer-Encoding: base64 Content-Disposition: inline; filename="favicon.jpg" iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ bWFnZVJlYWR5ccllPAAAAlFJREFUeNqUU8tOFEEUPVVdNV3dPe8xYRBnjGhmBgKjKzCIiQvBoIaN bly5Z+PSv3Aj7DSiP2B0rwkLGVdGgxITSCRIJGSMEQWZR3eVt5sEFBgTb/dN1yvnnHtPNTPG4Pqd HgCMXnPRSZrpSuH8vUJu4DE4rYHDGAZDX62BZttHqTiIayM3gGiXQsgYLEvATaqxU+dy1U13YXap XptpNHY8iwn8KyIAzm1KBdtRZWErpI5lEWTXp5Z/vHpZ3/wyKKwYGGOdAYwR0EZwoezTYApBEIOb yELl/aE1/83cp40Pt5mxqCKrE4Ck+mVWKKcI5tA8BLEhRBKJLjez6a7MLq7XZtp+yyOawwCBtkiB VZDKzRk4NN7NQBMYPHiZDFhXY+p9ff7F961vVcnl4R5I2ykJ5XFN7Ab7Gc61VoipNBKF+PDyztu5 lfrSLT/wIwCxq0CAGtXHZTzqR2jtwQiXONma6hHpj9sLT7YaPxfTXuZdBGA02Wi7FS48YiTfj+i2 NhqtdhP5RC8mh2/Op7y0v6eAcWVLFT8D7kWX5S9mepp+C450MV6aWL1cGnvkxbwHtLW2B9AOkLeU d9KEDuh9fl/7CEj7YH5g+3r/lWfF9In7tPz6T4IIwBJOr1SJyIGQMZQbsh5P9uBq5VJtqHh2mo49 pdw5WFoEwKWqWHacaWOjQXWGcifKo6vj5RGS6zykI587XeUIQDqJSmAp+lE4qt19W5P9o8+Lma5D cjsC8JiT607lMVkdqQ0Vyh3lHhmh52tfNy78ajXv0rgYzv8nfwswANuk+7sD/Q0aAAAAAElFTkSu QmCC enmime-0.9.3/testdata/mail/attachment-only-no-disposition.raw000066400000000000000000000022351417532643400243410ustar00rootroot00000000000000To: bob@test.com From: alice@test.com Subject: Test Message-ID: <56A0AA5F.4020203@test.com> Date: Thu, 21 Jan 2016 10:52:31 +0100 MIME-Version: 1.0 Content-Type: image/jpeg; name="favicon.jpg" Content-Transfer-Encoding: base64 iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ bWFnZVJlYWR5ccllPAAAAlFJREFUeNqUU8tOFEEUPVVdNV3dPe8xYRBnjGhmBgKjKzCIiQvBoIaN bly5Z+PSv3Aj7DSiP2B0rwkLGVdGgxITSCRIJGSMEQWZR3eVt5sEFBgTb/dN1yvnnHtPNTPG4Pqd HgCMXnPRSZrpSuH8vUJu4DE4rYHDGAZDX62BZttHqTiIayM3gGiXQsgYLEvATaqxU+dy1U13YXap XptpNHY8iwn8KyIAzm1KBdtRZWErpI5lEWTXp5Z/vHpZ3/wyKKwYGGOdAYwR0EZwoezTYApBEIOb yELl/aE1/83cp40Pt5mxqCKrE4Ck+mVWKKcI5tA8BLEhRBKJLjez6a7MLq7XZtp+yyOawwCBtkiB VZDKzRk4NN7NQBMYPHiZDFhXY+p9ff7F961vVcnl4R5I2ykJ5XFN7Ab7Gc61VoipNBKF+PDyztu5 lfrSLT/wIwCxq0CAGtXHZTzqR2jtwQiXONma6hHpj9sLT7YaPxfTXuZdBGA02Wi7FS48YiTfj+i2 NhqtdhP5RC8mh2/Op7y0v6eAcWVLFT8D7kWX5S9mepp+C450MV6aWL1cGnvkxbwHtLW2B9AOkLeU d9KEDuh9fl/7CEj7YH5g+3r/lWfF9In7tPz6T4IIwBJOr1SJyIGQMZQbsh5P9uBq5VJtqHh2mo49 pdw5WFoEwKWqWHacaWOjQXWGcifKo6vj5RGS6zykI587XeUIQDqJSmAp+lE4qt19W5P9o8+Lma5D cjsC8JiT607lMVkdqQ0Vyh3lHhmh52tfNy78ajXv0rgYzv8nfwswANuk+7sD/Q0aAAAAAElFTkSu QmCC enmime-0.9.3/testdata/mail/attachment-only-text-attachment.raw000066400000000000000000000004621417532643400244750ustar00rootroot00000000000000To: bob@test.com From: alice@test.com Subject: Test Message-ID: <56A0AA5F.4020203@test.com> Date: Thu, 21 Jan 2016 10:52:31 +0100 MIME-Version: 1.0 Content-Type: text/plain; name="test.csv" Content-Disposition: attachment; filename="test.csv" Content-Transfer-Encoding: base64 VGVzdDtUZXN0Cg== enmime-0.9.3/testdata/mail/attachment-only.raw000066400000000000000000000023301417532643400213610ustar00rootroot00000000000000To: bob@test.com From: alice@test.com Subject: Test Message-ID: <56A0AA5F.4020203@test.com> Date: Thu, 21 Jan 2016 10:52:31 +0100 MIME-Version: 1.0 Content-Type: image/jpeg; name="favicon.jpg" Content-Transfer-Encoding: base64 Content-Disposition: attachment; filename="favicon.jpg" iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ bWFnZVJlYWR5ccllPAAAAlFJREFUeNqUU8tOFEEUPVVdNV3dPe8xYRBnjGhmBgKjKzCIiQvBoIaN bly5Z+PSv3Aj7DSiP2B0rwkLGVdGgxITSCRIJGSMEQWZR3eVt5sEFBgTb/dN1yvnnHtPNTPG4Pqd HgCMXnPRSZrpSuH8vUJu4DE4rYHDGAZDX62BZttHqTiIayM3gGiXQsgYLEvATaqxU+dy1U13YXap XptpNHY8iwn8KyIAzm1KBdtRZWErpI5lEWTXp5Z/vHpZ3/wyKKwYGGOdAYwR0EZwoezTYApBEIOb yELl/aE1/83cp40Pt5mxqCKrE4Ck+mVWKKcI5tA8BLEhRBKJLjez6a7MLq7XZtp+yyOawwCBtkiB VZDKzRk4NN7NQBMYPHiZDFhXY+p9ff7F961vVcnl4R5I2ykJ5XFN7Ab7Gc61VoipNBKF+PDyztu5 lfrSLT/wIwCxq0CAGtXHZTzqR2jtwQiXONma6hHpj9sLT7YaPxfTXuZdBGA02Wi7FS48YiTfj+i2 NhqtdhP5RC8mh2/Op7y0v6eAcWVLFT8D7kWX5S9mepp+C450MV6aWL1cGnvkxbwHtLW2B9AOkLeU d9KEDuh9fl/7CEj7YH5g+3r/lWfF9In7tPz6T4IIwBJOr1SJyIGQMZQbsh5P9uBq5VJtqHh2mo49 pdw5WFoEwKWqWHacaWOjQXWGcifKo6vj5RGS6zykI587XeUIQDqJSmAp+lE4qt19W5P9o8+Lma5D cjsC8JiT607lMVkdqQ0Vyh3lHhmh52tfNy78ajXv0rgYzv8nfwswANuk+7sD/Q0aAAAAAElFTkSu QmCC enmime-0.9.3/testdata/mail/attachment.raw000066400000000000000000000010511417532643400204010ustar00rootroot00000000000000From: James Hillyerd Subject: Attachment Date: Thu, 18 Oct 2012 22:48:39 -0700 Message-Id: <07B7061D-2676-487E-942E-C341CE4D13DC@makita.skynet> To: greg@inbucket Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="Enmime-Test-100" --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii A text section --Enmime-Test-100 Content-Transfer-Encoding: base64 Content-Type: text/html; name="test.html" Content-Disposition: attachment; filename=test.html PGh0bWw+Cg== --Enmime-Test-100-- enmime-0.9.3/testdata/mail/ctype-bug.raw000066400000000000000000000143141417532643400201560ustar00rootroot00000000000000Delivered-To: deepak@redsift.io Received: by 10.76.55.35 with SMTP id o3csp106612oap; Fri, 10 Jul 2015 13:12:34 -0700 (PDT) X-Received: by 10.170.119.147 with SMTP id l141mr25507408ykb.89.1436559154116; Fri, 10 Jul 2015 13:12:34 -0700 (PDT) Return-Path: Received: from mail135-10.atl141.mandrillapp.com (mail135-10.atl141.mandrillapp.com. [198.2.135.10]) by mx.google.com with ESMTPS id k184si6630505ywf.180.2015.07.10.13.12.34 for (version=TLSv1.2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128); Fri, 10 Jul 2015 13:12:34 -0700 (PDT) Received-SPF: pass (google.com: domain of bounce-md_30112948.55a02731.v1-163e4a0faf244a2da6b0121cc7af1fe9@mandrill.papertrailapp.com designates 198.2.135.10 as permitted sender) client-ip=198.2.135.10; Authentication-Results: mx.google.com; spf=pass (google.com: domain of bounce-md_30112948.55a02731.v1-163e4a0faf244a2da6b0121cc7af1fe9@mandrill.papertrailapp.com designates 198.2.135.10 as permitted sender) smtp.mail=bounce-md_30112948.55a02731.v1-163e4a0faf244a2da6b0121cc7af1fe9@mandrill.papertrailapp.com; dkim=pass header.i=@papertrailapp.com; dkim=pass header.i=@mandrillapp.com DKIM-Signature: v=1; a=rsa-sha1; c=relaxed/relaxed; s=mandrill; d=papertrailapp.com; h=From:Subject:To:Message-Id:Date:MIME-Version:Content-Type; i=support@papertrailapp.com; bh=2tw/BU7QN7gmFr2K2wnVpETYxbU=; b=T+PzWzjbOoKO3jNANsmqsnbM+gnbgT9EQBP8DOSno75iHQ9AuU6xcDCPctvJt50Exr6aTs9qJmEG baCa39danDRIx5zXsdaSy34+SKfDODdgmwEEfKFeULQGPwF1g73tXeX4k0kwt+bm6f0baWbaLwR1 RdhUd42jEMossTKuD9w= DomainKey-Signature: a=rsa-sha1; c=nofws; q=dns; s=mandrill; d=papertrailapp.com; b=Cv9EE+3+CO+puDhpfQOsuwuP6YqJQBA/Z6OofPTXqWf/Asr/edsi7aoXIE+forQ/q8DjhhMMuMiD bQ1tlRXMFckw08GjqU7RN+ouwJEMXOpzxUgp6OwrITvddwhddEg6H3uYRva5pNJqonDDykshHyjA EVeAdcY4tjYQrcRxw/0=; Received: from pmta03.mandrill.prod.atl01.rsglab.com (127.0.0.1) by mail135-10.atl141.mandrillapp.com id hk0jj41sau80 for ; Fri, 10 Jul 2015 20:12:33 +0000 (envelope-from ) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=mandrillapp.com; i=@mandrillapp.com; q=dns/txt; s=mandrill; t=1436559153; h=From : Subject : To : Message-Id : Date : MIME-Version : Content-Type : From : Subject : Date : X-Mandrill-User : List-Unsubscribe; bh=eW2QM8XcfLCwIBTvTJaT619pYOD3YrxBvxC9cZ2gxe0=; b=quxFFNbO04KKNNB8yMd9Zch6wogobVbNFlpGIOQI/jA9FuhdZvMxQwwZ2jeno7c17v2eXY Vp3c1vwvVERCboNaPwwxrKkrhqMxM8rb15n8xM3v0IplkQ3vs9G5agiTT1qqxErsrS6xAqmj UNUPKEXuSjr24HqmQzxPry0aIgHdI= From: Papertrail Subject: Welcome to Papertrail Return-Path: Received: from [67.214.212.122] by mandrillapp.com id 163e4a0faf244a2da6b0121cc7af1fe9; Fri, 10 Jul 2015 20:12:33 +0000 To: Message-Id: <55a02731af510_7b0b33f2c7821d@pt02w01.papertrailapp.com.tmail> X-Report-Abuse: Please forward a copy of this message, including all headers, to abuse@mandrill.com X-Report-Abuse: You can also report abuse here: http://mandrillapp.com/contact/abuse?id=30112948.163e4a0faf244a2da6b0121cc7af1fe9 X-Mandrill-User: md_30112948 Date: Fri, 10 Jul 2015 20:12:33 +0000 MIME-Version: 1.0 Content-Type: multipart/alternative; boundary="_av-rPFkvS5QROAYLq2cQTUr1w" --_av-rPFkvS5QROAYLq2cQTUr1w Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 7bit What's next? Head over to the Quick Start: https://papertrailapp.com/start You'll be able to: - Add systems. Aggregate syslog and app log files - any text data. - Search and tail - on the Web and command-line. - Share access with coworkers. - React and analyze. From the dashboard, group related servers, save and share searches, receive email alerts, and configure webhooks. Question? Just hit reply. Enjoy, All of us at Papertrail -- Can we help? support@papertrailapp.com - http://help.papertrailapp.com/ - https://papertrailapp.com/chat --_av-rPFkvS5QROAYLq2cQTUr1w Content-Type: text/html; charset=utf-8 Content-Transfer-Encoding: 7bit Papertrail

What's next? Head over to the Quick Start.

  • Add systems. Aggregate syslog and app log files - any text data.
  • Search and tail: Web, command-line.
  • Share access with coworkers.
  • React and analyze. From the dashboard, group related systems, save and share searches, receive email alerts, and configure webhooks.

Question? Just hit reply or join chat. Enjoy,

All of us at Papertrail

Can we help?
Email - Docs & support - Chat

--_av-rPFkvS5QROAYLq2cQTUr1w-- enmime-0.9.3/testdata/mail/epilogue-sample.raw000066400000000000000000000010711417532643400213430ustar00rootroot00000000000000From: Pedro Mendez Subject: Epilogue Date: Thu, 02 Nov 2017 22:48:39 -0700 Message-Id: <07B7061D-2676-487E-942E-C341CE4D13DC@open.ch> To: pemmemo8@gmail.com Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="Enmime-Test-100" --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii A text section --Enmime-Test-100 Content-Transfer-Encoding: base64 Content-Type: text/html; name="test.html" Content-Disposition: attachment; filename=test.html PGh0bWw+Cg== --Enmime-Test-100--> Potentially malicious content enmime-0.9.3/testdata/mail/erroneous.raw000066400000000000000000000000321417532643400202700ustar00rootroot00000000000000// Not an RFC-822 Documentenmime-0.9.3/testdata/mail/header-only.raw000066400000000000000000000004051417532643400204620ustar00rootroot00000000000000From: James Hillyerd Subject: Attachment Date: Thu, 18 Oct 2012 22:48:39 -0700 Message-Id: <07B7061D-2676-487E-942E-C341CE4D13DC@makita.skynet> To: greg@inbucket Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="Enmime-Test-100" enmime-0.9.3/testdata/mail/html-mime-bad-charset-inline.raw000066400000000000000000000047751417532643400236110ustar00rootroot00000000000000From: James Hillyerd Content-Type: multipart/alternative; boundary="Apple-Mail=_E091454E-BCFA-43B4-99C0-678AEC9868D6" Subject: MIME test 1 Date: Sat, 13 Oct 2012 15:33:07 -0700 Message-Id: <4E2E5A48-1A2C-4450-8663-D41B451DA93E@makita.skynet> To: greg@nobody.com Mime-Version: 1.0 (Apple Message framework v1283) X-Mailer: Apple Mail (2.1283) --Apple-Mail=_E091454E-BCFA-43B4-99C0-678AEC9868D6 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset="charset=us-ascii" Test of text section --Apple-Mail=_E091454E-BCFA-43B4-99C0-678AEC9868D6 Content-Type: multipart/related; type="text/html"; boundary="Apple-Mail=_D2ABE25A-F0FE-404E-94EE-D98BD23448D5" --Apple-Mail=_D2ABE25A-F0FE-404E-94EE-D98BD23448D5 Content-Transfer-Encoding: 7bit Content-Type: text/html; charset="charset=us-ascii" Test of HTML section --Apple-Mail=_D2ABE25A-F0FE-404E-94EE-D98BD23448D5 Content-Transfer-Encoding: base64 Content-Disposition: inline; filename=favicon.png Content-Type: image/png; x-unix-mode=0644; name="favicon.png" Content-Id: <8B8481A2-25CA-4886-9B5A-8EB9115DD064@skynet> iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ bWFnZVJlYWR5ccllPAAAAlFJREFUeNqUU8tOFEEUPVVdNV3dPe8xYRBnjGhmBgKjKzCIiQvBoIaN bly5Z+PSv3Aj7DSiP2B0rwkLGVdGgxITSCRIJGSMEQWZR3eVt5sEFBgTb/dN1yvnnHtPNTPG4Pqd HgCMXnPRSZrpSuH8vUJu4DE4rYHDGAZDX62BZttHqTiIayM3gGiXQsgYLEvATaqxU+dy1U13YXap XptpNHY8iwn8KyIAzm1KBdtRZWErpI5lEWTXp5Z/vHpZ3/wyKKwYGGOdAYwR0EZwoezTYApBEIOb yELl/aE1/83cp40Pt5mxqCKrE4Ck+mVWKKcI5tA8BLEhRBKJLjez6a7MLq7XZtp+yyOawwCBtkiB VZDKzRk4NN7NQBMYPHiZDFhXY+p9ff7F961vVcnl4R5I2ykJ5XFN7Ab7Gc61VoipNBKF+PDyztu5 lfrSLT/wIwCxq0CAGtXHZTzqR2jtwQiXONma6hHpj9sLT7YaPxfTXuZdBGA02Wi7FS48YiTfj+i2 NhqtdhP5RC8mh2/Op7y0v6eAcWVLFT8D7kWX5S9mepp+C450MV6aWL1cGnvkxbwHtLW2B9AOkLeU d9KEDuh9fl/7CEj7YH5g+3r/lWfF9In7tPz6T4IIwBJOr1SJyIGQMZQbsh5P9uBq5VJtqHh2mo49 pdw5WFoEwKWqWHacaWOjQXWGcifKo6vj5RGS6zykI587XeUIQDqJSmAp+lE4qt19W5P9o8+Lma5D cjsC8JiT607lMVkdqQ0Vyh3lHhmh52tfNy78ajXv0rgYzv8nfwswANuk+7sD/Q0aAAAAAElFTkSu QmCC --Apple-Mail=_D2ABE25A-F0FE-404E-94EE-D98BD23448D5-- --Apple-Mail=_E091454E-BCFA-43B4-99C0-678AEC9868D6-- enmime-0.9.3/testdata/mail/html-mime-bad-unknown-charset-inline.raw000066400000000000000000000047751417532643400253060ustar00rootroot00000000000000From: James Hillyerd Content-Type: multipart/alternative; boundary="Apple-Mail=_E091454E-BCFA-43B4-99C0-678AEC9868D6" Subject: MIME test 1 Date: Sat, 13 Oct 2012 15:33:07 -0700 Message-Id: <4E2E5A48-1A2C-4450-8663-D41B451DA93E@makita.skynet> To: greg@nobody.com Mime-Version: 1.0 (Apple Message framework v1283) X-Mailer: Apple Mail (2.1283) --Apple-Mail=_E091454E-BCFA-43B4-99C0-678AEC9868D6 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset="charset=us-askey" Test of text section --Apple-Mail=_E091454E-BCFA-43B4-99C0-678AEC9868D6 Content-Type: multipart/related; type="text/html"; boundary="Apple-Mail=_D2ABE25A-F0FE-404E-94EE-D98BD23448D5" --Apple-Mail=_D2ABE25A-F0FE-404E-94EE-D98BD23448D5 Content-Transfer-Encoding: 7bit Content-Type: text/html; charset="charset=us-askey" Test of HTML section --Apple-Mail=_D2ABE25A-F0FE-404E-94EE-D98BD23448D5 Content-Transfer-Encoding: base64 Content-Disposition: inline; filename=favicon.png Content-Type: image/png; x-unix-mode=0644; name="favicon.png" Content-Id: <8B8481A2-25CA-4886-9B5A-8EB9115DD064@skynet> iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ bWFnZVJlYWR5ccllPAAAAlFJREFUeNqUU8tOFEEUPVVdNV3dPe8xYRBnjGhmBgKjKzCIiQvBoIaN bly5Z+PSv3Aj7DSiP2B0rwkLGVdGgxITSCRIJGSMEQWZR3eVt5sEFBgTb/dN1yvnnHtPNTPG4Pqd HgCMXnPRSZrpSuH8vUJu4DE4rYHDGAZDX62BZttHqTiIayM3gGiXQsgYLEvATaqxU+dy1U13YXap XptpNHY8iwn8KyIAzm1KBdtRZWErpI5lEWTXp5Z/vHpZ3/wyKKwYGGOdAYwR0EZwoezTYApBEIOb yELl/aE1/83cp40Pt5mxqCKrE4Ck+mVWKKcI5tA8BLEhRBKJLjez6a7MLq7XZtp+yyOawwCBtkiB VZDKzRk4NN7NQBMYPHiZDFhXY+p9ff7F961vVcnl4R5I2ykJ5XFN7Ab7Gc61VoipNBKF+PDyztu5 lfrSLT/wIwCxq0CAGtXHZTzqR2jtwQiXONma6hHpj9sLT7YaPxfTXuZdBGA02Wi7FS48YiTfj+i2 NhqtdhP5RC8mh2/Op7y0v6eAcWVLFT8D7kWX5S9mepp+C450MV6aWL1cGnvkxbwHtLW2B9AOkLeU d9KEDuh9fl/7CEj7YH5g+3r/lWfF9In7tPz6T4IIwBJOr1SJyIGQMZQbsh5P9uBq5VJtqHh2mo49 pdw5WFoEwKWqWHacaWOjQXWGcifKo6vj5RGS6zykI587XeUIQDqJSmAp+lE4qt19W5P9o8+Lma5D cjsC8JiT607lMVkdqQ0Vyh3lHhmh52tfNy78ajXv0rgYzv8nfwswANuk+7sD/Q0aAAAAAElFTkSu QmCC --Apple-Mail=_D2ABE25A-F0FE-404E-94EE-D98BD23448D5-- --Apple-Mail=_E091454E-BCFA-43B4-99C0-678AEC9868D6-- enmime-0.9.3/testdata/mail/html-mime-inline.raw000066400000000000000000000047511417532643400214300ustar00rootroot00000000000000From: James Hillyerd Content-Type: multipart/alternative; boundary="Apple-Mail=_E091454E-BCFA-43B4-99C0-678AEC9868D6" Subject: MIME test 1 Date: Sat, 13 Oct 2012 15:33:07 -0700 Message-Id: <4E2E5A48-1A2C-4450-8663-D41B451DA93E@makita.skynet> To: greg@nobody.com Mime-Version: 1.0 (Apple Message framework v1283) X-Mailer: Apple Mail (2.1283) --Apple-Mail=_E091454E-BCFA-43B4-99C0-678AEC9868D6 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii Test of text section --Apple-Mail=_E091454E-BCFA-43B4-99C0-678AEC9868D6 Content-Type: multipart/related; type="text/html"; boundary="Apple-Mail=_D2ABE25A-F0FE-404E-94EE-D98BD23448D5" --Apple-Mail=_D2ABE25A-F0FE-404E-94EE-D98BD23448D5 Content-Transfer-Encoding: 7bit Content-Type: text/html; charset=us-ascii Test of HTML section --Apple-Mail=_D2ABE25A-F0FE-404E-94EE-D98BD23448D5 Content-Transfer-Encoding: base64 Content-Disposition: inline; filename=favicon.png Content-Type: image/png; x-unix-mode=0644; name="favicon.png" Content-Id: <8B8481A2-25CA-4886-9B5A-8EB9115DD064@skynet> iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ bWFnZVJlYWR5ccllPAAAAlFJREFUeNqUU8tOFEEUPVVdNV3dPe8xYRBnjGhmBgKjKzCIiQvBoIaN bly5Z+PSv3Aj7DSiP2B0rwkLGVdGgxITSCRIJGSMEQWZR3eVt5sEFBgTb/dN1yvnnHtPNTPG4Pqd HgCMXnPRSZrpSuH8vUJu4DE4rYHDGAZDX62BZttHqTiIayM3gGiXQsgYLEvATaqxU+dy1U13YXap XptpNHY8iwn8KyIAzm1KBdtRZWErpI5lEWTXp5Z/vHpZ3/wyKKwYGGOdAYwR0EZwoezTYApBEIOb yELl/aE1/83cp40Pt5mxqCKrE4Ck+mVWKKcI5tA8BLEhRBKJLjez6a7MLq7XZtp+yyOawwCBtkiB VZDKzRk4NN7NQBMYPHiZDFhXY+p9ff7F961vVcnl4R5I2ykJ5XFN7Ab7Gc61VoipNBKF+PDyztu5 lfrSLT/wIwCxq0CAGtXHZTzqR2jtwQiXONma6hHpj9sLT7YaPxfTXuZdBGA02Wi7FS48YiTfj+i2 NhqtdhP5RC8mh2/Op7y0v6eAcWVLFT8D7kWX5S9mepp+C450MV6aWL1cGnvkxbwHtLW2B9AOkLeU d9KEDuh9fl/7CEj7YH5g+3r/lWfF9In7tPz6T4IIwBJOr1SJyIGQMZQbsh5P9uBq5VJtqHh2mo49 pdw5WFoEwKWqWHacaWOjQXWGcifKo6vj5RGS6zykI587XeUIQDqJSmAp+lE4qt19W5P9o8+Lma5D cjsC8JiT607lMVkdqQ0Vyh3lHhmh52tfNy78ajXv0rgYzv8nfwswANuk+7sD/Q0aAAAAAElFTkSu QmCC --Apple-Mail=_D2ABE25A-F0FE-404E-94EE-D98BD23448D5-- --Apple-Mail=_E091454E-BCFA-43B4-99C0-678AEC9868D6-- enmime-0.9.3/testdata/mail/html-only-inline.raw000066400000000000000000000045201417532643400214540ustar00rootroot00000000000000From: James Hillyerd Content-Type: multipart/alternative; boundary="Apple-Mail=_E091454E-BCFA-43B4-99C0-678AEC9868D6" Subject: MIME test 1 Date: Sat, 13 Oct 2012 15:33:07 -0700 Message-Id: <4E2E5A48-1A2C-4450-8663-D41B451DA93E@makita.skynet> To: greg@nobody.com Mime-Version: 1.0 (Apple Message framework v1283) X-Mailer: Apple Mail (2.1283) --Apple-Mail=_E091454E-BCFA-43B4-99C0-678AEC9868D6 Content-Type: multipart/related; type="text/html"; boundary="Apple-Mail=_D2ABE25A-F0FE-404E-94EE-D98BD23448D5" --Apple-Mail=_D2ABE25A-F0FE-404E-94EE-D98BD23448D5 Content-Transfer-Encoding: 7bit Content-Type: text/html; charset=ISO-8859-1 Test of HTML section --Apple-Mail=_D2ABE25A-F0FE-404E-94EE-D98BD23448D5 Content-Transfer-Encoding: base64 Content-Disposition: inline; filename=favicon.png Content-Type: image/png; x-unix-mode=0644; name="favicon.png" Content-Id: <8B8481A2-25CA-4886-9B5A-8EB9115DD064@skynet> iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ bWFnZVJlYWR5ccllPAAAAlFJREFUeNqUU8tOFEEUPVVdNV3dPe8xYRBnjGhmBgKjKzCIiQvBoIaN bly5Z+PSv3Aj7DSiP2B0rwkLGVdGgxITSCRIJGSMEQWZR3eVt5sEFBgTb/dN1yvnnHtPNTPG4Pqd HgCMXnPRSZrpSuH8vUJu4DE4rYHDGAZDX62BZttHqTiIayM3gGiXQsgYLEvATaqxU+dy1U13YXap XptpNHY8iwn8KyIAzm1KBdtRZWErpI5lEWTXp5Z/vHpZ3/wyKKwYGGOdAYwR0EZwoezTYApBEIOb yELl/aE1/83cp40Pt5mxqCKrE4Ck+mVWKKcI5tA8BLEhRBKJLjez6a7MLq7XZtp+yyOawwCBtkiB VZDKzRk4NN7NQBMYPHiZDFhXY+p9ff7F961vVcnl4R5I2ykJ5XFN7Ab7Gc61VoipNBKF+PDyztu5 lfrSLT/wIwCxq0CAGtXHZTzqR2jtwQiXONma6hHpj9sLT7YaPxfTXuZdBGA02Wi7FS48YiTfj+i2 NhqtdhP5RC8mh2/Op7y0v6eAcWVLFT8D7kWX5S9mepp+C450MV6aWL1cGnvkxbwHtLW2B9AOkLeU d9KEDuh9fl/7CEj7YH5g+3r/lWfF9In7tPz6T4IIwBJOr1SJyIGQMZQbsh5P9uBq5VJtqHh2mo49 pdw5WFoEwKWqWHacaWOjQXWGcifKo6vj5RGS6zykI587XeUIQDqJSmAp+lE4qt19W5P9o8+Lma5D cjsC8JiT607lMVkdqQ0Vyh3lHhmh52tfNy78ajXv0rgYzv8nfwswANuk+7sD/Q0aAAAAAElFTkSu QmCC --Apple-Mail=_D2ABE25A-F0FE-404E-94EE-D98BD23448D5-- --Apple-Mail=_E091454E-BCFA-43B4-99C0-678AEC9868D6-- enmime-0.9.3/testdata/mail/inlinemultipart.raw000066400000000000000000000031211417532643400214710ustar00rootroot00000000000000Subject: Test To: alice@intern From: bob@extern Content-Type: multipart/mixed; boundary="0__=4EBB0828DFD318FF8f9e8a93df938690918c4EBB0828DFD318FF" Content-Disposition: inline MIME-Version: 1.0 --0__=4EBB0828DFD318FF8f9e8a93df938690918c4EBB0828DFD318FF Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Simple text. --0__=4EBB0828DFD318FF8f9e8a93df938690918c4EBB0828DFD318FF Content-Type: application/pdf; name="test.pdf"; name="test.pdf" Content-Transfer-Encoding: base64 Content-Disposition: attachment; filename="test.pdf" JVBERi0xLjINCg0KNCAwIG9iag0KPDwNCi9FIDYxOTUNCi9IIFsgMTIxMSAxNDUgXQ0KL0wgNjUy MA0KL0xpbmVhcml6ZWQgMQ0KL04gMQ0KL08gNw0KL1QgNjM5MA0KPj4gICAgICAgICAgICAgICAg ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg DQplbmRvYmoNCg0KeHJlZg0KNCA4DQowMDAwMDAwMDEyIDAwMDAwIG4NCjAwMDAwMDEwODggMDAw MDAgbg0KMDAwMDAwMTIxMSAwMDAwMCBuDQowMDAwMDAxMzU2IDAwMDAwIG4NCjAwMDAwMDE2MDcg MDAwMDAgbg0KMDAwMDAwMTcxNiAwMDAwMCBuDQowMDAwMDAxODI0IDAwMDAwIG4NCjAwMDAwMDQz MTkgMDAwMDAgbg0KdHJhaWxlcg0KPDwNCi9BQkNwZGYgODEwNw0KL0lEIFsgPDNENzc5REU2NjU0 RDM4ODczMTYzODBBNTY3REY0MDhFPg0KPDc5OEQ0RjVGNkUyNzIzQjA1MTFCQzU0QzYwMTczRDI3 PiBdDQovSW5mbyAzIDAgUg0KL1ByZXYgNjM4MA0KL1Jvb3QgNSAwIFINCi9TaXplIDEyDQovU291 cmNlIChXZUpYRnh --0__=4EBB0828DFD318FF8f9e8a93df938690918c4EBB0828DFD318FF Content-Type: text/plain; name="test.txt"; name="test.txt" Content-Disposition: inline Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename="test.txt" Text attachment. --0__=4EBB0828DFD318FF8f9e8a93df938690918c4EBB0828DFD318FF-- enmime-0.9.3/testdata/mail/malformed-multiple-address-header-values.raw000066400000000000000000000031721417532643400262240ustar00rootroot00000000000000Subject: Test To: alice@intern From: Bob Dunite Bobby Dunite Content-Type: multipart/mixed; boundary="0__=4EBB0828DFD318FF8f9e8a93df938690918c4EBB0828DFD318FF" Content-Disposition: inline MIME-Version: 1.0 --0__=4EBB0828DFD318FF8f9e8a93df938690918c4EBB0828DFD318FF Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Simple text. --0__=4EBB0828DFD318FF8f9e8a93df938690918c4EBB0828DFD318FF Content-Type: application/pdf; name="test.pdf"; name="test.pdf" Content-Transfer-Encoding: base64 Content-Disposition: attachment; filename="test.pdf" JVBERi0xLjINCg0KNCAwIG9iag0KPDwNCi9FIDYxOTUNCi9IIFsgMTIxMSAxNDUgXQ0KL0wgNjUy MA0KL0xpbmVhcml6ZWQgMQ0KL04gMQ0KL08gNw0KL1QgNjM5MA0KPj4gICAgICAgICAgICAgICAg ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg DQplbmRvYmoNCg0KeHJlZg0KNCA4DQowMDAwMDAwMDEyIDAwMDAwIG4NCjAwMDAwMDEwODggMDAw MDAgbg0KMDAwMDAwMTIxMSAwMDAwMCBuDQowMDAwMDAxMzU2IDAwMDAwIG4NCjAwMDAwMDE2MDcg MDAwMDAgbg0KMDAwMDAwMTcxNiAwMDAwMCBuDQowMDAwMDAxODI0IDAwMDAwIG4NCjAwMDAwMDQz MTkgMDAwMDAgbg0KdHJhaWxlcg0KPDwNCi9BQkNwZGYgODEwNw0KL0lEIFsgPDNENzc5REU2NjU0 RDM4ODczMTYzODBBNTY3REY0MDhFPg0KPDc5OEQ0RjVGNkUyNzIzQjA1MTFCQzU0QzYwMTczRDI3 PiBdDQovSW5mbyAzIDAgUg0KL1ByZXYgNjM4MA0KL1Jvb3QgNSAwIFINCi9TaXplIDEyDQovU291 cmNlIChXZUpYRnh --0__=4EBB0828DFD318FF8f9e8a93df938690918c4EBB0828DFD318FF Content-Type: text/plain; name="test.txt"; name="test.txt" Content-Disposition: inline Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename="test.txt" Text attachment. --0__=4EBB0828DFD318FF8f9e8a93df938690918c4EBB0828DFD318FF-- enmime-0.9.3/testdata/mail/mime-alternative.raw000066400000000000000000000011221417532643400215130ustar00rootroot00000000000000Message-ID: <5081A889.3020108@jamehi03lx.noa.com> Date: Fri, 19 Oct 2012 12:22:49 -0700 From: James Hillyerd User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:16.0) Gecko/20121010 Thunderbird/16.0.1 MIME-Version: 1.0 To: greg@inbucket.com Subject: Multipart Mixed Content-Type: multipart/alternative; boundary="Enmime-Test-100" --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii Section one --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii Section two --Enmime-Test-100-- enmime-0.9.3/testdata/mail/mime-bad-8bit-filename.raw000066400000000000000000000011561417532643400223540ustar00rootroot00000000000000Date: Wed, 22 Feb 2021 13:29:24 +0800 From: "Pavel Bazika" To: , Subject: Malformed test Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="=====003_Dragon323481247347_=====" This is a multi-part message in MIME format. --=====003_Dragon323481247347_===== Content-Type: text/plain; charset=us-ascii Text part --=====003_Dragon323481247347_===== Content-Type: application/msword; name=管理.doc Content-Transfer-Encoding: base64 Content-Disposition: attachment; filename=管理.doc PGh0bWw+Cg== --=====003_Dragon323481247347_=====-- enmime-0.9.3/testdata/mail/mime-bad-content-transfer-encoding.raw000066400000000000000000000061611417532643400250110ustar00rootroot00000000000000Received: from HVD-Exch16-01. internal.contoso.dom (192.168.1.50) by HVD-Exch16-01. internal.contoso.dom (192.168.1.50) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256_P256) id 15.1.1591.10 via Mailbox Transport; Sun, 20 Jan 2019 14:33:43 -0600 Message-ID: Content-Type: multipart/alternative; boundary="ABOUNDARY.075224149159994985" Subject: alert Date: Sun, 20 Jan 2019 23:33:39 +0300 From: example To: example Return-Path: doonglol@contoso.com MIME-Version: 1.0 --ABOUNDARY.075224149159994985 Content-Type: text/plain; format=flowed; charset="UTF-8" Content-Transfer-Encoding: base64 ploozdect nispplul swunkglap zarplontruskdremp fruftsist ploystoov smohsless cregzass smoosknuspgeftlut priptyasp smoorbrooft pukstrooz pruhrept frorblilcroospbrod spakkess ruzyoost toctswing grumsmach birflugjechfapt broostspust nudyet changlig droontmunt chapplooypectsoy gafral slipblept geskchant gluddrach prontcrundhaysnind spomdast sahshooy yinzess snutgruch breyloossstrolflooy vuptsling ploorlish hondcrud mebnush flendblutslawspint groowpromp maspflid grashsmey ploozblust blanzafbumyuct gligplask fasstross voongluf gluzzast luzflooctlerpach brishhonk troowtrict flezskooft honkglok bloofjooshshuhtuz pogpreh clinttrost strachlunk strehslonk flinflumcrashshipt prehhach stooplet tucttoog prindskech bloopdrarhoomgont grabtesp prilput froontnooch nubstrisk lectkampswulgrupt pangyef spawsnef snoondshin noonskunk sperglikvimpcrag flizfan frogplusk claspcrav flacttink blandpahcroskbropt plobnof sniwmact flobskenk grotslish crosppampsooskslut dekprich slitpruw smupwiw skikstint hostslaglinboh glovyev groolshict mirbrod tooddih smendgloshtooctsmak nanclih plantfloog plosszav chosksmek shechsnupfrashdik smooyhipt skulclod glubsmak swakproog snostslahshoplan sledtroosk wizpek voofwoss ploospmoow daskspongbrawbroop suhspat jatnoft wofthah froolyup slootgankcreshglol pidshek nimpchest smichmund clofflask lundzoonkroonsmiss pleskting wechslapt yulfut charshisk jospharpoffroor flubfraw smirslang konsmiy zivblad sterchoowpooyjik huctkul blonghum vaptslooft gloostplemp puttagslesssuft shivvuh strongslog craftjoob lidfreh rizskachbumloow kifkid druksnoopt drumpclir govbluf siptgeptgrozstrooss glarhaft flanyef zetkel kifspunk gefhewsnohpur spewskoosh rafgob zoochbiw foptgav fuyyonkbrictspob plenrimp fruchbremp brooshchep smafbluh shoochcrakkangstrub moontdoomp glooctclog jiffrint droozkuz zopprumpspaftheb joofstub blidguss deshdrect gromstasp clebtusppruskgov dudtrosk prudskil wengdesp stransloh droobglizhondsmop dewspook vedsosp trempwusp siyfrool kuchstrissnookflest croptgum leshkess ploobfrey blintsung rengjictbrushgrooss jiptwiv kugstug bruchkop giftchir piwclisksloozhist frispgech glactspang cruvvob prushswach plegpitwoospvow chuzgrunt --ABOUNDARY.075224149159994985 Content-Type: text/html; charset="utf-8" Content-Transfer-Encoding: base64 PG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC1UeXBlIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJz ZXQ9dXRmLTgiPgoKPGJyPgpUaGlzIGlzIGFuIGV4YW1wbGUKPGJyPg== --ABOUNDARY.075224149159994985-- enmime-0.9.3/testdata/mail/mime-bad-content-type.raw000066400000000000000000000034201417532643400223550ustar00rootroot00000000000000From: "easyHotel Booking" To: Subject: Confirmation of Booking with easyHotel Date: Fri, 12 Jun 2015 23:24:01 +0100 Message-ID: MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="----=_NextPart_000_057C_01D0A566.DD8BD380" X-Mailer: Microsoft CDO for Windows 2000 Content-Class: urn:content-classes:message Importance: normal Priority: normal X-MimeOLE: Produced By Microsoft MimeOLE V6.1.7601.17609 Return-Path: bookings@easyHotel.com X-OriginalArrivalTime: 12 Jun 2015 22:24:01.0592 (UTC) FILETIME=[7BC76B80:01D0A55E] This is a multi-part message in MIME format. ------=_NextPart_000_057C_01D0A566.DD8BD380 Content-Type: text/plain Content-Transfer-Encoding: 7bit Thank you for booking with easyHotel. ------=_NextPart_000_057C_01D0A566.DD8BD380 Content-Type: application/pdf name="Invoice_302232133150612.pdf" Content-Transfer-Encoding: base64 Content-Disposition: attachment; filename="Invoice_302232133150612.pdf" JVBERi0xLjINCg0KNCAwIG9iag0KPDwNCi9FIDYxOTUNCi9IIFsgMTIxMSAxNDUgXQ0KL0wgNjUy MA0KL0xpbmVhcml6ZWQgMQ0KL04gMQ0KL08gNw0KL1QgNjM5MA0KPj4gICAgICAgICAgICAgICAg ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg DQplbmRvYmoNCg0KeHJlZg0KNCA4DQowMDAwMDAwMDEyIDAwMDAwIG4NCjAwMDAwMDEwODggMDAw MDAgbg0KMDAwMDAwMTIxMSAwMDAwMCBuDQowMDAwMDAxMzU2IDAwMDAwIG4NCjAwMDAwMDE2MDcg MDAwMDAgbg0KMDAwMDAwMTcxNiAwMDAwMCBuDQowMDAwMDAxODI0IDAwMDAwIG4NCjAwMDAwMDQz MTkgMDAwMDAgbg0KdHJhaWxlcg0KPDwNCi9BQkNwZGYgODEwNw0KL0lEIFsgPDNENzc5REU2NjU0 RDM4ODczMTYzODBBNTY3REY0MDhFPg0KPDc5OEQ0RjVGNkUyNzIzQjA1MTFCQzU0QzYwMTczRDI3 PiBdDQovSW5mbyAzIDAgUg0KL1ByZXYgNjM4MA0KL1Jvb3QgNSAwIFINCi9TaXplIDEyDQovU291 cmNlIChXZUpYRnh ------=_NextPart_000_057C_01D0A566.DD8BD380-- enmime-0.9.3/testdata/mail/mime-blank-media-name.raw000066400000000000000000000047771417532643400223020ustar00rootroot00000000000000From: "easyHotel Booking" To: Subject: Confirmation of Booking with easyHotel Date: Fri, 12 Jun 2015 23:24:01 +0100 Message-ID: MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="----=_NextPart_000_057C_01D0A566.DD8BD380" X-Mailer: Microsoft CDO for Windows 2000 Content-Class: urn:content-classes:message Importance: normal Priority: normal X-MimeOLE: Produced By Microsoft MimeOLE V6.1.7601.17609 Return-Path: bookings@easyHotel.com X-OriginalArrivalTime: 12 Jun 2015 22:24:01.0592 (UTC) FILETIME=[7BC76B80:01D0A55E] This is a multi-part message in MIME format. ------=_NextPart_000_057C_01D0A566.DD8BD380 Content-Type: text/plain Content-Transfer-Encoding: 7bit Thank you for booking with easyHotel. You may review your booking details here: https://bookings.easyHotel.com/AccClient/MyInformation-MyBookings.asp You may review your account including payment information here: https://bookings.easyHotel.com/AccClient/MyInformation-MyAccount.asp You may obtain general help and assistance here: http://support.easyhotel.com/hc/en-gb/categories/200490531-Frequently-asked-questions You can review our terms and conditions (including refund policy) again here: https://bookings.easyhotel.com/secbook/GB/tandc.html You will need Adobe Acrobat Reader to open the attached invoice. If you do not already have it installed on your computer, you can download it free of charge from http://get.adobe.com/uk/reader/ We wish you a pleasant stay. easyHotel ------=_NextPart_000_057C_01D0A566.DD8BD380 Content-Type: application/pdf name="" Content-Transfer-Encoding: base64 Content-Disposition: attachment; filename="Invoice_302232133150612.pdf" JVBERi0xLjINCg0KNCAwIG9iag0KPDwNCi9FIDYxOTUNCi9IIFsgMTIxMSAxNDUgXQ0KL0wgNjUy MA0KL0xpbmVhcml6ZWQgMQ0KL04gMQ0KL08gNw0KL1QgNjM5MA0KPj4gICAgICAgICAgICAgICAg ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg DQplbmRvYmoNCg0KeHJlZg0KNCA4DQowMDAwMDAwMDEyIDAwMDAwIG4NCjAwMDAwMDEwODggMDAw MDAgbg0KMDAwMDAwMTIxMSAwMDAwMCBuDQowMDAwMDAxMzU2IDAwMDAwIG4NCjAwMDAwMDE2MDcg MDAwMDAgbg0KMDAwMDAwMTcxNiAwMDAwMCBuDQowMDAwMDAxODI0IDAwMDAwIG4NCjAwMDAwMDQz MTkgMDAwMDAgbg0KdHJhaWxlcg0KPDwNCi9BQkNwZGYgODEwNw0KL0lEIFsgPDNENzc5REU2NjU0 RDM4ODczMTYzODBBNTY3REY0MDhFPg0KPDc5OEQ0RjVGNkUyNzIzQjA1MTFCQzU0QzYwMTczRDI3 PiBdDQovSW5mbyAzIDAgUg0KL1ByZXYgNjM4MA0KL1Jvb3QgNSAwIFINCi9TaXplIDEyDQovU291 cmNlIChXZUpYRnh ------=_NextPart_000_057C_01D0A566.DD8BD380-- enmime-0.9.3/testdata/mail/mime-duplicate-param.raw000066400000000000000000000050771417532643400222620ustar00rootroot00000000000000From: "easyHotel Booking" To: Subject: Confirmation of Booking with easyHotel Date: Fri, 12 Jun 2015 23:24:01 +0100 Message-ID: MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="----=_NextPart_000_057C_01D0A566.DD8BD380" X-Mailer: Microsoft CDO for Windows 2000 Content-Class: urn:content-classes:message Importance: normal Priority: normal X-MimeOLE: Produced By Microsoft MimeOLE V6.1.7601.17609 Return-Path: bookings@easyHotel.com X-OriginalArrivalTime: 12 Jun 2015 22:24:01.0592 (UTC) FILETIME=[7BC76B80:01D0A55E] This is a multi-part message in MIME format. ------=_NextPart_000_057C_01D0A566.DD8BD380 Content-Type: text/plain Content-Transfer-Encoding: 7bit Thank you for booking with easyHotel. You may review your booking details here: https://bookings.easyHotel.com/AccClient/MyInformation-MyBookings.asp You may review your account including payment information here: https://bookings.easyHotel.com/AccClient/MyInformation-MyAccount.asp You may obtain general help and assistance here: http://support.easyhotel.com/hc/en-gb/categories/200490531-Frequently-asked-questions You can review our terms and conditions (including refund policy) again here: https://bookings.easyhotel.com/secbook/GB/tandc.html You will need Adobe Acrobat Reader to open the attached invoice. If you do not already have it installed on your computer, you can download it free of charge from http://get.adobe.com/uk/reader/ We wish you a pleasant stay. easyHotel ------=_NextPart_000_057C_01D0A566.DD8BD380 Content-Type: application/pdf; name="Invoice_302232133150612.pdf"; name="Invoice_302232133150612.pdf" Content-Transfer-Encoding: base64 Content-Disposition: attachment; filename="Invoice_302232133150612.pdf" JVBERi0xLjINCg0KNCAwIG9iag0KPDwNCi9FIDYxOTUNCi9IIFsgMTIxMSAxNDUgXQ0KL0wgNjUy MA0KL0xpbmVhcml6ZWQgMQ0KL04gMQ0KL08gNw0KL1QgNjM5MA0KPj4gICAgICAgICAgICAgICAg ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg DQplbmRvYmoNCg0KeHJlZg0KNCA4DQowMDAwMDAwMDEyIDAwMDAwIG4NCjAwMDAwMDEwODggMDAw MDAgbg0KMDAwMDAwMTIxMSAwMDAwMCBuDQowMDAwMDAxMzU2IDAwMDAwIG4NCjAwMDAwMDE2MDcg MDAwMDAgbg0KMDAwMDAwMTcxNiAwMDAwMCBuDQowMDAwMDAxODI0IDAwMDAwIG4NCjAwMDAwMDQz MTkgMDAwMDAgbg0KdHJhaWxlcg0KPDwNCi9BQkNwZGYgODEwNw0KL0lEIFsgPDNENzc5REU2NjU0 RDM4ODczMTYzODBBNTY3REY0MDhFPg0KPDc5OEQ0RjVGNkUyNzIzQjA1MTFCQzU0QzYwMTczRDI3 PiBdDQovSW5mbyAzIDAgUg0KL1ByZXYgNjM4MA0KL1Jvb3QgNSAwIFINCi9TaXplIDEyDQovU291 cmNlIChXZUpYRnh ------=_NextPart_000_057C_01D0A566.DD8BD380-- enmime-0.9.3/testdata/mail/mime-mixed-related.raw000066400000000000000000000017711417532643400217330ustar00rootroot00000000000000Message-ID: <5081A889.3020108@jamehi03lx.noa.com> Date: Fri, 19 Oct 2012 12:22:49 -0700 From: James Hillyerd User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:16.0) Gecko/20121010 Thunderbird/16.0.1 MIME-Version: 1.0 To: greg@inbucket.com Subject: Multipart Mixed Content-Type: multipart/mixed; boundary="Enmime-Test-100" --Enmime-Test-100 Content-Type: multipart/related; boundary="Enmime-Test-101"; type="multipart/alternative" --Enmime-Test-101 Content-Type: multipart/alternative; boundary="Enmime-Test-102"; MIME-Version: 1.0 --Enmime-Test-102 Content-Type: text/html; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable

html1

--Enmime-Test-102-- --Enmime-Test-101-- --Enmime-Test-100 Content-Type: multipart/alternative; boundary="Enmime-Test-103"; MIME-Version: 1.0 --Enmime-Test-103 Content-Type: text/html; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable

html2

--Enmime-Test-103-- --Enmime-Test-100--enmime-0.9.3/testdata/mail/mime-mixed.raw000066400000000000000000000011141417532643400203040ustar00rootroot00000000000000Message-ID: <5081A889.3020108@jamehi03lx.noa.com> Date: Fri, 19 Oct 2012 12:22:49 -0700 From: James Hillyerd User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:16.0) Gecko/20121010 Thunderbird/16.0.1 MIME-Version: 1.0 To: greg@inbucket.com Subject: Multipart Mixed Content-Type: multipart/mixed; boundary="Enmime-Test-100" --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii Section one --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii Section two --Enmime-Test-100-- enmime-0.9.3/testdata/mail/mime-signed.raw000066400000000000000000000032001417532643400204450ustar00rootroot00000000000000Message-ID: <5081A889.3020108@jamehi03lx.noa.com> Date: Fri, 19 Oct 2012 12:22:49 -0700 From: James Hillyerd User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:16.0) Gecko/20121010 Thunderbird/16.0.1 MIME-Version: 1.0 To: greg@inbucket.com Subject: Multipart Signed Content-Type: multipart/mixed; boundary="Enmime-Test-100" --Enmime-Test-100 Content-Type: multipart/signed; boundary="Enmime-Test-200" Content-Disposition: inline --Enmime-Test-200 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii Section one --Enmime-Test-200 Content-Type: application/pgp-signature; name="signature.asc" Content-Description: Digital signature -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.12 (GNU/Linux) iQIVAwUBUvPn/Tk1h9l9hlALAQisew//Ve290uPsmsI4LRoQs/KQPssAdfkxuzNI KG/TBJUkh+h0xcKpdumL/i1I8fuUEB7V9fvb3t5ReMuk+Q7gjXyccW8zhMU9oTaN Xb34fduyx2TcmAJ9eMsjoT7/qfzEd83C1luDc/by+ud/AECcS/Ud0t6LlmcolCY+ B47NhK5QwP+s7fkuYG3jWcXwrEarVcXRd6Wt6EDB4zRNO6UQ5yZK5knRtv9JwE9I 7l1fYUBG6Lsne7m0znaqIEpAvZzAMVYPOjAYX3EZk4a25nDg/PXHl0agtsD5s0c6 lgL1JjVENhM4XGntqreVaK1peE/6P6EUdXkHvuPXEZf+7z8xitnCiM2rQgdR46OI T0jXWzkVeMy54jNzCC03k4uSCkrxnGrPA5bPIn+Sml0NfJEXRH/t+9Cq0Gbn40Xe O4KN+a3YKPu/DGliU0NQzlUBO4TL+2N2YjkYz4akzPjNEPA8tCJ6w76SK+832HCw sOAxlxyYjFIoG6ZLysi5b6u3Nt+9tJHAt9P3/VHy1TsXuY08qlrBWjxe6Hp6tQrC xN47E2pZCC2pd8Z78Te3nIFLbRrGZkuqUyzVue73aLmIyvXombh5NoioN7J5J6sD hCwqizTI/x29sMQecKS2sBWsCH4ohPQcTExrgMftxix+PkW9QEYU5qV3MU8kLqxz at6toX0WKAE= =dBVL -----END PGP SIGNATURE----- --Enmime-Test-200-- --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii Section two --Enmime-Test-100-- enmime-0.9.3/testdata/mail/mime-unquoted-tspecials-param.raw000066400000000000000000000050351417532643400241330ustar00rootroot00000000000000From: "easyHotel Booking" To: Subject: Confirmation of Booking with easyHotel Date: Fri, 12 Jun 2015 23:24:01 +0100 Message-ID: MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="----=_NextPart_000_057C_01D0A566.DD8BD380" X-Mailer: Microsoft CDO for Windows 2000 Content-Class: urn:content-classes:message Importance: normal Priority: normal X-MimeOLE: Produced By Microsoft MimeOLE V6.1.7601.17609 Return-Path: bookings@easyHotel.com X-OriginalArrivalTime: 12 Jun 2015 22:24:01.0592 (UTC) FILETIME=[7BC76B80:01D0A55E] This is a multi-part message in MIME format. ------=_NextPart_000_057C_01D0A566.DD8BD380 Content-Type: text/plain Content-Transfer-Encoding: 7bit Thank you for booking with easyHotel. You may review your booking details here: https://bookings.easyHotel.com/AccClient/MyInformation-MyBookings.asp You may review your account including payment information here: https://bookings.easyHotel.com/AccClient/MyInformation-MyAccount.asp You may obtain general help and assistance here: http://support.easyhotel.com/hc/en-gb/categories/200490531-Frequently-asked-questions You can review our terms and conditions (including refund policy) again here: https://bookings.easyhotel.com/secbook/GB/tandc.html You will need Adobe Acrobat Reader to open the attached invoice. If you do not already have it installed on your computer, you can download it free of charge from http://get.adobe.com/uk/reader/ We wish you a pleasant stay. easyHotel ------=_NextPart_000_057C_01D0A566.DD8BD380 Content-Type: application/pdf; name=Invoice_(302232133150612).pdf Content-Transfer-Encoding: base64 Content-Disposition: attachment; filename="Invoice_(302232133150612).pdf" JVBERi0xLjINCg0KNCAwIG9iag0KPDwNCi9FIDYxOTUNCi9IIFsgMTIxMSAxNDUgXQ0KL0wgNjUy MA0KL0xpbmVhcml6ZWQgMQ0KL04gMQ0KL08gNw0KL1QgNjM5MA0KPj4gICAgICAgICAgICAgICAg ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg DQplbmRvYmoNCg0KeHJlZg0KNCA4DQowMDAwMDAwMDEyIDAwMDAwIG4NCjAwMDAwMDEwODggMDAw MDAgbg0KMDAwMDAwMTIxMSAwMDAwMCBuDQowMDAwMDAxMzU2IDAwMDAwIG4NCjAwMDAwMDE2MDcg MDAwMDAgbg0KMDAwMDAwMTcxNiAwMDAwMCBuDQowMDAwMDAxODI0IDAwMDAwIG4NCjAwMDAwMDQz MTkgMDAwMDAgbg0KdHJhaWxlcg0KPDwNCi9BQkNwZGYgODEwNw0KL0lEIFsgPDNENzc5REU2NjU0 RDM4ODczMTYzODBBNTY3REY0MDhFPg0KPDc5OEQ0RjVGNkUyNzIzQjA1MTFCQzU0QzYwMTczRDI3 PiBdDQovSW5mbyAzIDAgUg0KL1ByZXYgNjM4MA0KL1Jvb3QgNSAwIFINCi9TaXplIDEyDQovU291 cmNlIChXZUpYRnh ------=_NextPart_000_057C_01D0A566.DD8BD380-- enmime-0.9.3/testdata/mail/non-mime-html-charset-header-only.raw000066400000000000000000000005441417532643400245740ustar00rootroot00000000000000From: alice@intern.com To: bob@extern.com Message-ID: <1913211271.2.1528358425638@zeus> Subject: Test MIME-Version: 1.0 Content-Type: text/html;charset="iso-8859-1" Content-Disposition: inline Content-Transfer-Encoding: quoted-printable Page Title Test M=FCller enmime-0.9.3/testdata/mail/non-mime-html.raw000066400000000000000000000011231417532643400207320ustar00rootroot00000000000000Date: Sun, 14 Oct 2012 16:09:01 -0700 To: greg@inbucket.com From: James Hillyerd Subject: test Sun, 14 Oct 2012 16:09:01 -0700 X-Mailer: swaks v20120320.0 jetmore.org/john/code/swaks/ Content-Type: text/html; charset="ISO-8859-1" Document Title

This is a test mailing

enmime-0.9.3/testdata/mail/non-mime-missing-charset.raw000066400000000000000000000005531417532643400230740ustar00rootroot00000000000000Date: Sun, 14 Oct 2012 16:09:01 -0700 To: greg@inbucket.com From: James Hillyerd Subject: test Sun, 14 Oct 2012 16:09:01 -0700 X-Mailer: swaks v20120320.0 jetmore.org/john/code/swaks/ Content-Type: text/html Unencoded Euro Sign: € enmime-0.9.3/testdata/mail/non-mime.raw000066400000000000000000000003561417532643400177770ustar00rootroot00000000000000Date: Sun, 14 Oct 2012 16:09:01 -0700 To: greg@inbucket.com From: James Hillyerd Subject: test Sun, 14 Oct 2012 16:09:01 -0700 X-Mailer: swaks v20120320.0 jetmore.org/john/code/swaks/ This is a test mailing enmime-0.9.3/testdata/mail/other-multi-related.raw000066400000000000000000000057761417532643400221620ustar00rootroot00000000000000From: James Hillyerd Subject: MIME test 1 To: greg@nobody.com Date: Mon, 12 Mar 2018 15:01:13 +0100 MIME-Version: 1.0 Content-Type: multipart/related; boundary="----=_NextPart_000_08F0_01D3BA13.018B07B0" This is a multipart message in MIME format. ------=_NextPart_000_08F0_01D3BA13.018B07B0 Content-Type: multipart/alternative; boundary="----=_NextPart_001_08F1_01D3BA13.018B07B0" ------=_NextPart_001_08F1_01D3BA13.018B07B0 Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable Plain text. ------=_NextPart_001_08F1_01D3BA13.018B07B0 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable HTML text. ------=_NextPart_001_08F1_01D3BA13.018B07B0-- ------=_NextPart_000_08F0_01D3BA13.018B07B0 Content-Type: image/png; name="image001.png" Content-Transfer-Encoding: base64 Content-ID: iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ bWFnZVJlYWR5ccllPAAAAlFJREFUeNqUU8tOFEEUPVVdNV3dPe8xYRBnjGhmBgKjKzCIiQvBoIaN bly5Z+PSv3Aj7DSiP2B0rwkLGVdGgxITSCRIJGSMEQWZR3eVt5sEFBgTb/dN1yvnnHtPNTPG4Pqd HgCMXnPRSZrpSuH8vUJu4DE4rYHDGAZDX62BZttHqTiIayM3gGiXQsgYLEvATaqxU+dy1U13YXap XptpNHY8iwn8KyIAzm1KBdtRZWErpI5lEWTXp5Z/vHpZ3/wyKKwYGGOdAYwR0EZwoezTYApBEIOb yELl/aE1/83cp40Pt5mxqCKrE4Ck+mVWKKcI5tA8BLEhRBKJLjez6a7MLq7XZtp+yyOawwCBtkiB VZDKzRk4NN7NQBMYPHiZDFhXY+p9ff7F961vVcnl4R5I2ykJ5XFN7Ab7Gc61VoipNBKF+PDyztu5 lfrSLT/wIwCxq0CAGtXHZTzqR2jtwQiXONma6hHpj9sLT7YaPxfTXuZdBGA02Wi7FS48YiTfj+i2 NhqtdhP5RC8mh2/Op7y0v6eAcWVLFT8D7kWX5S9mepp+C450MV6aWL1cGnvkxbwHtLW2B9AOkLeU d9KEDuh9fl/7CEj7YH5g+3r/lWfF9In7tPz6T4IIwBJOr1SJyIGQMZQbsh5P9uBq5VJtqHh2mo49 pdw5WFoEwKWqWHacaWOjQXWGcifKo6vj5RGS6zykI587XeUIQDqJSmAp+lE4qt19W5P9o8+Lma5D cjsC8JiT607lMVkdqQ0Vyh3lHhmh52tfNy78ajXv0rgYzv8nfwswANuk+7sD/Q0aAAAAAElFTkSu QmCC ------=_NextPart_000_08F0_01D3BA13.018B07B0 Content-Type: image/png; name="image002.png" Content-Transfer-Encoding: base64 Content-ID: iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ bWFnZVJlYWR5ccllPAAAAlFJREFUeNqUU8tOFEEUPVVdNV3dPe8xYRBnjGhmBgKjKzCIiQvBoIaN bly5Z+PSv3Aj7DSiP2B0rwkLGVdGgxITSCRIJGSMEQWZR3eVt5sEFBgTb/dN1yvnnHtPNTPG4Pqd HgCMXnPRSZrpSuH8vUJu4DE4rYHDGAZDX62BZttHqTiIayM3gGiXQsgYLEvATaqxU+dy1U13YXap XptpNHY8iwn8KyIAzm1KBdtRZWErpI5lEWTXp5Z/vHpZ3/wyKKwYGGOdAYwR0EZwoezTYApBEIOb yELl/aE1/83cp40Pt5mxqCKrE4Ck+mVWKKcI5tA8BLEhRBKJLjez6a7MLq7XZtp+yyOawwCBtkiB VZDKzRk4NN7NQBMYPHiZDFhXY+p9ff7F961vVcnl4R5I2ykJ5XFN7Ab7Gc61VoipNBKF+PDyztu5 lfrSLT/wIwCxq0CAGtXHZTzqR2jtwQiXONma6hHpj9sLT7YaPxfTXuZdBGA02Wi7FS48YiTfj+i2 NhqtdhP5RC8mh2/Op7y0v6eAcWVLFT8D7kWX5S9mepp+C450MV6aWL1cGnvkxbwHtLW2B9AOkLeU d9KEDuh9fl/7CEj7YH5g+3r/lWfF9In7tPz6T4IIwBJOr1SJyIGQMZQbsh5P9uBq5VJtqHh2mo49 pdw5WFoEwKWqWHacaWOjQXWGcifKo6vj5RGS6zykI587XeUIQDqJSmAp+lE4qt19W5P9o8+Lma5D cjsC8JiT607lMVkdqQ0Vyh3lHhmh52tfNy78ajXv0rgYzv8nfwswANuk+7sD/Q0aAAAAAElFTkSu QmCC ------=_NextPart_000_08F0_01D3BA13.018B07B0-- enmime-0.9.3/testdata/mail/other-parts.raw000066400000000000000000000012371417532643400205270ustar00rootroot00000000000000From: Valere JEANTET Subject: Emoji from Gmail Date: Thu, 18 Oct 2012 22:48:39 -0700 To: contact@vjeantet.fr Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="Enmime-Test-100" --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii A text section --Enmime-Test-100 Content-Type: image/gif; name="B05.gif" Content-Transfer-Encoding: base64 X-Attachment-Id: B05@goomoji.gmail Content-ID: R0lGODlhDwAPAKIFAN7r81uw7ACJ46PQ7QBGdN3t+gAAAAAAACH5BAEAAAUALAAAAAAPAA8AAANA WCWkS7A5AUajI1tHRmidIAaf0pVFRAjoKTlp670MQUqugs0cn86vH8M0GMJCuIDxGITAntDo8gG1 Ga1BUzObAAA7 --Enmime-Test-100-- enmime-0.9.3/testdata/mail/qp-ascii-header.raw000066400000000000000000000003531417532643400212110ustar00rootroot00000000000000Date: Sun, 14 Oct 2012 16:09:01 -0700 To: greg@inbucket.com From: James Hillyerd Subject: =?US-ASCII?Q?Test_QP_Subject=21?= X-Mailer: swaks v20120320.0 jetmore.org/john/code/swaks/ This is a test mailing enmime-0.9.3/testdata/mail/qp-utf8-header-no-break.raw000066400000000000000000000076541417532643400225160ustar00rootroot00000000000000Message-ID: <5081A889.3020108@jamehi03lx.noa.com> Date: Fri, 19 Oct 2012 12:22:49 -0700 From: James Hillyerd , =?ISO-8859-1?Q?Andr=E9?= Pirard Sender: =?ISO-8859-1?Q?Andr=E9?= Pirard User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:16.0) Gecko/20121010 Thunderbird/16.0.1 MIME-Version: 1.0 To: =?UTF-8?Q?Miros=C5=82aw_Marczak?= Subject: =?utf-8?q?MIME_UTF8_Test_=c2=a2?= More Text Content-Type: multipart/alternative; boundary="------------020203040006070307010003" This is a multi-part message in MIME format. --------------020203040006070307010003 Content-Type: text/plain; charset=ISO-8859-1 Content-Transfer-Encoding: quoted-printable Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam = venenatis ante fermentum justo varius hendrerit. Sed adipiscing = adipiscing nisi at placerat. Sed luctus neque pellentesque sem laoreet = dapibus. Class aptent taciti sociosqu ad litora torquent per conubia = nostra, per inceptos himenaeos. In eu scelerisque nibh. Fusce faucibus, = nisl vel tincidunt sodales, quam est condimentum lorem, vitae semper = lacus nisl vel lacus. Vestibulum vitae iaculis urna. Donec pellentesque = pellentesque ipsum sit amet suscipit. Ut aliquam vestibulum justo sit = amet congue. Mauris nisl lacus, varius a ultrices id, suscipit mattis = libero. Donec iaculis dapibus purus in accumsan. Cras faucibus, orci dictum = molestie congue, neque magna adipiscing lacus, ut laoreet sapien ante et = mi. Suspendisse potenti. Nullam sed dui magna. Nullam vel purus augue, = imperdiet vehicula purus. Lorem ipsum dolor sit amet, consectetur = adipiscing elit. In aliquet, ante at tincidunt ultricies, arcu dolor = rutrum odio, quis tempus lacus leo a quam. Fusce hendrerit, urna sed = elementum pulvinar, dolor sem imperdiet arcu, ut ornare erat metus sit = amet diam. Duis sagittis libero ut metus vulputate dictum. Etiam eget = augue dolor, in lacinia felis. Nulla facilisi. Proin sollicitudin = laoreet vehicula. Praesent non nibh odio. --------------020203040006070307010003 Content-Type: text/html; charset=ISO-8859-1 Content-Transfer-Encoding: 7bit

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam venenatis ante fermentum justo varius hendrerit. Sed adipiscing adipiscing nisi at placerat. Sed luctus neque pellentesque sem laoreet dapibus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In eu scelerisque nibh. Fusce faucibus, nisl vel tincidunt sodales, quam est condimentum lorem, vitae semper lacus nisl vel lacus. Vestibulum vitae iaculis urna. Donec pellentesque pellentesque ipsum sit amet suscipit. Ut aliquam vestibulum justo sit amet congue. Mauris nisl lacus, varius a ultrices id, suscipit mattis libero.

Donec iaculis dapibus purus in accumsan. Cras faucibus, orci dictum molestie congue, neque magna adipiscing lacus, ut laoreet sapien ante et mi. Suspendisse potenti. Nullam sed dui magna. Nullam vel purus augue, imperdiet vehicula purus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. In aliquet, ante at tincidunt ultricies, arcu dolor rutrum odio, quis tempus lacus leo a quam. Fusce hendrerit, urna sed elementum pulvinar, dolor sem imperdiet arcu, ut ornare erat metus sit amet diam. Duis sagittis libero ut metus vulputate dictum. Etiam eget augue dolor, in lacinia felis. Nulla facilisi. Proin sollicitudin laoreet vehicula. Praesent non nibh odio.

--------------020203040006070307010003-- enmime-0.9.3/testdata/mail/qp-utf8-header-recursed.raw000066400000000000000000000101761417532643400226250ustar00rootroot00000000000000Message-ID: <5081A889.3020108@jamehi03lx.noa.com> Date: Fri, 19 Oct 2012 12:22:49 -0700 From: James Hillyerd , =?UTF-8? Q?=3D=3Futf-8=3FQ=3FWirelessCaller=5F=3D28203=3D29=5F4?= =?UTF-8?Q?02-5984=5FWirelessCaller=5F=3D28203=3D29=5F402-=3F?= =?UTF-8?Q?=3D=0A_=3D=3Futf-8=3FQ=3F5984=5FWirelessCaller=5F=3D?= =?UTF-8?Q?28203=3D29=5F402-5984=3F=3D?= Sender: =?ISO-8859-1?Q?Andr=E9?= Pirard User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:16.0) Gecko/20121010 Thunderbird/16.0.1 MIME-Version: 1.0 To: =?UTF-8?Q?Miros=C5=82aw_Marczak?= Subject: =?utf-8?q?MIME_UTF8_Test_=c2=a2?= More Text Content-Type: multipart/alternative; boundary="------------020203040006070307010003" This is a multi-part message in MIME format. --------------020203040006070307010003 Content-Type: text/plain; charset=ISO-8859-1 Content-Transfer-Encoding: quoted-printable Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam = venenatis ante fermentum justo varius hendrerit. Sed adipiscing = adipiscing nisi at placerat. Sed luctus neque pellentesque sem laoreet = dapibus. Class aptent taciti sociosqu ad litora torquent per conubia = nostra, per inceptos himenaeos. In eu scelerisque nibh. Fusce faucibus, = nisl vel tincidunt sodales, quam est condimentum lorem, vitae semper = lacus nisl vel lacus. Vestibulum vitae iaculis urna. Donec pellentesque = pellentesque ipsum sit amet suscipit. Ut aliquam vestibulum justo sit = amet congue. Mauris nisl lacus, varius a ultrices id, suscipit mattis = libero. Donec iaculis dapibus purus in accumsan. Cras faucibus, orci dictum = molestie congue, neque magna adipiscing lacus, ut laoreet sapien ante et = mi. Suspendisse potenti. Nullam sed dui magna. Nullam vel purus augue, = imperdiet vehicula purus. Lorem ipsum dolor sit amet, consectetur = adipiscing elit. In aliquet, ante at tincidunt ultricies, arcu dolor = rutrum odio, quis tempus lacus leo a quam. Fusce hendrerit, urna sed = elementum pulvinar, dolor sem imperdiet arcu, ut ornare erat metus sit = amet diam. Duis sagittis libero ut metus vulputate dictum. Etiam eget = augue dolor, in lacinia felis. Nulla facilisi. Proin sollicitudin = laoreet vehicula. Praesent non nibh odio. --------------020203040006070307010003 Content-Type: text/html; charset=ISO-8859-1 Content-Transfer-Encoding: 7bit

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam venenatis ante fermentum justo varius hendrerit. Sed adipiscing adipiscing nisi at placerat. Sed luctus neque pellentesque sem laoreet dapibus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In eu scelerisque nibh. Fusce faucibus, nisl vel tincidunt sodales, quam est condimentum lorem, vitae semper lacus nisl vel lacus. Vestibulum vitae iaculis urna. Donec pellentesque pellentesque ipsum sit amet suscipit. Ut aliquam vestibulum justo sit amet congue. Mauris nisl lacus, varius a ultrices id, suscipit mattis libero.

Donec iaculis dapibus purus in accumsan. Cras faucibus, orci dictum molestie congue, neque magna adipiscing lacus, ut laoreet sapien ante et mi. Suspendisse potenti. Nullam sed dui magna. Nullam vel purus augue, imperdiet vehicula purus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. In aliquet, ante at tincidunt ultricies, arcu dolor rutrum odio, quis tempus lacus leo a quam. Fusce hendrerit, urna sed elementum pulvinar, dolor sem imperdiet arcu, ut ornare erat metus sit amet diam. Duis sagittis libero ut metus vulputate dictum. Etiam eget augue dolor, in lacinia felis. Nulla facilisi. Proin sollicitudin laoreet vehicula. Praesent non nibh odio.

--------------020203040006070307010003-- enmime-0.9.3/testdata/mail/qp-utf8-header.raw000066400000000000000000000076561417532643400210240ustar00rootroot00000000000000Message-ID: <5081A889.3020108@jamehi03lx.noa.com> Date: Fri, 19 Oct 2012 12:22:49 -0700 From: James Hillyerd , =?ISO-8859-1?Q?Andr=E9?= Pirard Sender: =?ISO-8859-1?Q?Andr=E9?= Pirard User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:16.0) Gecko/20121010 Thunderbird/16.0.1 MIME-Version: 1.0 To: =?UTF-8?Q?Miros=C5=82aw_Marczak?= Subject: =?utf-8?q?MIME_UTF8_Test_=c2=a2?= More Text Content-Type: multipart/alternative; boundary="------------020203040006070307010003" This is a multi-part message in MIME format. --------------020203040006070307010003 Content-Type: text/plain; charset=ISO-8859-1 Content-Transfer-Encoding: quoted-printable Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam = venenatis ante fermentum justo varius hendrerit. Sed adipiscing = adipiscing nisi at placerat. Sed luctus neque pellentesque sem laoreet = dapibus. Class aptent taciti sociosqu ad litora torquent per conubia = nostra, per inceptos himenaeos. In eu scelerisque nibh. Fusce faucibus, = nisl vel tincidunt sodales, quam est condimentum lorem, vitae semper = lacus nisl vel lacus. Vestibulum vitae iaculis urna. Donec pellentesque = pellentesque ipsum sit amet suscipit. Ut aliquam vestibulum justo sit = amet congue. Mauris nisl lacus, varius a ultrices id, suscipit mattis = libero. Donec iaculis dapibus purus in accumsan. Cras faucibus, orci dictum = molestie congue, neque magna adipiscing lacus, ut laoreet sapien ante et = mi. Suspendisse potenti. Nullam sed dui magna. Nullam vel purus augue, = imperdiet vehicula purus. Lorem ipsum dolor sit amet, consectetur = adipiscing elit. In aliquet, ante at tincidunt ultricies, arcu dolor = rutrum odio, quis tempus lacus leo a quam. Fusce hendrerit, urna sed = elementum pulvinar, dolor sem imperdiet arcu, ut ornare erat metus sit = amet diam. Duis sagittis libero ut metus vulputate dictum. Etiam eget = augue dolor, in lacinia felis. Nulla facilisi. Proin sollicitudin = laoreet vehicula. Praesent non nibh odio. --------------020203040006070307010003 Content-Type: text/html; charset=ISO-8859-1 Content-Transfer-Encoding: 7bit

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam venenatis ante fermentum justo varius hendrerit. Sed adipiscing adipiscing nisi at placerat. Sed luctus neque pellentesque sem laoreet dapibus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In eu scelerisque nibh. Fusce faucibus, nisl vel tincidunt sodales, quam est condimentum lorem, vitae semper lacus nisl vel lacus. Vestibulum vitae iaculis urna. Donec pellentesque pellentesque ipsum sit amet suscipit. Ut aliquam vestibulum justo sit amet congue. Mauris nisl lacus, varius a ultrices id, suscipit mattis libero.

Donec iaculis dapibus purus in accumsan. Cras faucibus, orci dictum molestie congue, neque magna adipiscing lacus, ut laoreet sapien ante et mi. Suspendisse potenti. Nullam sed dui magna. Nullam vel purus augue, imperdiet vehicula purus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. In aliquet, ante at tincidunt ultricies, arcu dolor rutrum odio, quis tempus lacus leo a quam. Fusce hendrerit, urna sed elementum pulvinar, dolor sem imperdiet arcu, ut ornare erat metus sit amet diam. Duis sagittis libero ut metus vulputate dictum. Etiam eget augue dolor, in lacinia felis. Nulla facilisi. Proin sollicitudin laoreet vehicula. Praesent non nibh odio.

--------------020203040006070307010003-- enmime-0.9.3/testdata/mail/quoted-printable-mime.raw000066400000000000000000000073711417532643400224700ustar00rootroot00000000000000Message-ID: <5081A889.3020108@jamehi03lx.noa.com> Date: Fri, 19 Oct 2012 12:22:49 -0700 From: James Hillyerd User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:16.0) Gecko/20121010 Thunderbird/16.0.1 MIME-Version: 1.0 To: greg@inbucket.com Subject: MIME Quoted Printable Content-Type: multipart/alternative; boundary="------------020203040006070307010003" This is a multi-part message in MIME format. --------------020203040006070307010003 Content-Type: text/plain; charset=ISO-8859-1 Content-Transfer-Encoding: quoted-printable Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam = venenatis ante fermentum justo varius hendrerit. Sed adipiscing = adipiscing nisi at placerat. Sed luctus neque pellentesque sem laoreet = dapibus. Class aptent taciti sociosqu ad litora torquent per conubia = nostra, per inceptos himenaeos. In eu scelerisque nibh. Fusce faucibus, = nisl vel tincidunt sodales, quam est condimentum lorem, vitae semper = lacus nisl vel lacus. Vestibulum vitae iaculis urna. Donec pellentesque = pellentesque ipsum sit amet suscipit. Ut aliquam vestibulum justo sit = amet congue. Mauris nisl lacus, varius a ultrices id, suscipit mattis = libero. Donec iaculis dapibus purus in accumsan. Cras faucibus, orci dictum = molestie congue, neque magna adipiscing lacus, ut laoreet sapien ante et = mi. Suspendisse potenti. Nullam sed dui magna. Nullam vel purus augue, = imperdiet vehicula purus. Lorem ipsum dolor sit amet, consectetur = adipiscing elit. In aliquet, ante at tincidunt ultricies, arcu dolor = rutrum odio, quis tempus lacus leo a quam. Fusce hendrerit, urna sed = elementum pulvinar, dolor sem imperdiet arcu, ut ornare erat metus sit = amet diam. Duis sagittis libero ut metus vulputate dictum. Etiam eget = augue dolor, in lacinia felis. Nulla facilisi. Proin sollicitudin = laoreet vehicula. Praesent non nibh odio. --------------020203040006070307010003 Content-Type: text/html; charset=ISO-8859-1 Content-Transfer-Encoding: 7bit

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam venenatis ante fermentum justo varius hendrerit. Sed adipiscing adipiscing nisi at placerat. Sed luctus neque pellentesque sem laoreet dapibus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In eu scelerisque nibh. Fusce faucibus, nisl vel tincidunt sodales, quam est condimentum lorem, vitae semper lacus nisl vel lacus. Vestibulum vitae iaculis urna. Donec pellentesque pellentesque ipsum sit amet suscipit. Ut aliquam vestibulum justo sit amet congue. Mauris nisl lacus, varius a ultrices id, suscipit mattis libero.

Donec iaculis dapibus purus in accumsan. Cras faucibus, orci dictum molestie congue, neque magna adipiscing lacus, ut laoreet sapien ante et mi. Suspendisse potenti. Nullam sed dui magna. Nullam vel purus augue, imperdiet vehicula purus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. In aliquet, ante at tincidunt ultricies, arcu dolor rutrum odio, quis tempus lacus leo a quam. Fusce hendrerit, urna sed elementum pulvinar, dolor sem imperdiet arcu, ut ornare erat metus sit amet diam. Duis sagittis libero ut metus vulputate dictum. Etiam eget augue dolor, in lacinia felis. Nulla facilisi. Proin sollicitudin laoreet vehicula. Praesent non nibh odio.

--------------020203040006070307010003-- enmime-0.9.3/testdata/mail/quoted-printable.raw000066400000000000000000000015241417532643400215350ustar00rootroot00000000000000From: James Hillyerd Content-Type: text/plain; charset=us-ascii Content-Transfer-Encoding: quoted-printable Subject: Quoted Printable Date: Thu, 18 Oct 2012 22:48:39 -0700 Message-Id: <07B7061D-2676-487E-942E-C341CE4D13DC@makita.skynet> To: greg@inbucket Mime-Version: 1.0 (Apple Message framework v1283) X-Mailer: Apple Mail (2.1283) Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus sit = amet arcu non lacus porta faucibus. Nulla gravida tempus rutrum. = Maecenas vehicula cursus libero sed faucibus. Morbi iaculis interdum = lacus, eget porta turpis ultrices id. Nulla sit amet massa mauris. Morbi = augue tellus, pharetra a varius at, dignissim eget orci. Nulla molestie = interdum tortor, id tincidunt purus lacinia ac. Integer sodales velit = sed neque faucibus egestas eu vel dolor.=20= enmime-0.9.3/testdata/mail/unknown-part-type.raw000066400000000000000000000012431417532643400216760ustar00rootroot00000000000000From: Valere JEANTET Subject: Emoji from Gmail Date: Thu, 18 Oct 2012 22:48:39 -0700 To: contact@vjeantet.fr Mime-Version: 1.0 Content-Type: multipart/abcdefghi; boundary="Enmime-Test-100" --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii A text section --Enmime-Test-100 Content-Type: image/gif; name="B05.gif" Content-Transfer-Encoding: base64 X-Attachment-Id: B05@goomoji.gmail Content-ID: R0lGODlhDwAPAKIFAN7r81uw7ACJ46PQ7QBGdN3t+gAAAAAAACH5BAEAAAUALAAAAAAPAA8AAANA WCWkS7A5AUajI1tHRmidIAaf0pVFRAjoKTlp670MQUqugs0cn86vH8M0GMJCuIDxGITAntDo8gG1 Ga1BUzObAAA7 --Enmime-Test-100-- enmime-0.9.3/testdata/parts/000077500000000000000000000000001417532643400157505ustar00rootroot00000000000000enmime-0.9.3/testdata/parts/badboundary.raw000066400000000000000000000004571417532643400207630ustar00rootroot00000000000000Content-Type: multipart/alternative; boundary="Enmime-Test-100" --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii A text section --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/html; charset=us-ascii An HTML section --Enmime-Test-100 enmime-0.9.3/testdata/parts/barren-content-type.raw000066400000000000000000000001521417532643400223610ustar00rootroot00000000000000Content-Type: ; name="" Content-Transfer-Encoding: base64 Content-Disposition: attachment; filename="" enmime-0.9.3/testdata/parts/bin-attach.raw000066400000000000000000000006131417532643400204750ustar00rootroot00000000000000Content-Type: multipart/mixed; boundary="Enmime-Test-100" --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii A text section --Enmime-Test-100 Content-Transfer-Encoding: base64 Content-Type: application/octet-stream; name="test.bin"; charset="us-ascii" Content-Disposition: attachment; filename=test.bin UEsDBBQACAAIAMICKUoAAA== --Enmime-Test-100-- enmime-0.9.3/testdata/parts/chardet-fail-non-txt.raw000066400000000000000000000003361417532643400224150ustar00rootroot00000000000000Content-Type: image/gif; name="rzkly.gif" Content-Transfer-Encoding: base64 Content-ID: R0lGODlhZAAEAIAAAABmzGb/mSH5BAAAAAAALAAAAABkAAQAAAIajI+py+0Po5y02uuA3rz7D4bi SJbmiabqKhYAOw== enmime-0.9.3/testdata/parts/chardet-fail-not-long-enough.raw000066400000000000000000000001421417532643400240210ustar00rootroot00000000000000Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: base64 5ZKM5byf5byfDQo= enmime-0.9.3/testdata/parts/chardet-fail.raw000066400000000000000000000003371417532643400210110ustar00rootroot00000000000000Content-Type: text/plain; name="rzkly.txt" Content-Transfer-Encoding: base64 Content-ID: R0lGODlhZAAEAIAAAABmzGb/mSH5BAAAAAAALAAAAABkAAQAAAIajI+py+0Po5y02uuA3rz7D4bi SJbmiabqKhYAOw== enmime-0.9.3/testdata/parts/chardet-success-big-5.raw000066400000000000000000000320171417532643400224470ustar00rootroot00000000000000Content-Type: text/plain; charset=us-ascii Content-Transfer-Encoding: base64 w0OsT65hsFYgIKVfu/QgIMNDpKexwA0KDQqo97LEpEOhQK21w+OhQML4w8ChQLLX qO4NCg0KrbXD47LEpFGkSw0KDQogICAgpNKkRaZ7pKekSKFBqKW7eaSjplChQaXN pcGkd6jToUGpVLFgtU2ob6FDptuhbaxLrO6hbrzQu/SopaSnDQq2x6FBoXHC98TM oXKl2Lehtfykp7hnoUGmubtcqOS4+6n6pKeq7KRdoUOr4aaztK22r7XboW2k6Kil oW6hQajkDQqopaRqs8ahQ7VNrNKm0qZXqqukp6ZQsqehQaSjxePBbsWqpKesT6tE pF2hQ7ZlvkelyKpgpLu4Z6FBsKq7pLjRDQqhbadmxP2hbqFCoW2yYatuoW6hQbNc t1azeaFtu6Gk5aFuoUG8Qr9Ru3OhbcTAplehbqFBqWyms8S0qnCwssLHDQqlSMPS rbWmcqbVoUOm06Vqu3m7UKS1ru2nT6FBqOS2obu0rauyTb9CoUG1U6W8pWm+5aFG pVulSKS6qKWlfqilDQqhQqvmqKWufailoULFqq1ZpKfD/qFBr3Goz6RIusOhQ65d qPuopbPQoW26uLaurbW4caFuoUGsT7p+pb2kSL9XDQqqvqTPu3mhQ6bcqfPDUaVA oUGmuajGpGqm5qFDsKq2UbZtpL2ko7jRpM+7eaFBpUissCAgqceyp6FDptuv97PW DQqr4aFBrbXD/b5XpVihQaZVprOkZ623oUG7vKzbq0SvuqFBq/ywqKSnv9mhQaW8 qr6xRaxPoUOmQKVIq9Kk/bOjDQqotqFBsNGu1aToq1WhQabS8vKlaqS1oUGssKSn p+mwSqFD3Xym07ZxpKehQb9XqvezrrtQrKWkVabVoUMNCiAgICCrbqTopPSkZ6lN rFihQajkrbWyTcF8ptOkwbjaoUGloqZir0KyTKFBqOTD46Zou8CrVaFDpV+k6KRz pHQNCrJgq3ChQajkrbWoSL9CptMgILZ3oUGxb6jkveiqvaFBqOTD46ZopWq7eaFD tU2rYbDDp2ekbKFBq26k6KywwHUNCqFGvlu4zKRwpEihQaVfpOissLdVoUOp9qpB ptO7UKSnvc2hQatupOikaLFmoUG8xqilpWnFR6FGuWqrrqbTxaUNCqjku3mhQaVf pOi0wrOloUGy16Tpw/ikwKFDptOrbqxWp2ShQrZWoUGlX8L4pmm4uKFBrNKms7Jg ufqhQaSjpWkNCqjjvdehQ6jkwtWloru0t0yqzKFBq2irbqRIpUi/+qywskOhQaVI pdussK5noUGlSL3irLC4cqFBpUisT6ywr+cNCqFGpV+kSKVIsWassKanoUGlSKZw rLC+p6FBpUi1taywqW6hQaVIrKKssKqtoUOmcKa5pKeo0qFBqOKloqzGpmgNCqFD pty/86R3qNOhQbDfqKOxWqRsrPmhQrFawqSo+8u7oUGn9a+qpK+hQqf1vaulU6fM oUG74ajGqKW1/KFBpNYNCqywpMGlv6FDp/Wpdbhgtduhba21w/2oTbrDoW6hQa7J prO/+aWioUa2p6XwpKezeaFtpMHD/aFuoUGu7aywsqgNCrOloUOnXq5hqOCka6FB wfamYqvEuFihQatLuqW3/qW/pKehRqRAqKWzX7TAoUGlSKywpHa4b6hvoUOkqqyw q34NCqqroUGlvKbSrtGwT6rMoUGko7Sxu7OmV6FBpryx5KnSqr6kXaFDDQogICAg pWqktailu3mhQa7Jq1Wko6ZQoUa12616pKekSKFBt6GhQq5MplWyp6FDoW27Yb5l sFa1/qFuoUGkz977DQqssKhqveahQaTPq72ssKnzqMShRqFtvtSw6rWmoW6ttaZG rLCnS6FBoW2/cKTRpGy2x6FurbW/z6ywtqGhRqFtDQq7oaTloW6ttbGurLC0xqFB xaql16ywsnKhRqFtpnKqTKFurbWs3aywpGalzKTPoUGttab5rLCor6FGoW3D/baw DQqhbqVIpqihQqS0oUKnu6FCtW6mWKaoqOLD/aFBrLChQqlfoUKvcaFCpdukwKdA pXyzuaFGp/W1bqFtwW7D/qFuDQqlSKh0rbWs/aFBvEKp96l2oW2pUKl4rbWhbsWq rbytWanToUamuajSrMa8c6FBpbK2t6bSrtWhQ6tlpUCkz7t5DQqhQaRTpmiko6TB oUGufaVQpcGhbaTyuNattaFupM/GSqywpmK7uKFBoW2lqrbHrbWhbqTB3dyssK57 vXShQaSjDQqlaajMq0ihQaXnrLCys6hvoUOktaSnvsekaKFBu3ml56Sjpb+hRqVq v1em86RIoUGlssCzwEio5LCwu/elR6FIDQqhbbNxq1Wk5aFupOqhR6F1pEqrx6hE pOq3aqFDoXakz6ywpVOrSqFDtU2raKVTt+2ttanSumGkz6FDpLWlX6tVDQqzcabm prmttaFBpeelart5pKeko6Vppc6qzKFD8ELqZKFBvnykSMRfpcmhQbftrbWnRbfQ oUGmv6turNKttcO/DQqrzKSnw7+hQ6fBpHO37a21rLCpX6FBpr+rbqzSqUmssK+r rOmkp6V1oUOmv7Ous7SoU6FBprmttbNRqfPD9qSkDQqhQaSjqr6kR6rMpvOp0qnT rtehQ6VIp16yTL7HoUGlvKSnq2W7RKRdoUMNCiAgICClX6RIpKettaFBpmilSMF8 oUKy96ywr3ihRrDfp/WpdbhgpKqhR6F1u/Su2aS9u1C63qXyqfOleKRXv9ENCqXv svehQapGs6Kk+rHmqKOu2aS9pGa2fabTpKOzrKFBrEeqvqnSqKWqzLL3pF2hQ7VN q2iy96FCr3ilsqSjplANCqlJoUOhdqa5rLCqvq21qG+hQw0KICAgIKTSqqvF6abb prO66/nSoUG66/nSv9ekp6ZutGOhRqRIpN+ms6nSpWio+qFBpWio+r/XpKembrRj oUOmuQ0KrbWoo6nzuK+seKFCrn3C5KFDptOqZaVfvsekaMWqoW2pfK7RoW62s6Zu pc20Y7H+oUOsT6ywpEC916qrxemhQQ0KpEC0TqRIsaGhQa7tpKOzcahvoUMNCiAg ICCoaqrMoUGoa6RspKes/LrZoUGlaq7Rpmiwsq3JrLCk96RsoUalX6RIuUW1TKRA pEipSaywqGqqzKFBpecNCqnSpbyz66FDsN+63qXyoUKtU7xXpKe4uaFBtreozKZy xaqm1aFDDQogICAgrtehR73RpnKu0aFBsmqqzLO+plehQanOpKq7ebX8oUGs0q21 qfO3XqTPoUOm27ivrHihba1upc6mcq1iDQqhbqTAsmqmcq21sFahR61ZsFam87BW pnehQbftrbWp87depM+hQaF1qfOyarNwu7uhdqFBoXWp87JqucWryKF2DQqhQaF1 smqlzqbwoXahQaF1smqxb6SvoXakp8P+rE+kXaFGrVmwZaV5pM6nVbX8oUG37a21 qG+3XqTPoUGhdaxHDQq62cBzsmqhdqFBoXWsR7rZpuWyaqF2oUGhdaazpcGkSLJq oXahQaF1prOqwL1esmqhdqFBoXWmq6lssmq6uKF2DQqhQaF1rsqhQr5HsmqozKF2 pKfD/qxPpF2hQ6a/q26m3KS1puamuaTAp0+hQaxMtU2p9r7loUam06plpV+yVqZQ DQqkQK21oUHB9qjMpWrFqqFBpKOlaabmqfOktaRdoUMNCiAgICCouKrMoUGlvKl3 pKe1/KFDoW2lqrbHoW6k6qFHoXWko6q+pNGkp7HzvnyouKFIp+2+fKdnprO4b6nz sK0NCq+rqLihSKF2oW2y+KRsoW6kqqFHoXWk0ai4oUimYai4oUihdqFtun6u0aFu pKqhR6F1rE+ouKFIq0SouKFIoXYNCqSnw/6sT6RdoUOm06VfpEinWalJrLCkXaFB peessLt+qG+hQ8P4qsyk6qFHoXWhccO0w+OhcqSqoUehebCuqVsNCqFBoW2p9qFu pKeq+aThqLihSKF6prmkU6ywpbypd8PjpUehSKF2taqk6qFHoXWm86ywpKO6uKFJ pFel/bzQsN0NCqFBpFWk6KZDvHelSKfppKem1aFDoXYNCiAgICCmv6tuvsekaMWq oW2lqrbHoW6hQaRmrNu2x616oUGm26ywpFqo0qFBrXim27HRpOqx0aFBpbSvfaRI rXgNCqTqsdGhQ73RsE+2x6W8qKO4ybHRpM+hQa59pVClwcWqoW2lqrbHoW6hQbDf pECzQqazprmttaFBpFOko6ilptsNCrHRoUKx0aRIpKenT6FBprmssKzvxnem1aFD DQogICAgpWqkSKSqoUehdbtJuGTD+L7joUOhdqVIqOSssMW6sPim26isoUGko6/g p0rAeaRdoUOnXqijpP2rSqV+DQqxraFBu3mmaKSjpb+hQaXnpdGkuqxWveKrT7PF oUGlfrVMqH2udqTNrEem1aFDseelQKazpECrSqFBucG576S4DQqr0ra866ahQabb s6+hdcOotnehdqFBpESmqKF18auscaF2oUGkuKvStaqkp6SqoUehdfGrsqeyRK23 oUGscatEDQqkeqTsoUOhdr/XoXWwcqZ7oXassKF1pcOme6F2oUGkuKvSsdKz+MKy pOWhQcKypOWkqqFHoXmpsKiwp2SkSqFBDQq5RaaopXHB9aFDoXamcKa5pKfD/qFB wXykZqzStU2hQ6S4q9Kk4rHQvdGkbKjNxaqhQaVIprmssLt8oUMNCiAgICCqZaVf pMGn8KZyrLClatp6oUG7UKR1oUKkvaFCpVykVKZypKOmUKFBru2ssLv3pF2hQ6Tx pUCms6RIplcNCsXeoUGm27rZrLDF1qFGple1YqFBptu62ayws0+hRqZXrKmhQabb utmssKhMoUamVyAgoUGm27rZrLDmfKFDq0QNCrDfrbXD/abfv/mhQaXnqM+o5Kjg rl3B17/Qr8mvxqhvoUMNCg0KwvjDwLLEpFGkRQ0KDQogICAgr3Wv867RuPGhQbdM xb2vZLdOoUOmv6tuv86kqqFHoXWk2MN8rtGyqKFBpGSova2xpdikXaFDoXap067K DQqhQqe6vmyrVaFBrNu7UKjGpKehQaxHtUy5ea9Ur1aqzKFDp16lrqnTqvm3fqFB pVupyrdSrauhQanSqKOqa67RDQql56ZooUGm0+b2st+lXKTSu+Gm3KFBuUWko6/g qM6qzKFBqH2l0bVMpMCsR6RdoUO1TabTprnDwKSjtre5TLrrDQqhQ6TSpamqzLPS ptO0vKrMvH6hQbFgrLCkSKnSp9Coz6FBp/PEsaywstahRq2zpfKxTr/yp9mhQbJg prOlSKRdDQqhQw0KICAgIKT9tmik1q23rHmkfqRooUG/vbSyplekSKFBwXylQLGp qr6o5K7RoUHCvaVIr+Cm272qpF2hQ7+9pGy2sw0KqEO826TqoUehdadetduhbbv0 rtGhbqFBsMemqKRAqOWhQaTls7mlsLhxoUGm27/XpWnGW6FGsN+lSLWnuPGxbw0K plehQaXnsqeoxqRdoUOhdqT9vcemYatgsk212KFBpH6+x8B1sdOhQavhwfakSsP2 oUGl57NRwqe5SqFDtVOlSA0KrtGkdaFBsVS58rhPutSkp7ahoUGor61Xtae1eKSn p9ChQbnBrqyr66TqoUehdbCyqM+nXqSjqr6u0aFBpWmkow0KptyktaTpqLihSKF2 pUimucZbpKehQbdWpMWlSK7RptupUqFDwfa1TaFBvHK1VKSnpEihQaVIr+Cu0ane wMKqzA0Kpmiob6FDrEe5RKSjplChQaSjrNussL/RpF2hQw0KICAgILHnpPOvtbvV tLK2aKVIqNOhQadeqKOkR6T9r3Wv86ZoqG+hQa5hpKS5wbFvpFGo96FGpOiqvrOz wfSpfg0KoUKov6XmpnuhQr+9sr2wc73RrtGhQbL2pKOxb7+qpKekp8XpoUGsR6xP rtGkp7JXt72hQ7+9sd+4YKnSxdyhQQ0KpESla614pn6k1q7JqmukXaFDDQogICAg rsqhQqe6pUio06FBpmiv4K7RqsyhQ6xHqOSuyatVoUG7vKzbrFapfKFBqdKms7Oh zG+hQbeipb+lacZbDQqhQaSjtUyrVaZyoUGrRKywpGq3bKFDptyx56TRusqkp7ah oUG0ta23pbzF3KFGpGqmUKSnpb2hQbNftMC0/qXNDQqhQ7+9pGy2s6fvqfamcsXp oUGq8rOupP274abmsLCmcqFGtMKzpbW/tU2hQaVIrLC3oqahoUG1ZarqpKOmqKFB DQqmaKnStsux0aFDptyssKRApnKhQbDfqKO8xsJJoUGpzqZrt3KwdaFBs3arS8Lg sr6hQ7q4q+G8WMR5oUGypKSjDQqlaazdoUOlX7TCs+C2w6SnvmyhQa7RuPG7wK2u oUGlW6VIsU27s7N5pnKhQbVUqeWsxqnzpr+rbqFDpESlSKbKDQqpwKywvH6hQail pM+ssMXcoUGko6XOrLC9faFBsGyo06ywwmuhQafzpc2ssMSsoUGl/aRIrLCm0aFB pnCmuatEDQqkQKFBuU26obhntsehQ7DfprOrwKS4vNCkdanzt6LB9aFBr2Sk36Rw vsehQavhpc2udqSnqsyys6FDrK2p87v0DQqlvaFBr7Wu0cK1vGehQb3lqfOpuaTp pmiob6FDDQogICAgpr+rbr5buMy2oaazoXG1Za7RveGhcqFBpESzs8H0qX6nzKRs p/m5RKRoqdKssKFGqOSkSKW8rMbD0aZyDQqhQbu0rLCteatooUGmq6ZXtlGudqFB pUCrVbbHq0ihQavhpc274aywqdK7fqRdoUMNCiAgICC1ZcO4pKekdaFBpeessKeu qG+hRqbbpWqmV6RooUGmaKnOr+Ckp6FDp16uYbnBprOx56S4q9Kk4rVlws0NCrO2 pdW5zq6wpM6wqLnPoUGl58P4pM6kXaFDqlqvUKTTpGywvq/gvGevdaFBp6SkV7ur q8ihQcBIqXnCSaxWoUENCqdZpqi8xqRIoUGlSLDdtaPAqaFBrNKqvqltpleob6FD v722TqFCvEKntaX9oUK8QsZGoUGow6Tlvsekd6V+oUENCrRfqM6muaproUPm9r5c pWqktaFBr1OlacRft1KhQ61ZqXilvLNxxeOhQahDs1GkvahwqM+lT6FBpeessLVU p9ANCqFDp2S/pMVVpGi63aVYqK208KpGpP2w6qjNraahQavhrLDC7atuqbKmRLq7 sNGteKFBprOkbKTqrnihQabotMINCqSkrtGxy6RIoUGk96RsqMOms7VertGkp8PA oUGk16eupKarQ6FBsWCzUaS4q9Kp0qjPoUGoQ8Nostur66FDtF4NCquwvEKpqKFB 6dGkp6RspF2hQaVLrLDFWcNNqbK63rBPoUKlraTzv6SlT6FBpH6+x6fWpGihQabT tWW1tK3boUMNCqvhwEiqWrOupP2kSri+oUGkVahjpKex0aFBuUWssLOwxUCteLVl pOSmv6Z4vsChQbtQvdGkdaWpwvizQqFDplYNCqjPpFS95bOjpKO+5bVloUGqvblC r8C3fqFBsFqoo6a5rqKlR6FIDQogICAgqbel2qSnp1GhQaVIq8Kk0aRVoUGl/aT9 qdKlSMZbvHe+3L3loUGl58DZqK2kp6vmsMikXaFDpr+rbr/XDQqlQKSnsWCuZ6FB pUissKdMrmehQathsMO+p6XNoUGmaKSjst+muaFGp0+ms7PVrmehQa56pH2q+L1i oUGsSanzDQqt46q6oUG0pcX9pMmtsKFBpUim5sKnsmqhQ6i+v22xRsP4oUGkRrVM qdKvcaFDtsPC96Snq+GhQaa5s065RaRgDQqhQ6plpV+k5aRooUGydr7lp0yuZ6FB q0SqvbivrHikQL1ioUGkd7jRsGynTKFBpFSkRfhAtrChQbFg7d26Yb3nDQqhQ8H2 tU2tbru0uFahQbpJrL7DfqFBpKPEQKa8vfqssKSnoUMNCiAgICCkUrhiqsyhQbh0 pEikp7d+pF2hRqb9qvGlQLVMtF+ozq52oUGmaKSjr+CkpKFDpWqqzKFBpFKlSKhN usMNCqFBpLWkSKXNusOp86RSoUam86rMoUimdblEq0i/0aFBsf2m5qRAqMahQaRS sW+0Y6j2oUGkz6VPDQqhQaa5pKe/16VHoUmlQqRRpKSku6RDoUGlSKywpFek4qFB ssqqvqRqt06hQaRTpKOpZaaxoUOkWq5nqV+wuKFBDQqm27VNpWKmrKFBpvOorL/g pF2hQ6VAtsekqqFHoXW40bOxtqeqzKFBrLCwranStvqhQaei6UmzaL1hoUGmaKSj DQq62a71oUOhdqdexluq8aVqpUio06FBpNe666euqsyhQbDfqMqp0KFCut7geKFC s6K/XKbVoUGs0rVMqXim7KFBDQqmaKnOv6moYaFBprmopaVPpEivcatIoUPFbK3I pUC69MRZsUuhQbFqrXSmuaZXoUGrS6az4Ee7fqFBpee617e9DQqkXaFDpM6sUKTl rbeu8KFBsnako7PSrLCkp6FDp165wb7HoW2ku6TQpqGhbqFBpeetyKVAtqKmbqZL oUG7RbFvDQqhbcBzrbqhbqFCoW2q97m8oW6hQqFtpcncRsXcoW6hQqFtpcm++qFu pFGzXLrYrtGhQbBRqES1TMXnoUG0TaXnDQqurL19oUOkWrOxtqekp7NOoUG7UKTR pmGt0aXNoUGl56ZOpfu8d6ZEoUGko6VppKOrSKFGpv2laLh0rEq7t6FBDQqlQLbH s06u0aFBrNKlWKx5q1WhQailw+O7wLJMoUHF56TWpmumaKFDptymcKTPpOSko6bm oUGzuqVIuUquYKFGDQrCa6fSsUixSqFBpKOnS6X7stehR6nrptOmaKfSoUGl57VM r3GkXaFDDQogICAguuKzTqXnrE+ku8PArW6oxqFGptular6npGi916TRuUShQal3 q9+++qrMoUGs0r7Hs3Gkp6FDtU2laaVIDQqt3an6oUGko6VppUixTbd+oUOmv6tu prm+x67tpNahQbDfrVO2p6+qrs+666SnoUGm7Kbcq26xZKTTpnWhQ6plDQqlX6Zo vuWmubNOoUMNCiAgICDC5aTopKeoxqFBqPqnrrelw/ihQaSjxFWmvLHkpUim26lS pF2hQ7dMuNHDxKnKoUGkcKRwqU2mWKFBqX4NCq5hsW+lSLHPq+ahQaXnrLCz06jG oUGs06hqwcShQq7vpfKz9KtoqOSkSKRdoUMNCiAgICChbcKnoW6k6qFHoXWnZ6Rs tUysR6Sjuf21XrfmoUOhdqVqqNOmV6RooUGmaKnSt1KmbqFDrK2p87HnquwNCqFB puerYaRsrl2hQaSjqr61XqrMoUG4uaazqdLC9qFGpGqmUKVIpb2hQbS1rbe5ebrJ oUO1TabTprm81tj+2P4NCrauvW+hQaazsmCo/at2oUmktaVAprG40aFBwfbF3Knz pWqhQbVTqKylSLpar6uxoaRdoUOw36SjpWmlT6azutkNCsVBoUGoo6fQvrG2UaFB s0Kkp6RVp6ShQaVIqPq03apNp06qpaSnsGShQ8C5pne5RLVTvkSkp6FBqnC6uLHk pUcNCqFJDQogICAgoW2uYbt5oW6k6qFHoXWnZ6RspKOz1aFBrLCo5K3dpua0Y7lE rEekXaFDoXahbb3Xu3mhbqSqoUehdaSjDQqms7PVq9mqzKVHoUissKSnoUG1U73l pUekd6FDoXa1TatouHSkSKSjpc6z1avZrLCx0KFGpv2lSL7Hqsyko6VpDQqxYLrr oUGms67Jr2itwqFBq2jFbKywpKehQbVTs9O5oa25qfy6zqFBpGG1Tbrdp6Sm1aFD ptymcKdkpNOkbKVIDQqssLVMr3GhQalSrbOsTL3XpKehRqT9tcKhQrivrHihQrOz qNSkp657oUGko7NcpdjGW6TisPWhQaa5qMO21L93DQqkp6fTpF2hQ6/gurissKjO oUOlaqywpGqz1atopLu65qFBpHCz1atopEcgIKFBpLW1TL7lqsyhQ6TxpUCp0qbm DQqhQaRAICCkUaRHICChXbTRoV6hQbzGs06yTLV1oUGko6ispWnm9qFDs/IgIKFd tNGhXqazpOK9zaFCp6TB9KSnDQql2KFBu+GssLauwLihRqb9pU+kSK/U5aahQbxv s+C56qZooUGko6VpsWCkXaFDDQogICAgp+uz/aSnwqehQarxpUC3VbrroUOlaqrM oUG56qVIpHCop6FBrLCo5KXapKfFRKRdoUOktatosN+x/ajkDQrFu6FBr3GmaK9x s9+hQaREprOtyqzxoUKxYbxDoUKvVLP9oUKwXKfAoULAc626pKemV6FDqOSk16eu qsyhQaazDQq9rKrhxbuhQ6a8q26pUCAgoUGlsKW/pKekbKFBt3y9XbZQwLKhQbZQ rbKkp6RsoUGow6/gpEC9YqV8pFG+bMW7DQqhQ7ZQpFO5waywpHC72aFBuG2z/ajk pX6hQblqu9mn66SnoUG1TKnSpaKkXaFDpty/86VIqNOhQaXnqKO8c7nnDQqhQsT1 s6690aT9oUGms6a5rtWo46FBwXyw6rlFtUyn67FvpEDFu6rMoUO8dbTRpeeq8aVA tq7AuKFBrvi3VMTADQrlpqFBrsmlaaywpKehQw0KDQqy16jussSkR6RRDQoNCiAg ICCmuqrMoUGkSKSnsWCkwKFBpKOlaadLpF2hQ6depn6kUaRFoUGtyLHnrmGz4LbD oUGo5Lahu1Cl1aRirLANCqXuqsyhQaXnsWC8xr36oUapr6nTvmy61qFBsW+m3Knz pLWhQ6VqpEikqqFHoXWkraRRpKOssKTUoUOhdqdepHcNCqS7pFG+bKFBrEek36la tU2hQaSjpUi03aZ+rLCpwKFDpf2ms623rvCkp69loUGxYLrDqWG1TaFBsuGu0a/A w2gNCqFBpUissKa8u3yhQw0KICAgIKX9p2el/aTSpEis0qW8wdmr2L/zwsKkc6FB rsi4rqa/s66qRrOioUOp07h0pb2hQaR3sdKoRLSts6OhQQ0Ksf3A575Fre2hQ7tY tkC957vIpsqo4qFBpHep87StpnukcK2lpV+mYb9OICChXb9qoV6hQatLrcilu7TC sl+oUw0KoUGsecL3pnCmuaFBvMakUaZ+tqGhQbW0qfPB2bHmoUOktcH2slakQKFB rmG5RMFqvWGhQabzpdG/7Ka5qV7A5w0KuOq2T6FIpUK0rbOjpsO3tKFBtUy0X6Rt v/KhQcHZs1GkVcDjoUGlvKywsW+tcKFDptupU6bbs2ShQbNlpN+o6A0KxeihQ61w p16lU6fMoUGko7ftpUu2aaFGpv2lSKr5sEmhQbCpptez5q56oUGkrapBpKekuqFB s8S1TKRApEihQQ0KvL22VqVMtm2hQbVMtF+46r2uoUaoz6a8taWoSLJfvHKn0KFB pUissKX9pUCkp66ioUasR8BTq1+kSLahoUGkow0KtLG8WaWioUOt3aVIpV+k6KxG sdDEWaTBoUGl/rVMwfSwaKrMrEekXaFDDQogICAgpLWmfqbRr2WrSaFBxWy1Talh qb+hQbBaqESzxsKnpUehSKRApOmp8cF1oUGoTq9EptOkd6FBpKOz0rRfDQq+eqFB wNSlSLFgpuehQ6X9pNKkSLHzrUmkp67JoUHE3aVAr+7ERKFBrmG27qrFraKhQaVT p8ylrq56oUG0w765DQqydsGhoUHCw6S6tUwgIKFdv2qhXqFDp1637apRtMOkR6Rv oUGm57RVpHelfqFBpECko7FvptvASKFBp8mkV7DfDQqsSaRDrFCqT6FGptymcMT6 qbik+qFCpcmzYqFCv/ykSKSnxN2hQajDtrewsazZoUHCs/J3qfq+uaFBrEeko7Fv DQrA56FBuE+7eNl+2X2hQcCxpmKopaV+oUO4/KVIxb6l0qiuoUHFqKRnptOkVaFB pa2mYbVMvFihRq1ZxN+r9LG9DQqko6q+pfyw7KFBt+2/dqRAsPSnQ8DwqfOlqqVr q2Wr4aFBwEissKhwsE+m1aFDxka64aTFs12qRaRMoUGu0rHmDQqyu+2voUGw36RV pdW1sLJNpPSwrrTHoUGko7FvprOwc6bXu+aqR6Snsr2hQ7/LpM2o0+xd5HSqzKFB pECs0qnaDQqkp6FDpryx5K1ZuUinXqTfoUGms6Vbpf2nraFBq2iztKT3pKOntaFB pmKmvKZ3pUehSKjkpLqo5aVcvHehQcBIDQqkT6nSptyhQaTFy9K63KXNuOqhQajP reG+a6RdoUOlfK7Jsr2qwaFBqVChQqTVqdKx0KFBsf2kSKTFprqo5L/LDQqhQaSj p9GntblEpF2hQ6hEvdGkuqjloUGraLVMr3GyaqFDsf6lzaywpKehQcK9vFe4b7LW oUOtWbP4qsm3paSnDQq8d6FBwffFU6SntGShQaazrsnCTqjRoUGkzqRDpOulYqq7 xPWs1qFBseap86a8pF2hQw0KICAgIKTVpGykp7iuv8ukXaFBpKqhR6F1pWqqzKFB udOm06SjvFihQ6VDqkam6KtupV+kp6RIpF2hQaSjpWmlSA0KpbHD0aRdoUOhdqnz rE+ryqSnsVKlfKTYoUO1Tatop2ekbMCzpUCm5rlEoUGl56azpKOmdbxYudOkp67J oUGqcA0KrLCoxrvaqdK5R6RdoUmnXqS1xfmuyKFBqK2tWa9CtrOhQbO6pbyqvqbz tm2sT6deuK6mYaFGsN+37a7wtbSrSw0Krkmkp6bVoUOmvLHkqXmlSLbHt360raZX rLCwyKFBpKOlacVVxcqmtMRboUGlSKj62KGoU6RdoUM= enmime-0.9.3/testdata/parts/chardet-success-iso-8859-1.raw000066400000000000000000002520571417532643400231170ustar00rootroot00000000000000Content-Type: text/plain; charset=us-ascii Content-Transfer-Encoding: base64 RElFIERyYWNoZW5r9nBmZSB1bnNlcmVyIEJvb3RlIGJvZ2VuIHVtIGRhcyBnZWxi ZSBTZWdlbC4gRGllIFBhcmFkZSB2b2xsem9nDQpzaWNoIGluIGVsZWdhbnRlbSBS YXVzY2hlbiwgd2lyIHdvbGx0ZW4gbWl0IE9zdHdpbmQgYW4gZGFzIGFuZGVyZSBF bmRlLCBiZWkNCk9zdHdpbmQgYW5kZXJ0aGFsYiBTdHVuZGVuIGRhY2h0ZW4gd2ly LCBlcyB3YXJlbiBkcmVp32lnIEtpbG9tZXRlci4gRGllDQpGbG90dGlsbGUgbGFn IGluIGVpbmVyIExpbmllLiBEaWUgUnVkZXIgc2FuZ2VuIGR1bXBmIHZlcmtuYXR0 ZXJ0LiBEYW5uDQpzY2jkdW10ZSBkYXMgV2Fzc2VyIGxvcywgdW5kIGRpZSBTZWdl bCBiZXVndGVuIHNpY2ggYWxsZS4NCg0KV2lyIGZ1aHJlbiBpbiBnbGVpY2hlciBM YWdlIHN0ZWlsIGluIGRpZSBncmF1ZSBX/HN0ZSBoaW5laW4uIERhcw0KZHVyY2hw Zmz8Z3RlIFdhc3NlciByad8gaW4gbmllIGFic3RlcmJlbmRlciBXZWxsZSBlaW5l biBzaWxiZXJuZW4gQm9nZW4NCvxiZXIgZGVuIExlZS4gRGllIELkdWNoZSBkZXIg U2VnZWwgbmVpZ3RlbiBzaWNoIHRpZWZlciB1bmQgc3RyZWlmdGVuIGRhcw0KZmFy Ymxvc2UgV2Fzc2VyIHVuZCBob2JlbiBzaWNoIHdpZWRlciBhdWZnZXRhdWNodCBp biByb3RlIFNvbm5lLiBEaWUNCkx1dnNlaXRlbiB35Gx6dGVuIHNpY2ggbWl0IGhl bGxlciBnZXN0cmljaGVuZW4gTGVpYmVybiB3ZWl0IGF1cyBkZW0gU2VlLA0KdW5k IGRlciBzaWxiZXJuZSBTcHJlbmtlbCBkZXIgbWl0bGF1ZmVuZGVuIGV3aWdlbiBX ZWxsZSB1bXN05HVidGUgdW5zIHZvbg0KZGVyIGFuZGVyZW4gbWl0IHdpbGRlbSBH ZWZsb2NrLg0KDQpBbGxlIEZsYWdnZW4gYW0gTWFzdCBsb2h0ZW4gc2NobWFsIGdl evxuZ2VsdCBpbiBkYXMgQmxhdS4NCg0KQWxzIGRpZSBzcGl0emUgV29sa2Ugendp c2NoZW4gZGVtIHZlcmxhc3NlbmVuIFNjaGxv3yB1bmQgdW5zIGhlcmVpbnNjaG/f LA0KZ2VyaWV0ZW4gZGllIEZyYXVlbiBpbiBCZXdlZ3VuZy4gRGllIG5hY2t0ZW4g QmVpbmUgbPZzZW4gZmF1bCBXYWRlIHZvbg0KV2FkZSwgc2llIHRyZW5uZW4gc2lj aCB2b24gTWFzdCB1bmQgZGVtIHNvbm5pZ2VuIFZlcmRlY2tlLCD8YmVyIGRlbiBk dW5rbGVuDQpCYWRlYW56/GdlbiBzY2hpbW1lcm4gZGllIGJ1bnRlbiBKYWNrZW4u IEVpbiBUcmF0c2NoIHNhdXN0IGhpbnRlbiBhdWYgZGFzDQpHZWJpcmcuIEv8aGwg Z2Vib2dlbiBzdGVodCB1bnNlciBIaW1tZWwgbm9jaCBibPxoZW5kIGFudGlrLg0K DQpFaW4gUmVnZW5ib2dlbiByb2xsdGUgZWluZSBOYXR0ZXIgZGFy/GJlci4gWndl aSBzaWViZW5mYXJiZW5lIEJy/GNrZW4NCnNjaG5lbGxlbiD8YmVyIGRpZSB2ZXJi bGHfdGUuIFNpZSByZW5uZW4gbWl0IHVucyB1bSBkaWUgV2V0dGUuIEdyb99lIEph Z2QNCmJlZ2lubnQuIERhcyBTY2hsb98gaXJyIGxldWNodGVuZCBpbiBmZXJuZXIg U29ubmUgc3RlaHQgc2NocuRnIGdlZHVja3QNCnVudGVyIGRlciBnZWJvZ2VuZW4g V3VjaHQgZGVzIEdld2l0dGVycy4gRGFy/GJlciBhYmVyIHf8dGV0IEplaG92YXMg ZWhlcm5lcg0KUmVnZW5ib2dlbiB1bmQgc2NobmVsbHQgbWl0IGds/GhlbmRlbSBG aW5nZXIgbmViZW4gdW5zIPxiZXIgZGFzIExhbmQuIERpZQ0KR2VnZW5kIHdpcmQg a2xlaW4gdW5kIGdyYXUgdW5kIGVudHr8bmRldCBzaWNoIHVudGVyIGlobSBtaXQg bWFnaXNjaGVtDQpHbGFuei4gVW50ZXIgaXJyZW0gU2NoZWluIGZhaHJlbiB3aXIu IE11c2lrIGluIGFsbGVuIFNlaWxlbi4NCg0KSmVzc2llcyBCbGljayB39mxidCBz aWNoIGF1cyBkZW4gRnJhdWVuIGhlcvxiZXIuIERpZSBSdWRlcnBpbm5lIHdpcmQg RWlzIGluDQptZWluZXIgSGFuZC4gRGllIFNlZ2VsIGxhdWZlbiBhdWYgZGFzIFdh c3NlciBuaWVkZXJnZWxlZ3QuIERhcyBHZXdpdHRlcg0KZmxhdHRlcnQg/GJlciB1 bnMgdW5kIGJsZWlidC4gTm9jaCBkdXJjaCBhbGxlIEz2Y2hlciBzY2hpZd90IGVp bmUgU+R1bGUNClNvbm5lLiBHdXJnZWxuZCBzY2h3ZW1tdCBkZXIgc2lsYmVybmUg TXVza2VsIGFtIExlZSBzZWluIFdhc3NlciBoaW5laW4uDQpKZXNzaWUgYmVnaW5u dCAtLSBrbmllbmQgenUgcHVtcGVuLCBzaWUgd2Vp3ywgZGHfIGljaCBkaWUgTmFj aHQgbmljaHQNCnNjaGxpZWYsIGzkY2hlbG5kIG1pdCBhYmdldHJpZWJlbmVtIE11 bmQuDQoNCkVybPZzdCBhdXMga2F0emVuaGFmdGVtIEVybGViZW4gZGVyIFNvbm5l IHNpbmQgZGllIEZyYXVlbiBhdWZnZXJhZmZ0LiBTaWUNCnN0ZWhlbiBmYXN0IGF1 ZiBNYXN0IHVuZCBTZWdlbCwgaWhyZSBG/N9lIHN0ZWhlbiBpbSBXYXNzZXIsIHNp ZSBzdGVoZW4gYXVmDQpMZWUgd2llIFN0YXR1ZW4sIHVuZCBkaWUgQmFja2JvcmRz ZWl0ZSBoZWJ0IHNpY2ggaGludGVyIGlocmVuIHZvbiBMYWNoZW4NCvxiZXJm/Gxs dGVuIE11bmRlbiB3aWUgZWluZSBkdW5rbGUgTXVzY2hlbCwg/GJlciBkaWUgaWhy IEhhYXIgbm9jaA0KbGV1Y2h0ZXQuDQoNCldpciBzZWhlbiBkYXMgVWZlciBkdXJj aCBTY2hhdW0uIFdpciByZWNobmVuLCBoYXJ0IGFtIFdpbmQsIG5vY2ggemVobg0K TWludXRlbi4gU2No5HVtZW5kZXIsIGdpZXJpZywgZWluIExpZWJlc3NjaHdlcnQg Ym9ocnQgc2ljaCBkaWUgU3BpdHplIG1pdA0KZmllYmVybmRlciBXb2xsdXN0IGlu IGRhcyBHZXdvZ2UuIEVpbiBkdW5rbGVyIEhhbGJrcmVpcyBzYXVzdCB2b20gVWZl cg0KaGVyYXVzIG1pdCBlaW5lciBnbGFzaGVsbGVuIEthbnRlLiBKZXNzaWUgbGF1 ZXJ0ISBEaWUgQvYuIERlciBHcm/fc2Nob3QNCmbkaHJ0IPxiZXIgZGllIFJvbGxl LCBkYXMgQm9vdCBkcmVodCBoZXJ1bWdld29yZmVuOiBkYXMgU2VnZWwsIGdyYXVl DQpBcG90aGVvc2UsIGVudGZhbHRldCBzaWNoLCByYXVzY2h0IGxvc2dlbGFzc2Vu LCB3aWxkZmxhdHRlcm5kIGhpbmVpbi4gV2lyDQpzdGVoZW4uDQoNCkplZGUgUGxh bmtlIHppdHRlcnQgaW0gSGVyenNjaGxhZy4NCg0KRGFubiBzdGVpZ3QgZGFzIEJv b3QsIGRpZSBzY2htYWxlIEZsYWdnZSB3ZWh0LiBEYXMgZWluZ2VyZWZmdGUgU2Vn ZWwgZ2z8aHQNCnVudGVyIEJsaXR6c3RyYWhsZW4sIGRpZSBkZW4gU2VlIHVtbGF1 ZmVuLiBFaW4gd2Vp32VyIFN0cmljaCBib2hyZW4gd2lyDQp3ZWl0ZXIsIHdldHRl cm4gZGllIEJvb3RlIGluIEL2IHVtIEL2LCBzdGVoZW4gc3RhcnIsIHVtZmxvc3Nl biB6d2lzY2hlbg0KcnVuZCB1bSB1bnMgYXVmZ2Vo5HVmdGVuIFdlbGxlbi4NCg0K SW4gc2llYnppZyBNaW51dGVuIGVycmVpY2h0ZW4gd2lyIGRhcyBFbmRlIGRlcyBT ZWVzLg0KDQpFcyB3YXIgZ2VnZW4gQWJlbmQuDQoNCldpciBibGllYmVuIGRyZWkg VGFnZS4NCg0KSW4gZGVyIGVyc3RlbiBOYWNodCBhYmVyIHd1Y2hzIEplc3NpZSB3 aWxkIGluIGRlciBMaWViZSB3aWUgZWluZSBTdHV0ZSwgc2llDQpzcHJhbmcgZHVy Y2ggZGFzIEZlbnN0ZXIuIERhIHN0YW5kIGVpbiBHYXJ0ZW4gbWl0IEf8bGRlbmxh Y2sgdW5kIE1hbHZlbiB1bmQNCnJvY2ggaW4gZGllIGR1bmtsZSBMdWZ0LCBpbiBk ZXIga2VpbiBNb25kIGhpbmcsIGFiZXIgU3Rlcm5lIGRpZSBmZXVjaHRlbg0KU2Vn ZWwg/GJlcmL8cmRldGVuLiBEaWUgTmFjaHQgd2FyIGhlad8gbmFjaCBuaWNodCBn ZWv8aGx0ZW0gR2V3aXR0ZXIuIEljaA0KaGF0dGUga2VpbmUgTHVzdCB6dSBzY2hs YWZlbiB1bmQgZm9sZ3RlIGloci4NCg0KSWNoIHJ1ZGVydGUgdW0gZGllIExhbmR6 dW5nZSwgZGEgd2FyIGRpZSBCdWNodCBwYXJhZGllc2lzY2ggZXJoZWxsdCwgcm90 DQpnZXNwaWVnZWx0IG1pdCB2aWVsZW0gR2xhcyBzY2hv3yBlaW4gS2FydXNzZWxs IGVpbmVuIEtyZWlzLCB1bmQgZWluZQ0KUHJvbWVuYWRlIG1pdCBlcmxldWNodGV0 ZW4gQuR1bWVuIGxpZWYg/HBwaWcgdm9uIGRlciBL/HN0ZSBpbiBkZW4gV2FsZC4N CtxiZXIgZGllIEJvb3RzaOR1c2VyIHNjaHdhbmdlbiBzaWNoIFJha2V0ZW4sIGVp bmUgZ2Vk5G1wZnRlIE11c2lrIGZsb2ggYXVzDQpkZW4gUGF2aWxsb25zIGhlcvxi ZXIsIGFiZXIgZGllIEJ1Y2h0IHdhciB2b2xsIEvkaG5lbiB1bmQgYWxsZSBTdGVy bmUgdW5kDQpIZWNrcyB0cnVnZW4gcm90ZSB1bmQgZ2VsYmUgQmFsbG9ucyB1bmQg bWFuY2hlIG1pdCBTcGFnYXQg/GJlcnNwYW5udGUNCmhhdHRlbiBHaXJsYW5kZW4s IExhbXBpb25lLiBTbyBzY2hhdWtlbHRlIHVudGVyIGlobmVuIGRpZSBTZWUuDQoN CkltIGhlbGxlciBnZXPkdHRpZ3RlbiBMaWNodCBsYWcgSmVzc2llcyBLb3BmIHdp ZSBQZXJsbXV0dGVyIGluIGRlbSBEdW5rZWwNCmhpbnRlciBpaHIgdW5kIGlocmUg YXVzIGRlciBMdXN0IGhlcmF1ZiBnZWJyb2NoZW5lbiBBdWdlbiBiYXRlbi4gRGEg ZnVocg0KaWNoIGFucyBMYW5kIHVuZCBuYWhtIHJvdGUgdW5kIGdlbGJlIFBhcGll cmt1Z2VsbiBm/HIgc2llLiBJaHIgQmVpbiBnbGl0dA0Kc2NobGFuZ2VuaGFmdCBk YW5rZW5kIPxiZXIgbWVpbiBLbmllLiC7RG9ubmEg6CBtb2JpbGWrIGzkY2hlbHRl biBpaHJlIG38ZA0KYXVmZ2VibOR0dGVydGVuIExpcHBlbi4gRGllIHdhciBzaWUg c28gd2Vp3yB1bmQgbWlsZC4NCg0KV+RybWUgdW5kIE11c2lrIGxhZ2VuIPxiZXIg ZGVyIEJ1Y2h0LCB1bmQgZGllIEluc2VsbiBkZXIgQm9vdGUgaGF0dGVuIGtlaW4N CkVuZGUgZGVzIExpZWdlbnMuIEJyZW5uZW5kIGRpZSByb3RlIHVuZCBnZWxiZSBM YXRlcm5lIHRyaWViZW4gd2lyIG5vY2gNCmds/GhlbmQgaW4gZGVyIETkbW1lcnVu ZyBnZWdlbiB1bnNlcmVuIFN0cmFuZC4gSmVzc2llcyBLb3BmIGxhZyB3ZWnfIHdp ZQ0KZWluZSBQdXBwZSBtaXQg/GJlcnNjaHdlcmVuIFJpZWdlbG4gZGVzIE11bmRl cyBpbiBtZWluZW0gU2Nob98uIFdlbm4gZGllDQpSdWRlciBzaWNoIPxiZXIgaWhy IHNjaGxvc3NlbiwgaG9iIHNpZSBkYXMgQXVnZSB1bmQgc2NobHVnIGVpbmVuIGJl YmVuZGVuDQpG5GNoZXIgZ2Vub3NzZW5lbiBMZWJlbnMgaGluYXVmLg0KDQpJbiBk ZW0gd2Vp32VuIE1vcmdlbiBzYd9lbiBkaWUgYW5kZXJlbiBGcmF1ZW4sIHN0YXJy IHVuZCBvaG5lIExhdXQgYW4gZGVyDQpL/HN0ZSwgd2FyZmVuIGRpZSBsYW5nZW4g U2NobvxyZSBuYWNoIFJhdWJmaXNjaGVuIGluIGRhcyBicm9kZWxuZGUgV2Fzc2Vy LA0KdW5kIGRpZSBncm/fZW4gZ2VsYmVuIHp1cvxja2tlaHJlbmRlbiBTdGFuZ2Vu IGlocmVyIEFuZ2VsbiBzdGVsbHRlbiBzaWNoDQp3aWUgZWluIEdpdHRlciB2b3Ig ZGVuIGv8aGxlbiBXaW5kIGRlcyBIb3Jpem9udHMuDQoNCkFiZXIgYWxzIHdpciBh bmxlZ3RlbiwgbGllYnRlIGljaCBKZXNzaWUgbmljaHQgbWVoci4NCg0KQW0gdmll cnRlbiBUYWdlLCBhbHMgd2lyIGF1c2Z1aHJlbiwgc3ByYW5nZW4gZGllIEdsb2Nr ZW4gbGFuZ3NhbSB1bSBkZW4NClNlZSwgYWJlciB3aXIgZnVocmVuIG1pdCBlaWdl bmVyIE11c2lrLiBBdWYgd2Vp32VuIFBsYW5rZW4sIHNwaWVnZWxuZCB2b3INCkxh Y2ssIGxhZyBTb25uZSB1bmQgYmVzY2hpZW4gZGllIHp1c2FtbWVuZ2Vyb2xsdGVu IEthdHplbi4gV2lyIGZ1aHJlbiBtaXQNCmRlbSBXaW5kLiBEYXMgd2Vp32UgU2Vn ZWwgbGFnIGF1c2dlbGFzc2VuIHdlaXQgaGluYXVzLCBkYWdlZ2VuIHN0YW5kZW4N CmFuZGVyZSBGcmF1ZW4gZ2VsZWhudCwgd2llIHZvciBkZW0gSGltbWVsIGhpbmdl d2FjaHNlbiwgZGllIGxhbmdlbg0Kc2NobGFua2VuIEJlaW5lIGF1ZiBkZXIgUmFo ZSB65HJ0bGljaCBzY2hhdWtlbG5kLg0KDQpFcyBnYWIgZ2VyaW5nZW4gV2luZCB1 bmQgaW4gZGllIHNjaPZuZW4gVGllcmUgc3RpZWcgZGllIGdyb99lIFRy5GdoZWl0 LiBTaWUNCnd1cmRlbiBzdGlsbCB1bmQgc2No9m5lciB1bmQgaGF0dGVuIGhhbGJn ZXNjaGxvc3NlbmUgQXVnZW4uIFRyYXViZW4gZmxvZ2VuDQpnZXdvcmZlbiB6dWVp bmFuZGVyLiBFbGxlbiBlcmtsZXR0ZXJ0ZSBkZW4gTWFzdC4gU2llIHRydWcgU2Fu ZGFsZW4sIGRlcmVuDQpnZWtyZXV6dGUgU2NobvxyZW4gd2Vp3yD8YmVyIGlocmVy IGJyYXVuZW4gSGF1dCBnZWdlbiBkYXMgS25pZQ0KaGluYXVmbGllZmVuLiBTaWUg c2HfIGF1ZiBkZXIgR2FmZmVsIHVuZCBibGllcyBGbPZ0ZSwgdm9uIGRlbQ0KYXVm YmF1c2NoZW5kZW4gU2VnZWwgZ2VnZW4gZGFzIGxlaWNodGUgQmxhdSBnZXRyYWdl bi4NCg0KRGFubiwgd2llIGRpZSBCcmlzZSBhbmxpZWYsIGthbSBlaW4gZnJlbWRl ciBSYWNrZXIgYXVmIHVucyB6dWdlc2Nob3NzZW4sDQpmcmVjaGVyIFNwZXJiZXIs IGtyZXV6dGUsIGZlaXh0ZSwgZGllIFJvbGxlbiBsaWVmZW4ga25pcnNjaGVuZCwg c2Vpbg0KZ2VzdHJlaWZ0ZXMgU2VnZWwgenVja3RlIGdpZXJpZy4gRXIgbGVndGUg cGFyYWxsZWwsIGVpbiBNYW5uIHN0YW5kIGluDQp3ZWnfZW4gZmxpZWdlbmRlbiBI b3NlbiBicmVpdCBhbSBCb3JkIHVuZCBwaG90b2dyYXBoaWVydGUgdW5zIHNpZWJl bm1hbC4NCg0KV2lyIGthbm50ZW4gZGFzIFNlZ2VsLg0KDQpEYXMgd2FyIGRpZSBG /HJzdGluLg0KDQpBYmVyIGljaCBoYXR0ZSBzaWUgbm9jaCBuaWNodCBnZXNlaGVu Lg0KDQpEYXMgQmx1dCBzdGllZyBtaXIgbGFuZ3NhbSBpbiBkaWUgQXVnZW4uDQoN CldpciBrcmV1enRlbiBlaW4gd2VuaWcsIGJvaHJ0ZW4gZ2VnZW4gaWhuIGxvcy4g RGFubiBzY2h3ZW5rdCBkaWUgUnVkZXJwaW5uZQ0KZWluZW4gUmllc2Vua3JlaXM6 IGVpbmVuIEhlcnpzY2hsYWcgbGFuZyBsaWVnZW4gd2lyIEJ1ZyBhbiBCdWcsIHVu c2VyZQ0KU3BpdHplIGRlY2t0IHNlaW4gU3RldWVyLiBFaW5lbiBBdWdlbmJsaWNr IGdlaWd0ZW4gZGllIFN0cmlja2UgYXVmZWluYW5kZXINCm1pdCBnbORzZXJuZW0g VG9uLiBCYXVzY2hlbmQgaW4gZHVua2xlbSBHZXf8aGwgc2Fua2VuIGRpZSBTZWdl bCBpbmVpbmFuZGVyDQotLSAtLSAtLSBpY2ggcmVpY2hlIGJlaWRlIEjkbmRlIGhp bvxiZXIuDQoNCk1pdCBlaW5lbSBadWcgc3RlaHQgZWluZSBGcmF1IGF1ZiB1bnNl cmVyIEt1ZmUsIHNjaHdlZmVsc2Nod2VyZXMNCkdlbGJqYWNrZXR0IPxiZXIgZGVy IFNjaHVsdGVyLiBTY2hvbiBzY2h3ZW5rZW4gd2lyIGF1cyBkZXIgV2luZHN0aWxs ZSwNCnNjaGF1ZmVsbiBXaW5kIHVuZCBzYXVzZW4uDQoNCldpciBoYWJlbiBlaW5l IEZyYXUgZ2VyYXVidC4NCg0KRGllIFZlcmZvbGd1bmcgYmVnYW5uLiBLbORmZmVu ZC4gTWl0IEdlc2NocmVpLiBXaXIgaGFiZW4gbWVociBRdWFkcmF0bWV0ZXINCmFt IEZvY2sgd2llIGRlciBLbGVpbmUgYW0gZ3Jv32VuLiBad2VpIEJvb3RlIHVtemlu Z2VsbiBpaG4sIG5laG1lbiBpaG0gZGVuDQpXaW5kIHVuZCB2ZXJzdG/fZW4gaWhu IGF1cyBkZXIgSmFnZC4gR2llcmlnZXIgU3BlcmJlciByYXN0IGVyIGFtIEhvcml6 b250DQpoaW4sIHfkaHJlbmQgZGllIGdyb99lbiBSYXVidvZnZWwgaW4gZGVuIGJs YXUgYXVmZ2Vicm9jaGVuZW4gTW9yZ2VuDQpoaW5laW5zdHJlaWNoZW4uDQoNClNp ZSB3YXIgZHVua2VsIHdpZSBlaW5lIFppZ2V1bmVyaW4sIGFiZXIgbWl0IHp3ZWkg c2Nod2VyZW4gaGVsbGVuDQpTb25uZW5rcmVpc2VuIPxiZXIgZGVuIGxvZGVybmRl biBBdWdlbi4gU2llIGtva2V0dGllcnRlLCBpbmRlbSBzaWUgZGVuDQpCbGljayBl cnr8cm50Lg0KDQq7R2VyYXVidCwgRvxyc3RpbiyrIGljaCBsYWNoZSB2b20gUnVk ZXIuDQoNClNpZSBsYWNodCwgd2lyZnQgZGllIEJyYXVlbiBpbiBkaWUgU3Rpcm4g d2llIFdlbGxlbiwgdW5kIHNwcmluZ3QgaW5zDQpXYXNzZXIuDQoNCldpciBoYWxz ZW4gdW5kIHppZWhlbiBzaWUgbGFjaGVuZCBoZXJhdXMuDQoNClf8dGVuZCBkdWNr dCBzaWUsIHNjaGF1dCBpbSBLcmVpcyBsYXVlcm5kIHVuZCBzY2h3ZWlndC4gRGFu biBzY2j8dHRlbHQgc2llDQpzaWNoIHVuZCBsZWd0IGRpZSBncm/fZSB2b2xsZSBG aWd1ciBnZWdlbiBkYXMgd2Vp32UgU2VnZWwgdW5kIGhlYnQgaWhyZW4NCkv2cnBl ciBpbiBkaWUgcHJhbGxlbmRlIHP832UgU29ubmUuDQoNCkFtIE1pdHRhZyBzdGVo ZW4gdW5zZXJlIFNjaGlmZmUgYXVmIGRlciBI9mhlIGlocmVzIEhhZmVucywgdmVu ZXppYW5pc2NoZQ0KU2No9m5oZWl0IGRlcyBlbnRnZWdlbmxhdWZlbmRlbiBMYW5k ZXMsIGds/GhlbmRlciBTY2h3dW5nIHZvbGwgU2VnZWwsIEJvb3QNCnVuZCBTdGVn ZW4gdW5kIEdld2lyciB2b24gTWVuc2NoZW4uIFdpciBsYXZpZXJlbi4NCg0KRWlu IEtyYW4gZ2VpZ3QuIERhcyBTZWdlbCBzdGVodCBzY2hsYXBwIGdlZ2VuIGRlbiBX aW5kLg0KDQpJY2ggZ3L832UgdGllZi4NCg0KRGllIGb8cnN0bGljaGUgS2F0emUg ZHVja3QgdW5kIHNwcmluZ3QuDQoNCldpciBzaW5kIGFsbGVpbi4NCg0KRGllIEZs b3R0ZSBrcmV1enQgenVy/GNrLiBFbGxlbiBsaWVndCB1bnRlciBkZXIgRmFobmUg ZWluZ2ViYXVzY2h0IHdpZSBpbg0KTG90b3NibOR0dGVyLiBEaWUgRmz2dGUgc3By aW5ndCBpbiBz/N9lbiBLdXJ2ZW4uIEthdGhhcnlzIE11bmRoYXJtb25pa2ENCnpp Z2V1bmVydCBkYXp3aXNjaGVuLiBEYXMgTGljaHQgd2FyIGhlad8gZvxyIGRhcyBC bHV0LiBFcyB3YXIgZWluZSB0b2xsZQ0KRmFocnQuDQoNCkdsZWljaHdvaGwgZ2lu ZyB3ZW5pZyBXaW5kLCBhYmVyIHVuc2VyZSBIaXJuZSB3dXJkZW4gZHVua2VsIHZv ciDcYmVybXV0IHVuZA0KQmVnaWVyZGUuIFNvIHNjaGF1a2VsdGVuIHdpciBkdXJj aCBkaWUgcnVoaWcgYXVmYmxhdWVuZGUgU2VlLCBr/GhsZSB3ZWljaGUNClVmZXIg /GJlcmFsbCBpbiBSdWhlIHVuZCBlaW5lIFN0YWR0IGluIE5lYmVsIGF1ZmdlYmF1 dCBnZWdlbiBkYXMgR2ViaXJnZS4NCldpciB3aWVndGVuIHVucy4NCg0KRGFubiBz YWhlbiB3aXIgZWluZSBNb2xlLiBTaWUga2FtIGluIGVpbmVyIFNwYW5nZSB65HJ0 bGljaCBpbiBkYXMgV2Fzc2VyDQpoaW5hdXNnZWxlZ3QsIGdhbnogd2VpY2ggdW5k IGT8bm4gbWl0IFPkdWxlbiB1bmQgVmFzZW4gdW5kIEthcHV6aW5lcmJsdW1lbi4N CkRhIGZ1aHJlbiB3aXIgaGluZWluLCBhbmtlcnRlbiwgYmVzdGllZ2VuIGRpZSBL 5GhuZSB1bmQgZnVocmVuIGFuIExhbmQuDQoNCkthaG4gdW0gS2FobiByYXVzY2h0 ZSBpbiBlaW4gR2V3ZWJlIHZvbiBCaW5kZW4sIGluIHdhcm1lcyBXYXNzZXIga25p ZWhvY2gNCnNwcmFuZ2VuIGRpZSBGcmF1ZW4sIGhvYmVuIE11c2NoZWxuIGluIGRh cyBMaWNodCwgcmllZmVuIHVuZCBzY2h3YW5nZW4gbWl0DQpkZW4gQXJtZW4gZGFz IFNjaGlmZiBhdXNlaW5hbmRlciAtLSAtLSAtLSBkYSBoaW5nIGRhcyBVZmVyIHZv ciBpaG5lbiwgdW5kDQphbHRlIELkdW1lIHN0YW5kZW4gbWl0IFdpcGZlbG5lc3Rl cm4gcmllc2lnIGluIFNjaGF0dGVuIGdlYnJlaXRldC4NCg0K3GJlciBkaWUgV2ll c2VuIHNwcmluZ2VuZCwgZXJncmlmZmVuIGRpZSBGcmF1ZW4gZGFzIEhldSB1bmQg d2FyZmVuIHNpY2gNCmhpbmVpbi4gRGFubiBzdPxybXRlbiBzaWUgZGllIELkdW1l IHVuZCBkdXJjaCBkaWUgWndlaWdlIGdsaXR0ZW4gbmFja3RlDQpCZWluZSwgaW4g ZGVuIEdpcGZlbG4gYmxpbmt0ZSBpaHIgRmxlaXNjaC4NCg0KQXVzIGVpbmVyIEtv bmlmZXJlIHRhbnp0ZSBLYXRoYXJ5IGF1ZiBlaW5lbSBBc3RzY2h3ZWlmLCBkZXIg dW50ZXIgaWhyDQp3b2d0ZS4gU2llIHRyYXQgYXVzIGRlciBLcm9uZSBpbiBkYXMg YnJhdXNlbmRlIExpY2h0LCBkYSBzYWggc2llIGRhcyBTY2hsb98NCmdlZ2Vu/GJl ciBhdXMgZGVyIGVudGZlcm50ZW4gS/xzdGUgdm9uIHNpbGJlcm5lbSBTb25uZW5z dHJpY2gNCmhlcmF1c2dlc3ByZW5ndCB1bmQgc2NocmllLiBJaHIgd2lsZGVzIFNj aHJlaWVuIHdlY2t0ZSBHZXNjaHJlaSBpbiBkZW4NCmJ1bnRlbiBC5HVtZW4sIGRp ZSDEc3RlIHp1bSBTZWUgZvxsbHRlbiBzaWNoIG1pdCBGcmF1ZW4sIGRpZSBkaWUg SGFhcmUgaW4NCmJ1bnRlbiBN/HR6ZW4gdHJ1Z2VuLg0KDQpLYXRoYXJ5cyBBc3Qg cmF1c2NodGUgaGludW50ZXIsIHdhcmYgc3By/GhlbmRlIFdlbGxlIGF1cyBkZW0g U2VlIHVuZA0Kc2NobmVsbHRlIHp1cvxjayBpbiBkYXMgTGljaHQuIFNvIGZsb2cg c2llIGhhbGIgbmFja3QgdW5kIHP83yB6d2lzY2hlbg0KU29ubmUgdW5kIFN0dXJt LiBEYWJlaSB3YXJmIHNpZSBtaXQgZWluZXIgaGVmdGlnZW4gQmV3ZWd1bmcgZGll IEjkbmRlIGFuDQpkZW4gTXVuZCB1bmQgYmxpZXMgaWhyZSBIYXJtb25pa2EsIGlu ZGVtIHNpZSBmbG9nLg0KDQpEYW5uIHdhcmZlbiBzaWNoIGFsbGUgRnJhdWVuIGlu IGRlbiBTZWUgYXVzIGRlbiBC5HVtZW4uIE9zdHdpbmQgdHJ1Zw0KV2VsbGVuYmVy Z2UgaGVy/GJlciB1bmQgd/xobHRlIHNpZSBhdWYgdW5kIHdhcmYgZGllIFNjaHdp bW1lbmRlbiBlaW5hbmRlcg0KenUg/GJlciBkaWUgZ2xhdHRlbiBUaWVycvxja2Vu IGRlciBXb2dlLiBJbW1lciBnYW1pbnRlIEthdGhhcnlzIEhhcm1vbmlrYQ0K/GJl ciBkZW0gd2Vp32VuIFppc2NoZW4uIERhIGhpZWx0IGljaCBuaWNodCBs5G5nZXIg dW50ZXIgaWhyZW4gZ3L8bmVuIEF1Z2VuDQp1bmQgdmVyZ2HfIEVsbGVucyBGbPZ0 ZSB1bmQgYmVoaWVsdCBLYXRoYXJ5cyBCbGljayBpbiBkZXIgR3VyZ2VsIGhpbnRl ciBkZXINClp1bmdlLg0KDQpXaWUgZWluZSBIZXJkZSBBbnRpbG9wZW4gc3RlaWdl biBkaWUgRnJhdWVuIGF1cyBkZW0gV2Fzc2VyIHVuZCByZW5uZW4gaW4NCmJyZWl0 ZXIgTGluaWUgaW4gZGVuIFBhcmsuIERhcyBNb29zIGZlZGVydCBpaHJlIFNvaGxl biBicmF1bnJvdCBpbiBkaWUNCkj2aGUsIHVuZCBkaWUgc2NobGFua2VuIFNjaGVu a2VsIGxldWNodGVuIHVudGVyIGRlbiBC5HVtZW4uDQoNCkF1ZiBlaW5lciBXaWVz ZSBiZWdhbm4gRWxsZW4gZGllIFNjaGxhY2h0LiBIZXUgYXVmcmFmZmVuZCwgbWl0 IGJlaWRlbiBBcm1lbg0KZXMgYW4gZGllIEJydXN0IGdlcHJl33QsIHdhcmYgc2ll IGRpZSBHYXJiZSBpbiBkaWUgTHVmdC4gRGEgc3ByYW5nZW4gYWxsZSwNCmRpZSBz Y2h3YXJ6ZW4gU2Nod2ltbWFuevxnZSBnbORuemVuZCB3aWUgUGFudGhlcmhhdXQs IGF1ZiBkZW4gUmFzZW4sIGJpZWdlbg0KZGllIEJy/HN0ZSB6dXL8Y2sgdW5kIHNj aGxldWRlcm4gZGFzIEdyYXMgaW4gZGllIE5hY2tlbiwgYXVmIGRhcyBHZXNpY2h0 Lg0KQWJlciBzY2hvbiBwcmFsbHQgZWluZSBEb2dnZSBpbiBkaWUgU2NobGFjaHQu DQoNCkF1cyBnZXRyZW5udGVtIEhvbHVuZGVyIHRyaXR0IHBs9nR6bGljaCBlaW5l IERhbWUgaW0gUmVpdGFuenVnIHZvciBkZW4NCmds/GhlbmRlbiBWb2xsenVnLiBC bGVpY2hlbiBHZXNpY2h0cyBibGVpYnQgc2llIGluIFNwYW5udW5nIHdpZSBlaW5l IEhlcm1lDQpzdGVoZW4sIGthdW0gYmViZW5kLiBEZXIgUmVpdHN0b2NrIGtsZW1t dCB1bnRlciBpaHJlbSBBcm0sIGVpbiByb3RlciBTdGVpbg0KaW0gR3JpZmYuIElj aCB0cmlua2UgaW0gV2VuZGVuIG5vY2ggS2F0aGFyeXMgZ3JhdXNhbWVzIEzkY2hl bG4uDQoNCkRpZSBGcmF1ZW4gcmVubmVuIGZsaWVoZW5kIG5hY2ggZGVyIEv8c3Rl LiBGbG90dCBnZW1hY2h0ZSBL5GhuZSByYXVzY2h0ZW4NCmR1cmNoIEJpbnNlbnNj aGxlaWVyLiBEaWUgRmxvdHRpbGxlIHdhcmYgU2VnZWwgYXVzIHVuZCBzdHJlaWZ0 ZSBpbiBkaWUgU2VlLg0KRWluIERhbXBmZXIgdm9sbCBNZW5zY2hlbiwgRmFobmVu IHVtIGRhcyBnYW56ZSBEZWNrLCBzdPxybXRlIHVucyBub2NoDQps5HV0ZW5kIHZv cvxiZXIuIERpZSBEcmFjaGVua/ZwZmUgZ2xpdHRlbiBzdG9seiBhbiBzZWluZW0g Z29sZGVuZW4gTPZ3ZW4NCnZvcmJlaS4NCg0KU2Nob24gYWJlciByYXVzY2h0ZW4g ZGllIFNlZ2VsLCBzaWNoIHNjaGF1a2VsbmQgdm9yIGRlbSBTY2hsb98uDQoNCkRl ciBBYmVuZCBnb98gc2ljaCBpbiBnbGFzaGVsbCBlcmxldWNodGV0ZXIgS3VwcGVs IGF1cy4gRGllIGdlbWFzZXJ0ZW4NCldlbGxlbmTkbW1lIGViYnRlbiB3aW5kbG9z IHp1IGJsZWllcm5lciBGbORjaGUsIGF1ZiBkaWUgaW4gZHVua2xlciBCcnVuc3QN CmRpZSBTb25uZSBoZXJhYmZpZWwuIE1hbmNobWFsIGxpZWZlbiBsYW5nc2FtIGF1 c2dlYXRtZXRlIEJvZ2VuIPxiZXIgZGVuIFNlZQ0Kdm9uIGVpbmVtIHN0dW5kZW5m ZXJuZW4gRGFtcGZlciB1bmQga2xpcnJ0ZW4gc2ljaCB0b3QgYW4gZGVyIFRlcnJh c3NlLiBEYW5uDQp0YW56dGVuIHVuZ2VoZXVyZSBGYXJiZW5i/HNjaGVsIGF1ZiBk ZW0gU3RhaGxkdW5rZWwgZGVzIFdhc3NlcnMgdW5kIGZpZWxlbg0Kd2llIGVpbiBi cmVubmVuZGVyIEbkY2hlciBpbiBOaWNodHMuIEF1cyBkZXIgRHVua2VsaGVpdCBr ZWhydGUgZWluIGtsZWluZXINCkhhbGJrcmVpcyBpbiBkYXMgQXVnZSB6dXL8Y2ss IGVpbiB3ZWnfZXMgQnJvZGVsbi4NCg0KSWNoIHdhcmYgbWljaCBhdWYgZGllIEVy ZGUgdW5kIGj2cnRlIGF1cyBkZXIgZmFzc3VuZ3Nsb3NlbiBOYWNodCBhbiBtZWlu ZW0NCkhlcnpzY2hsYWcgZGVuIFB1bHMgZGVyIHdpbGQgYXVzIEZ1cmNodCB0b2xs IGVycmVndGVuIEhhdXQgZGVzIFdhc3NlcnMNCnNjaGxhZ2VuLg0KDQpEYW5uIGZ1 aHIgaWNoIG1pdCBKYWNrbCBoaW5hdXMsIGRpZSBsZXR6dGVuIFNlZ2VsIHp1IHJl ZmZlbi4gQXVmIGRlcg0KVGVycmFzc2UgbGFnIGRlciBBbnNjaGxhZyBlaW5lcyBn ZWTkbXBmdGVuIEtsYXZpZXJzLiBBbHMgd2lyIHp1cvxja2Z1aHJlbiwNCmz2c2No dGVuIGRpZSBMaWNodGVyIGF1cy4NCg0KQWJlciBkaWUgbW9uZGxvc2UgSnVsaW5h Y2h0IHdhciBzY2h3ZWxsZW5kIHVuZCB1bmVydHLkZ2xpY2ggZ2V3b3JkZW4uIEF1 Zg0KdW5kIGFiIGdlaGVuZCBkaWUgS/xzdGUgd/xobHRlIPxiZXIgZGVyIFN0YXJy ZSBkZXIgU2VlIG1laW4gSGVyeiBzaWNoIGF1Zi4NCtxiZXIgZGFzIFNjaHdlaWdl biBkZXIgZXJyZWd0ZW4gRHVua2VsaGVpdCBrYW0gaWhtIGVpbmUgWWFjaHQsIHVu ZCBhdWYgZGVyDQpHYWZmZWwgaGluZ2VuIHp3ZWkgc2NobGFua2UgaGVsbGUgQmVp bmUsIGxhbmdlIEZpbmdlciBzcGFubnRlbiBlaW5lIEZs9nRlDQp2b3IgZGVuIE11 bmQuIEVzIGdhYiBlaW5lbiBTY2hlaW4sIGRlciB2b24gZGVtIFNlZ2VsIHJhc2No IHZlcnNjaHdlbmRldCwNCmVybG9zY2ggaW4gZGllIE5hY2h0IHp1cvxjay4gQWJl ciBkYWdlZ2VuIGVyaG9iIHNpY2ggZGllIHdpbGRlIEthdHplIGF1cw0KZGVtIFBh cmsgdW5kIHNjaHJpZTogSWNoIHfkaGx0ZTogS2F0aGFyeXMgWuRobmUgdW5kIEVs bGVucyBUaWVyYXVnZW4uDQoNCi0tIC0tIC0tIGRhIHNjaGllbiBlcyBtaXIgYmVy YXVzY2hlbmQsIEthdGhhcnkgYXVmenVzcGFyZW4genUgaWhyZW0NCkzkY2hlbG4s IGRhcyBpY2ggZWluZ2V0cnVua2VuIHVuZCBkZXNzZW4gQmVnZWhyIGhlaXNlciBp biBtZWluZW0gSGFsc2Ugc2HfLg0KDQpJY2ggem9nIEVsbGVuIHZvci4NCg0KQWxz IG1laW4gS29wZiD8YmVyIGRlciBCcvxzdHVuZyBpaHJlcyBaaW1tZXJzIGF1ZnNj aHdlYnRlLCB0cmFmZW4gbWljaCBpaHJlDQpncm/fZW4gd2FybWVuIExpcHBlbiB1 bmQga/zfdGVuIG1pY2gg/GJlciBkYXMgZ2FuemUgR2VzaWNodDogaWNoIGxpZWJl DQpkaWNoLCBpY2ggbGllYmUgZGljaC4NCg0KRGFzIEtsYXZpZXIgZG9ubmVydGUg ZmVybiBkdXJjaCBkaWUgS29ycmlkb3JlLCBlaW5nZXNjaGx1bmdlbiBqYWd0ZSBk aWUNCkhhcm1vbmlrYSBkYXp3aXNjaGVuLiBEaWUgU3Rlcm5lIGhhdHRlbiBzY2h3 ZXJlIExhc3QsIG1vbmRsb3MgenUgdHJhZ2VuLg0KDQpEdXJjaCBhbGxlIE1hdWVy biBzY2h3b2xsIFNlaG5zdWNodCB3aWUgRmllYmVyLiBEaWUgV+RuZGUgZGVobnRl biBzaWNoIHdpZQ0KQm9nZW4uIERpZSBMdWZ0IGhhdHRlIEJsdXQgZWluZ2Vzb2dl bi4gTXVzaWsgd/xobHRlIGVpbmUgZmV1cmlnZSBXb2xrZSB1bQ0KZGFzIFNjaGxv 3y4gQWxsZSBzYWhlbiBlcywgZGllIG5hY2h0cyB2b3L8YmVyZnVocmVuIGluIGRl bSB3aW5kbG9zZW4gU2VlLA0KZHVua2VsIGRpZSBSYWhlbiB1bmQgZWluIExpY2h0 IGlyZ2VuZHdvIGFuIEJvcmQuDQoNCkltIGZy/GhlbiBNb3JnZW4gbGFnIGRhcyBM YW5kIGhlbGwgbWl0IHdlaXRlciBTZWUuIFNpZSBzY2hsaWVmIG1pdA0Keml0dGVy bmRlbSBNdW5kLCBlaW4gUm9zYSBhdWYgZGVuIFdhbmdlbi4gU2llIGZs/HN0ZXJ0 ZSBpbSBTY2hsYWYsIGFscyBtaWNoDQpkaWUgU2VobnN1Y2h0IGF1ZnRyaWViLiBJ Y2ggc3RpZWcgYXVzIGlocmVtIEJldHQgaW4gZGVuIEdhcnRlbi4NCg0KRGEgcm9j aCBkZXIgQm9kZW4gc3Rhcmsgd2llIGVpbiBSYXVidGllci4gRGllIEJlZXJlbiBs ZXVjaHRldGVuLiBBdWYgZGVtDQpTdGVnIGxhZyBUYXUgaW4gZWluZW0gYmxhdWVu IEdsYW56LiBVbnNlcmUgRmxvdHRlIHN0YW5kIGVpbmdlZnJvcmVuIGF1Zg0KdW5i ZXdlZ3RlbSBTcGllZ2VsLiBad2VpIEZpc2NoZXJib290ZSBzdHJpY2hlbiBsYXV0 bG9zIGluIGRlbiB3ZWnfZW4gTW9yZ2VuDQp1bmQgc3Bhbm50ZW4gZWluIE5ldHog bWl0IGxhbmdlbiBTY2hu/HJlbi4NCg0KRGVyIE1vdG9yIHRhbnp0ZSBpbiBkYXMg V2Fzc2VyLCBsZWd0ZSBzaWNoIHNjaHLkZyB1bmQgc3RyaWNoIHNjaG1laWNoZWxu ZCwNCnNlaW5lIFR1cmJpbmUgcmnfIGRpZSB0b25sb3NlIEViZW5lIG1vcmdlbmxp Y2hlbiBXYXNzZXJzIGluIHp3ZWkgbGFuZ2UNCkxpbmllbiB2b24ga3JlaXNlbmRl biBE/G5lbiwgZGllIGhpbnRlciB1bnMgYmxpZWJlbi4gRGVyIEhpbW1lbCBzdGFu ZA0KbGF1dGxvcyB1bmQga/xobGJsYXUuIEF1Y2ggZGllIEx1ZnQgd2FyIGdlZ29z c2VuLCBkdXJjaCBkaWUgaWNoIGVyZ3JpZmZlbg0KamFndGUuIFVuZCBkYW5uIGth bSBkZXIgSGFmZW4sIGthbSBkZXIgSGFmZW4gbWl0IEZsYWdnZW4gdW5kIHZlbmV6 aWFuaXNjaGVuDQpHb25kZWxuLiBEYSBnaW5nIGRpZSBTb25uZSBhdWYuDQoNCkVu ZGxpY2ggZ2VnZW4gTWl0dGFnIHRyYWYgaWNoIG1laW5lIEJldXRlLiBJaHIga2xl aW5lciBSYWNrZXIgZnVociBlaW4gYXVzDQpkZXIgVGllZmUgZGVzIFNlZXMsIGlj aCBlcmthbm50ZSBkYXMgU2VnZWwuIEF1c3N0ZWlnZW5kIGdpbmcgZGllIEb8cnN0 aW4NCmF1ZiBkZXIgU3RyYd9lIHp3aXNjaGVuIGRlbiBMaW5kZW4uIEFscyB3aXIg dW5zIGdlZ2Vu/GJlcnN0YW5kZW4sIGz2c3RlDQpzaWNoIGRpZSBL/HN0ZSBhdXMg ZGVtIER1bnN0LCB1bmQgd2llIGVpbiBnZWRyZWh0ZXIgUXVhcnpibG9jayBsZXVj aHRldGUNCmRhcyBCZXJnc2NobG/fIGR1bXBmIHVuZCB3aXJyLiBEaWUgTGlwcGVu IGVpbmdlem9nZW4sIHr8cm50ZSBzaWUgbWl0DQphdWZnZXJlY2t0ZXIgQnJhdWUu DQoNCkFiZXIgc2Nob24gaGllbHQgaWNoIG5pY2h0IG1laHI6ILtHZXJhdWJ0ZSBG cmF1IC4gLiAuqyBkYSByad8gZGVyDQpIZXJ6c2NobGFnIGRpZSBXb3J0ZSBpbSBN dW5kLCB1bmQgaWNoIGv833RlIHNpZS4gU3RhcnIgc3RlaGVuZCwgbmFobSBzaWUN CmRpZSBL/HNzZSwgZGllIPxiZXIgc2llIHN0/HJ6dGVuLiBEYW5uIHNhbmsgaWhy ZSBCcnVzdCwgdW5kIG1pdCBsZWljaHRlcg0KRXJoZWJ1bmcgaG9iIHNpZSBkYXMg R2VzaWNodC4gRGEgbGFnIGVpbiBTY2hlaW4gdW0gaWhyZW4gZHVua2VsZW4gS29w ZiB1bmQNCm1hY2h0ZSBpaG4gc/zfIHp1bSBXZWluZW4uIElociBNdW5kLCBpcnIg ZW50YmzkdHRlcnQsIG5haG0gS/xzc2UgYXVmLCBpaHJlDQpMaXBwZW4gYm9nZW4g c2ljaCB1bnRlciBkZW0gc3VjaGVuZGVuIE11bmQuIFNpZSB0cnVnIG5pY2h0IGRh cw0KU2Nod2VmZWxqYWNrZXR0LCBzaWUgd2FyIGJsYXUgdW5kIGR1bmtlbC4gV2ll IGFiZXIgbWVpbiBhdGVtbG9zZXIgTXVuZCB6dQ0Kc2NoZWx0ZW4gYmVnYW5uIHZv ciBpaHIsIHVuZCBtZWluZSBadW5nZSBhbmZpbmcsIHZvbiBMaWViZSBkZW38dGln IHVuZA0KbmllZHJpZywgc2llIHp1IHByZWlzZW4sIGRhIGZpZWwgZWluIGdyb99l ciB1bnZlcnN05G5kbGljaGVyIEJyYW5kIGF1cw0KaWhyZW4gQXVnZW4sIHVuZCBu dW4gd2FyIEdsYW56IHVtIHNpZSwgZGHfIGljaCBmYXN0IHZlcmdpbmcuDQoNCk1p dCBnZWJsZW5kZXRlbiBBdWdlbiD8YmVyIGRpZSBE9nJmZXIgaGluLCB3aWUgaW4g ZWluZW0gUmVnZW5ib2dlbg0Kc3RyYWhsZW5kLCBmYWhyZW4gd2lyIGltIFdhZ2Vu IGhpbi4gQWxsZSBEaW5nZSBoYWJlbiBUaWVmZSB2b3IgdW5zZXJlbQ0KQXVnZS4g SW1tZXIgbGllZ3QgZGllIExhbmRzY2hhZnQgdm9yIHVucy4gR290dCBsaWXfIHVu cyB1bnNlcmUgQmxpY2tlIG5pZQ0Kc2VoZW4sIHZvciBXb25uZSBzdPxyYmVuIHdp ci4NCg0KRGFubiBzYWhlbiB3aXIgTmV0emUgaGluZ2Vo5G5ndCB2b3IgZGllIFNv bm5lLCB1bmQgZGllIFNvbm5lIGxlZ3Qgc2ljaCBhdWYNCmplZGVuIFRyb3BmZW4s IGRlciBhdXMgZGVuIE1hc2NoZW4gc2ljaCBs9nN0IHVuZCB6dXIgRXJkZSBm5Gxs dC4NCg0KSGllciBtdd90ZSBkYXMgRW5kZSBkZXIgV2VsdCBzZWluLiBIaWVyIHN0 ZWlnZW4gd2lyIGF1cy4gV2lsZGUgS/xoZQ0Kc3ByYW5nZW4gYXVmIGVpbmVyIHph cnRlbiBXaWVzZSB1bmQgd28gc2llIGZlcnRpZyB3YXIsIGRhIHdhciBlaW4gU2Vl Lg0KDQpJbiBlaW4gQm9vdCBtZWluZSBCZXV0ZS4NCg0KRGllIEx1ZnQgaXN0IHN0 YWhsYmxhdS4gRGllIFNvbm5lIGVpbiBC/G5kZWwgU2Nod2VydGVyLCBkZXJlbiBT cGl0emVuDQp6ZXJwcmFzc2VsbiB3aWUgRmxhbW1lbnNjaHdlcnRlciBkZXIgQ2hl cnViaW0uIFdpbmQgd2VodCBtaXQgc3T8cm1lbmRlcg0KR2V3YWx0LCBzdGV0LCB1 bmF1Zmj2cmxpY2gsIGVpbiBlbmRsb3NlciBXaW5kLCBzdGV0cyBmbGFja2VydCBk YXMgSGFhci4gRGFzDQpXYXNzZXIgZm9ybXQgc2ljaCB1bnRlciBpaG0genUgdGF1 c2VuZCBrbGVpbmVuIFT8cm1lbi4gRHVyY2ggdGF1c2VuZCBU/HJtZSwNCmRpZSBz Y2htZXR0ZXJuZCBkaWUgV+RuZGUgemVyc2NobGFnZW4sIGVyendpbmdlbiB3aXIg ZWluZSBJbnNlbC4NCg0KR2VoZW4gaW5zIFdhc3NlciAtLSB1bmQgbnVuIGv8c3Nl biB3aXIgdW5zLg0KDQpBbSBTdHJhbmQgbGllZ2VuZCwga29tbXQgYXVzIHVuc2Vy ZW4gSGVyemVuIGRpZSBWZXJrbORydW5nLCB1bmQgZGllDQpMYW5kc2NoYWZ0IGxp ZWd0IGFuZGVycyBnZWZvcm10Og0KDQpaZXJyaXNzZW5lIFNvbm5lIHdpcmZ0IGRl ciBXaW5kIGluIEZ1bmtlbiBkdXJjaCBkaWUgTHVmdCwgYWJlciBlcyB3aXJkIGVp bg0KS3JhbnosIGRlciBhdWZ35GNoc3QgYW0gSG9yaXpvbnQgdW5kIGlobiBydW5k IG1hY2h0IHVuZCBncm/fLiBOdW4gd2lyZCBkaWUNCmdld2VpdGV0ZSBXaWVzZSB2 b3IgdW5zIEViZW5lIG1pdCBncm/fZW4gU3TkZHRlbiB2b3IgaWhtLCBwYXJhZGll c2lzY2hlDQpUaWVyZSBzcGllbGVuIGluIHNhbmZ0ZW4gU3By/G5nZW4sIHVuZCBn cm/fZSBmZWllcmxpY2hlIFdvbGtlbiBiZWdpbm5lbg0KaGludGVyIGlociBhdWZ6 dXN0ZWlnZW4gdW5kIHdlad8gZGVuIEhpbW1lbCB6dSD8YmVycnVuZGVuLg0KDQpL ZWluIFd1bmRlciBzY2hlaW50IGZyZW1kLCBkaWUgRXJkZSB3aXJkIGlubmlnIHVu ZCB3YXJtLiBEZXIgU2VlIHdpcmZ0DQpNdXNjaGVsbiBoZXJhdXMgdW5kIHNlbHRl bmUgRmlzY2hlLCBtaXQgQuRydGVuIHVuZCBzYW10ZHVua2xlbiBBdWdlbi4gSWNo DQpzYW1tbGUgaWhyIGFsbGVzLCBpY2ggc3RlaGUgYmlzIHp1ciBI/GZ0ZSBpbSBX YXNzZXIgdW5kIHJ1ZmUgaGlu/GJlciwgZGHfDQppY2ggc2llIGxpZWJlLiBNZWlu IEF1Z2UgZmHfdCBkaWUgd2lsZGUgUm9iaW5zb25hZGUuIERpZSBXZWl0ZSBoYXQN CnVuZW5kbGljaGUgTmV1ZS4NCg0KQWJlciBtZWluIEhlcnogd3VyZGUgbWlsZGVy LCBpY2ggaGFiZSBkaWVzIG5pZSBnZWthbm50Lg0KDQpEZXIgRvxyc3RpbiBzY2h3 ZXJlIEJyYXVlbiB6dWNrdGVuIG1pdCBHb2xkIPxiZXIgZGVuIHNjaHdhcnplbiBB dWdlbiwgdW5kDQpkZXIgd2Vp32UgU2FuZCwgYXVmIGRlbSBzaWUgbGFnLCB3dXJk ZSBnbGFuemxvcyB1bmQgZGllbmVuZCB2b3IgaWhyLiBNYW5jaGUNCmhvaGUgV2Vs bGUgZXJyZWljaHRlIHVuc2VyZSBCcnVzdC4NCg0KRGEgYnJhY2ggcGz2dHpsaWNo IGRlciBTY2hsZWllciBpaHJlcyBBdWdlcywgdW5kIGVpbmUgd2lsZGUgWuRydGxp Y2hrZWl0DQplbnRzdHL2bXRlIGloci4gVW5kIGRhIGtvbm50IGljaCBuaWNodCBo YWx0ZW4sIGFiZXIgaWNoIHNjaHJpZSBuaWNodC4gRG9jaA0KaWNoIGtvbm50ZSBl cyBuaWNodCBoYWx0ZW4sIHVuZCBpY2ggZmz8c3RlcnRlLiBNZWluIEhlcnogd2Fy ZiBzaWNoIGR1cmNoDQptZWluZSBCcnVzdCwgYWJlciBpY2ggYmV3ZWd0ZSBrYXVt IGRpZSBMaXBwZW4uIEFiZXIgc2llIHNjaHdhbmQgYXVmIG1laW5lbQ0KSGlybiBh bHMgZGllIGJ1bnRlIEJldXRlIHVuZCB1bmdla2FubnRlIFrkcnRsaWNoa2VpdCBo b2Igc2llIG9obmUgSGFsdC4NCg0KSWNoIHd133RlLCBkYd8gaWNoIHNpZSBsaWVi ZW4gd/xyZGUgaW4gU2NobXV0eiB1bmQgaW4gVW5nbPxjaywgZGHfIGljaCBzaWUN CmxpZWJlbiB3/HJkZTogSWhyZW4gSGFscywgaWhyZSBaZWhlbiwgamVkZW4gU2No bWVyeiB1bmQgZGllIFdvbGx1c3QgdW5kIGRpZQ0KS3JhbmtoZWl0LCBlcyBnYWIg a2VpbiBFbmRlLiBJY2ggd2FyIHZvbGwgdW5kIPxiZXJzdHL2bXRlLiBJY2ggaGll bHQgZXMNCm5pY2h0IG1laHIgdW5kIGZs/HN0ZXJ0ZSBrYXVtIG1pdCBkZW4gTGlw cGVuLCBlcyBnYWIga2VpbmUgR3JlbnplbiBkZXINClZlcnr8Y2t1bmcuIEljaCB3 aWxsIGRpciBkaWVuZW4sIGZs/HN0ZXJ0ZSBtZWluIEhlcnosIGljaCB3aWxsIGRp Y2ggdPZ0ZW4uDQpBYmVyIGFsbGVzIHdhciBzaW5ubG9zLCBkZW5uIG1laW4gSGVy eiB3YXIgbuRycmlzY2gsIGRlbm4gZGllcyBoYXR0ZSBlcyBuaWUNCmdla2FubnQu DQoNClVuZCBpY2ggc3RyaWNoIGlociD8YmVyIGRpZSBIYWFyZSB1bmQgc2FndGU6 ILtJY2ggbGllYmUgZGVpbmUgWmVoZW4sIGljaA0KbGllYmUgZGVpbmVuIFNjaG1l cnogdW5kIGRlbiBTY2htdXR6IHVuZCBkaWUgS3JhbmtoZWl0LqsgQWJlciBlcyB3 YXIgd2VuaWcNCm51ciwgd2FzIGljaCB2ZXJzcHJhY2gsIGRlbm4gbWVpbiBHZWb8 aGwgd2FyIHZpZWwgZ3L232VyLCB1bmQgZGllcyB3YXIgbm9jaA0KbGFuZyBuaWNo dCBkaWUgR3JlbnplLCB1bmQgc2llIGzkY2hlbHRlIGds/GNrbGljaCB1bmQgZmVy bi4gSWNoIGhhdHRlDQp2aWVsZXMsIHdhcyBpY2ggbm9jaCBrZWluZXIgRnJhdSBn ZWdlYmVuLCBpY2ggaGF0dGUgWmFobGxvc2VzLCB3YXMgaW4gbWlyDQphdWZicmFj aCwgZGHfIGljaCB2b3IgR2z8Y2sgdmVyZ2luZy4gSWNoIGthbm50ZSBrZWluIEVu ZGUsIGljaCB3YXIgZGllDQpXZWxsZSwgZGVyIFNlZSB1bmQgZGllIEluc2VsIHVu ZCBmbPxzdGVydGUgbWl0IGplZGVtIEdlcuR1c2NoOiBvIGRh3yBpY2gNCmRpY2gg bGllYmUsIDAgZGHfIGljaCBkaWNoIGxpZWJlLCB1bmQgbWVpbiBNdW5kIHd1cmRl IHN0dW1tIHZvciDcYmVybWHfLg0KDQpOdW4gd3VyZGUgZGllIExhbmRzY2hhZnQg c3RpbGwuIERhcyBXYXNzZXIgbWlsZGVydGUgc2ljaCB1bmQgZ2VyYW5uIHp1DQpk dW5rbGVtINZsLCB1bmQsIHp1c2FtbWVuZ2VzY2hsb3NzZW4gaW4gZW5kbG9zZSBS dWhlLCBzdGllZyD8YmVyIGVpbmVtDQpTZWdlbGJvb3QsIGRhcyB0cuR1bXRlLCBk ZXIgVGFnIHppZWxsb3MuDQoNCkRpZSBJbnNlbCBnbPxodGUgbWl0IGR1bmtsZW0g QmFzYWx0IGluIGRlbSBy9nRsaWNoZW4gV2Fzc2VyLiBTaWUgaGF0dGUgZWluDQpH bORuemVuLiBFcyB3YXIgZWluIGdydW5kbG9zZXMgR2zkbnplbi4gSWNoIGFiZXIg d3XfdGUsIGRh3yBpY2ggYWxsZXMgZvxyDQpkaWVzZSBGcmF1IHR1biB3/HJkZSwg ZGVubiBzaWUgd2FyIHVuZ2VoZXVlciBpbiBtaXIuIFNlbGlna2VpdCBmbG/fIPxi ZXINCmRpZSBS5G5kZXIgZGVzIFRhZ2VzLg0KDQpFcyB3dXJkZSBBYmVuZC4NCg0K V2lyIGZ1aHJlbiB6dSBkZW4gWvxnZW4sIG5vY2ggZWggZGFzIExpY2h0IGF1c2xv c2NoLiBOb2NoIHN0YW5kIGRpZSBTb25uZQ0K/GJlciBkZXIgRWJlbmUsIGRpZSBz aWUgc2Nob24gYmVy/GhydGUsIHVuZCBkZXIgS3JhbnogaWhyZXMgTGljaHRlcyBi cmFjaA0Kc2ljaCBuYWNoIG9iZW4gaW4gZWluZXIgc3RpbGxlbiBicvxuc3RpZ2Vu IEdsdXQuDQoNCkFsbGVpbiBhdWYgZGVyIFRlcnJhc3NlIGRlcyBCYWhuaG9mcyBi ZXNjaGxv3yBzaWUgenUgYmxlaWJlbiB1bmQgbmljaHQgenUNCmZhaHJlbiwgZGVu IEJsaWNrIG5pZSB2b24gZGVtIFNlZSB1bnRlciBpaHIgbPZzZW5kLCBkZXIgaW1t ZXIgbeRjaHRpZ2VyIGRpZQ0KV2VsbGVuIGRlciBMYW5kc2NoYWZ0IGF1ZnNjaGxv 3yB1bmQgaW4gZGFzIExpY2h0IGRlciB1bnPkZ2xpY2hlbiBSdWhlDQpoaW5laW50 cnVnLg0KDQq7SWNoIG1133RlIGRpY2ggaGFiZW4sIEb8cnN0aW4uIEFiZXIgZGHf IGljaCBkaWNoIHNvIGxpZWJ0ZSwgbmllIGjkdHRlIGljaA0KZGFzIGdlZ2xhdWJ0 IC4gLiAuqywgc3RhbW1lbHRlIG1laW4gTXVuZC4NCg0KRGEgbmFobSBzaWUgZGVu IEJsaWNrIHZvbiBkZXIgR2VnZW5kLCB1bmQgaW4gZWluZW0gZmFzc3VuZ3Nsb3Nl biBadWVpbmFuZGVyDQp3YXJmIHVucyBlaW4gS3XfIHp1c2FtbWVuLCBhdWZnZXf8 aGx0IGRpZSBIZXJ6ZW4gaW4gZGVuIExpcHBlbiB0cmFnZW5kLA0KaWhyZSB6dWNr ZW5kZW4gV29ydGU6IGljaCBsaWViZSBkaWNoLCBpY2ggbGllYmUgZGljaC4NCg0K QWJlciBlcnN0LCBhbHMgZGVyIFp1ZyB1bnRlciBy9nRsaWNoZW4gV29sa2VuIGFu em9nLCBlcmthbm50ZSBpY2ggaW4gaWhyZW0NCktvcGYsIGRlciwgZWluZSBkdW5r bGUgU2NoYWxlLCBhdXMgZGVyIETkbW1lcnVuZyBoZXJhdXMgdmVyZ2VoZW5kIHNp Y2gNCmZvcm10ZSwgZGFzIEF1Z2UgaW4gbGV0enRlciBUaWVmZS4gRGEgZXJzY2hy YWsgbWVpbiBIZXJ6LCB1bmQgaWNoIHd1cmRlIGlycg0Kdm9yIFNlaG5zdWNodCB1 bmQgbWHfbG9zIGdldHJpZWJlbiB2b20gR2Vm/GhsLCByaWVmIG1laW4gTXVuZDog TyBkYd8gc2llDQpzdPxyYmUsIG8gZGHfIHNpZSBzdPxyYmUsIHdpZSB1bmVuZGxp Y2ggd/xjaHNlIG1laW4gR2Vm/GhsLg0KDQpBYmVyIGljaCB3YXIgZWluIE5hcnIg dW5kIHd133RlIG5pY2h0cyB2b24gVG9kLg0KDQpVbmQgYWxzIGRlciBNb3RvciB1 bnRlciBtaXIgZGllIE5hY2h0IGR1cmNoYnJhY2ggdW5kIG1pdCBncvxuZW4gTGlj aHRlcm4NCmRhcyBTY2hsb98gc3VjaHRlLCBkYSB6aXR0ZXJ0ZSBtZWluIEhlcnog bm9jaCBlaW5tYWwg/GJlcm38dGlnIHZvbg0KR2Vub3NzZW5lbSB1bmQgaWNoIGds YXVidGUsIG5pY2h0cyD8YmVydHLkZmUgZGllIEdlZvxobGUgZGVzIEJlc2l0emVz Lg0KDQpNZWluZSBBdWdlbiBzY2h1ZmVuIGZ1bmtlbG5kZSBEaW5nZSBpbiBkZW4g UmF1bS4gSWNoIHdhciD8YmVybeTfaWcgZ2Vm/GxsdA0KdW5kIHNwcvxodGUuIE1l aW5lIEF1Z2VuIHNldHp0ZW4gR2x1dCBpbiBkaWUgTmFjaHQsIHVuZCBkYXMgRGFz ZWluIHpvZyBzaWNoDQp6dXNhbW1lbjsgZXMgd3VyZGVuIEZyYXVlbi4NCg0KS2F0 aGFyeXMgbmljaHQgZ2Vub3NzZW5lcyBLbmllLCBpaHIgdW5nZWthbm50ZXMgbGV0 enRlcyBMYWNoZW4gcmVpenRlbg0Kc2NobWVyemhhZnQgbWVpbiBCZWdlaHIuIERp ZXMgd2FyIG5vY2ggbmljaHQgYmVlbmRldC4NCg0KQWJlciBkZW5ub2NoLCB3aWUg c2Nod2FuZCBlcyBoaW4gdW50ZXIgZGVtIGVpbmVuIEdlZvxobC4NCg0KVW5kIGlo ciBLb3BmIHN0cvZtdGUgd2llZGVyIGF1cyBtZWluZW4gQXVnZW4gaW4gZGllIER1 bmtlbGhlaXQgdW5kIHdhbmR0ZQ0Kc2ljaCBnZWdlbiBtaWNoLiBTbyB0cnVnIGlj aCBzaWUgaW4gbWlyLiBVbmQgc2llIHRpbGd0ZSBkaWUgR2VnZW5zdORuZGUsDQpi aXMgbmljaHRzIG1laHIgYmxpZWIgYWxzIGlocmUgTuRoZSwgZGEgcGz2dHpsaWNo IHN0/HJ6dGUgdW5iZWdyZWlmbGljaA0KVHJhdWVyIGluIG1laW4gSGVyeiwgYWxz IGljaCBzaWUgc2FoLiBBYmVyIGljaCBoYXR0ZSBuaWUgVHJhdXJpZ2tlaXQNCmdl a2FubnQgdm9uIEZyYXVlbiwgaWNoIHdvbGx0ZSBuaWNodCBsZWlkZW4sIHVuZCBp Y2ggYmnfIGF1ZiBkZW4gTXVuZCB1bmQNCmhvYiBkaWUgQnJ1c3QuDQoNClVuZCBk YW5uIHNjaHJpZSBpY2ggZ2VnZW4gaWhyIEdlc2ljaHQsIGRh3yBpY2ggbmljaHQg bGVpZGUuDQoNCkRhIHRyYXQgZGVyIFNjaGVpbiB1bSBpaHIgdmVybPZzY2hlbmRl cyBHZXNpY2h0LCB1bmQgaWhyIEdlc2ljaHQgd2FyIGtyYW5rDQp1bmQgc/zfIHp1 bSBXZWluZW4uIERhIG5laWd0ZSBpY2ggZGVuIEtvcGY6DQoNCkF1Y2ggZGEgd2ls bCBpY2ggYmVpIGRpciBzZWluLg0KDQpVbmQgbnVuIHd133RlIGljaCwgZGHfIGlj aCBHcmVuemVubG9zZXMgdW0gc2llIGxlaWRlbiB3ZXJkZSwgZGHfIGljaCBzdHVt bQ0KaW4gU2NobWVyemVuIHZpZWxsZWljaHQgc3T8cmJlLCBkYd8gZGllc2UgTGll YmUgbWljaCBkdXJjaCBhbGxlIEj2bGxlbg0KcmVp32UsIGRh3yBpY2ggYW4gU3Ry Yd9lbmVja2VuIHZlcmdpbmdlIGFtIEdlcnVjaCBlaW5lcyBCYXVtZXMgYW4NCkVy aW5uZXJ1bmcsIHVuZCBkYd8gZGllIFdlbHQgYXVzIG1laW5lbSBIaXJuIGdhbnog aGluYXVzZ2luZ2UgdW0gc2llLg0KDQpEYSB3dXJkZSBtZWluIEhlcnogZWlubWFs IG5vY2ggd2lsZCB1bmQgdW5nZWR1bGRpZywgdW5kIGJlc2Nod29yIEdvdHQgdW0N CktyYWZ0IHVuZCBab3JuIGdlZ2VuIGRpZXNlIExpZWJlLCB1bmQgaWNoIGJyZWl0 ZXRlIGRpZSBBcm1lIGF1cyB1bmQgc3RhbmQNCmFsbGVpbiBpbSBMaWNodCBtZWlu ZXIgTGF0ZXJuZSBhdWYgZGVtIE1vdG9yLCBkZXIgZGFzIFdhc3NlciB6ZXJ3/Ghs dGUsDQpnZWdlbiBkaWUgRHVua2VsaGVpdCBnZWtyZXV6aWd0Lg0KDQpVbmQgaWNo IHNjaHJpZSBpaG4gdW5nZWR1bGRpZyBhbjoNCg0Ku1dhcnVtIGdhYnN0IGR1IG1p ciBlaW4gd/ZsZmlzY2hlcyB1bmQgd2lsZGVzIEhlcno/qw0KDQpBYmVyIHNjaG9u IHNjaHdhbmQgZGVyIFpvcm4gdW50ZXIgZGVyIEluYnJ1bnN0LiBEZXIgSG9yaXpv bnQgc2NoaWVuIGVuZGxvcw0KdmVydGllZnQuIElociBCaWxkIGxhZyBhdWZnZXNj aGxhZ2VuIPxiZXJhbGwgaW4gbWVpbmVtIEJsdXQuDQoNCk1laW4gSGVyeiB3YXIg ZnJldWRpZyBhbGxlcyB6dSB0cmFnZW4uIEF1Y2ggZGVyIFNlZSB0cnVnIGVpbmUg c2NobWVyemxpY2hlDQpSZWluaGVpdC4gRGVyIFN0cmFuZCBsZXVjaHRldGUgd2Vp 3y4gU3DkdGVyIHdhcmYgR290dCBkZW4gTW9uZCBpbiBnbPxoZW5kZW0NCkJvZ2Vu IGR1cmNoIGRpZSBOYWNodC4NCg0KDQoNCg0KSkFFTA0KDQoNCk1JVFRFTiBpbSBn bGl0emVybmRlbiBHZXNjaHJlaSBlaW5lciBHYWxlcmllIHZvbiBQYXBhZ2VpZW4g ZmFuZCBpY2ggZGljaCBhbg0KZWluZW0gVGFnZSwgRvxyc3RpbiwgdW5kIHdpciB2 ZXJlaW50ZW4gdW5zLiBEdSBzdGFuZGVzdCB3aWxkIHVuZCBnbGVpdGVuZCwNCmlu ZGVtIGRpZSBidW50ZW4gVvZnZWwgZGljaCBtaXQgbGFuZ2VuIFJ1ZmVuIHVtc2No d2VidGVuLiBBbHMgaWNoIGRlaW5lDQpIYW5kIGv833RlLCBlcmhvYmVuIHNpY2gg YWxsZSBhdWYgaWhyZW4gU2NoYXVrZWxuIHVuZCBzY2h3ZW5rdGVuIGj2bGxpc2No DQpkaWUgRmz8Z2VsLCBkYSBicmFjaCBlcnN0IGRlaW4gZ2zkc2VybmVzIEdlc2lj aHQgdW50ZXIgZGVyIFL8aHJ1bmcuINxiZXINCmRlbSBHYXJ0ZW4gaGluZyBpbSBC bGF1IGRhcyBTaWxiZXJ6ZWljaGVuIHNjaG1hbHN0ZW4gTW9uZGVzLg0KDQpaZWJy YXMgdGFuenRlbiBnbORuemVuZCB3aWUgUGVybG11dHQgaW4gcXVlY2tzaWxiZXJu ZW4gQvZnZW4gYXVmIGRlciBXaWVzZS4NCkVpbnNhbSBzY2h3YW1tIGRlciBS/GNr ZW4gc3RvbHplbiBEcm9tZWRhcmVzIPxiZXIgZGVtIEdlYvxzY2ggbmViZW4gZGVp bmVyDQpBY2hzZWwuDQoNCkR1IGJpc3QgdHLkdW1lcmlzY2guIFdpZSBkaWUgU3Bp ZWdlbCBkZXIgT2x5bXBpYSwgZGVyIEdlcnVjaCBkZXIgT3BlciB1bmQNCmRpZSBX ZWhtdXQgZGVyIGJlbnppbmR1ZnRlbmRlbiBBdmVudWVuIGlzdCBkZWluZSBQdXBp bGxlIHZvbGwgTmViZWwsIHVuZA0KZGllIHN0aWxsZXJlbiBGYWhydGVuIGRlcyBC b2lzLCBHbGFueiB1bmQgUnVkZXJlciBsZXVjaHRlbiBkYXJhdWYgLiAuIC4gZHUN CnJpY2h0ZXN0IGRlbiBCbGljayBnZXJhZGU6IHVuZCBlcyBzdGVodCBlaW4gRG9s Y2ggZGFyaW4uIERpZXNlciBBYmVuZCBuYWhtDQprZWluIEVuZGUsIGRlbiB3aXIg ZHVyY2hzY2hyaXR0ZW4sIGVyIHNjaGllbiB3aWUgZWluIHBmaW5nc3RsaWNoZXMg RmVuc3Rlcg0KYXVmIGRlbiBHYXJ0ZW4gZHVyY2ggZGllIETkbW1lcnVuZy4NCg0K UGZhdWUgc3ByYW5nZW4gaW4gZGllIELkdW1lIHVuZCBzY2hsdWdlbiBkcm9oZW5k IHVuZXJo9nJ0ZSBS5GRlciBnZWdlbiBkZW4NCmdlcvZ0ZXRlbiBXZXN0ZW4gdW5k IHNjaHJpZW4gdm9yIFNlaG5zdWNodC4gR2VnZW4gZGllIEdhdHRlciB3dWNoc2Vu IGF1cw0KZGVuIFp3aW5nZXJuIHdlad9lIELkcmVuLCBicvxsbGVuZCwgd2llIEdl a3JldXp0ZSB1bmQgYmlzc2VuIHVudGVyIGdy9t9lcg0Kd2VyZGVuZGVtIE1vbmQg aW4gZGllIEVpc2VuLiDcYmVyIGRlbiBUZWljaGVuIGxhZyBTdGlsbGUgdW5kIPxi ZXIgZGVuIFVmZXJuDQpzdGVsenRlbiBzY2h35HJtZXJpc2NoIGVycmVndGUgRmxh bWluZ29zLg0KDQpQbPZ0emxpY2ggc2NocmllIGRlciBFbGVmYW50LiBEaWUgU3Rp bGxlIHd1Y2hzIHdpZSBIZXJ6c2NobGFnIPxiZXIgZGVuDQpHYXJ0ZW4uIERhbm4g YWJlciBlcmhvYiBzaWNoIG1pdCBlaW5lbSBUb24gZGllIFN0aW1tZSBkZXMgZ2Fu emVuIEdhcnRlbnMuDQpUaWVyZSBzY2hyaWVuIGluIGRlbiBGcvxobGluZywgZGVu ZW4gQmx1dCBkdXJjaCBkaWUgS2VobGVuIHNvdHQuIFNpZQ0Kc2NocmllbiBuYWNo IGRlbSBNb25kIGluIGRlciBE5G1tZXJ1bmcgaW1tZXIgbGF1dGVyIHZvciBXaWxk aGVpdCB1bmQNClNlaG5zdWNodCwgZXMgd2FyIGVpbiB0b2xsZXIgQWJlbmQsIEb8 cnN0aW4sIGRlciBnYW56ZSBUaWVya3JlaXMgcXVhbG10ZSB1bQ0KdW5zIHZvciBT Y2h3ZWnfIHVuZCBCZWdpZXJkZSwgZGVyIERhbXBmIHNjaG9iIHNpY2ggaW4gdW5z ZXJlIE78c3Rlcm4uDQoNCkR1IGhhdHRlc3QgZGllIExpZGVyIGhhbGIgZ2VzY2hs b3NzZW4uIER1IGxhY2h0ZXN0LCBhbHMgZGFzIEJsdXQgZGVzDQpSYXViemV1Z3Mg YXVmIHVucyBzdPxyenRlLCB1bmQgaWNoIGJlZ2VocnRlIGRpY2ggd2llIGVpbiBX b2xmIG1pdCBkZW4NClrkaG5lbi4NCg0KRHUgZnVocnN0IG1pdCB6d2VpIHRyYWJl bmRlbiBQZmVyZGVuIGhpbndlZyBkaWUgQWxsZWUgaGluZHVyY2gsIGRpZSBIdWZl DQprbG9wZnRlbiBub2NoIGR1cmNoIGRlbiBOZWJlbCwgYWxzIGljaCBkaWNoIGJs aXR6aGFmdCBFbnRmbG9oZW5lIG5pY2h0IG1laHINCmVyYmxpY2t0ZS4NCg0KQW0g TW9yZ2VuIGJyYWNoIGljaCBiZWkgZGlyIGVpbiwgaG9sdGUgZGljaCBmdW5rZWxu ZGVuIEZhc2FuIGF1cyBoZWxsZW0NCkJvdWRvaXIsIGF1ZiBtZWluZW4gQXJtZW4g cm9sbHRlc3QgZHUsIENvcHJhLCBpY2ggdHJ1ZyBkaWNoIGhpbnVudGVyIPxiZXIN CmRpZSBUcmVwcGVuIGluIGRhcyBzY2htYWxlIEF1dG8sIHdpciBibGl0enRlbiBn bPxoZW5kIGR1cmNoIGRpZSBTdGFkdCwNCmR1cmNoc2F1c3RlbiBkZW4gV2FsZC4g V2lyIGj2cnRlbiBkaWUgaGVsbGVuIEdsb2NrZW4g/GJlciBkaWUgV2Fzc2VyDQpi ZWxsZW4sIGljaCBob2IgZGljaCBhdWYgZGFzIFZlcmRlY2suDQoNClVuc2VyIERh bXBmZXIgd2FyIHdlad8gdW5kIHBvcnplbGxhbmVuLCBlciB3ZWlkZXRlIHNpY2gg aW4gZGVtIE1vcmdlbiwNCnNlaW5lIEthavx0ZW4gd2FyZW4gZWl0ZWwsIHNlaW5l IFJhaGVuIGZsYW1tdGVuLiBXaXIgZnVocmVuIGRlbiBSaGVpbg0KaGludW50ZXIg dm9sbCB2b24gTGljaHQuDQoNCk5pZSBzYWggaWNoIHZvbiBtZWluZW4gdmllbGVu IEZyYXVlbiBlaW5lIGhlcnJsaWNoZXIgYWxzIGRpY2g6IHdpZSBkdQ0Kc3RhbmRl c3QhIEJyYXVuLCBtZWluZSBq/GRpc2NoZSBG/HJzdGluLCBncm/fIGJpcyBhbiBt ZWluZW4gU2NoZWl0ZWwsIHZvbg0KZGVyIExvaXJlIGR1cmNoc/zfdCB1bmQgZGVu IEF0ZW0gZGVyIFN0ZXBwZW4gaW4gZGVuIE78c3Rlcm4sIGF1ZiBkZW0NClZlcmRl Y2sgbWl0dGVuIGluIFNvbm5lLiBEaWUgSORuZGUgaGF0dGVzdCBkdSBncm/fIHVu ZCBmcmVjaCBpbiBzY2htYWxlbg0KVGFzY2hlbiB2b3IgZGVpbmVtIEdlc2NobGVj aHQuIERlaW5lIExlbmRlbiBmbG9zc2VuIHZvciBMaW5pZW4gc2VpZGVud2VpY2gN CmR1cmNoIGRpZSBMdWZ0LiBEaWUgV2FnZSBkZXIgSPxmdGVuIHdpZWd0ZSD8YmVy IGRlbSBTcHJpbmdicnVubiBkZXIgYmVpZGVuDQpTY2hlbmtlbCB1bmQgZGVuIHRh bnplbmRlbiBGZWlnZW4gZGVpbmVyIEtuaWVlLg0KDQpEdSB6b2dzdCBkaWUgU2No dWx0ZXJuIGxlaWNodCBpbiBnZXf2bGJ0ZSBCb2dlbiB1bmQgc2Foc3QgcnVoaWcg bmFjaCBkZW4NClVmZXJuLiBBYmVyIGRlaW4gR2VzaWNodCB3YXIgdm9uIEJy5HVu ZSBzbyB3aWxkLCBkYd8gZGllIFlhY2h0ZW4gdW0gdW5zDQpoZXVsdGVuIHZvciBT ZWhuc3VjaHQuIEdsaXR0ZW4gRGFtcGZlciB1bnMgZ3L832VuZCB2b3L8YmVyLCBz Y2hyaWVuIGRpZQ0KU2lyZW5lbiBpbiBkZW4gTW9yZ2VuLiBEaWUgV2VsbGVuIHN0 b2JlbiB0b2xsIGhlcmF1ZiBpbiBkZWluZSBI9mhlLiBXaW5kDQr8YmVyc3T8cnp0 ZSBkaWNoLCB09mRsaWNoIHNjaPZuZSBT5HVsZSBq/GRpc2NoZW4gRmxlaXNjaGVz LCBG/HJzdGluLg0KDQpBbHMgZGVpbiBUdWNoIGZpZWwsIGtuaWV0ZSBlaW4gZHVu a2VsZXIgTWF0cm9zZSwgdW5kIGVpbmUgRmxhbW1lIHN0YW5kDQp6d2lzY2hlbiBz ZWluZW4gQnJhdWVuLg0KDQpEZWluIEJsdXQgd2FyIG3kY2h0aWcsIGRh3yBkZXIg U3Ryb20gaGludGVyIHVucyBoaW5ibGljaCB1bmQgZGllIFNjaGFyZW4NCmRlciBC dXJnZW4gYXVzZ2Vs9nNjaHQgaGludGVyIGRpZSBTb25uZSBrcm9jaGVuLCBkYd8g ZGVyIEFuc3R1cm0gZGVyIFVmZXINCmFicmnfIHdpZSBlaW4gU2Nodd8uIER1IHRp bGdzdCBkaWUgR2VnZW5kIGhpbndlZy4NCg0KU3RvbHogendpc2NoZW4gZGVuIHdl ad9lbiBGcmF1ZW4gZGVyIFBhc3NhZ2llcmUgYmlzdCBkdSBuaWNodCBtZWhyIGRp ZQ0KRvxyc3RpbiwgZHUgd+RjaHN0IPxiZXIgc2llIGhpbmF1cy4gSWNoIGhhYmUg ZGlyIGVpbmVuIGFuZGVyZW4gTmFtZW4NCmdlZ2ViZW4sIER1cmNobGF1Y2h0LCBp biBmbGll32VuZGUgU2VpZGUgR2Vo/GxsdGUsIGFiZXIgaWNoIHNvbGx0ZSBkaWNo DQpEZWJvcmEgbmVubmVuLg0KDQpEZW5uIGR1IHN0ZWhzdCAtLSB1bmQgbWVpbmUg QXVnZW4gZmxhbW1lbiBlcyBuYWNoIHdpZSBTb25uZW4gLS0gYXVmZ2VyZWNrdGUN ClJpY2h0ZXJpbiBhdWYgZGVtIEdlYmlyZ2UgRXBocmFpbS4gVXJhbHRlcyBCbHV0 IHdhbmRlbHQgc2ljaCB6dXL8Y2sgaW4NCmRlaW5lIEZpZ3VyLiBEaWUgZWlzZXJu ZW4gV2FnZW4gcm9sbGVuIGhpbnRlciBkaXIg/GJlciBkZW4gSG9yaXpvbnQuIEhl ZXJlDQpmYWxsZW4gbmllZGVyIHZvciBkaXIgYmV05HVidCB1bmQgcHJlaXNlbmQs IGRlcmVuIEhhYXIgZWluZSBGbGFtbWUgYXVmZ2VodA0K/GJlciBkZW4gUGFsbWVu c3TkZHRlbiwgVHJpdW1waCBzaW5nZW5kIGF1cyB0b3NlbmRlciBLZWhsZSD8YmVy IGRlbg0KUG9zYXVuZW4sIFNjaGx1Y2h0ZW4gZvxsbGVuZCBtaXQgZGVpbmVyIFN0 aW1tZSB3aWUgZWluZSBXb2xrZSwgYnJhdW4gdW5kDQppbmJy/G5zdGlnIHZvbiBk b25uZXJuZGVyIEdvdHRoZWl0IGR1cmNocmFzdGUgaW0gTW9uZCD8YmVyIEp1ZGEg c3RlaGVuZGUNCm5hY2t0ZSBUaWdlcmluLg0KDQpWb3IgZGlyIHJvbGxlbiBhdXMg ZGVtIEdlYmlyZ2UgZGllIFN0cvZtZSBkZXIgSGVlcmUgaW4gZGllIEViZW5lLiBO YWNrZW4NCmdlZuRsbHRlciBL9m5pZ2Ugc2llaHN0IGR1IGzkY2hlbG5kLCBpcnIg ZGVyIE11bmQgenVyIFNlaXRlIGdlem9nZW4uIFNpZQ0Kc3RlbGxlbiBkaWUgTGFk ZSB2b3IgZGljaC4gU2llIGVyc2NoYXVlcm4gaW4gaWhyZW4gS25vY2hlbiwgdW5k IHRhdXNlbmQNClN0cmVpdHdhZ2VuIGJyYXVzZW4gYXVzIGRlbiBU5Gxlcm4gaW4g ZGllIEViZW5lIGhpbmVpbi4NCg0KRmV1cmlnZXIgYWxzIGRpZSBkdW5rbGUgU29u bmUgRXVyb3BhcyBzdGVodCD8YmVyIGRlbSBTdGV1ZXIgZ2VwZmxhbnp0IGF1Zg0K ZGVtIEZsdd8gZGVyIFN0cmFobGVuc2NobGV1ZGVyIGRlaW5lcyB2aXNpb27kcmVu IGxlaWNodCBnZXf2bGJ0ZW4gTGVpYmVzDQp3ZWnfZmxhbW1lbmQgaW4gc2VpbmVy IEZpZ3VyLg0KDQpEYSBicmljaHQgaW4gZGllIG15c3Rpc2NoZSBHZWJ1cnQgQXNp ZW5zIGRhcyBMYXVlcm4gZGVpbmVzIHNjaHLkZ2VuDQpBdWdlbmxpZGVzLiBJY2gg Zmz8c3RlcmUgu0doZXR0b6ssIHVuZCBkZWluIEhh3yBzdGljaHQgaW4gbWljaCB3 aWUgZWluZQ0KS2xpbmdlLCBpY2ggYmFkZXRlIGluIGRlaW5lbSBIYd8gdW5kIHNj aHdvciBnZWdlbiBkZW4gV2luZCwgZGHfIGVyIHp1bQ0KU3T8cm1lbiBzdGVpZ2Us IGFiZXIgZGVyIFdpbmQgd2FyIGZlaWcgdW5kIGxhZyBhbiBkZWluZW0gRnXfIHdp ZSBlaW4gUmVoLg0KDQpEdSB0cnVnc3QgbGVobXJvdGUgVPxjaGVyIHVtIGRpY2gg bWl0IFNjaHdlZmVsc3Rlcm5lbiBhbSBBYmVuZCBhdWYgdW5zZXJlbQ0KQmFsa29u LiBXaXIgdHJhbmtlbiBkdW5rZWxlbiBXZWluLCBkZXIgc2No5HVtdGUgdW5kIGRh bm4gdGFuenRlc3QgZHUgYXVzDQpkZW0gWmltbWVyIGF1ZiBkaWUgVmVyYW5kYSwg YXVmIGRlciBkZXIgTW9uZCBzY2hvbiBuYWNoIGRpciBncmlmZi4gRGEgcmnfDQpp Y2ggZGllIFT8Y2hlciB2b24gZGlyLiBEaWVzZSBmdXJpb3NlIEVudGtsZWlkdW5n ISBFcyB3YXIgZWluZSBM9ndpbiwgZGllDQppY2ggdW1hcm10ZS4NCg0KQWJlciBh bGxlaW4sIGluZGVtIGRhcyBEdW5rZWwgZGVzIFJhdW1lcyBkaWNoIHZvbiBtaXIg YWJzY2hsb98sIHRhbnp0ZXN0IGR1DQpkaWUgU3By/G5nZSBkZWluZXMgdXJhbHRl biBCbHV0ZXMuIERlaW5lIFNjaHVsdGVybiBib2dlbiBzaWNoIPxiZXIgZGVuDQpB Y2hzZWxuLCBkZXIgUmhlaW4gaGluZyB3ZWnfIGdlc3Bhbm50IHVudGVyIGRpciBt aXQgZWluZW0gaGVsbGVuIG1ldGFsbGVuZW4NClRvbiwgc3RyYWhsZW5kIGhvYiBz aWNoIGRlciBCb2dlbiBkZWluZXMgSGFsc2VzLCBzY2j2biB1bmQgZ2V6b2dlbiB3 aWUgdm9uDQpzdG9semVuIEthbWVsZW4sIHVtIGRlcmVuIEtlaGxlbiBnb2xkZW5l IFNwYW5nZW4gbGllZ2VuLiBVbmQgYWxzIGR1DQp1bXRyYXRzdCwgdW5kIGRlciBN b25kIGRlaW5lbiBCYXVjaCB0cmFmIHVuZCBlbnRmYWNodGUsIGRhIHd1cmRlIGlj aA0Kd2FobnNpbm5pZywgRvxyc3RpbiwgdW5kIGR1IHRhbnp0ZXN0LCBtZXNvcG90 YW1pc2NoZSBL9m5pZ2luLCBnb2xkZ2VsYg0KZ2VmbGVja3QgZGllIFdlaWNoZW4g d2llIGVpbmUgVGlnZXJpbiwg/GJlciBkaWUgWmFja2VuIGRlcyBHZWJpcmdlcw0K RXBocmFpbSwgdW5kIGljaCByYXVidGUgZGljaCBhdWYgbWVpbmUgQXJtZSwgd2ll IHJvY2hzdCBkdSBuYWNoIE5hcmRlbiB1bmQNCnNjaHJpZXN0Lg0KDQpEZWluIEZ1 3yBpc3QgY2hpbmVzaXNjaCwgZGVpbmUgV2FkZSBhYmVyIHN0ZWh0IHNjaG9uIHZv bGwgV29sbHVzdC4NCg0KRGVpbmUgWnVuZ2UgaXN0IHZvbGwgVW56dWNodCB3aWUg ZWluZSBnaWVyaWdlIFBvc2F1bmUuIEljaCB3aWxsIGRlaW5lbSBNYW5uDQpkYXMg SGlybiD8YmVyIHNlaW5lbSBUaXRlbCBlaW5zY2hsYWdlbiwgZGVubiBkZWluZSBT Y2hlbmtlbCBzaW5kIGR1bmtlbA0KdmVyc3RyaWNrdCB1bmQgc3TkcmtlciBhbHMg TmFja2VuIGRlciBTdGllcmUuIERlaW4gd2lsZGVyIExlaWIgc2No5HVtdCD8YmVy DQp1bmQgbOTfdCBtaWNoIGlycmVuIGFuIEdvdHQuIER1IGzkY2hlbHN0LCBkaWUg ZGVyIE1vbmQgc2FsYnRlLCBpbQ0KRmV1ZXJyZWdlbiBkZXIgS/xzc2UsIGRlaW4g TXVuZCB6ZXJmbGVpc2NodCBtZWluZW4gQXJtLCBkZWluZSBnZWz2c3Rlbg0KTGlw cGVuIHdpcmJlbG4gdm9uIGZldWNodGVuIFdvcnRlbiwgZGVpbmUgWuRobmUgc2lu ZCBzcGl0eiB3aWUgdm9uIEhhaWVuDQp1bmQgZGllIFNvbm5lIGRlaW5lcyBMZWli ZXMgc2NoZWludCB0b2xsIGluIGRpZSBEdW5rZWxoZWl0LiBEZWluZSBCcvxzdGUN CmhlYmVuIHNpY2ggYnJhdXNlbmQgdW50ZXIgbWVpbmVtIE11bmQgd2llIGhlad9l IFF1ZWxsZW4sIHVuZCBkZWluIEhhbHMNCmVyaGVidCBzaWNoIHVuZCBzaW5ndCB3 aXJyIHdpZSBpbSBGaWViZXIuDQoNClNpZWhlIGFsbGVzIGlzdCBKb3JkYW4gZHJh dd9lbiB1bmQgZGllIEx1ZnQgc3RhcnJ0IHZvbiBQb3NhdW5lbiwgdGF1c2VuZA0K ZWlzZXJuZSBXb2dlbiByb2xsZW4gZG9ubmVybmQg/GJlciBkZW0gSGFsYmtyZWlz IHL2dGxpY2ggdW1mbGFtbXRlbg0KR2ViaXJnZXMuIEFsbGVzIHT2bnQgRXBocmFp bSBiaXMgaW4gZGllIEViZW5lLg0KDQpTY2hsYW5rZSBU5G56ZXJpbiBHb3R0ZXMs IG1pdCBkZW4g/HBwaWdlbiBMZW5kZW4gaW0gRmV1ZXIgZGVyIEJlcnVmdW5nLA0K QXVmZ2VyaWNodGV0ZSwgUmFzZW5kZSBtaXQgZGVuIEj8ZnRlbiwgS/ZuaWdpbiBs YW5nZW4gQmx1dGVzLCBEZWluIE11bmQNCnNpbmd0IGhlaXNlciB3aWUgZWluIFdv bGYgdW5kIGds/Gh0IHdpZSBlaW4gU3Rlcm4uDQoNCk5pZSBzYWggaWNoIEjkbmRl LCBsYW5nLCBicmF1biB1bmQgc2VsdGVuIHdpZSBkZWluZS4gQmxhdWVzIEhhYXIg ZGVpbmVyDQpTY2hs5GZlbiBsaWVndCB1bSBtZWluZSBLZWhsZSBnZXNjaGx1bmdl biB1bmQgbWVpbiBNdW5kIHNhdWd0IGF1cyBkZW0NCkVpbmRydWNrIGRlciBLaXNz ZW4gZGVuIEdlcnVjaCBkZWluZXMgRmxlaXNjaGVzIHp1cvxjaywgZGFzIGRhbXBm dCB1bmQNCnNjaGFyZiBpc3Qgd2llIHZvbiBkZW4gVGllcmVuIGRlciBX/HN0ZS4g RGllIGdvbGRlbmVuIFNpZWdlbCBkZWluZXINCnNjaHdlcmVuIEJyYXVlbiB6dWNr ZW4gdm9yIExpY2h0LiDcYmVyIHVucyByZW5udCBkYXMgcm90ZSBTZWdlbCBkZXMg TW9uZGVzLg0KQXVmIGRlbiBTcGl0emVuIGRlaW5lciBGaW5nZXIgZ2z8aGVuIGR1 bmtsZSBGbGFtbWVuLiBNZWluIEhlcnogc2NoYXVlcnQNCndpbGQgdm9yIGRpci4N Cg0KSGludGVyIGRlaW5lciBoZWnfZW4gU3RpbW1lIGxpZWd0IGVpbmUsIHdlaWNo IHVuZCBmbGF1bWlnIGJpcyB6dW0gUmFzZW4gZGVyDQpWZXJ6/GNrdW5nLCB1bmQg d2VubiBkdSBkZW4gZ3Jv32VuIE5hY2tlbiB6dXL8Y2t3aXJmc3QgdW5kIGphdWNo emVuZCBsZWlzDQplcnN09mhuZXN0LCBkYW5uIGphZ2VuIHdpciBpbSBTcGllbCBk ZWluZXIgSPxmdGUgYmVpZGUgYXVmIGRvbm5lcm5kZW0gV2FnZW4NCvxiZXIgZGll IEViZW5lIHZvciB6dWNrZW5kZW0gR2ViaXJnZSBFcGhyYWltLCBXaW5kIGRlcyBT aWVnZXMgZ2z8aHQg/GJlcg0KZGllIFN0aXJuZW4sIHVuZCBkaWUgU2lnbmFsZSBK YWh3ZXMsIGRlaW5lIEhhYXJlLCBmbGFtbWVuIHdpZSBlaW5lIGhlaWxpZ2UNCk1l dXRlIGhpbnRlciB1bnMuDQoNCkRlaW5lIEhhdXQgaXN0IGJyYXVuIG1pdCBzaWxi ZXJuZW0gRmxhdW0gdW5kIGdsYXR0IHdpZSBkZWluZSBadW5nZS4gRGVpbg0KR2Fu ZyBpc3QgZvxyc3RsaWNoZXIgYWxzIGRlaW4gTmFtZS4gQWxsZSBBdWdlbiBncvzf ZW4gZGljaCBhdWYgYWJlbmRsaWNoDQpmZXN0bGljaGVuIFByb21lbmFkZW46IEv2 bmlnaW4gZGVyIEF2ZW51ZSBXYWdyYW0gdW5kIGRlciBncm/fZW4gUmV2dWVuLCBh dWYNCmRlbiBE5G1tZW4g/GJlciBkZW0gYmxhdWVuIE1lZXIgbWl0IGRlbiBGYWhu ZW4sIGluIGRlciBoZWxsZW4gU2No9m5oZWl0IGRlcg0KS29yc29zIHVuZCBCbHVt ZW53YWdlbi4gSWNoIGFiZXIgZORtcGZlIGRlaW4gQmx1dC4NCg0KTGFjaHN0IGR1 LCB3ZWlsIG1laW4gUHlqYW1hIHdlad8gaW0gTW9uZCBzY2hpbW1lcnQgd2llIGVp bmVzIFBpZXJyb3QNCi4gLiAuIC4gRGllc2UgTmFjaHQgdG9idCBtaXQgcm90ZW4g TGF3aW5lbiBpbSBSaGVpbi4NCg0KSWNoIHNvbGx0ZSBkaWNoIERlYm9yYSBuZW5u ZW4uDQoNCiAgIEFiZXIgaWNoIGhhYmUgZGljaA0KICAgICAgICAgSkFFTA0KICAg ICAgICBnZW5hbm50Lg0KDQoNCldlaWwgZXMgd2lsZCBrbGluZ3Qgd2llIGVpbmUg Z2VzY2htZWlkaWdlIEz2d2luIHVuZCBpbmJy/G5zdGlnIHdpZSBkYXMNCm1ldGFs bGVuZSBTY2hyZWllbiBkZXIgSPZybmVyLCB1bmQgd2VpbCBpY2ggbmljaHQgd2Vp 3ywgd2VubiBpY2ggYXVmIGRlbg0KS3JhdGVybiBkZWluZXIgQnL8c3RlIHNjaGxh ZmUsIG9iIGR1IG1pciBuaWNodCBkdXJjaCBtZWluIEhpcm4gZWluZW4gTmFnZWwN CmluIG1laW5lbiBTY2hsYWYgc2NobORnc3QsIGJlcm5zdGVpbuR1Z2lnZXIgUGFu dGhlciB2b24gTGliYW5vbi4NCg0KDQoNCg0KRElFIEFCRU5URVVFUkxJQ0hFIE5B Q0hUDQoNCg0KSU4gZWluZXIgTmFjaHQgZnL8aGVyIGVudGRlY2t0ZW4gd2lyIHNj aHdlaWdlbmQgZGVuIGJlZmVzdGlndGVuIEhvZiwNCnplcnNjaGx1Z2VuIGVpbiBG ZW5zdGVyLCBzdPxybXRlbiBpaG4gdW5kIHN0YW5kZW4gdm9yIGplbmVyIGVuZGxv c2VuIEZsdWNodA0Kdm9uIFppbW1lcm4uDQoNCk51biwgd28gTmViZWwgZ2VzY2hp Y2h0ZXQgbGllZ3Qgendpc2NoZW4gbWlyIHVuZCBkZXIgRvxyc3Rpbiwgd28gd2ly DQpsZWlkZW4sIG51biBsZWJlIGljaCB0YWdlbGFuZyBtaXQgd2VuaWdlbiBkZXIg S2FtZXJhZGVuIGF1ZiBkZW0gSG9mLiBEaWUNCkVpbnNhbWtlaXQgd2VpY2h0IGlt bWVyIHRpZWZlciB2b20gSGltbWVsIGFiIHVuZCBy/GNrdCD8YmVyIGRhcyBSaWVk IGdlZ2VuDQp1bnMgYW4uIE5hY2h0cyBrb21tZW4gd2Vp32UgZ3Jv32UgS2F0emVu IGR1cmNoIGRlbiBNb25kIGdlZ2VuIGRpZSBzaWViZW4NCkFrYXppZW4gdm9yIGRl bSBUb3IuDQoNCkdhbnogZmVybmUgQmF1ZXJuIG51ciBtYW5jaG1hbCBoZWJlbiBk aWUgSGFuZCD8YmVyIGRpZSBCcmF1ZW4gdW5kIHNlaGVuDQphYmdlc2NoYXR0ZXRl biBHZXNpY2h0cyBuYWNoIGRlbiBTdHJlaWZlbmRlbi4gUmFzY2ggYWJlciB2ZXJt 5GhsZW4gc2ljaA0KaWhyZSBCZXdlZ3VuZ2VuIHdpZWRlciBkYW1wZmVuZGVyIEVy ZGUgdW5kIGVybnRlbmRlbSBHZXLkdC4NCg0KSGllciBpc3QgZGFzIFBhcmFkaWVz LiBXaXIgd2VyZGVuIGlubmlnIG1pdCBkZW4gVGllcmVuLiBBdWYgZGVuIETkbW1l bg0KbGF1ZmVuZCwgc2VoZSBpY2ggdm9tIEhvZiBLb21tZW5kZSwgdm9tIEhvZiBH ZWhlbmRlIHVuZCBhbGxlIGhhYmVuIG1laHIgYWxzDQptZW5zY2hsaWNoZSBBbm11 dCwgd2VubiBzaWUgZGllIEdy5GJlbiD8YmVyc3ByaW5nZW4sIGRpZSBkaWUgTGFu ZHNjaGFmdA0Kd2lsZCB6ZXJzY2huZWlkZW4sIHVuZCBpbiBTY2hpbGYgc2Nob24g ZWluZ2V0YXVjaHQgd2llZGVyIGF1ZiBsYW5nZW4gRORtbWVuDQpoaW5nZWhlbiwg buRoZXIgZGVtIEhpbW1lbCBhbHMgamUuIEFiZW5kcyBzaXR6ZW4gd2lyIGF1ZiBk ZXIgcnVuZGVuIE1hdWVyDQp1bmQgc2VoZW4sIHdpZSBkaWUgaGVyYnN0d2Vp32Vu IExlaWJlciBkZXIgV2VpZGVuIHNpY2ggdm9yIGRlbiBIb3Jpem9udA0Kb3JkbmVu IHVuZCByaWVzZW5oYWZ0IGxvaGVuLg0KDQpNb3JnZW5zIHppZWh0IE5lYmVsIGlu IGRpZSBHZWdlbmQgdW5kIFJlaGUgbmFoZW4gZGVyIE1hdWVyIHVuZCB3ZWljaGVu DQpuaWNodC4gVW0gbWVpbmVuIEdhbmcgYW4gZGVuIEthbuRsZW4gc2Nod2lycmVu IEZhc2FuZSwgcm9zdHJvdGUgTGVpYmVyDQrkbmdzdGVuZCB6d2lzY2hlbiBkZW0g WnVja2ZsdWcgZGVyIHNjaG1hbGVuIEZs/GdlbCB1bmQgZWluIFBmZWlmZW4gaW0g TXVuZCwNCmRhcyBkaWUgU3RpbGxlIGVyc3Qgd2llZGVyIHNhbmZ0IG1hY2h0Lg0K DQpIaWVyIHNpbmQgbnVyIFRpZXJlLiBVbmQgc2VsYnN0IGRpZSBIYXNlbiBsYXVm ZW4gaW4gQm9nZW4gdW0gdW5zIGhlcnVtIHVuZA0KaGFsdGVuIGRpZSBPaHJlbiB3 ZWljaCBhbiBkZW4gSGFscyBnZWxlZ3QuIFdpciBoYWJlbiBkYXMgUmllZCD8YmVy c2Nod2VtbXQsDQphYmVyIHdpciBy/GhyZW4gbmljaHQgYW4gZGllc2VuIEZyaWVk ZW4uIFdpciBuZWlnZW4gdW5zIHp1IGRlbSBUaWVyIHVuZCBkYXMNClRpZXIgdmVy d+RjaHN0IHVuc2VyZXIgQmV3ZWd1bmcuIERpZSB3ZWnfZSBCbHVtZSBkZXIgUmVo aW4gbGV1Y2h0ZXQgdW5zIHp1Lg0KV2VpaGUga3JlaXNlbiBtaXQgc3RpbGxlbiBG bPxnZW4gdW0gdW5zZXJlbiBLb3BmLg0KDQpBYmVuZHMgZHVyY2ggZGVuIHNpbGJl cm5lbiBOZWJlbCBrb21tdCB2ZXJrbORydCB2b24gbWlsZGVuIFNjaGVpbmVuIGVp bg0KSGlyc2NoIPxiZXIgZGllIEFsdHJoZWluLUJy/GNrZSwgdW5kIGdlaHQgYXVm IHVucyB6dSD8YmVyIGRpZSBo9mx6ZXJuZQ0KUGxhbmtlLCBkaWUgaGludGVyIGlo bSBhbSBFbmRlIHNpY2ggdW5pcmRpc2NoIHNjaG9uIHZlcmVuZ3QuDQoNCkVpbm1h bCBudXIgbWFjaHRlbiB3aXIgZWluZSBtZW5zY2hsaWNoZSBSZXZvbHRlIGdlZ2Vu IGRpZSBQYXJhZGllc2lzY2hrZWl0DQp1bmQgbGllZmVuIGluIGVpbmVtIFVtenVn IG1pdCBHZWtyZWlzY2ggdW5kIE11c2lrIGJpcyB6dXIgRuRocmUuDQpadXL8Y2tr ZWhyZW5kLCBzdGVodCB1bnNlciBIb2YsIGhhbGIgenVnZXdhY2hzZW4gdm9uIGZl cm4gZHVyY2ggU2NoaWxmIHVuZA0KV2VpZGUgdW5kIGdlc2Nod3VuZ2VuZSBMYW5k c2NoYWZ0IHNhZnRpZ2VyIEthbuRsZSwg/GJlcnNjaG5pdHRlbiB2b24NCkTkbW1l biwgdm9yIGVpbmVtIGxvZGVybmRlbiBIZXJic3RoaW1tZWwsIGVyc3RhcnJ0IG1p dCBkZW4gRmVuc3Rlcm4sIHVuZA0KZHVua2VsbmQgc2Nod2luZ2VuIHNpY2ggc2Vp bmUgd2Vp32VuIFNjaG9ybmUgZHJvaGVuZCBpbiBkZW4gUmF1bSB3aWUNCkZsYW1t ZW4gYXVzIEVyei4gSmVkZXMgVGllciBzY2h3ZWlndCB1bSBkYXMga3ViaXNjaGUg R2Vi5HVkZSwgdW5kIGRpZSBsYW5nZQ0KRmx1Y2h0IGRlciBEaWVsZSwgZHVyY2gg ZGllIHNjaG9uIFNhbGllciBzY2hyaXR0ZW4sIGxpZWd0IGluIGJsYXVlbg0KU2No d2VmZWxzY2hhdHRlbi4gU2Nob24gc3T8cnp0IHdpZWRlciD8YmVyIG5vY2ggZmxh Y2tlcm5kZSBTdGltbWVuIGRpZQ0KRWluc2Fta2VpdCBkdXJjaCBkZW4ga2z2c3Rl cmxpY2hlbiBHYXJ0ZW4gYXVmIGRlbiBIb2YuDQoNCldpciBzdHJldXRlbiB1bnMg /GJlciBkYXMgTGFuZCwgd2lyIHRyYW5rZW4gaW4gcXVlbGxlbmRlciBMYW5kc2No YWZ0IHdpZQ0KbPxzdGVybmUgV/ZsZmUgS3VobWlsY2ggYXVzIGRlbiBFdXRlcm4s IHNjaHdhbW1lbiB6dW0gR2Fzc2VuZ2VmdW5rZWwgZGVyDQpOYWNodCD8YmVyIGRl biBSaGVpbiBpbiBrbGVpbmUgQmVyZ3N05GR0ZSwgd2lyIHplY2h0ZW4gZHVyY2gg dW1idXNjaHRlDQpE9nJmZXIgdW5kIG1hY2h0ZW4gUHJhc3NlcmVpIG1pdCBkZW4g VmVyd2FsdGVybiBhdWYgZ3Jv32VuIEf8dGVybi4gTmFjaHRzDQppbSBJbm5lbmhv ZiwgZ2zkbnplbmQgdm9yIFRhdWx1ZnQsIHVuZCBHZXN0aXJuZSBmcmVtZCD8YmVy IGRlbSBIYXVwdCwNCmJhZGV0ZW4gd2lyIHVudGVyIGRvbm5lcm5kZXIgQnJ1bm5l bmZsdXQuDQoNCklyZ2VuZGVpbmVyIG5haG0gZWluZW4gS2llbnNwYW4gdW5kIGxp ZWYgbmFja3QgZHVyY2ggZGllIHdlbGtlbiBCbOR0dGVyIHVtDQpkaWUgcnVuZGUg Umllc2VubWF1ZXIsIHVuZCBhbmRlcmUgZm9sZ3Rlbiwgc3R1bW0gdm9yIEphZ2Vu Lg0KDQpMYW5nIHZvcmJlcmVpdGV0IGVyc2NoaWVuIGRpZSBhYmVudGV1ZXJsaWNo ZSBOYWNodCwgd28gYWxsZXMgd2Vp3yBnbPxodGUNCm1pdCB1bmdlaGV1ZXJlciBJ bm5pZ2tlaXQuDQoNCkdyb99lIFNjaHfkcm1lIHZvbiBSYWJlbiBzY2h3YW5nZW4g aW4gbGFuZ2VuIEtyZWlzZW4gdW0gZGllIGhhbGJlIFNjaGVpYmUNCmRlcyBzY2hv biBhdXNnZWR1bmtlbHRlbiBIaW1tZWxzLCBhYmVyIGRpZSBhbmRlcmUgSORsZnRl IHdhciB2b24gTGljaHRlcm4NCmlyciD8YmVyc2No/HR0ZXQsIHVuZCBkaWUgZ2Vp c3RlcmhhZnRlbiBa/GdlIHdpbGRlciBFbnRlbiBzY2h3YW1tZW4gZHVyY2gNCmRh cyBHZWZsYWNrZXIgc2FuZnQgaW0gU3Ryb20gZGFoaW4uDQoNCkluIGRpZXNlciBO YWNodCB0YW56dGVuIGRpZSBy9nRsaWNoZW4gTeR1c2UgaW4gc3RpbGxlbiBXaXJi ZWxuIGR1cmNoIG1laW4NCmdyb99lcyBoZWxsZXMgWmltbWVyLCB1bmQgZHVyY2gg ZGllIHplcmJyb2NoZW5lbiBGZW5zdGVyIGxlZ3RlIHNpY2ggZGllDQpidXNjaHJl aWNoZSBMYW5kc2NoYWZ0IGluIGVpbmVyIFdlbGxlIHZvciBtaWNoIGhpbiwgdW5k IGRhIHd1Y2hzIG1laW5lDQpTZWhuc3VjaHQgdW5kIGljaCBsYWcgc3R1bmRlbmxh bmcgaW0gRmllYmVyLg0KDQpVbmQgYWxzIGljaCBnbPxodGUgdW5kIHdpcnIgdm9y IExlaWRlbnNjaGFmdCBkaWUgTGFuZHNjaGFmdCBiZWdlaHJ0ZSB1bmQNCmRlbiBN b25kLCBkYSBzY2hyaWUgZGllIEVsc3RlciBpbiBkZXIgSG9mcGxhdGFuZSBlbnRz ZXR6bGljaCwgdW5kIGRpZQ0Kc2NobWFsZSBo/G5kaW5oYWZ0ZSBI/GZ0ZSBkZXIg SG9sb3BhaW5lbiBy/GhydGUgYW4gbWVpbiBCbHV0Lg0KDQpBYmVyIGljaCBrYW5u dGUgc2llIGthdW0gbWVociB1bmQgZmz8c3RlcnRlILtBbmdlbGlxdWWrIHVuZCBt ZWluIHp1ciBTZWl0ZQ0KZmFsbGVuZGVyIEJsaWNrIHRyYWYgZGVuIGlocmVuLiBV bmQgZGllIEdlZ2VuZCB3dXJkZSB1bmR1cmNoc2ljaHRpZ2VyDQpoaW50ZXIgaWhy IHVuZCBpaHIgcvZ0bGljaGVzIEhhYXIgd2FyZCBibGHfIGluIEJsb25kaGVpdCB1 bmQgZGllIEF1Z2VuDQpzY2h3YW1tZW4gaWhyIHdlad9lci4NCg0Ku1dhcyB3aWxs c3QgZHU/qyByaWVmIGljaCB1bmQgZmx1Y2h0ZSBhdWYgZGllIEVsc3Rlci4NCg0K u0RpZSBBYmVuZGUgdm9uIFBhc3N5qywgc2FndGUgc2llLCB1bmQgWnVja2VuIGxp ZWYgdW0gaWhyZW4gc2xhdmlzY2hlbg0KTXVuZC4gQWJlciBzb2ZvcnQga2FtIGRp ZSBMaXBwZSBpbiBzcHJpbmdlbmRlcyBSZWRlbiB1bmQgd/ZsYnRlIHNpY2gga/xo bDoNCrtFaW5tYWwgYmVpbSBFcndhY2hlbiB3YXIgZGVpbmUgSGFuZCwgZGllIG1p Y2ggaGllbHQsIHNvIGdyb98sIGRh3yBpY2gNCnVtc2FuayB2b3IgTGllYmUuIERh cyB3YXIsIGFscyBkdSBpbSBQaGFydXNrZWdlbCBkZXIgQXV0b2xhdGVybmVuIEph aW5pa29mZg0KaW4gZGVuIE11bmQgaGllYnN0IHVuZCBtZWluIGZpbm5pc2NoZXIg SW1hdHJhIGVyYnJhdXN0ZS4gRXMgZvxsbHQgbWVpbmUNClRhZ2UuIEVzIGb8bGx0 IG1laW5lIE7kY2h0ZS6rDQoNCklociBNdW5kIHd1cmRlIGJpdHRlci4NCg0Ku0lj aCBtdd8gbWVpbiBIZXJ6IG5vY2ggaORydGVyIG1hY2hlbqssIHNhZ3RlIGljaCB1 bmQgaGF0dGUga2VpbiBNaXRsZWlkLg0KDQpEYSBsb3NjaCBlaW4gc2lsYmVybmVy IFN0cmFobCD8YmVyIGlociBHZXNpY2h0IHVuZCBpaHJlIEj8ZnRlbiBnbGl0dGVu IGZhc3QNCnVuYmV3ZWd0IGFiZXIgZXJyZWdlbmQsIHVuZCBzaWUgd2llcyBhdWYg aWhyZSBoZXJybGljaGVuIEJlaW5lOiC7QXVjaCBzaWUNCmdlbHRlbiBkaXIgbmlj aHQgbWVociwgbWl0IGRlbmVuIGljaCBkdXJjaCBkaWUgc2NocmVpZW5kZW4gQ2Fi YXJldHMgZGVzDQpNb250bWFydHJlIHZvciBkaXIgdGFuenRlLCBkaWUgZHUga/zf dGVzdCB2ZXJnZWhlbmQsIG5hY2hkZW0gc2llIGF1ZiBkZW4NCkL8dHRlbiBhbGxl ciBDYWbpcyBnZWds/Gh0P6sNCg0KRGEgd3VyZGUgbWVpbiBNdW5kIHNlaHIgem9y bmlnIPxiZXIgaWhyIFF15GxlbiB1bmQgaWNoIHNjaOR1bXRlLiBBYmVyIHNpZQ0K cmljaHRldGUgZGVuIEJsaWNrIGxhbmcgYXVmIGlobiwgYmlzIGVyIHNpY2ggcnVo aWdlciBsZWd0ZS4NCg0KRG9jaCB3YXIgZXMgc2Nob24gbmljaHQgbWVociBkaWUg VORuemVyaW4sIHNvbmRlcm4gZXMgd2FyIGluIHNjaGxhbmtlcg0KRvxsbGUgZWlu ZSBhbmRlcmUsIGVzIHdhciBZbG9uYSwgdW5kIGhvYiBzaWNoIG1pdCBmb3JkZXJu ZGVyIExpcHBlIGdlZ2VuDQptaWNoOg0KDQq7RHUgdHVzdCBVbnJlY2h0LqsNCg0K u0phLKsgc2FndGUgaWNoLCC7d2VpbCBpY2ggYmVyZWl0IGJpbiwgZXMgdGF1c2Vu ZG1hbCB6dSBi/N9lbi6rDQoNCrtEaWVzIGhpbGZ0IG1pciBuaWNodC6rDQoNCkFi ZXIgaWNoIHNhZ3RlIGlociwgZGHfIHNpZSBzaWNoIHNlbGJlciBoZWxmZSB1bmQg dORuemVyaXNjaCBzaWNoIGJld2VnZQ0K/GJlciBkaWUgZPxubmUgZ2zkc2VybmUg S3VwcGVsIGRlcyBMZWlkZXMuDQoNCkRhIHd1cmRlIGlociBHZXNpY2h0IG1pbGQg dW5kIG1vbmR3YXJtIHVuZCBzaWUgc2FndGU6ILtEdSBiaXN0IG5vY2ggbmljaHQN CnNvIHdlaXQuqw0KDQpJY2ggc2FoIHNpZSBhbi4NCg0KU2llIHNhZ3RlIGxhbmdz YW06ILtNZWluIG5ldWVyIFBlbHogaXN0IHNjaPZuLCBkb2NoIGZyZXV0IGVyIG1p Y2ggbmljaHQuDQpJY2ggc2VoZSB2aWVsZSBVbWFybXVuZ2VuLiBTaWUgc3Rv32Vu IG1pciBpbnMgSGVyei4gSWNoIHNlaGUgZmV0dGUgQWFsZSBpbg0KZGVuIExhZGVu c2NoZWliZW4uIEljaCB3ZWnfIG5pZW1hbmQsIGRlbSBpY2ggc2llIHNlbmRlLiBW aWVsZSBN5G5uZXINCmJlZ2VocmVuIG1pY2guIEljaCBt9mNodGUgbWljaCBrZWlu ZW0gZ2ViZW4uIFVuZCBnZWJlIGljaCBtaWNoIGVpbmVtLCBpc3QNCmVzIG51dHps b3MgZvxyIG1laW4gQmx1dC4gRXMgZ2lidCBuaWNodHMsIGRhcyBtZWluZXIgU2Vo bnN1Y2h0IG5haCBr5G1lLg0KRGVubiBkdSBiaXN0IHdpZSBlaW4gR2VzZXR6IGRh cvxiZXIgdW5kIGR1IGhhc3QgYW4gYWxsIGRlbiBEaW5nZW4ga2VpbmVuDQpUZWls LqsNCg0KRG9jaCBkYSBzY2hyaWUgaWNoOg0KDQq7R2xhdWJzdCBkdSwgZXMgcXXk bGUgbmljaHQsIGRh3yBqZWRlcyBHbPxjayBkYXN0ZWh0LCBzY2hvbiB6dXNhbW1l bmdlaGF1ZW4NCnZvbiBkZW0gbmV1ZW4uIFdlad90IGR1IG1laW4gSGVyeiwgZGFz IGluYnL8bnN0aWcgYmVnZWhydCB6dSBoYWx0ZW4gdW5kIGRhcw0KZGVyIFRha3Rz Y2hsYWcgc2VpbmVzIEFuZ3JpZmZzIHdlaXRlciByZWnfdC4gQWxsZXMgcmlubnQg YXVzIGRlbiBI5G5kZW4sDQpkZXJlbiBXaWxsZSBlcyBpc3QsIG5pY2h0cyB6dSB0 dW4gYWxzIHp1IGhhbHRlbi4gQWJlciBzaWUgZ3JlaWZlbiBudXIuIFVucw0KaXN0 IGtlaW4gQmV0dCwga2VpbiBTdHVobC4gVW5zZXIgQmx1dCBzY2hyZWl0IEhlaW1h dCwgYWJlciBlcyBzdHL2bXQgaW4NCmJ1bnRlIEVyZ3JpZmZlbmhlaXQuIFdpciBo YWJlbiBrZWluZSB3YXJ0ZW5kZSBCcnVzdC4gV2lyIGhhYmVuIGRlbiBGbHVjaA0K ZGVyIFplcnJpc3NlbmVuIGF1cyBkZXIgU2VobnN1Y2h0IHVuZCBt/HNzZW4gdmVy evxja3QgSXJyZW5kZSBzZWluLqsNCg0Ku0R1IGhhc3QgZGVuIEdsYXViZW4gbmlj aHSrLCBzYWd0ZSBzaWUuDQoNCkFiZXIgbWVpbiBIZXJ6IHdpZXMgbGFjaGVuZCBh dWYgc2VpbmUgV3VuZGVuLCB1bmQgZXMgc2NoaWVuIHZvciBtaXIgc2VsYnN0LA0K Z2VwZmxhbnp0IPxiZXIgZGVyIExhbmRzY2hhZnQuDQoNClNvIHNhaCBpY2ggZXMg c2VsYmVyIHdpZSBhdXMgS3Jpc3RhbGwgd2Vp3yBlcnN0cmFobGVuZCBtaXQgc2ll YmVuIERvbGNoZW4sDQp1bmQgYmx1dGlnZXMgSGFyeiBxdW9sbCBkYXJhdXMuDQoN ClVuZCBab3JuIPxiZXJmaWVsIG1pY2guIFVuZCBpY2ggd2llcyBhdWYgZGllIFNl aG5zdWNodCwgZGllIG1laW4gSGVyeg0KcXXkbHRlOiC7V2Vp33QgZHUgbmljaHQs IGRh3yBpY2ggaW4gV2lycnVuZ2VuIGxlYmUsIHdpbGRlciB3aWUgZGllIGV1cmVu LA0KdW5kIGluIFNjaG1lcnplbiwgZGllIGV1cmUg/GJlcnN0ZWlnZW4uIERh3yBp Y2ggZXVjaCBhbGxlIHZlcmdh3ywgdW5kDQp6ZXJxdWV0c2NodCB2b3IgU2VobnN1 Y2h0IHN0cmVpdGUgdW0gZGllIEb8cnN0aW4uqw0KDQpVbmQgbWVpbmUgQXVnZW4g dHLkbnRlbiD8YmVyLCB1bmQgaWNoIHNhaCBkZW4gZW50ZmVybnRlbiBMZWliIGRl ciBG/HJzdGluDQp3aWVkZXIgdm9yIGFsbGUgRGluZ2UgZ2VzY2hvYmVuOg0KDQq7 V2VtIGlzdCBiZXN0aW1tdCwgZ2z8Y2tsaWNoIHp1IHNlaW4/IFNpZWgsIHdpZSB3 aXIgYWxsZSB1bWVpbmFuZGVyIGluDQpadWNrdW5nZW4gbGllZ2VuLiBBYmVyIGVz IGxlYmUgZGFzIHVuZ2VzY2hsYWdlbmUgSGVyei6rDQoNCkplZG9jaCBkZXIgWm9y biB1bSBkaWUgRvxyc3RpbiD8YmVyd2FuZCBtaWNoIHZvciBZbG9uYXMgQXVnZW4g dW5kIGljaCBzdGFyYg0KZmFzdCB2b3IgU2NobWVyeiwgdW5kIG5pY2h0cyBoYXR0 ZSBXZXJ0IG1laHIgaW4gZGllc2VyIFNla3VuZGUgZ2VnZW4gaWhyZW4NCkxlaWIu IFVuZCB6dXNhbW1lbnNpbmtlbmQsIGZs/HN0ZXJ0ZSBpY2gsIHVuZCByaWVmIGlo ciBCaWxkIGF1ZnMgaGVmdGlnc3RlDQp2b3IgbWVpbmUgQXVnZW46DQoNCrtJY2gg aGFiZSB3ZW5pZyBMdXN0IGFuIGFuZGVyZW4gRnJhdWVuLCBkaWUgRmFzYW5lIHVu ZCBkaWUgcnVuZGUgTWF1ZXIgdW5kDQpkaWUgUmVoaW4gc2luZCBvaG5lIEJlbGFu Zy4gTWljaCBzdPZydCBkaWUgaW5icvxuc3RpZ2UgR2x1dCBkZXIgc3RlcmJlbmRl bg0KV2VpZGVuLiBNZWluIFJ1aG0gaXN0IEzkY2hlcmxpY2hrZWl0LCBnZW1lc3Nl biBhbiBkZWluZW0gS25pZS4gQWxsZXMgd2lsZGUNClR1biBpc3QgaXJyZXIgV2Vn IHVuZCBkdSBudXIgYmlzdCBaaWVsLCBiaXN0IGRpZSBTZWhuc3VjaHQuqw0KDQpX aWVkZXIgc2FoIGljaCBtaWNoIHNlbGJlciBnZXN0/HJ6dCBpbiBkaWUgTGFuZHNj aGFmdCwgdW5kIGZlcm4gaW0gd2Vp32VuDQpMaWNodCBrbmlldGUgWWxvbmEgYXVm IGRlciBFYmVuZSwgdW5kIGhpbnRlciBpaHIgd3VjaHNlbiB3aWUgbGljaHRlcmUN CkZsYW1tZW4gYW5kZXJlIHp1IGVpbmVyIHJpZXNpZ2VuIEtldHRlIPxiZXIgZGll IEViZW5lLCB1bmQgYWxsZSBzY2hyaWVuIGlocg0KTGVpZCBzaWNoIGluIGRpZSBH ZXNpY2h0ZSB1bmQgd3VyZGVuIGxhbmdzYW0gcnVoaWcgdW5kIHN0aWxsLg0KDQpB YmVyIGFscyBpY2ggbWl0IHp1cvxja2tlaHJlbmRlbSBCbGljayBkZW4gWWxvbmFz IHRyYWYsIGjkcnRldGUgaWNoIG1laW4NCnp1Y2tlbmRlcyBIZXJ6IHVuZCBpY2gg c2FndGUgaWhyLCBkYd8gbWlyIG5pY2h0IGJlc3RpbW10IHNlaSwgYW4gU2VobnN1 Y2h0DQp6dSBzdGVyYmVuLiBVbmQgZGHfIGljaCD8YmVyIGRpZSBMZWlkZW4gc3By aW5nZW5kIHZpZWxlcyB0dW4gd29sbGUuIERh3w0KemFobHJlaWNoZSBGcmF1ZW4g YXVmIG1pY2ggd2FydGV0ZW4sIGRh3yBpY2ggRWhyZW4gZ2VpbCBlcnN0cmViZSwg RmFocnRlbg0KdW5lbmRsaWNoIHVudGVybuRobWUgdW5kIHN0cmFobGVuZGUgR3Jv 32hlcnpvZ2lubmVuIGJlc+TfZSwgc3T8cmJlIGF1Y2gNCmRhcnVudGVyIHdlZyBk YXMgSGVyeiB2b3IgVHJhdWVyIHdpZSBlaW5lIGFiZ2ViaXNzZW5lIEZydWNodC4N Cg0KRGEgc2FoIHNpZSBtaWNoIGFuIHVuZCBs5GNoZWx0ZS4NCg0KVW5kIGlociBM 5GNoZWxuIHdhcmQgc28gaXJyIHVuZCBz/N8sIGRh3yBpY2ggd2lsZCBlcnNjaHJh aywgdW5kLCBI9mxsZW4NCmFobmVuZCwgZGllIGljaCBuaWNodCBrYW5udGUsIGRp ZSBTZWhuc3VjaHQgYXVmc2Nob98gZ2VnZW4gZGllIEVpbnNhbWtlaXQuDQoNCkFi ZXIgc2llIHRhdCBpaHIgTORjaGVsbiBuaWNodCB3ZWcsIHVuZCBkYSBoaWVsdCBp Y2ggZXMgbmljaHQgbWVociBhdXMuDQoNCkljaCBzdGFuZCBhdWYuDQoNCkljaCBn aW5nIGhpbvxiZXIgaW4gZGVuIFNhYWwuDQoNCk1pdCBicm9uemVuZXIgUmVpdGVy cGF1a2UsIGRpZSBHcm/fZW4gRnJpZWRyaWNocyBSZWdpbWVudGVyIGluIGRpZSBT Y2hsYWNodA0KZ2VkcvZobnQsIGJlZ2FubiBpY2ggZGVuIFVtenVnLiBTdGFyciB1 bmQgemVyZW1vbmllbGwuIEZlaWVybGljaCBwYXVrdGUgaWNoDQpkdXJjaCBkZW4g ZW5kbG9zZW4gR2FuZyB1bmQgamVkZXMgWmltbWVyLg0KDQpVbmQgamVkZXMgQmV3 b2huZXIgc2NobG/fIHNpY2ggYW4uDQoNCkVpbmVyIG5hY2ggZGVtIGFuZGVybiBp biB3ZWnfZW4gS2xlaWRlcm4gZ2luZ2VuIHdpciBkdXJjaCBkaWUgRmx1cmUgdW5k DQpS5HVtZSwgamVkZXMgR2VzYW5nIHdhciB3aWxkZXIgdW5kIGlycmVyIGluIGRp ZXNlciBOYWNodC4NCg0KRGllIER1bmtlbGhlaXQgZGVyIEZlbnN0ZXIgbGFnIGJs aW5kIGdlZ2VuIGRpZSBNb25kbmFjaHQuIExhbmRzY2hhZnQgZ2z8aHRlDQp2ZXJn ZWhlbmQgaW4gbWFnaXNjaGVtIFdlad8uIEF1cyBHaWViZWwgdW5kIEdlYuRsayBi cmFjaCBlaW4gc2NocmVpZW5kZXINCkV1bGVuc2Nod2FybS4gRmxlZGVybeR1c2Ug d2FyZmVuIHNpY2ggZW50c2V0enQgaW4gZGVuIFp1Zy4NCg0KRGEga2FtZW4gd2ly IGR1cmNoIGRpZSBuaWVkZXJlIFT8ciBpbiBkZW4gR2FydGVuLiBVbnNlciBM5HJt ZW4gc2Nod29sbCBhbg0KdW5kIHdhcmYgc2ljaCB2ZXJzY2hsaW5nZW5kIGluIGRp ZSBzdGFycmUgSGVsbGlna2VpdCBkZXIgTmFjaHQuDQoNClRpZXJlIG5haHRlbiBz YW5mdCBlcnNjaHJlY2t0LiBEaWUgTGFuZHNjaGFmdCBib2cgc2ljaCBpbSBNb25k IHVudGVyIGRlbg0KUGF1a2VuLiBHcm/fZSB3ZWnfZSBLYXR6ZW4gZ2xpdHRlbiD8 YmVyIGRlbiBIb2YgYW4gZGllIE1hdWVyLCB1bmQgdW5zZXINCmxhbmdzYW1lciBa dWcsIHN0YXJyIGluIHdlad9lbiBQeWphbWFzIGJlZ2FubiBzZWluZW4gZ3JhdWVu aGFmdGVuIEdhbmcgaW4NCmRpZSBsYW5kc2NoYWZ0bGljaGUgTmFjaHQuDQoNCkFs bGVzIHNjaHdpZWcgZmVpbmRsaWNoIGJlc2VlbHQsIHVuZCB2b24gdW5zIGtlaW5l bSBrYW0gYXVzIGRlciBTcHJhY2hlIGVpbg0KVG9uLg0KDQpEaWVzIHdhciBkaWUg d2Vp32UgYWJlbnRldWVybGljaGUgTmFjaHQsIGRpZSwgdm9sbGVyIEVyc2NoZWlu dW5nIHdpZQ0Kendpc2NoZW4gemF1YmVyaGFmdGVuIG1pbGRlbiBFaXNiZXJnZW4g aGluc2NocmVpdGVuZCwgd2lyIG5vY2gNCmdlc3BlbnN0aXNjaGVyIG1pdCBSZWl0 ZXJ0cm9tbWVsbiB1bnMgdW50ZXIgZGllIEb832Ugc2NobHVnZW4sIGJpcyBlbmRs aWNoDQpz/N9lciBNb3JnZW4gbWl0IFNpbGJlcnJvdCB1bnMgYmVmcmVpZW5kIGdl Z2VuIGRpZSBnZWJvZ2VuZW4gU3Rpcm5lbg0KcHJhbGx0ZS4NCg0KDQoNCg0KQlJJ RUYNCg0KDQpNRUlOIE11bmQgaXN0IHZvbGwgdm9uIFBmZWlmZW4sIG1laW5lIFN0 aXJuIGJyZW5udCB2b3IgU29ubmUsIG1laW4gWmltbWVyDQp35Gx6dCBzaWNoIGlu IExpY2h0LiBSYXNlbmQgdm9yIE11c2lrIGlzdCBkZXIgUmF1bSwgZXIgaXN0IHdp ZSBlaW4gZ3Jv32VzDQpUaWVyLCBkYXMgaWNoIGxpZWJlIHVtIHNlaW5lciBzdGFy a2VuIEZsYW5rZW4gdW5kIHNlaW5lciBzY2htYWxlbiBUcmV1ZSwNCmRpZSBtaWNo IG5pY2h0IHRy9nN0ZXQsIHVuZCBkZXIgaWNoIG1pY2ggbmllIGhpbmdhYiBpbiBk ZXIg/GJlbGVuIFplaXQNCi4gLiAuIC4gTyBhbHMgZGVpbiBCcmllZiBrYW0sIHdh cmQgTW9yZ2VuIGlyZ2VuZHdpZSBpbiBtZWluZXIgTfxkaWdrZWl0LA0KbWVpbiBC ZXR0IGhvYiBzaWNoIHVtIG1pY2ggd2Vp3yB1bmQgZ2zkbnplbmQsIHVuZCBlcyB3 YXJkIGVpbg0KYmxpdHpzY2huZWxsZXIgU3BhbHQgaW4gbWVpbmVtIFNjaGxhZiwg dW5kIGljaCBzYWggZGVpbmVuIEJyaWVmLCBG/HJzdGluLA0KdW5kIGxhY2h0ZS4g VW5kIHNjaGxpZWYgZWluIGluIG1laW4gTGFjaGVuIGhpbmVpbi4gSmEsIGVzIHdh cmQgTW9yZ2VuLCBlaW5lDQprbGVpbmUgZ2z8aGVuZGUgU3Bhbm5lIG5hY2ggendl aSBO5GNodGVuLCBkaWUgaWNoIG5pY2h0IHNjaGxpZWYuDQoNClNpZWgsIGdhbnog aXN0IG1laW4gTXVuZCB2b2xsIFBmZWlmZW4uIFdpZSB3YXIgdW5zZXIgZXJzdGVy IFRhZyB3aWVkZXIsIHdpZQ0Kd2FyIHVuc2VyIFRhZyBuZXVsaWNoIHZvbGwgTGFj aGVuLg0KDQpEYXMgRnV0dGVyIGRlaW5lcyBCcmllZmVzIGlzdCBoZXJhdXNnZWZh bGxlbiwgaWNoIGhhYmUgZXMgZ2VwYWNrdCwgYWxzIGVzDQppbiBTdHVmZW4gbmFj aCBkZW0gQm9kZW4gc2Nod2VidGUuIEljaCBoYWJlIGVzIGdlcGFja3QgdW5kIHpl cnJpZWJlbiB2b3INCkZyZXVkZSB1bmQgZGFubiBoYWJlIGljaCBlcyBnZWds5HR0 ZXQgdW5kIGdla/zfdCB1bmQgdmVyYnJhbm50Lg0KDQpEdSAuIC4gLiB1bnNlciBU YWcgLiAuIC4gYWxzIHdpciD8YmVyIGRpZSBCcvxja2UgZ2luZ2VuLiBLZWluZXMg c2FndGU6IEljaA0KaGFiZSBkaWNoIHZpZWxlIE1vbmF0ZSBuaWNodCBnZXNlaGVu LiBOZWluLiBOaWVtYW5kIHNhZ3RlOiBJY2ggaGFiZQ0KVW5lbmRsaWNoZXMgZ2Vs aXR0ZW4uDQoNClL2dGUgbnVyIGdpbmcgcmF1c2NoZW5kIPxiZXIgZGVuIEhpbW1l bC4gVPxybWUgdW5kIEt1cHBlbG4gc2Nod2FtbWVuDQpzdHJhaGxlbmQgdW5kIGR1 bmtlbCBnZWJpbGRldCD8YmVyIGRpZSBHbHV0IGRlcyBBYmVuZHMuIFdpbmQgcmnf IGRpZSBsZXR6dGUNClNvbm5lIGR1cmNoIHVuc2VyIEhhYXIuDQoNCldpciBzcHJh Y2hlbiBuaWNodCBG/HJzdGluLCBudXIgdW5zZXJlIEF1Z2VuIPxiZXJ3YW5kZXJ0 ZW4gZGVuIEhpbW1lbCB1bmQNCnVuc2VyZSBNdW5kZSBiZWJ0ZW4gdm9yIFN0dW1t aGVpdC4gUGz2dHpsaWNoIGFiZXIgYmxpZWJlbiB3aXIgc3RlaGVuOiBEdQ0KaGFz dCBlaW4gZ3L8bmVzIEtsZWlkIC4gLiAuIC4gRHUgaGFzdCBlaW5lbiBoZWxsZW4g SHV0LiAtLSAtLSBTdGF1bmVuIGZh33RlDQp1bnMgd2llIEtpbmRlci4gV2lyIHdh cmVuIHdpZSBhdWYgSW5zZWxuIGVpbmUgQmVnZWdudW5nLiBEdSBoYXN0IGVpbiBn cvxuZXMNCktsZWlkIC4gLiAuIC4gTyB3aWUgd2FyIHVuc2VyIFRhZyB2b2xsIEdl bORjaHRlci4NCg0KRGFzIHdhcmVuIGRpZSBhbHRlbiBI5HVzZXIgYW0gTWFpbiwg YXVmIGRpZSBkaWUgU29ubmUgbm9jaCBlaW5tYWwgU3RydWRlbA0Kdm9uIExpY2h0 IHN0/HJ6dGUsIGRh3yBzaWUgZXJiZWJ0ZW4uIERhcyB3YXJlbiBhbHRlIFBhcHBl bG4gdW5kIHZpZWxlDQpGaXNjaGVybmV0emUuIERhcyB3YXJlbiB2aWVsZSBEaW5n ZSwg/GJlciBkaWUgd2lyIGjkdHRlbiB3ZWluZW4gbfZnZW4gdm9yDQpTZWhuc3Vj aHQsIGFiZXIgd2lyIHN0YW5kZW4gaW0gV2luZCB1bmQgbGFjaHRlbi4NCg0KV2ly IHNh32VuIGltIERvbSB6d2lzY2hlbiBhcm1lbiBMZXV0ZW4gdW5kIGRlbiBi9nNl biBtaXR0bGVyZW4gQmVkcvxja3RlbiwNCmVpbmdla2VpbHQsIGR1IEb8cnN0aW4s IG1pdCBkZW4gc2No9m5lbiBI/GZ0ZW4uIFdpZSBzdHJhaGx0ZSB1bnMgZGllIGR1 bmtsZQ0KRWNrZSB2b24gSG9seiB1bmQgZGFzIEZlbnN0ZXIgdW5kIGRhcyByb3Rl IExpY2h0Lg0KDQpBdWNoIGhhc3QgZHUgZ2VrbmlldCwgZWlubWFsLCBlcyB3YXIg ZWluZSBWZXJ6/GNrdW5nLCBtZWluZSBGaW5nZXJzcGl0emVuDQpyYXVzY2h0ZW4g dm9yIFNlbGlna2VpdCwgaWNoIGjkdHRlIGRpY2ggdHL2c3RlbiBr9m5uZW4uDQoN CkR1IHdhcnN0IGv2bmlnbGljaGVyIGdld29yZGVuLiBFcyB3YXIgbWl0IGplZGVt IFNjaHJpdHQsIGFscyBvYiBkdSBncm/fDQpkdXJjaCBlaW5lIFf8c3RlIGvkbWVz dC4gVW5kIGRpZSBTdGlsbGUgdW0gZGljaCB3YXIgd2llIGRhcyB2ZXJrbmlyc2No ZW5kZQ0KR2VoZXVsIGVpbmVyIGJldOR1YmVuZGVuIE1lbmFnZXJpZS4NCg0KV2ll IHdhcmVuIGRlaW5lIFNjaGVua2VsIHN0b2x6IHVuZCB3aWxkLiBJbW1lciB3YXIg ZXM6IGljaCBt/HNzZSBlaW4gV29ydA0Kc2FnZW4sIHBsYXR6ZW5kIHZvbiBLcmFm dCB1bmQg/GJlcnJlaWYgdm9uIFP832lna2VpdCAuIC4gLiAuIGljaCBoYWJlIGRp ZQ0KVGlnZXJpbiB3aWVkZXIgLiAuIC4gLiBkZWluZSBGbGFua2VuIGxldWNodGVu IC4gLiAuIC4gZGVpbiBBdWdlIGlzdCB3aXJyDQptZWluZSBLYXR6ZSB1bnRlciBk ZXIgZ29sZGVuZW4gV2VsbGUgZGVyIEJyYXVlIC4gLiAuIC4gaWNoIGJpbiBpbSBX YWhuc2lubg0Kdm9yIEds/GNrIC0tIC0tIC0tIHVuZCBhbHMgbfxzc2UgaWNoIGzk Y2hlbG5kIG1pdCBtZWluZW4gSORuZGVuIPxiZXIgZGVpbmUNCmJyYXVuZW4gV2Fu Z2VuIGhpbnVudGVyZmFocmVuIPxiZXIgZGVpbmUgSPxmdGVuLCBiaXMgYW4gZGll IEtuaWUsIGFuIGRlcmVuDQpSdW5kaGVpdCBtZWluZSBGaW5nZXIgdmVyZ2VoZW4g dm9yIEJlc2l0ei4NCg0KV2llIHdhcnN0IGR1IHNjaPZuLCBG/HJzdGluLCBhbHMg ZGFzIFppbW1lciBkZWluZXMgSG90ZWxzIGRpY2ggdW1nYWIgdW5kDQpkaWUgU3Bp ZWdlbCB1bmQgZGVpbmUgUmluZ2UsIGljaCB3ZWnfIGVzIGthdW0gbm9jaCwgU29u bmUgZmxhbW10IGluDQpTdHJ1ZGVsbiB1bSBtZWluZW4gVGlzY2guIER1IGhhdHRl c3QgdmllbGUga/ZzdGxpY2hlIERlY2tlbiwgQmF0aWsgdW5kDQpCbHV0cm90IGZs b3NzZW4gaW5laW5hbmRlci4NCg0KRGVpbmUgQnJ1c3QgYWJlciBzY2h3ZWJ0ZSBs ZXVjaHRlbmQgdW50ZXIgZGVyIEJsdXNlIHdpZSBkYXMgRWxmZW5iZWluIGRlcg0K UHNhbG1lbi4gV2llIHdhciBkaWVzZXIgVGFnIGR1bmtlbOR1Z2lnIHZvciBTdGF1 bmVuLCBz/N8gdm9uIEdlbORjaHRlci4NCg0KQWJlciBpY2ggaGFiZSBkaWNoIG5p Y2h0IGdla/zfdC4NCg0KRG9jaCBub2NoIGj2aGVyIHJp3yB1bnMgd2llIGRpZXNl ciBSYXVzY2ggZGllIFN0dW5kZSBpbiBkZW0gZ3Jv32VuIFNhYWwgbWl0DQpibGl0 emVuZGVtIFNpbGJlciwgZGVtIFdlad8sIGRlbiBMaWNodGVybiB1bmQgZGVyIE11 c2lrIHZvbiB0YXVzZW5kDQpyZWRlbmRlbiBNZW5zY2hlbiAuIC4gLiAuIGFsbGVz IHVtIGRpY2ggd2llIGVpbiBXaXJiZWwsIGRlciBkaWNoIHNjaG38Y2t0ZSwNCmdl c2NoYXJ0LiBBbHMgd2lyIGVpbmVuIHNjaPZuZW4gRmlzY2ggendpc2NoZW4gdW5z IHRlaWx0ZW4sIHVuZCBkdSBkZW4NCmJ1cmd1bmRpc2NoZW4gV2VpbiB6d2lzY2hl biBkZW0gaW5uZXJlbiBSb3NhIGRlaW5lciBsYW5nZW4gSORuZGUgaGllbHRlc3Qs DQpkZXIgd2llIFdhY2hzIHdhciB1bmQg1mwgdW5kIG5hY2ggRXJkZSBzY2htZWNr dGUsIGhlcmIgdW5kIGhlcmJzdGxpY2guDQoNCldpciByZWRldGVuLCB1bmQgdW5z ZXJlIFNpbGJlbiBsaWVmZW4gd2llIFNjaGxpdHRzY2h1aGzkdWZlciBhdGVtbG9z DQphdWZlaW5hbmRlciB6dSB1bmQgdHJhZmVuIHNpY2ggbWHfbG9zIGJlc2VlbHQg aW4gZWluZW0gZW5kbG9zZW4gQmF1bSB2b24NClZlcnr8Y2t1bmcuIFRyYXVtIHVu ZCBTY2htZXJ6ZW4gc2Nod2VsbHRlbiBtaWNoLCBhbHMgd2lyIGRhbWFscyB1bnRl cg0KTWVuc2NoZW4gZ2luZ2VuLCB1bSBhbGxlaW4genUgc2Vpbi4NCg0KVW5kIHZl cmdp3yBuaWNodCBkZW4gRmlzY2hlcmp1bmdlbiwgZGVyIHVucyBkZW4gV2VnIGFt IFVmZXIgemVpZ3RlLCBkaWUNCmZs9nRlbmhhZnRlIE7kY2h0bGljaGtlaXQgZGVy IE1hcmllbmthcGVsbGUsIHVuZCBkYd8gaWNoIGVpbm1hbCBuYWNoIGRlaW5lcg0K SGFuZCBoYXNjaHRlLg0KDQpFcyB3YXIuIEVzIHdhciBFd2lna2VpdC4gQXVjaCBk aWVzLg0KDQpEdSBoYXN0IG1pciwgYWxzIGRlciBIYd8gendpc2NoZW4gdW5zIGF1 c2JyYWNoLCBkdSBoYXN0IG1pciB2b3IgZHJlaQ0KTW9uYXRlbiBlaW5tYWwgaW5z IEdlc2ljaHQgZ2VzY2hsYWdlbi4NCg0KS2VpbiBNYW5uIHZlcmdp33QgZGFzLg0K DQpXaWUgaXN0IGRlaW4gR2FuZyBudW4ga/ZuaWdsaWNoLg0KDQpEZWluZSBBdWdl biwgaW4gZGVuZW4gR2VmYWhyIGlzdCwgdW5kIPxiZXIgZGVuZW4gZWluIGV3aWdl cyBMb3NzY2huZWxsZW4NCmjkbmd0LCBzaW5kIG1pdCBH/HRlIHZlcmR1bmtlbHQu IFdpZSBncm/fIHNpbmQgc2llLg0KDQpBbHMgZGVyIEJhaG5ob2YgbWl0IGRpciBl bnRzY2h3ZWJ0ZSwgYWxzIGljaCBmdWhyIGFuIGRpZXNlbSBUYWdlIHVuZCBkZWlu ZW4NCmFiZ2V3ZW5kZXRlbiBS/GNrZW4gc2FoLCBkZXIgc2ljaCB2b24gbWlyIGJl d2VndGUsIHZvbiBS/GhydW5nIHVuc2FnYmFyDQr8YmVybGF1ZmVuLCB1bmQgaWNo IGRlaW4gR2VzaWNodCBkYWhpbnRlciBhaG50ZSwgdmVyevxja3Qgdm9yIFNlbGln a2VpdCwNClRy5G5lbiBoaW5laW5nZW5pZXRldCwgZGEgZmllbCBkaWUgRmluc3Rl cm5pcyBnZWz2c2NodGVyIExhdGVybmVuIHdpZQ0KcHJhbGxlbmRlciBSZWdlbiBh dWYgZGllIEhhbGxlLCBkaWUgenVy/GNrc2Fuay4NCg0KQWJlciBtZWluIEhlcnog d2FyIGxldWNodGVuZCB3aWUgZWluIFPkYmVsLiBJaG0gYmxpZWIgZGllIER1bmtl bGhlaXQgZmVybi4NCkVpbm1hbCBzY2hvbiBG/HJzdGluLCBlaW5tYWwgc2Nob24g d2FyZmVuIHVucyBa/GdlIGF1c2VpbmFuZGVyLCB1bmQNClRyYXVyaWdrZWl0IHN0 /HJ6dGUg/GJlciBtZWluIEhlcnogYW4gZGVuIFNlZW4uIE51biBhYmVyIHNjaGF1 dCBlcyBzdGlsbGVyDQppbiBkaWUgV2VsdC4NCg0KSWNoIHdlcmRlIGRpY2gsIGRp ZSBpY2ggYmVzYd8gd2llIGtlaW5lLCBpY2ggd2VyZGUgZGljaCBhdWNoIG5vY2gg bmljaHQNCmv8c3Nlbiwgd2VubiBkdSBtb3JnZW4ga29tbXN0Lg0KDQpCbHVtZW4g d2lsbCBpY2ggYW4gZGVpbiBCaWxkIGhlZnRlbiBhbiBkZXIgV2FuZC4gRnJldWRl IHNvbGwgZGljaA0Kc2Nod2VsbGVuLCB3ZW5uIGR1IGhlcmVpbnRyaXR0c3QuIFZp ZWxlcyB3aWxsIGljaCBkaXIgc2NoZW5rZW4uDQoNCkR1IHNvbGxzdCBhbGxlcyBo YWJlbiwgbWVpbmUgd2lsZGUgS2F0emUuIE1laW5lIFByZWlzZSB3aWxsIGljaCBk aXIgZ2ViZW4sDQpkaWUgc2lsYmVybmVuIFBva2FsZSwgZGllIEJpbGRlciB1bmQg ZGllIFNwaXR6ZW4sIG1laW5lIEZpZ3VyZW4gd2lsbCBpY2gNCmRpciBzY2hlbmtl bi4gTmljaHRzIHNvbGwgbWlyIG5vY2ggc2Vpbi4gSGV1dGUgTmFjaHQgd2lsbCBp Y2ggZGVuIEVpbmRydWNrDQpkZWluZXMgQmlsZGVzIG1pdCBtZWluZW4gQmxpY2tl biBpbiBkZW4gQmF1bSBzY2hsZXVkZXJuLCBkYd8gZXMsIHVuc+RnbGljaA0KZ2Vo b2Jlbiwgd2llIGVpbiB6dWNrZW5kZXIgU3Rlcm4gZGVuIEhpbW1lbCBkdXJjaGJy aWNodC4NCg0KQWJlciBpY2ggd2VyZGUgZGljaCBuaWNodCBiZXNpdHplbi4NCg0K RHUgLiAuIC4gLiBtZWluIEJsdXQgLiAuIC4gLiBNZWluIEJsdXQgaXN0IHdpZSBl aW4gQvxmZmVsIGF1ZiBkZXIgU3RlcHBlIGltDQpGcvxobGluZyBuYWNoIGRpciwg SWNoIHdpbGwgZXMgZHVtcGYgbWFjaGVuLiBJY2ggd2lsbCBkaWUgSGVyemtsYXBw ZQ0Kc2NobGll32VuLCBkYd8gc2llIGFuc2Nod2lsbHQuIEljaCB3aWxsIGVzIGVy dHJhZ2VuLg0KDQpJY2ggd2lsbCBs5GNoZWxuLCB1bmQgZGllIFp1bmdlIGluIGRl biBIYWxzIHp1cvxja3N0b99lbiwgZGHfIGljaCBlcnN0aWNrZQ0KYW0gZWlnZW5l biBBdGVtLCBkZXIgbmFjaCBkZWluZW0gTXVuZGUgcmF1c2NodC4gRmllYmVyIHdp cmQgbWljaCBhdXNicmVubmVuDQotLSBpY2ggYWJlciB3aWxsIGRlaW5lIEhhbmQg aGFsdGVuIHJ1aGlnIHVuZCBzZWxpZyB3aWUgZWluIEtpbmQgZGllIFNjaG51cg0K c2VpbmVzIERyYWNoZW4sIGRlciBncm/fIHVuZCBzY2j2biBpbiBlaW5lbSBmbG9j a2lnZW4gQWJlbmQgc3RlaHQuDQoNCkljaCB3aWxsIG1laW4gQmx1dCB6/GNodGln ZW4sIGRh3yBlcyBuaWNodCB3ZWl0ZXIgZmxpZd90IHdpZSBiaXMgYW4gZGllDQpI YW5kZ2VsZW5rZS4gTfZnZW4gS2F0YXJha3RlIGluIG1laW5lIEtuaWUgc3T8cm1l biwgZHUgd2lyc3QgbmljaHQgc2VoZW4sDQp3ZW5uIHNpZSBhdWZnZXf8aGx0IHN0 ZWhuLg0KDQpEZW5uIGVzIGdpYnQgZWluZW4gVGFnLCBkZXIgYmxlaWJlbiBtdd86 IGF1Zmdlcmlzc2VuIHVuZCBr/GhuIPxiZXIgamVkZXINClVtYXJtdW5nIC4gLiAu IC4gZ2lidCBlaW5lbiBUYWcgZGVyIGJsZWliZW4gbXXfLiBGcmV1ZGUgc3RpcmJ0 IGluIGplZGVyDQpVbWFybXVuZzogVW5lbmRsaWNoZSBGcmV1ZGUgdW5zZXJlcyBz dGF1bmVuZGVuIExhY2hlbnMgYW0gZXJzdGVuIFRhZ2Ugd2lyZA0KZGFyaW4gc3Rl cmJlbi4gQWJlciB3aXIgaGF0dGVuIHp1IHZpZWwgVHJhdXJpZ2tlaXQsIHdpciBo YXR0ZW4genUgdmllbA0KZWluc2FtZSBO5GNodGUgdm9sbCBXYWhuc2lubiwgZHUg aGFzdCBtaWNoIGdlZvxyY2h0ZXQsIHVuZCBpY2ggaGHfdGUgZGljaCwNCndpciBi cmF1Y2hlbiBkaWVzZSBaZWl0Lg0KDQpTZWxpZ2tlaXQgc29sbCBlaW53YWNoc2Vu LCBG/HJzdGluLCBpbiB1bnNlcmUgU2VlbGUgenVlcnN0IHVuZCBzaWNoZXINCndp ZWRlciwgYmlzIHNpZSBrbGFyIGRhcmluIHNjaHdlYnQgd2llIGVpbmUgS3VwcGVs IGluIEthdGhlZHJhbGVuLCB3aWUgZWluDQpEb2xjaCBpbiBkZWluZW0gZ2VydW5k ZXRlbiBXYXBwZW4uIERhcnVtIEb8cnN0aW4gd2lsbCBpY2ggbWVpbiBCbHV0DQpu aWVkZXJ3ZXJmZW4sIHdpZSBNb3NlcyBkaWUgQW1hbGVraXRlciBoaW5zY2hsdWcs IGluZGVtIGVyIGRpZSBIYW5kDQpob2Noc3RpZd8sIHNlbmtyZWNodCBpbiBkZW4g SGltbWVsLg0KDQpEaWVzIGlzdCBtZWhyIC0tIHVuZCBpY2ggd2Vp3yBlcyBicmVu bmVuZCB1bmQgc3TkcmtlciBhdXMgdmllbGVuIFVtYXJtdW5nZW4NCi0tIGFscyBt b3JnZW4gc2Nob24gZGllIGJy/G5zdGlnZSBOYWNodCBtaXQgZGlyOiBkYd8gaWNo IHNw5HRlciD8YmVyIGFsbGVuDQpSYXVzY2ggaGlud2VnLCBkZXIga29tbWUsIG51 ciBkaWUgcmVpbmUgdW5lbmRsaWNoIGdyb99lIEx1ZnQgZGVyIEV3aWdrZWl0DQpk aWVzZXIgendlaSBUYWdlIHNw/HJlLCB3ZW5uIGljaCBhbiBkaWNoIGRlbmtlLCB3 aWUgaWNoIGVzIHRhdCwgYWxzIGljaA0KbmFjaCBIYXVzZSBnaW5nIHVuZCBkZWlu ZW4gQnJpZWYgZmFuZCwgZGVyIGRpY2ggYW5zYWd0ZSB3aWVkZXIgLiAuIC4gLiB1 bmQNCmFscyBkaWUgU2NoYXR0ZW4gbm9jaCB1bmJla25vc3B0ZXIgQmlya2VuIGlu IE1vbmQgdW5kIETkbW1lcnVuZyBhdWYgZGVuDQpBc3BoYWx0ZW4gZnJvcmVuIC4g LiAuIC4gd2llIGVzIHN0ZWh0IGluIG1pciB05G56ZXJpc2NoIHVuZCBzdGVpbCBh dWYgZGVyDQpob2NoZ2VyaXNzZW5zdGVuIFdlbGxlOiBXaWUgZHUgYXVmIGRlciBB bHRlbiBNYWluYnL8Y2tlIHN0YW5kZXN0Lg0KV2Fzc2VycnVjaCBkaWNoIHVtc3Bh bm50ZSwgbGV0enRlIFNvbm5lLCBhbHMgZGVyIEZsdd8sIHJ1aGlnZXIgdmVyc3Ry 9m1lbmQsDQpkaWNoIHBs9nR6bGljaCBsaWVidGUsIEhvcml6b250IGF1ZmJyYWNo IHVtIGRpY2gsIGdlbGIgdW5kIHVuZ2VoZXVlciwgdW5kDQpkaWNoIG1pdCB3aWxk ZW4gU2NocmVpZW4gZGllIE1pbGRoZWl0IGh1bmRlcnQgd2Vp32VyIE32dmVuIHVt ZmxhdHRlcnRlDQouIC4gLiAuIHVuZCBkYW5uIHdpZSBkdSBkdXJjaCBkZW4gTGF0 ZXJuZW5hYmVuZCBX/HJ6YnVyZ3MgbmViZW4gbWlyIGdpbmdzdA0KaW4gZGVyIGZs aWXfZW5kZW4gU2No9m5oZWl0IGRlaW5lcyBm/HJzdGxpY2ggZ3L8bmVuIEtsZWlk ZXMsIHVuZCwgZGllIGljaA0KZGlyIGluIGVpbmVtIFdhZ2VuIGFtIFVmZXIgZ2Vr YXVmdCBoYWJlLCBkaWUgZ2xhc2dvbGRlbmVuIEt1Z2VsbiB2b24gendlaQ0KQXBm ZWxzaW5lbiBpbiBkZW4gSORuZGVuLCBzdHJhaGxlbmQgd2llIGRlaW5lIGVpZ2Vu ZW4gQnL8c3RlIPxiZXIgZGllDQpLYWlzZXJzdHJh32UgdHJ1Z3N0Lg0KDQoNCg0K DQpUUkFVTQ0KDQoNCkRJRVMgZXJzdGUgZ2luZyByYXNjaCB2b3L8YmVyLCB3aXIg d2FyZW4gZHVyY2ggV2FsZCBnZWZhaHJlbiwgZGVyIFdhZ2VuDQpoaWVsdC4gV2ly IHN0ZWlnZW4gYXVzLiBEaWUgUGZlcmRlIHJlbm5lbiB3ZWl0ZXIuIE51biBpc3Qg ZXMgU29tbWVyLg0KDQpEaWUgc2lsYnJpZ2UgQWxsZWUgZHJlaHQgdW0uIEluIGdl bGJlciBTb25uZSBsZXVjaHRldCBtaXQgU3BpZWdlbHNjaGVpYmVuDQpkYXMgZnJh bnr2c2lzY2hlIExhbmRoYXVzLiBTeXJpbmdlbiB1bmQgU3ByaW5nYnJ1bm5lbiBz aW5kIGRhcnVtIGdlem9nZW4uDQpEaWUgRvxyc3RpbiBs5GNoZWx0IGF1cyBicmF1 bmVtIEdlc2ljaHQsIHVuZCBpaHIgTORjaGVsbiB3aXJmdCBhbGxlcw0KenVy/GNr LCBkaWUgWmVpdCB1bmQgZGllIFNjaG1lcnplbi4gV2lyIHNpbmQgZGEuIEljaCBy ZWnfZSBzaWUgaGluZWluLg0KDQpJbiBpaHJlbiBHZWxlbmtlbiBzY2hhdWtlbHQg TGllYmUsIHNpZSBiZXJhdXNjaHQgZGllIEx1ZnQuIFNpZSBnbGVpdGV0DQpkdXJj aCBkaWUgUuR1bWUuIElocmUgRmluZ2VyIHdlaXNlbiwgemVpZ2VuLCBkZXV0ZW4s IFfkbmRlLCBCaWxkZXIsIGRpZQ0KVmFzZW4sIHNpZSBs5GNoZWx0IHZvciBTZWhu c3VjaHQsIGRhcyBicmF1bmUgR2VzaWNodCBzdHJhaGx0IGluIHdpbGRlbQ0KU2No ZWluIGF1ZiwgaWhyIGZlZGVybmRlcyBCZXdlZ2VuIHr8bmRldCBidW50ZSBBYmVu dGV1ZXJsaWNoa2VpdCBpbiBkaWUNCkxhbmRzY2hhZnQuIERhIHN0/HJ6ZW4gZGll IE11bmRlIHp1c2FtbWVuLg0KDQpIaWVyIGlzdCBlaW4gU29tbWVyLCBkZW4gd2ly IGR1cmNobGViZW4gd29sbGVuLiBHYW56IPxiZXIgZGVtIEhvcml6b250DQpzdGVo dCBibGF1ZXIgZHVmdGVuZGVyIEhpbW1lbCBnZXNwYW5udCD8YmVyIGRlbiBN5Ghu ZW4gZGVyIGJsb25kZW4NCldlaXplbmZlbGRlciwgdW5kIGVyIHdpcmQgbm9jaCB6 5HJ0bGljaGVyIHVtIGlocmUgRnJlbWRoZWl0LCBkaWUgbWl0DQpHb2xkcmVnZW4g ZGllIEJs5HVlIHZlcmJsYd90LCB1bmQgc2ljaCB2ZXJzdHLkaG50IGRlbSBkdW5r bGVuIER1ZnQgZGVzDQpGbGllZGVycy4gRGllIEZlbnN0ZXIgc3RlaGVuIHdlaXQg Z2VnZW4gZGllIExhbmRzY2hhZnQuDQoNCkRhbm4ga2FtIGRlciBUcmF1bToNCg0K SW4gZGllc2VyIGVyc3RlbiBOYWNodCwgd28gVGF1IGR1cmNoIGRpZSBNb25kZORt bWVydW5nIHNwYW5uIGltIFBhcmssDQp0cuR1bXRlIGljaCwgZGHfIGljaCBkaWUg Rvxyc3RpbiBzdWNoZSwgdW5kIGltIFNjaGxhZiB3YXIgZGllIEdlZ2VuZA0KdmVy 5G5kZXJ0IGltIEdydW5kLiBJY2ggd2FyIGluIGVpbmVyIFN0YWR0IG1pdCBhbHRl biBHaWViZWxuLCBkdXJjaCBkaWUNCmVpbmUgU3RyYd9lIGxpZWYgbWl0IHNjaHLk Z2VyIGVuZ2VyIEZyb250LiBEaWUgSOR1c2VyIGVyaGllbHRlbiBI9mhlIG1pdA0K Z3Jv32VuIEJhcmFja3RvcmVuLCBtaXQgRXJrZXIgdW5kIHZlcm1vb3N0ZXIgU2t1 bHB0dXIgdW5kIGdlc3RhZmZlbHRlbQ0KRGFjaHp1Zy4gRGVubm9jaCBzY2hpZW4g ZWlucyBkZW0gYW5kZXJuIGdsZWljaC4gRWluZSBMdWZ0IGxhZyBkaWNrIHVuZA0K ZHVtcGYgaW4gZGVyIFN0cmHfZS4gRGllIEZlbnN0ZXIgc2NoaWVuZW4gYmxpbmQg dW5kIHJlZ2xvcy4gS2VpbiBHZXLkdXNjaCwNCmtlaW4gVG9uIGR1cmNoZHJhbmcg ZGllIEx1ZnQuIFNlbGJzdCBtZWluZW4gU2Nocml0dCBo9nJ0ZSBpY2ggbmljaHQu DQoNCkljaCB0cmF0IGluIGVpbmUgVG9yZmFocnQsIGRpZSBG/HJzdGluIHp1IHN1 Y2hlbiwgZGEgc2NoaWVuIHNpZSBtaWNoDQp2ZXJ0cmF1dCB1bmQgZnJldW5kbGlj aCBhdWZ6dW5laG1lbi4gSWNoIHNhaCBtaWNoIHVtLiBEYSBrYW0gZXMgbWlyLCBk Yd8NCmljaCBzaWUgb2Z0IG1pdCBpaHIgZHVyY2hzY2hyaXR0ZW4gaGF0dGUsIHVu ZCBS/GhydW5nIGR1cmNobGllZiBtaWNoIHRpZWYuDQpEdXJjaCBlaW5lbiBzY2ht YWxlbiBIb2YgYW4gU2VpdGVuZmz8Z2VsbiBoaW51bnRlcnNjaHJlaXRlbmQsIGj2 cnRlIGljaA0KV2Fzc2VyLCBlcyB3YXIsIGFscyBsYXVmZSBlaW4gRmx13yBoaW50 ZXIgZGVtIEdlYuR1ZGUuIEljaCB0cmF0IGVpbi4gU2llYmVuDQpLaW5kZXIgbWl0 IGhlbGxlbiBIYWFyZW4gdW1yaW5ndGVuIG1pY2gsIGFiZXIgc2llIGthbm50ZW4g ZGllIEb8cnN0aW4NCm5pY2h0LCBhbHMgaWNoIGRhbmFjaCBmcmFndGUuIERlbm5v Y2ggZHVyY2hzdWNodGUgaWNoIGFsbGUgWmltbWVyLCBpY2gNCnZlcnNjaG9udGUg bmljaHRzLCBhYmVyIGljaCBmYW5kIHNpZSBuaWNodCB1bmQgc3RhbmQgbWl0IGVp bmVtbWFsIG5ldSBhdWYNCmRlciBTdHJh32UuDQoNCkluIGRlciBTY2h3/GxlIHdh ciBlaW5lIGxlaWNodGUgQmV3ZWd1bmcsIGljaCBiZWdyaWZmIHNpZSBuaWNodCB1 bmQgaG9yY2h0ZQ0KZXJzdGF1bnQuIERhbm4gYWJlciBtZXJrdGUgaWNoLCBkYd8g ZGllIGdyb99lbiBTY2hlaWJlbiBkZXIgTORkZW4gRmFsdGVuDQpoYXR0ZW4gdW5k IHNpY2ggaW0gS3JlaXNlIGRyZWhlbmQgaW4gZGllIFN0cmHfZSBoaW5laW5zY2hs dWdlbiB1bmQNCnp1cvxja2ViYnRlbi4gSWNoIGJsaWViIHN0ZWhlbiB1bmQgYmVz YWggZGllIEjkdXNlciBhbGxlIG5hY2hkZW5rbGljaC4NCg0KRGFubiBuYWhtIGlj aCBlaW4gYW5kZXJlcyBIYXVzIHVuZCB0cmF0IGhpbmVpbiwgdW5kIHN0aWVnIG9o bmUgUGF1c2UgYXVmDQplaW5lciBpbW1lciBnZWRyZWh0ZW4gVHJlcHBlLiBFcyBs aWVmZW4gdmllbGUgR+RuZ2Ugc3RyYWhsZW5m9nJtaWcgZGF2b24NCmF1cy4gQWJl ciBpY2ggbGll3yBzaWUgaG9jaG38dGlnIHVuZCBzY2hsdWcgZWluZSBrbGVpbmUg U2VpdGVubG9nZSBlaW4gdW5kDQp3dd90ZSBudW4gc29mb3J0IGFuIGRlciBU9m51 bmcgZGVyIFfkbmRlLCBhbSBHZXJ1Y2ggZGVyIEdlbORuZGVyLCBpY2ggd3XfdGUN CmVzIHdpZSBpbSBJcnJzaW5uLCBoaWVyIHNlaSBkaWUgRvxyc3RpbiwgdW5kIEZy ZXVkZSBicmFjaCBtaXIgYXVzIGRlbQ0KR2VzaWNodC4NCg0KSWNoIHNhaCBlaW5l IFT8ciB1bmQgZHL8Y2t0ZSBkaWUgS2xpbmtlIGF1ZiB1bmQgdHJhdCBlaW4uIERh cyBaaW1tZXIgc3RhbmQNCnZvbGwgbWl0IEdlcuR0LiBEb2NoIGljaCBs5GNoZWx0 ZS4gSWNoIGhhdHRlIGdlaXJydCBpbiBkZXIgSGFuZGx1bmcuIEljaA0Kd2FyIHp1 IHNlaHIgdm9sbCBTZWhuc3VjaHQuIE1laW5lIEjkbmRlIGthbm50ZW4gZWluZSBi ZXNzZXJlIFT8ci4NCg0KRXMgd2FyIGVpbmUgc2Nod2FyemUgRWljaGVudPxyIGlt IFNlaXRlbmtvcnJpZG9yLiBWb3IgaWhyIGJsaWViIGljaCBsYW5nZQ0Kc3RlaGVu LCBkZW4gS29wZiBpbiBkaWUgSGFuZG11bGRlbiBnZXNlbmt0LiBEYW5uIHRyYXQg aWNoIGVpbi4gRWluIGdlbGJsaWNoDQpicmF1bmVyIFZvcmhhbmcgc2NobG/fIGRh cyBaaW1tZXIgYWIgdm9uIGRlciBXZWx0LiBEaWUgTHVmdCB3YXIgYWx0IHVuZA0K YmFuZywgYWJlciBpY2ggd2FyIG5pY2h0IHp1IHTkdXNjaGVuLCBpY2ggcm9jaCBl aW5lbiBEdWZ0LCBkZXIgaWhyZW0gZ2xpY2guDQpJaHJlIGt1cGZyaWdlIFR1bmlr YSBoaW5nIPxiZXIgZWluZW0gQvxnZWwuIEljaCBu5GhlcnRlIG1laW4gR2VzaWNo dCwgaWNoDQpsaWXfIGVzIGhpbmVpbmZhbGxlbiB1bmQgd/xobHRlIGRpZSBI5G5k ZSBoaW5laW4gdW5kIHNjaGx1Y2h6dGUgdm9yDQpTZWhuc3VjaHQuIEljaCByb2No IHNpZSB3aWVkZXIuIFdpZSBlbnRmbGFtbXRlIG1laW4gSGVyeiBkYXJhbiENCg0K RGllIFfkbmRlIHdhcmVuIGR1cmNoYnJvY2hlbiBtaXQgS2Fzc2V0dGVuIGF1cyBo ZWxsZW0gU3RlaW4uIERhcvxiZXIgd2FyZW4NCmdyZWxsZSBmcmVtZGUgU2VpZGVu IGdlc3Bhbm50LiBBdWYgZWluZW0gU29ja2VsIHN0YW5kIGVpbiBGYXVuIGluIG9i c3r2bmVyDQpIYWx0dW5nLiBEYXMgZWluemlnZSBGZW5zdGVyIGhpbmcg/GJlciBt ZWluZW0gS29wZiB1bmQgc2llYnRlIGRpZSBTb25uZS4gSW4NCm1laW5lbSBS/GNr ZW4gaGluZ2VuIGFsbGUgaWhyZSBCaWxkZXIsIGRpZSBWYXNlbiwgZGllIGdlbGll YnRlbiBX5G5kZSwgaWNoDQpkcmVodGUgbWljaCBuaWNodCB1bSwgZGVubiBpY2gg d3XfdGUgbmljaHQsIG9iIG1laW4gSGVyeiBuaWNodCBicmFjaC4NCg0KRGFubiBz dGFuZCBpY2ggYXVmIHVuZCBnaW5nIGhpbmF1cy4gSWNoIHNhaCBtaWNoIG5pY2h0 IHVtOiC7bmljaHQgZGVuIEZhdW4sDQpuaWNodCBkaWUgV2FuZCwgbmljaHQgZGll IFR1bmlrYasgZmz8c3RlcnRlIG1laW4gQmx1dC4gu1NpZasgc3RhbW1lbHRlIGVz Lg0KU28ga2FtIGljaCBhdWYgZGllIFN0cmHfZS4gRGVyIEhpbW1lbCB3YXIgc2No d2Vycm90LCBnbGF0dCBtaXQgRW1haWwNCvxiZXJnb3NzZW4gdW5kIHNjaGxldWRl cnRlIEFiZ2xhbnogaW4gZGllIEZlbnN0ZXIsIGRpZSBM5GRlbiwgZGllIEdlaHN0 ZWlnZQ0KdW5kIGRpZSBrbGVpbmVuIFBm/HR6ZW4sIGRpZSB3aWUgQmFsbG9uZSBm dW5rZWx0ZW4uIE51biBnaW5nIGljaCBpbiBIYXVzIHVtDQpIYXVzLg0KDQpBYmVy IGplZGVzIGdsaWNoIGRlbSBhbmRlcm4sIHVuZCBiYWxkIHdhciBpY2ggc28gdmVy d2lycnQsIGRh3yBpY2ggbWljaA0Kc2VsYnN0IGltIEJpbGRlIHNhaCwgdmVycvxj a3Qgdm9yIFN1Y2hlbiB1bmQgZ2VzY2hsYWdlbiB2b24gZGVyIFNlaG5zdWNodC4N CkRhIGVyc2Nob2xsIGRlciBUb24gZWluZXIgTGF1dGUuDQoNCk51biBs5GNoZWx0 ZSBpY2ggdW5kIHRyYXQgaW4gZWluIHL2dGxpY2hlcyBIYXVzIG9obmUgWvZnZXJ1 bmcuIFZvbGwNClNpY2hlcmhlaXQgc3RpZWcgaWNoIHp1bSBHaWViZWwuIERhbm4g Z2luZyBpY2ggbGFuZ3NhbSB3aWVkZXIgaGVydW50ZXIgdW5kDQpob3JjaHRlIGFu Z2VzcGFubnQuIEluIGRlciB6d2VpdGVuIEV0YWdlIHN0cmVpZnRlIGljaCBlaW5l IFT8ciwgdW5kIGFscyBpY2gNCnZvcvxiZXIgd2FyLCBkcmVodGUgaWNoIHVtLCB1 bmQgdW5uZW5uYmFyIHZvbGwgR2V3ad9oZWl0IGdpbmcgaWNoIGF1ZiBlaW4NClBh cGllciB6dSwgZGFzIGRhcmFuIGtsZWJ0ZS4gTWVpbmUgQXVnZW4gd2FyZW4gYXVm Z2Vzb2dlbiB2b24gZGVtIFdlad8sIGRhcw0KaWhyZW4gTmFtZW4gdHJhZ2VuIHf8 cmRlLiBJY2ggd2FyIHNvIHZvbGwgdm9uIFNpY2hlcmhlaXQsIGRh3yBpY2ggZGll IEF1Z2VuDQpzY2hsb98gaW0g3GJlcm11dCwgdW5kIGR1cmNoIGRpZSBMaWRlciBz YWggaWNoIGlocmVuIE5hbWVuIGJsYXUgdW5kIHNjaHLkZw0KYXVmIGRlbiBLYXJ0 b24gZ2VtYWx0LCBpaHJlbiB3aWxkZW4gYmVyYXVzY2hlbmRlbiBOYW1lbiwgZGVu IGVyc3RlbiBncm/fZW4NCmhlcnJzY2hlbmRlbiBCdWNoc3RhYmVuIHVuZCBkaWUg c3RlaWZlbiBpbiBMZWlkZW5zY2hhZnQgZXJzdGFycnRlbiBkZXINCmFuZGVyZW4g LiAuIC4gLiB1bmQgdm9ydHJldGVuZCwgZGllIExpZGVyIGdlc3BlcnJ0LCBsYXMg aWNoIGVpbmVuIGZyZW1kZW4NCnJ1c3Npc2NoZW4gTmFtZW4sIGdsZWljaGf8bHRp ZyB3aWUgRWlzLiBBbGxlaW4gaWNoIGzkY2hlbHRlLiBTaWNoZXJoZWl0DQp2ZXJs aWXfIG1pY2ggbmljaHQuDQoNCkRpZSBU/HIgc3RhbmQgaW0gU3BhbHQsIHVuZCBp Y2ggc2FoIGhpbmVpbi4gSW4gZGVyIEVja2UgaG9ja3RlIGVpbg0KaOTfbGljaGVy IGJyYXVuZXIgS2VybCwgaWNoIGthbm50ZSBpaG4gbmljaHQuIE1pdHRlbiBhYmVy LCBtaXR0ZW4gc3RhbmQgZGllDQpG/HJzdGluIHVuZCBzY2hsdWcgZGllIEJhbGFs YWlrYS4gRGFzIGhhdHRlIGljaCBpbW1lciBzY2hvbiBnZWj2cnQuDQoNCkFiZXIg YWxzIG1laW4gQmxpY2sgc2llIGJlZ2VocnRlLCB1bmQgbWVpbiBCZWluIHNjaG9u IGZlZGVydGUgaW0gU3BydW5nLA0KdHJhZiBtaWNoIGR1cmNoIGRpZSBMdWZ0IGVp biBTY2hsYWcsIGljaCBzdGFuZCBnZWzkaG10LiBFcyBrYW0gdm9uIGlociwgaWNo DQpm/GhsdGUgZXMsIGRlbm4gbnVyIHNpZSBoYXR0ZSBkaWVzZSBmcmVtZGUgTWFj aHQg/GJlciBtZWluIEhpcm4uIEljaCB3YW5kdGUNCm1pY2ggdW0sIHVuZCB6dSBl aW5lbSBibGF15HVnaWdlbiBLaW5kLCBkYXMgaGludGVyIG1pciBzdGFuZCwgZ2V3 ZW5kZXQsDQpmcmFndGUgaWNoOiC7TWFuIHRyaXR0IG5pY2h0IGVpbiAuIC4gLiAu qy4gQWJlciBkYXMgS2luZCBzY2hhdXRlIHZvciBzaWNoDQpoaW4gb2huZSBBbnR3 b3J0Lg0KDQpEYSBnaW5nIGljaCBncmFkIHVuZCBsYW5nc2FtIGJpcyBhbnMgRW5k ZSBkZXMgR2FuZ2VzLiBBbiBlaW5lbSBGZW5zdGVyIG1pdA0KZ3L8bmVuIEdsYXNr YWNoZWxuIHNpY2hlcnRlIGljaCBkZW4gUmV2b2x2ZXIgcnVoaWcgdW5kIGJlc2lu bnVuZ3Nsb3MgdW5kDQp3YXJ0ZXRlLCBhbiBkaWUgV2FuZCBnZWxlaG50Lg0KDQpC YWxkIGJyYWNoIGRpZSBNdXNpayBhYi4gRGllIEb8cnN0aW4gdHJhdCBhdXMgZGVt IFppbW1lciwgYm9nIHVuZCBnaW5nIGRhcw0KZW50Z2VnZW5nZXNldHp0ZSBTdPxj ayBkZXMgR2FuZ3MuIElocmUgUvZja2UsIGF1ZmdlYmF1c2NodCBtaXQgTGlsaWVu IGF1Zg0Kd2Vp32VtIEdydW5kLCB39mxidGVuIHNpY2gg/GJlciBkZW4gSPxmdGVu IHNjaHdhY2ggYmV3ZWd0LiBJbW1lciB3YXIgZWluDQpSYXVtIHp3aXNjaGVuIGlo cmVtIExlaWIgdW5kIGlocmVuIEtsZWlkZXJuLCBkdXJjaCBqZWRlcyBHZXdhbmQg c2FoIGljaA0KaWhyZSBlaWdlbnRsaWNoZSBGb3JtLiBBYmVyIHdpZSBzaWUgZ2lu ZyBzbywgc2Nob98gaWNoIG5pY2h0IG5hY2ggaWhyLCBpY2gNCmtvbm50ZSBuaWNo dHMgdHVuIHdpZSBzaWUgYW5zZWhlbiB1bmQgdmVyZ2VoZW4gdm9yIFf8bnNjaGVu LiBJc3QgZGllcyBkaWUNCkZyYXUsIGdlZ2VuIGRpZSBpY2ggc2Nod2FjaCBiaW4s IGZyYWd0ZSBpY2ggc3RhdW5lbmQgdmVyd2lycnQsIGRvY2ggc2Nob24NCnZlcmdp bmcgbWVpbmUgV3V0LCBkZW5uIGljaCBzYWggZ2zkbnplbmQgaW0gU2NoYXR0ZW4g ZGVyIFN0aWVnZW4gYmVpbQ0KV2VuZGVuIGlociBQcm9maWwuDQoNCkhpbnRlciBp aHIgZ2luZyBkZXIgQnJhdW5lIHVuZCBzZWluZSBHZXN0YWx0LCBub2NoIGjk32xp Y2ggd2llIGVpbiBBZmZlDQphYmVyIHN0YXJrIHdpZSBlaW4gVGllciBpbSBaaW1t ZXIsIHpvZyBnZWJldWd0IG1pdCBwYXJhbHl0aXNjaGVuIEJlaW5lbg0KaGludGVy IGlociBoZXIsIHVuZCBlaW4gc/zfbGljaGVyIEdlcnVjaCB3aWUgdm9uIExlaWNo ZW4gc3Ry9m10ZSBsYW5nc2FtDQp2b24gaWhtIGRlbiBHYW5nIGhlcmF1Zi4NCg0K RXMgc2NoaWVuIGR1bmtlbCBpbSBHYW5nLCBhbHMgaWNoIG1pY2ggdW1zYWguIFNj aG1lcnogc2HfIGluIGFsbGVuIEVja2VuLg0KRGFzIEtpbmQgaG9ja3RlIG51biBz cGllbGVuZCBhdWYgZGVtIHNjaHLkZ2VuIERhY2ggZWluZXMgTmFjaGJhcmhhdXNl cyB1bmQNCndhcmYgZ2xpdHplcm5kZSBLdWdlbG4gaW4gZGllIEx1ZnQuDQoNCkxh bmdzYW0gZ2luZyBpY2ggZGllIFRyZXBwZSBoaW51bnRlciwgZGllIExpcHBlbiBy ZWRlbmQ6ILtFcyB3YXIgbmljaHQgZGllDQpG/HJzdGluIC4gLiAuIC4gRXMgd2Fy IG5pY2h0IGRpZSBG/HJzdGluIC4gLiAuIC6rIEFiZXIgZXMgd2FyIGRvY2ggZGll DQpG/HJzdGluLCB1bmQgaWNoIGJlbG9nIG1pY2ggbnVyLg0KDQpBdWYgZGVyIFN0 cmHfZSBhYmVyIGJlZ2FubiBtZWluIEhlcnogenUgdGFuemVuIHZvciBGdXJjaHQs IGRh3yBpY2ggc2llDQpuaWNodCBm5G5kZSB1bmQgenfkbmdlLCBpY2ggc3ByYW5n LCBkaWUgRuR1c3RlIGluIGRlbiBTY2hs5GZlbiwgaW4gZWluZW4NCkxhZGVuLCBk dXJjaGVpbHRlIGlobiB1bmQgZXJibGlja3RlIGVpbmUgVPxyLiBEYXMgTGljaHQg aGluZyBsYW5nIHVuZA0KZ2zkbnplbmQgaW4gaWhyZW0gU3BhbHQuIERhcyBaaW1t ZXIgd2FyIGhhbGIgd2Vp3yB1bmQgd2llZGVyIGJsYXUgdW5kIHZvbg0KZWluZW0g bWFnaXNjaGVuIExldWNodGVuIGVyZvxsbHQuIERyZWkgTWVuc2NoZW4gYmV3ZWd0 ZW4gc2ljaCBkYXJpbg0KZ2VnZW5laW5hbmRlciBtaXQgd2VpdCD8YmVyIFNpY2h0 YmFyZXMgaGluYXVzZ2VoZW5kZXIgQmV3ZWd1bmcuDQoNCkVpbmVyIHdhciBkZXIg UnVzc2UgQXBocm9kaXRpLCBpaG4gZXJrYW5udGUgaWNoIHNvZm9ydCwgZGVyIFTk bnplciBtaXQgZGVyDQphbmFyY2hpc2NoZW4gU2VlbGUuIEVyIHRydWcgZWluIGJs YXVlcyBLbGVpZCwgdW5nZWf8cnRldCwgZGFzIGJpcyB6dSBkZW4NCktuaWVuIHJl aWNodGUgdW5kIGRlbiBIYWxzIGZyZWkgbGll3y4gRXMgd2FyLCBhbHMgZm9sZ2Ug ZXIgZWluZXIgZ3JhdXNhbWVuDQp1bnNpY2h0YmFyZW4gTXVzaWsuIEluIGRlbiBI 5G5kZW4gc2Nod2FuZyBlciB3ZWnfZSBDYWxsYXMgaW1tZXIgbmFjaA0KZGVtc2Vs YmVuIFNhdHouIERpZSBiZWlkZW4gYW5kZXJlbiB3YXJlbiBGcmF1ZW4sIGVpbmUg a2FubnRlIGljaCBuaWNodC4NCg0KQWJlciBkaWUgYW5kZXJlIHdhciBkaWUgRvxy c3Rpbi4gRGllc21hbCBzYWggaWNoIHNpZSBkZXV0bGljaC4NCg0KSWNoIHNhaCBk ZW4gcm90ZW4gU3Rlcm4gdW50ZXIgaWhyZXIgbGlua2VuIEFjaHNlbC4gU2llIGhh dHRlIGVpbg0KUGFudGhlcmZlbGwgdW0gZGllIFRhaWxsZSBnZXNjaGx1bmdlbiwg c29uc3Qgd2FyIHNpZSBuYWNrdC4gSWhyZSBCcvxzdGUNCmhvYmVuIHNpY2ggYnJl aXQgdW5kIHJ1bmQgdW5kIGFuIGRlbiBTcGl0emVuIGVpbiB3ZW5pZyBnZXJlY2t0 IG5hY2ggb2Jlbi4NCkVpbmUgaG9oZSBN/HR6ZSBhdXMgd2Vp32VtIHVuZ2Vib3Jl bmVuIEzkbW1lcmZlbGwga3L2bnRlIGFscyBIZWxtIGlociBIYWFyLg0KU2llIHNw cmFuZyB0YW56ZW5kIHZvciB1bmQgenVy/GNrLCBkaWUgTGlwcGVuIGJlcmF1c2No dCBnZfZmZm5ldCwgd2lsZCB1bmQNCnNjaOR1bWVuZCwgZGllIGJyYXVuZW4gTXVz a2VsbiB1bnRlciBpaHJlbSBLbmllIGJhbGx0ZW4gc2ljaCB1bmQgZW50d2lycnRl bg0Kc2ljaCB3aWVkZXIsIGlociBBdWdlIGZsYW1tdGUsIGRpZSBnb2xkZW5lbiBC cmF1ZW4gZ2z8aHRlbi4gRGFzIHdhciBkaWUNCkb8cnN0aW4uIEljaCBrYW5udGUg amVkZSBTcHVyIGlocmVzIEv2cnBlcnMuDQoNCkRhIHNwcmFuZyB1bnRlciBtZWlu ZXIgQmVnaWVyZGUgZGllIEzkaG11bmcsIGljaCBzY2hyaWUuIEFiZXIgQXBocm9k aXRpLA0KZ2VnZW4gZGllIFdhbmQgZ2VzdGVsbHQsIGxpZd8gZGllIENhbGxhcyBm YWxsZW4gdW50ZXIgZGVtIFNjaHJlaSwgdW5kDQpuZWlndGUgc2VpbmVuIEtvcGYg YXVmIGRpZSBzZWl0bGljaGUgU2NodWx0ZXIuIERhbm4gbGVndGUgZXIgc2VpbmUg bGlua2UNCkhhbmQgbWl0IGRlbSBS/GNrZW4gd2lkZXIgZGllIFdhbmQgdW5kIHNj aGx1ZyBlaW5lbiBEb2xjaCBoaW5laW4gYmlzIGFucw0KSGVmdC4gQWJlciBlcyBr YW0ga2VpbiBCbHV0Lg0KDQpEYSByad8gaWNoIGRpZSBU/HIgYXVmLCBudW4gd2Fy IHNpZSBtZWluLCBhYmVyIGRpZSBU/HIgc2NoaWVuIGF1cyBFcnouIERpZQ0KTHVm dCBkYWhpbnRlciBpbSBaaW1tZXIgd3VyZGUgdW5lcnRy5GdsaWNoIGJsYXUuIERh IHNjaGx1ZyBpY2ggZHL2aG5lbmQNCm1laW5lIEhhbmQgZHVyY2ggZGllIFBsYW5r ZW4uIEljaCBzY2hsdWcgaGluZHVyY2guIEljaCBoYXR0ZSBzaWUgemVyaGF1ZW4u DQoNCkFiZXIgd2llIGljaCBhdWYgZGVyIFNjaHdlbGxlIHN0YW5kLCB3YXIgYWxs ZXMgdW1zb25zdC4gRGFzIEdlc2ljaHQgZGVyDQpG/HJzdGluIHZlcndhbmRlbHRl IHNpY2ggYXVmIGRlciBPYmVyZmzkY2hlLCBkZXIgdG9sbGUgZ3Jv32UgWnVnIGRl ciBOYXNlDQp1bmQgZGVzIE11bmRlcyB2ZXJ0YXVzY2h0ZSBzaWNoLiBJY2ggc2Fo IG1pdCBtZWluZW4gQXVnZW4gd2llIGlociBLb3BmIHNpY2gNCmZvcm10ZSBpbiBl aW4gdW5iZWthbm50ZXMgZnJlY2hlcyBHZXNpY2h0LCB1bmQgaW5kZW0gZGFzIEhl cnogaW4gV3V0IHVuZA0KU2NobWVyeiB6ZXJzcGxpdHRlcnRlLCB3aWUgZGllIEZy ZW1kZSwga29rb3R0ZW5oYWZ0IGluIEFwaHJvZGl0aXMgQXJtIHNpY2gNCnNjaGF1 a2VsbmQsIGltIFBhcyBkZSBsJ291cnMgZGllIEj8ZnRlbiBzY2h3ZW5rZW5kLCBl aW5lbiBzY2hsZWNodGVuDQpTY2hpZWJlciBiZWdhbm4uDQoNCkljaCBoYXR0ZSBz aWUgYmVpbmFoZSBnZWhhYnQuIEljaCB3b2xsdGUgc2llIGdhbnogaGFiZW4gdW5k IHJ1aHRlIG5pY2h0Lg0KDQpab3JuaWcgdW5kIHZlcuRjaHRsaWNoIGdpbmcgaWNo IGhpbnVudGVyIGF1ZiBkaWUgU3RyYd9lLiC7SWNoIHdpbGwgc2llDQpoYWJlbiwg aWNoIHdpbGwgc2llIGhhYmVuLKsgc28gdHJvbW1lbHRlIG1laW4gSGVyeiB1bmQg YWxsZXMgd2FyIG1pcg0KZ2xlaWNoLCBpY2ggd2FyIGltIEZpZWJlciwgaWNoIG7k aG1lIHNpZSBhbHMgSHVyZSwgaWNoIHdpbGwgc2llIGhhYmVuLA0KbmljaHRzIGFu ZGVyZXMgd3XfdGUgbWVpbiBIZXJ6LiBEYXMgUm90IPxiZXIgZGVuIETkY2hlcm4g d2FyIGRy/GNrZW5kIHVuZA0KZHVua2VsIGdld29yZGVuLiBFcyBnbPxodGUgendp c2NoZW4gZGVuIHNjaHdhcnplbiBsYW5nZW4gTGluaWVuIGRlciBI5HVzZXINCmhl cmF1cy4gSWNoIHNw/HJ0ZSBrZWluZSBIaXR6ZSwgYWJlciBEcnVjay4gUGz2dHps aWNoIG1133RlIGljaCB3ZW5kZW4NCi4gLiAuIC4gZGEgc2FoIGljaCB1bnRlbiBh dWYgZGVyIFN0cmHfZSBzZWhyIGZlcm4sIGRhcyBGZWxsIPxiZXIgZGllIEFjaHNl bA0KbmFjaGzkc3NpZyBnZWxlZ3QsIGltIEF1dG9kcmXfIGRpZSBG/HJzdGluLCD8 YmVyIGRhcyBQZmxhc3RlciBnZWhlbmQsDQpsZWljaHQgZWluIHdlbmlnIHNpY2gg d2llZ2VuZCwga/ZuaWdsaWNoIHVuZCBz/N8gaW4gZGVuIEj8ZnRlbi4NCg0KSWNo IHdvbGx0ZSBydWZlbiwgaWNoIGhvYiBkaWUgQXJtZS4gQWJlciBzaWUgd2FyZW4g QmxlaS4gRGllIFN0aWNrbHVmdA0KZHJhbmcgaW4gZGllIEtlaGxlLCBkaWVzIGlz dCBkZXIgVG9kLCBibGl0enRlIG1laW4gSGlybiwgZGVyIEhpbW1lbCBzdGFuZA0K aW0gQmVyc3RlbiAuIC4gLiAuIHVuZCBpbiBkZW0gQXVnZW5ibGljaywgYWxzIGRp ZSBG/HJzdGluLCBtaXQgZGVtIEZlbGwNCnNwaWVsZW5kLCBsZWljaHRoaW4gYXVm IGRlbiBibGF1cm90ZW4gSG9yaXpvbnQgenVnZWhlbmQsIGZhc3QgZGVuIFJhbmQg ZGVzDQpHZXf2bGtzIGVycmVpY2h0ZSB1bmQgYWJib2csIHJp3yBlaW5lIGJy/Gxs ZW5kZSBFeHBsb3Npb24gYWxsZXMgYXVzZWluYW5kZXINCi4gLiAuIC4NCg0KRGEg ZXJ3YWNodGUgaWNoLiBFbnRzZXR6dC4NCg0KRGllIEF1Z2VuIGF1Zmdlcmlzc2Vu IHNw5Gh0ZSBpY2ggaW4gRHVua2VsaGVpdC4gQWJlciBkaWUgU29tbWVybGFuZHNj aGFmdA0Kc3RhbmQgbWl0IG1pbGRlbSBTaWxiZXIgaW4gZGVtIFJhdW0sIHVuZCBE dWZ0IHZvbiBGbGllZGVyIHpvZyBkdXJjaCBkYXMNClppbW1lci4gQWJlciBub2No IHdhciBpY2ggaXJyLiBJY2ggcmnfIHNpZSBoZXL8YmVyLCB1bmQgc2llIGVyd2Fj aHRlIGluDQptZWluZW4gQXJtIGhpbmVpbiwgu2R1LKsgcmllZiBpY2ggc3RhbW1l bG5kOiC7aWNoIGhhYmUgZGljaCAuIC4gLiAuIGljaA0KaGFiZSBkaWNoLqsgVW5k IG5vY2ggaGFsYiBpbSBTY2hsYWYgZXJ3YWNodGUgaWhyIGdlbPZzdGVyIE11bmQg aW4gbWVpbmVtLA0KdW5kIG1pdCBkZXIgd2FybWVuIE7kaGUgaWhyZXMgTGVpYmVz IGhpZWx0IGljaCB3aWVkZXIgdW5lbmRsaWNoZXMgRGFzZWluDQptaXQgc2FuZnRl bSBIZXJ6c2NobGFnIGVyZG9ubmVybmQgYW4gbWVpbmVyIEJydXN0LiBJY2ggd3Vy ZGUgcnVoaWcgd2llIGVpbg0KVGllciwgdW5kLCBkaWUgR2xpZWRlciBhbiBpaHJl biBnZWz2c3QsIG1pdCBzY2h3aW5kZW5kZW0gR3JhdWVuIGRhcvxiZXIsDQpkYd8g ZmVpbmRsaWNoIGlyZ2VuZHdvIGVpbiBTY2hpY2tzYWwgVW5nZWhldXJlcyBhdd9l cmhhbGIgZGVyIE1hY2h0IG1laW5lcg0KQXJtZSB6dSBoYWx0ZW4gdmVybfZjaHRl LCBtaXQgc2Nob24gZW50ZmVybnQgc2ljaCBmbPxjaHRlbmRlbSBHZWb8aGwgZGVz DQpUcmF1bXMsIGRlbSBBdWdlbmJsaWNrIHVuc3RlcmJsaWNoIGhpbmdlZ2ViZW4s IHNjaGxpZWYgaWNoIGhpbmVpbiBpbiBpaHJlbg0KYmVzaXR6ZW5kZW4gS3XfLg0K DQoNCg0KDQoNCg0KDQoNCg0KR0VEUlVDS1QgQkVJIFBPRVNDSEVMICYgVFJFUFRF IElOIExFSVBaSUc= enmime-0.9.3/testdata/parts/empty-ctype-bad-content.raw000066400000000000000000000011121417532643400231320ustar00rootroot00000000000000Content-Type: multipart/alternative; boundary="----=_Part_10541_215446765.98298273965074084" ------=_Part_10541_215446765.98298273965074084 Content-Type: Use the View Invoices function or locate the account on the List of Accounts table.

Please do not reply to this e-mail message.

Your Team


If you have received this notification in error, or if you need further assistance accessing your invoice, please contact us.

Content-Transfer-Encoding: quoted-printable ------=_Part_10541_215446765.98298273965074084-- enmime-0.9.3/testdata/parts/empty-header.raw000066400000000000000000000003451417532643400210510ustar00rootroot00000000000000Content-Type: multipart/alternative; boundary="Enmime-Test-100" --Enmime-Test-100 A text section --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/html; charset=us-ascii An HTML section --Enmime-Test-100-- enmime-0.9.3/testdata/parts/header-only.raw000066400000000000000000000001741417532643400206740ustar00rootroot00000000000000Content-Type: text/html; name=file.html; charset="utf-8" Content-Disposition: inline Content-ID: enmime-0.9.3/testdata/parts/malformed-content-type-params.raw000066400000000000000000000000441417532643400243370ustar00rootroot00000000000000Content-Type: text/html;iso-8859-1 enmime-0.9.3/testdata/parts/missing-closing-param-quote.raw000066400000000000000000000002221417532643400240150ustar00rootroot00000000000000Content-type: text/HTML; charset="UTF-8Return-Path: bounce-810_HTML-769869545-477063-1070564-43@bounce.email.oflce57578375.com MIME-Version: 1.0 enmime-0.9.3/testdata/parts/missing-ctype-root.raw000066400000000000000000000001761417532643400222430ustar00rootroot00000000000000MIME-Version: 1.0 According to RFC 822, content type defaults to text/plain at the root. Charset should default to us-ascii. enmime-0.9.3/testdata/parts/missing-ctype.raw000066400000000000000000000004051417532643400212550ustar00rootroot00000000000000Content-Type: multipart/alternative; boundary="Enmime-Test-100" --Enmime-Test-100 Content-Transfer-Encoding: 7bit A text section --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/html; charset=us-ascii An HTML section --Enmime-Test-100-- enmime-0.9.3/testdata/parts/multialtern.raw000066400000000000000000000004601417532643400210230ustar00rootroot00000000000000Content-Type: multipart/alternative; boundary="Enmime-Test-100" --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii A text section --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/html; charset=us-ascii An HTML section --Enmime-Test-100-- enmime-0.9.3/testdata/parts/multibase64.raw000066400000000000000000000005361417532643400206260ustar00rootroot00000000000000Content-Type: multipart/mixed; boundary="Enmime-Test-100" --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii A text section --Enmime-Test-100 Content-Transfer-Encoding: base64 Content-Type: text/html; name="test.html" Content-Disposition: attachment; filename=test.html PGh0bWw+Cg== --Enmime-Test-100-- enmime-0.9.3/testdata/parts/multimixed-no-closing-boundary.raw000066400000000000000000000004021417532643400245270ustar00rootroot00000000000000Content-Type: multipart/mixed; boundary="Enmime-Test-100" --Enmime-Test-100 Content-Type: text/html; charset=UTF-8 Content-Transfer-Encoding: quoted-printable

=0DHello,


enmime-0.9.3/testdata/parts/multimixed.raw000066400000000000000000000004451417532643400206470ustar00rootroot00000000000000Content-Type: multipart/mixed; boundary="Enmime-Test-100" --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii Section one --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii Section two --Enmime-Test-100-- enmime-0.9.3/testdata/parts/multiother.raw000066400000000000000000000004501417532643400206560ustar00rootroot00000000000000Content-Type: multipart/x-enmime; boundary="Enmime-Test-100" --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii Section one --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii Section two --Enmime-Test-100-- enmime-0.9.3/testdata/parts/nestedmulti.raw000066400000000000000000000013301417532643400210150ustar00rootroot00000000000000Content-Type: multipart/alternative; boundary="Enmime-Test-100" --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii A text section --Enmime-Test-100 Content-Type: multipart/related; boundary="Enmime-Test-200" --Enmime-Test-200 Content-Transfer-Encoding: 7bit Content-Type: text/html; charset=us-ascii An HTML section --Enmime-Test-200 Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename=attach.txt Content-Type: text/plain; name="attach.txt" An inline text attachment --Enmime-Test-200 Content-Transfer-Encoding: 7bit Content-Disposition: inline Content-Type: text/plain; name="attach2.txt" Another inline text attachment --Enmime-Test-200-- --Enmime-Test-100-- enmime-0.9.3/testdata/parts/quoted-printable-invalid.raw000066400000000000000000000036701417532643400233740ustar00rootroot00000000000000Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Content-Disposition: inline https://www.amazon.ca/gp/product/B002M8EEW8/ref=od_aui_detailpages00?ie=UTF8&psc=1 Stuffs’s Weekly Summary Sunday, January 15th – Saturday, January 21st Hope you had a good weekend! Here's a summary of what happened on your team last week: 1 member was added: Jane Smith, chris, John Smith, and Polly the poll bot 1 account was disabled: Polly the poll bot View a complete list of member changes: https://stuff.slack.com/x-tnnnnnnnnnn-0/admin/billing/changes --- Your team sent a total of 323 messages last week (that's 83 fewer than the week before). Of those, 16% were in public channels, 27% were in private channels, and 57% were direct messages. Your team also uploaded 6 files (that's 10 fewer than the week before). Looking for more stats? Check out your team's stats page: https://stuff.slack.com/x-tnnnnnnnnnn-0/admin/stats --- The busiest hour last week was 9-10am on Monday, when your team sent 44 messages. Holy speedtyping! --- Your team has 3 owners: Jane Smith chris John Smith (primary owner) In total there are 20 people on your team (up 1 from last week) (that's not including 22 disabled accounts). Remember: it's important to keep the list of owners and admins up to date since they control your team's settings: https://stuffs.slack.com/x-tnnnnnnnnnn-0/admin Your team is on Slack's Free plan, which is free to use for as long as you want for teams of all sizes. Interested in unlimited archive access and integrations, single sign-on, custom data retention, and more? Check out our paid plans to learn all about it: https://stuffs.slack.com/x-tnnnnnnnnnn-0/pricing --- This email is sent to Team Owners and Admins of active teams. If you'd prefer not to receive these emails, you can unsubscribe here: https://stuffs.slack.com/unsub/nnnnnnnnnn-0-weekly enmime-0.9.3/testdata/parts/quoted-printable.raw000066400000000000000000000001661417532643400217450ustar00rootroot00000000000000Content-Type: text/plain; charset=us-ascii Content-Transfer-Encoding: quoted-printable Start=3D=41=42= =43=3DFinish= enmime-0.9.3/testdata/parts/similar-boundary.raw000066400000000000000000000010141417532643400217400ustar00rootroot00000000000000Content-Type: multipart/mixed; boundary="Enmime-Test-100" --Enmime-Test-100 Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii Section one --Enmime-Test-100 Content-Type: multipart/alternative; boundary="Enmime-Test-100_alt" --Enmime-Test-100_alt Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii A text section --Enmime-Test-100_alt Content-Transfer-Encoding: 7bit Content-Type: text/html; charset=us-ascii An HTML section --Enmime-Test-100_alt-- --Enmime-Test-100-- enmime-0.9.3/testdata/parts/textplain.raw000066400000000000000000000001551417532643400204740ustar00rootroot00000000000000Content-Transfer-Encoding: 7bit Content-Type: text/plain; charset=us-ascii Test of text/plain section enmime-0.9.3/testdata/parts/unquoted-ctype-param-special.raw000066400000000000000000000001521417532643400241630ustar00rootroot00000000000000Content-Type: text/calendar; method=text/calendar Content-Disposition: attachment; filename=calendar.ics