pax_global_header 0000666 0000000 0000000 00000000064 14175326434 0014523 g ustar 00root root 0000000 0000000 52 comment=41326393d2ae42a07b3728e34608e352deef3e42
enmime-0.9.3/ 0000775 0000000 0000000 00000000000 14175326434 0013006 5 ustar 00root root 0000000 0000000 enmime-0.9.3/.gitattributes 0000664 0000000 0000000 00000001001 14175326434 0015671 0 ustar 00root root 0000000 0000000 # 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/ 0000775 0000000 0000000 00000000000 14175326434 0014346 5 ustar 00root root 0000000 0000000 enmime-0.9.3/.github/ISSUE_TEMPLATE.md 0000664 0000000 0000000 00000000231 14175326434 0017047 0 ustar 00root root 0000000 0000000 What 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/ 0000775 0000000 0000000 00000000000 14175326434 0016403 5 ustar 00root root 0000000 0000000 enmime-0.9.3/.github/workflows/build-and-test.yml 0000664 0000000 0000000 00000001545 14175326434 0021747 0 ustar 00root root 0000000 0000000 name: 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.yml 0000664 0000000 0000000 00000002162 14175326434 0021341 0 ustar 00root root 0000000 0000000 name: 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/.gitignore 0000664 0000000 0000000 00000000542 14175326434 0014777 0 ustar 00root root 0000000 0000000 # Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
# goland ide
.idea
# vim swp files
*.swp
cmd/mime-dump/mime-dump
cmd/mime-extractor/mime-extractor
enmime-0.9.3/CHANGELOG.md 0000664 0000000 0000000 00000020360 14175326434 0014620 0 ustar 00root root 0000000 0000000 Change 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.md 0000664 0000000 0000000 00000002115 14175326434 0015236 0 ustar 00root root 0000000 0000000 How 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/LICENSE 0000664 0000000 0000000 00000002123 14175326434 0014011 0 ustar 00root root 0000000 0000000 The 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/Makefile 0000664 0000000 0000000 00000001162 14175326434 0014446 0 ustar 00root root 0000000 0000000 SHELL := /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.md 0000664 0000000 0000000 00000003216 14175326434 0014267 0 ustar 00root root 0000000 0000000 # enmime
[][Pkg Docs]
[][Build Status]
[][Go Report Card]
[][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.go 0000664 0000000 0000000 00000016361 14175326434 0015167 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000032146 14175326434 0016225 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000022411 14175326434 0014763 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000066125 14175326434 0016034 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14175326434 0013551 5 ustar 00root root 0000000 0000000 enmime-0.9.3/cmd/example_test.go 0000664 0000000 0000000 00000005733 14175326434 0016602 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14175326434 0015443 5 ustar 00root root 0000000 0000000 enmime-0.9.3/cmd/mime-dump/README.md 0000664 0000000 0000000 00000001403 14175326434 0016720 0 ustar 00root root 0000000 0000000 mime-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.go 0000664 0000000 0000000 00000002145 14175326434 0017666 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000003315 14175326434 0020725 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14175326434 0016511 5 ustar 00root root 0000000 0000000 enmime-0.9.3/cmd/mime-extractor/mime-extractor.go 0000664 0000000 0000000 00000003740 14175326434 0022004 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000006224 14175326434 0023043 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000010035 14175326434 0015237 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000006344 14175326434 0016306 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000005634 14175326434 0014615 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000010507 14175326434 0015647 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000012664 14175326434 0014603 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000017030 14175326434 0015632 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000005575 14175326434 0014623 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000025424 14175326434 0015161 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000105104 14175326434 0016212 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000003617 14175326434 0014475 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000006202 14175326434 0015525 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000014643 14175326434 0016037 0 ustar 00root root 0000000 0000000 package 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.mod 0000664 0000000 0000000 00000001152 14175326434 0014113 0 ustar 00root root 0000000 0000000 module 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.sum 0000664 0000000 0000000 00000005357 14175326434 0014153 0 ustar 00root root 0000000 0000000 github.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.go 0000664 0000000 0000000 00000014603 14175326434 0014571 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000022510 14175326434 0015624 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000003555 14175326434 0015012 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000005614 14175326434 0016047 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14175326434 0014622 5 ustar 00root root 0000000 0000000 enmime-0.9.3/internal/coding/ 0000775 0000000 0000000 00000000000 14175326434 0016065 5 ustar 00root root 0000000 0000000 enmime-0.9.3/internal/coding/base64.go 0000664 0000000 0000000 00000003570 14175326434 0017505 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000002560 14175326434 0020542 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000042602 14175326434 0020234 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000005244 14175326434 0021274 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000005223 14175326434 0020367 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000007526 14175326434 0021436 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000001140 14175326434 0020155 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000001643 14175326434 0021224 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000007010 14175326434 0020770 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000010533 14175326434 0022033 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14175326434 0017026 5 ustar 00root root 0000000 0000000 enmime-0.9.3/internal/stringutil/addr.go 0000664 0000000 0000000 00000000620 14175326434 0020265 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000002127 14175326434 0021330 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000001503 14175326434 0020507 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000003200 14175326434 0021542 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000001077 14175326434 0020330 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000000476 14175326434 0021371 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000001523 14175326434 0020327 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000002664 14175326434 0021375 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14175326434 0015601 5 ustar 00root root 0000000 0000000 enmime-0.9.3/internal/test/golden.go 0000664 0000000 0000000 00000007171 14175326434 0017406 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000006563 14175326434 0020451 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14175326434 0017412 5 ustar 00root root 0000000 0000000 enmime-0.9.3/internal/test/testdata/test.golden 0000664 0000000 0000000 00000000016 14175326434 0021560 0 ustar 00root root 0000000 0000000 one
two
three
enmime-0.9.3/internal/test/testing.go 0000664 0000000 0000000 00000011715 14175326434 0017612 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000016010 14175326434 0020642 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000004352 14175326434 0014435 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000012545 14175326434 0015477 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14175326434 0014767 5 ustar 00root root 0000000 0000000 enmime-0.9.3/mediatype/mediatype.go 0000664 0000000 0000000 00000030231 14175326434 0017276 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000034511 14175326434 0020342 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000032050 14175326434 0014303 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000060513 14175326434 0015347 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000002300 14175326434 0014610 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000001402 14175326434 0015651 0 ustar 00root root 0000000 0000000 package 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.nix 0000664 0000000 0000000 00000000313 14175326434 0014632 0 ustar 00root root 0000000 0000000 with import {};
stdenv.mkDerivation rec {
name = "env";
env = buildEnv { name = name; paths = buildInputs; };
buildInputs = [
go
golint
];
hardeningDisable = [ "fortify" ];
}
enmime-0.9.3/testdata/ 0000775 0000000 0000000 00000000000 14175326434 0014617 5 ustar 00root root 0000000 0000000 enmime-0.9.3/testdata/attach/ 0000775 0000000 0000000 00000000000 14175326434 0016063 5 ustar 00root root 0000000 0000000 enmime-0.9.3/testdata/attach/fake.png 0000664 0000000 0000000 00000000021 14175326434 0017470 0 ustar 00root root 0000000 0000000 Not really a PNG
enmime-0.9.3/testdata/encode/ 0000775 0000000 0000000 00000000000 14175326434 0016054 5 ustar 00root root 0000000 0000000 enmime-0.9.3/testdata/encode/build-qp-addr-headers.golden 0000664 0000000 0000000 00000000451 14175326434 0023304 0 ustar 00root root 0000000 0000000 Content-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.golden 0000664 0000000 0000000 00000000434 14175326434 0024015 0 ustar 00root root 0000000 0000000 Content-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.golden 0000664 0000000 0000000 00000005363 14175326434 0022441 0 ustar 00root root 0000000 0000000 Content-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.golden 0000664 0000000 0000000 00000000406 14175326434 0022210 0 ustar 00root root 0000000 0000000 Content-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.golden 0000664 0000000 0000000 00000000121 14175326434 0023253 0 ustar 00root root 0000000 0000000 Content-Transfer-Encoding: quoted-printable
=E2=98=86 No header, only content. enmime-0.9.3/testdata/encode/part-content-only.golden 0000664 0000000 0000000 00000000032 14175326434 0022636 0 ustar 00root root 0000000 0000000
No header, only content. enmime-0.9.3/testdata/encode/part-default-headers.golden 0000664 0000000 0000000 00000000474 14175326434 0023254 0 ustar 00root root 0000000 0000000 Content-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.golden 0000664 0000000 0000000 00000000000 14175326434 0021336 0 ustar 00root root 0000000 0000000 enmime-0.9.3/testdata/encode/part-file-mod-date.golden 0000664 0000000 0000000 00000000305 14175326434 0022617 0 ustar 00root root 0000000 0000000 Content-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 MIME enmime-0.9.3/testdata/encode/part-header-only-default-encoding.golden 0000664 0000000 0000000 00000000054 14175326434 0025626 0 ustar 00root root 0000000 0000000 Content-Type: text/plain
X-Empty-Header:
enmime-0.9.3/testdata/encode/part-header-only.golden 0000664 0000000 0000000 00000000032 14175326434 0022414 0 ustar 00root root 0000000 0000000 Content-Type: text/plain
enmime-0.9.3/testdata/encode/part-plain.golden 0000664 0000000 0000000 00000000142 14175326434 0021312 0 ustar 00root root 0000000 0000000 Content-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.golden 0000664 0000000 0000000 00000000061 14175326434 0023473 0 ustar 00root root 0000000 0000000 Content-Type: text/plain; charset=utf-8
Hello= enmime-0.9.3/testdata/encode/part-quoted-content.golden 0000664 0000000 0000000 00000000177 14175326434 0023170 0 ustar 00root root 0000000 0000000 Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain; charset=utf-8
=C2=A1Hola, se=C3=B1or! Welcome to MIME enmime-0.9.3/testdata/encode/part-quoted-headers.golden 0000664 0000000 0000000 00000000664 14175326434 0023132 0 ustar 00root root 0000000 0000000 Content-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.golden 0000664 0000000 0000000 00000000757 14175326434 0025113 0 ustar 00root root 0000000 0000000 Content-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.golden 0000664 0000000 0000000 00000000505 14175326434 0022753 0 ustar 00root root 0000000 0000000 Content-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/ 0000775 0000000 0000000 00000000000 14175326434 0017106 5 ustar 00root root 0000000 0000000 enmime-0.9.3/testdata/low-quality/bad-final-boundary.raw 0000664 0000000 0000000 00000001106 14175326434 0023255 0 ustar 00root root 0000000 0000000 From: 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.raw 0000664 0000000 0000000 00000000636 14175326434 0022735 0 ustar 00root root 0000000 0000000 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.raw 0000664 0000000 0000000 00000001146 14175326434 0022546 0 ustar 00root root 0000000 0000000 From: 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.raw 0000664 0000000 0000000 00000000635 14175326434 0022211 0 ustar 00root root 0000000 0000000 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/html-only-inline.raw 0000664 0000000 0000000 00000004516 14175326434 0023026 0 ustar 00root root 0000000 0000000 From: 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.raw 0000664 0000000 0000000 00000002542 14175326434 0023243 0 ustar 00root root 0000000 0000000 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: 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.raw 0000664 0000000 0000000 00000001102 14175326434 0024105 0 ustar 00root root 0000000 0000000 From: 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.raw 0000664 0000000 0000000 00000000314 14175326434 0023717 0 ustar 00root root 0000000 0000000 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
A plain text email
enmime-0.9.3/testdata/low-quality/missing-content-type2.raw 0000664 0000000 0000000 00000000675 14175326434 0024013 0 ustar 00root root 0000000 0000000 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
Content-Transfer-Encoding: 7bit
An HTML section
--Enmime-Test-100--
enmime-0.9.3/testdata/low-quality/unk-charset-html-only.raw 0000664 0000000 0000000 00000000423 14175326434 0023765 0 ustar 00root root 0000000 0000000 From: 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.raw 0000664 0000000 0000000 00000001106 14175326434 0023007 0 ustar 00root root 0000000 0000000 From: 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.raw 0000664 0000000 0000000 00000001110 14175326434 0023137 0 ustar 00root root 0000000 0000000 From: 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/ 0000775 0000000 0000000 00000000000 14175326434 0015541 5 ustar 00root root 0000000 0000000 enmime-0.9.3/testdata/mail/attachment-application.raw 0000664 0000000 0000000 00000001101 14175326434 0022676 0 ustar 00root root 0000000 0000000 From: 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.raw 0000664 0000000 0000000 00000001174 14175326434 0021523 0 ustar 00root root 0000000 0000000 From: 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.raw 0000664 0000000 0000000 00000001207 14175326434 0026114 0 ustar 00root root 0000000 0000000 MIME-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.raw 0000664 0000000 0000000 00000002324 14175326434 0022640 0 ustar 00root root 0000000 0000000 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: 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.raw 0000664 0000000 0000000 00000002235 14175326434 0024341 0 ustar 00root root 0000000 0000000 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
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.raw 0000664 0000000 0000000 00000000462 14175326434 0024475 0 ustar 00root root 0000000 0000000 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: text/plain; name="test.csv"
Content-Disposition: attachment; filename="test.csv"
Content-Transfer-Encoding: base64
VGVzdDtUZXN0Cg==
enmime-0.9.3/testdata/mail/attachment-only.raw 0000664 0000000 0000000 00000002330 14175326434 0021361 0 ustar 00root root 0000000 0000000 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"
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.raw 0000664 0000000 0000000 00000001051 14175326434 0020401 0 ustar 00root root 0000000 0000000 From: 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.raw 0000664 0000000 0000000 00000014314 14175326434 0020156 0 ustar 00root root 0000000 0000000 Delivered-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
--_av-rPFkvS5QROAYLq2cQTUr1w--
enmime-0.9.3/testdata/mail/epilogue-sample.raw 0000664 0000000 0000000 00000001071 14175326434 0021343 0 ustar 00root root 0000000 0000000 From: 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.raw 0000664 0000000 0000000 00000000032 14175326434 0020270 0 ustar 00root root 0000000 0000000 // Not an RFC-822 Document enmime-0.9.3/testdata/mail/header-only.raw 0000664 0000000 0000000 00000000405 14175326434 0020462 0 ustar 00root root 0000000 0000000 From: 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.raw 0000664 0000000 0000000 00000004775 14175326434 0023611 0 ustar 00root root 0000000 0000000 From: 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.raw 0000664 0000000 0000000 00000004775 14175326434 0025306 0 ustar 00root root 0000000 0000000 From: 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.raw 0000664 0000000 0000000 00000004751 14175326434 0021430 0 ustar 00root root 0000000 0000000 From: 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.raw 0000664 0000000 0000000 00000004520 14175326434 0021454 0 ustar 00root root 0000000 0000000 From: 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.raw 0000664 0000000 0000000 00000003121 14175326434 0021471 0 ustar 00root root 0000000 0000000 Subject: 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.raw 0000664 0000000 0000000 00000003172 14175326434 0026224 0 ustar 00root root 0000000 0000000 Subject: 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.raw 0000664 0000000 0000000 00000001122 14175326434 0021513 0 ustar 00root root 0000000 0000000 Message-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.raw 0000664 0000000 0000000 00000001156 14175326434 0022354 0 ustar 00root root 0000000 0000000 Date: 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.raw 0000664 0000000 0000000 00000006161 14175326434 0025011 0 ustar 00root root 0000000 0000000 Received: 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.raw 0000664 0000000 0000000 00000003420 14175326434 0022355 0 ustar 00root root 0000000 0000000 From: "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.raw 0000664 0000000 0000000 00000004777 14175326434 0022302 0 ustar 00root root 0000000 0000000 From: "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.raw 0000664 0000000 0000000 00000005077 14175326434 0022262 0 ustar 00root root 0000000 0000000 From: "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.raw 0000664 0000000 0000000 00000001771 14175326434 0021733 0 ustar 00root root 0000000 0000000 Message-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.raw 0000664 0000000 0000000 00000001114 14175326434 0020304 0 ustar 00root root 0000000 0000000 Message-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.raw 0000664 0000000 0000000 00000003200 14175326434 0020445 0 ustar 00root root 0000000 0000000 Message-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.raw 0000664 0000000 0000000 00000005035 14175326434 0024133 0 ustar 00root root 0000000 0000000 From: "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.raw 0000664 0000000 0000000 00000000544 14175326434 0024574 0 ustar 00root root 0000000 0000000 From: 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.raw 0000664 0000000 0000000 00000001123 14175326434 0020732 0 ustar 00root root 0000000 0000000 Date: 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.raw 0000664 0000000 0000000 00000000553 14175326434 0023074 0 ustar 00root root 0000000 0000000 Date: 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.raw 0000664 0000000 0000000 00000000356 14175326434 0017777 0 ustar 00root root 0000000 0000000 Date: 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.raw 0000664 0000000 0000000 00000005776 14175326434 0022162 0 ustar 00root root 0000000 0000000 From: 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.raw 0000664 0000000 0000000 00000001237 14175326434 0020527 0 ustar 00root root 0000000 0000000 From: 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.raw 0000664 0000000 0000000 00000000353 14175326434 0021211 0 ustar 00root root 0000000 0000000 Date: 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.raw 0000664 0000000 0000000 00000007654 14175326434 0022516 0 ustar 00root root 0000000 0000000 Message-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.raw 0000664 0000000 0000000 00000010176 14175326434 0022625 0 ustar 00root root 0000000 0000000 Message-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.raw 0000664 0000000 0000000 00000007656 14175326434 0021024 0 ustar 00root root 0000000 0000000 Message-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.raw 0000664 0000000 0000000 00000007371 14175326434 0022470 0 ustar 00root root 0000000 0000000 Message-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.raw 0000664 0000000 0000000 00000001524 14175326434 0021535 0 ustar 00root root 0000000 0000000 From: 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.raw 0000664 0000000 0000000 00000001243 14175326434 0021676 0 ustar 00root root 0000000 0000000 From: 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/ 0000775 0000000 0000000 00000000000 14175326434 0015750 5 ustar 00root root 0000000 0000000 enmime-0.9.3/testdata/parts/badboundary.raw 0000664 0000000 0000000 00000000457 14175326434 0020763 0 ustar 00root root 0000000 0000000 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
Content-Type: text/html; charset=us-ascii
An HTML section
--Enmime-Test-100
enmime-0.9.3/testdata/parts/barren-content-type.raw 0000664 0000000 0000000 00000000152 14175326434 0022361 0 ustar 00root root 0000000 0000000 Content-Type: ;
name=""
Content-Transfer-Encoding: base64
Content-Disposition: attachment;
filename=""
enmime-0.9.3/testdata/parts/bin-attach.raw 0000664 0000000 0000000 00000000613 14175326434 0020475 0 ustar 00root root 0000000 0000000 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/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.raw 0000664 0000000 0000000 00000000336 14175326434 0022415 0 ustar 00root root 0000000 0000000 Content-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.raw 0000664 0000000 0000000 00000000142 14175326434 0024021 0 ustar 00root root 0000000 0000000 Content-Type: text/plain; charset="UTF-8"
Content-Transfer-Encoding: base64
5ZKM5byf5byfDQo=
enmime-0.9.3/testdata/parts/chardet-fail.raw 0000664 0000000 0000000 00000000337 14175326434 0021011 0 ustar 00root root 0000000 0000000 Content-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.raw 0000664 0000000 0000000 00000032017 14175326434 0022447 0 ustar 00root root 0000000 0000000 Content-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.raw 0000664 0000000 0000000 00000252057 14175326434 0023117 0 ustar 00root root 0000000 0000000 Content-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.raw 0000664 0000000 0000000 00000001112 14175326434 0023132 0 ustar 00root root 0000000 0000000 Content-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.raw 0000664 0000000 0000000 00000000345 14175326434 0021051 0 ustar 00root root 0000000 0000000 Content-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.raw 0000664 0000000 0000000 00000000174 14175326434 0020674 0 ustar 00root root 0000000 0000000 Content-Type: text/html; name=file.html; charset="utf-8"
Content-Disposition: inline
Content-ID:
enmime-0.9.3/testdata/parts/malformed-content-type-params.raw 0000664 0000000 0000000 00000000044 14175326434 0024337 0 ustar 00root root 0000000 0000000 Content-Type: text/html;iso-8859-1
enmime-0.9.3/testdata/parts/missing-closing-param-quote.raw 0000664 0000000 0000000 00000000222 14175326434 0024015 0 ustar 00root root 0000000 0000000 Content-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.raw 0000664 0000000 0000000 00000000176 14175326434 0022243 0 ustar 00root root 0000000 0000000 MIME-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.raw 0000664 0000000 0000000 00000000405 14175326434 0021255 0 ustar 00root root 0000000 0000000 Content-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.raw 0000664 0000000 0000000 00000000460 14175326434 0021023 0 ustar 00root root 0000000 0000000 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
Content-Type: text/html; charset=us-ascii
An HTML section
--Enmime-Test-100--
enmime-0.9.3/testdata/parts/multibase64.raw 0000664 0000000 0000000 00000000536 14175326434 0020626 0 ustar 00root root 0000000 0000000 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/parts/multimixed-no-closing-boundary.raw 0000664 0000000 0000000 00000000402 14175326434 0024527 0 ustar 00root root 0000000 0000000 Content-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.raw 0000664 0000000 0000000 00000000445 14175326434 0020647 0 ustar 00root root 0000000 0000000 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/parts/multiother.raw 0000664 0000000 0000000 00000000450 14175326434 0020656 0 ustar 00root root 0000000 0000000 Content-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.raw 0000664 0000000 0000000 00000001330 14175326434 0021015 0 ustar 00root root 0000000 0000000 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-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.raw 0000664 0000000 0000000 00000003670 14175326434 0023374 0 ustar 00root root 0000000 0000000 Content-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.raw 0000664 0000000 0000000 00000000166 14175326434 0021745 0 ustar 00root root 0000000 0000000 Content-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.raw 0000664 0000000 0000000 00000001014 14175326434 0021740 0 ustar 00root root 0000000 0000000 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-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.raw 0000664 0000000 0000000 00000000155 14175326434 0020474 0 ustar 00root root 0000000 0000000 Content-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.raw 0000664 0000000 0000000 00000000152 14175326434 0024163 0 ustar 00root root 0000000 0000000 Content-Type: text/calendar; method=text/calendar
Content-Disposition: attachment; filename=calendar.ics