add vendoring

This commit is contained in:
Aine
2022-11-16 12:08:51 +02:00
parent 14751cbf3a
commit c1d33fe3cb
1104 changed files with 759066 additions and 0 deletions

24
vendor/github.com/jhillyerd/enmime/.gitattributes generated vendored Normal file
View File

@@ -0,0 +1,24 @@
# 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

31
vendor/github.com/jhillyerd/enmime/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,31 @@
# 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

23
vendor/github.com/jhillyerd/enmime/.golangci.yml generated vendored Normal file
View File

@@ -0,0 +1,23 @@
run:
timeout: 5m
linters:
enable:
- asciicheck
- bodyclose
- deadcode
- exportloopref
- errcheck
- gofmt
- goimports
- gosimple
- govet
- ineffassign
- misspell
- megacheck
- prealloc
- rowserrcheck
- staticcheck
- structcheck
- typecheck
- unused
- varcheck

293
vendor/github.com/jhillyerd/enmime/CHANGELOG.md generated vendored Normal file
View File

@@ -0,0 +1,293 @@
Change Log
==========
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## [0.10.0] - 2022-07-20
### Added
- Support for parser options! (#248)
- Option to skip parsing of malformed parts (#248)
- Envelope.Date() method for parsing date (#253)
- Option to handle missing multipart boundaries (#257)
### Fixed
- Remove trailing HTML tags (#252)
- Buffer overrun in quoted-printable (#254)
- Corrected quoted-printable equals counting (#255)
- Improve splitting inside quoted text (#256)
## [0.9.4] - 2022-05-16
### Added
- Remove HTML tags in malformed content types (#229)
- Maximal number of errors recorded in Part limited (#240)
- Builder: Support other parts (#244)
- Additional decoding in mail address (#247)
- Integration test include Go 1.18
### Fixed
- Fix for quote-printed utf-8 header with quotes (#237)
- Parse address joined with semicolons (#238)
- Use extended parser after fixing address list (#239)
- Parse media types which are escaped at first rune (#246)
### Changed
- Rely on stdlib for decoding to UTF-8, simplifies address parsing (#234)
## [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.4...master
[0.9.4]: https://github.com/jhillyerd/enmime/compare/v0.9.3...v0.9.4
[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.

37
vendor/github.com/jhillyerd/enmime/CONTRIBUTING.md generated vendored Normal file
View File

@@ -0,0 +1,37 @@
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!

20
vendor/github.com/jhillyerd/enmime/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,20 @@
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.

34
vendor/github.com/jhillyerd/enmime/Makefile generated vendored Normal file
View File

@@ -0,0 +1,34 @@
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'

42
vendor/github.com/jhillyerd/enmime/README.md generated vendored Normal file
View File

@@ -0,0 +1,42 @@
# enmime
[![PkgGoDev](https://pkg.go.dev/badge/github.com/jhillyerd/enmime)][Pkg Docs]
[![Build Status](https://travis-ci.org/jhillyerd/enmime.svg?branch=master)][Build Status]
[![Go Report Card](https://goreportcard.com/badge/github.com/jhillyerd/enmime)][Go Report Card]
[![Coverage Status](https://coveralls.io/repos/github/jhillyerd/enmime/badge.svg?branch=master)][Coverage Status]
enmime is a MIME encoding and decoding library for Go, focused on generating and
parsing MIME encoded emails. It is being developed in tandem with the
[Inbucket] email service.
enmime includes a fluent interface builder for generating MIME encoded messages,
see the wiki for example [Builder Usage].
See our [Pkg Docs] for examples and API usage information.
## Development Status
enmime is near production quality: it works but struggles to parse a small
percentage of emails. It's possible the API will evolve slightly before the 1.0
release.
See [CONTRIBUTING.md] for more information.
## About
enmime is written in [Go][Golang].
enmime is open source software released under the MIT License. The latest
version can be found at https://github.com/jhillyerd/enmime
[Build Status]: https://travis-ci.org/jhillyerd/enmime
[Builder Usage]: https://github.com/jhillyerd/enmime/wiki/Builder-Usage
[Coverage Status]: https://coveralls.io/github/jhillyerd/enmime
[CONTRIBUTING.md]: https://github.com/jhillyerd/enmime/blob/master/CONTRIBUTING.md
[Inbucket]: http://www.inbucket.org/
[Golang]: http://golang.org/
[Go Report Card]: https://goreportcard.com/report/github.com/jhillyerd/enmime
[Pkg Docs]: https://pkg.go.dev/github.com/jhillyerd/enmime

253
vendor/github.com/jhillyerd/enmime/boundary.go generated vendored Normal file
View File

@@ -0,0 +1,253 @@
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
}

371
vendor/github.com/jhillyerd/enmime/builder.go generated vendored Normal file
View File

@@ -0,0 +1,371 @@
package enmime
import (
"bytes"
"errors"
"io/ioutil"
"mime"
"net/mail"
"net/textproto"
"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
}
b, err := ioutil.ReadFile(path)
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
}
b, err := ioutil.ReadFile(path)
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)
}
// AddOtherPart returns a copy of MailBuilder that includes the specified embedded part.
// fileName may be left empty.
// It's useful when you want to embed image with CID.
func (p MailBuilder) AddOtherPart(
b []byte,
contentType string,
fileName string,
contentID string,
) MailBuilder {
part := NewPart(contentType)
part.Content = b
part.FileName = fileName
part.ContentID = contentID
p.inlines = append(p.inlines, part)
return p
}
// AddFileOtherPart returns a copy of MailBuilder that includes the specified other part.
// Filename and contentID will be populated from the base name of path.
// Content type will be detected from the path extension.
func (p MailBuilder) AddFileOtherPart(path string) MailBuilder {
// Only allow first p.err value
if p.err != nil {
return p
}
b, err := ioutil.ReadFile(path)
if err != nil {
p.err = err
return p
}
name := filepath.Base(path)
ctype := mime.TypeByExtension(filepath.Ext(name))
return p.AddOtherPart(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
// | |- other parts..
// | `- 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/other 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)
}

94
vendor/github.com/jhillyerd/enmime/detect.go generated vendored Normal file
View File

@@ -0,0 +1,94 @@
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, multipartWOBoundaryAsSinglepart bool) bool {
// Parse top-level multipart
ctype := root.Header.Get(hnContentType)
mtype, params, _, err := mediatype.Parse(ctype)
if err != nil {
return false
}
if boundary := params[hpBoundary]; multipartWOBoundaryAsSinglepart && boundary == "" {
return false
}
// According to rfc2046#section-5.1.7 all other multipart should
// be treated as multipart/mixed
return 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
}

238
vendor/github.com/jhillyerd/enmime/encode.go generated vendored Normal file
View File

@@ -0,0 +1,238 @@
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)
if err := p.encodeHeader(b); err != nil {
return err
}
if len(p.Content) > 0 {
if _, err := b.Write(crnl); err != nil {
return err
}
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 {
if _, err := b.Write(marker); err != nil {
return err
}
if _, err := b.Write(crnl); err != nil {
return err
}
if err := c.Encode(b); err != nil {
return err
}
c = c.NextSibling
}
if _, err := b.Write(endMarker); err != nil {
return err
}
if _, err := b.Write(crnl); err != nil {
return err
}
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) error {
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] = ' '
if _, err := b.Write(wb); err != nil {
return err
}
}
}
return nil
}
// 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
}
if _, err := b.Write(crnl); err != nil {
return err
}
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
}
}

57
vendor/github.com/jhillyerd/enmime/enmime.go generated vendored Normal file
View File

@@ -0,0 +1,57 @@
// 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"

345
vendor/github.com/jhillyerd/enmime/envelope.go generated vendored Normal file
View File

@@ -0,0 +1,345 @@
package enmime
import (
"fmt"
"io"
"mime"
"net/mail"
"net/textproto"
"strings"
"time"
"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)]
values := make([]string, 0, len(rawValues))
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)
}
return ParseAddressList(e.header.Get(key))
}
// Date parses the Date header field.
func (e *Envelope) Date() (time.Time, error) {
hdr := e.GetHeader("Date")
if hdr == "" {
return time.Time{}, mail.ErrHeaderNotPresent
}
return mail.ParseDate(hdr)
}
// 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.
// Uses default parser.
func ReadEnvelope(r io.Reader) (*Envelope, error) {
return defaultParser.ReadEnvelope(r)
}
// ReadEnvelope is the same as ReadEnvelope, but respects parser configurations.
func (p Parser) ReadEnvelope(r io.Reader) (*Envelope, error) {
// Read MIME parts from reader
root, err := p.ReadParts(r)
if err != nil {
return nil, errors.WithMessage(err, "Failed to ReadParts")
}
return p.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) {
return defaultParser.EnvelopeFromPart(root)
}
// EnvelopeFromPart is the same as EnvelopeFromPart, but respects parser configurations.
func (p Parser) EnvelopeFromPart(root *Part) (*Envelope, error) {
e := &Envelope{
Root: root,
header: &root.Header,
}
if detectMultipartMessage(root, p.multipartWOBoundaryAsSinglePart) {
// 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
}

77
vendor/github.com/jhillyerd/enmime/error.go generated vendored Normal file
View File

@@ -0,0 +1,77 @@
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"
// ErrorMalformedChildPart name.
ErrorMalformedChildPart = "Malformed child part"
)
// MaxPartErrors limits number of part parsing errors, errors after the limit are ignored. 0 means unlimited.
var MaxPartErrors = 0
// 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.addProblem(&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.addProblem(&Error{
name,
fmt.Sprintf(detailFmt, args...),
false,
})
}
// addProblem adds general *Error to the Part error slice.
func (p *Part) addProblem(err *Error) {
if (MaxPartErrors == 0) || (len(p.Errors) < MaxPartErrors) {
p.Errors = append(p.Errors, err)
}
}

242
vendor/github.com/jhillyerd/enmime/header.go generated vendored Normal file
View File

@@ -0,0 +1,242 @@
package enmime
import (
"bufio"
"bytes"
"fmt"
"mime"
"net/mail"
"net/textproto"
"strings"
"github.com/jhillyerd/enmime/internal/coding"
"github.com/jhillyerd/enmime/internal/stringutil"
"github.com/jhillyerd/enmime/mediatype"
"github.com/pkg/errors"
)
const (
// Standard MIME content dispositions
cdAttachment = "attachment"
cdInline = "inline"
// Standard MIME content types
ctAppOctetStream = "application/octet-stream"
ctMultipartAltern = "multipart/alternative"
ctMultipartMixed = "multipart/mixed"
ctMultipartPrefix = "multipart/"
ctMultipartRelated = "multipart/related"
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,
}
// ParseAddressList returns a mail.Address slice with RFC 2047 encoded names converted to UTF-8.
// It is more tolerant of malformed headers than the ParseAddressList func provided in Go's net/mail
// package.
func ParseAddressList(list string) ([]*mail.Address, error) {
parser := mail.AddressParser{WordDecoder: coding.NewExtMimeDecoder()}
ret, err := parser.ParseList(list)
if err != nil {
switch err.Error() {
case "mail: expected comma":
// Attempt to add commas and parse again.
return parser.ParseList(stringutil.EnsureCommaDelimitedAddresses(list))
case "mail: no address":
return nil, mail.ErrHeaderNotPresent
}
return nil, err
}
for i := range ret {
// try to additionally decode with less strict decoder
ret[i].Name = coding.DecodeExtHeader(ret[i].Name)
ret[i].Address = coding.DecodeExtHeader(ret[i].Address)
}
return ret, nil
}
// 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'
}

74
vendor/github.com/jhillyerd/enmime/inspect.go generated vendored Normal file
View File

@@ -0,0 +1,74 @@
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
}

View File

@@ -0,0 +1,64 @@
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
}

View File

@@ -0,0 +1,339 @@
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)<meta.*charset="?\s*(?P<charset>[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 ""
}

View File

@@ -0,0 +1,135 @@
package coding
import (
"fmt"
"io"
"mime"
"strings"
)
// NewExtMimeDecoder creates new MIME word decoder which allows decoding of additional charsets.
func NewExtMimeDecoder() *mime.WordDecoder {
return &mime.WordDecoder{
CharsetReader: NewCharsetReader,
}
}
// 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
}
header, err := NewExtMimeDecoder().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()
}

View File

@@ -0,0 +1,26 @@
package coding
import (
"net/url"
"strings"
)
// FromIDHeader decodes a Content-ID or Message-ID header value (RFC 2392) into a utf-8 string.
// Example: "<foo%3fbar+baz>" 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) + ">"
}

View File

@@ -0,0 +1,161 @@
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++
}
// safeWriteByte outputs a single byte, storing overflow for next read. Updates counters.
safeWriteByte := func(in byte) {
if n < destLen {
dest[n] = in
n++
} else {
qp.overflow = append(qp.overflow, in)
}
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) {
safeWriteByte(b)
} 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])
}

View File

@@ -0,0 +1,88 @@
package stringutil
import (
"bytes"
"net/mail"
"strings"
)
// 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()
}
// EnsureCommaDelimitedAddresses is 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 i, 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 == ';' {
inDomain = false
if i == len(s)-1 {
// omit trailing semicolon
continue
}
sb.WriteRune(',')
continue
}
if r == ',' {
inDomain = false
sb.WriteRune(r)
continue
}
if r == ' ' {
inDomain = false
sb.WriteRune(',')
sb.WriteRune(r)
continue
}
}
}
sb.WriteRune(r)
}
return sb.String()
}

View File

@@ -0,0 +1,39 @@
package stringutil
// FindUnquoted returns the indexes of the instance of v in s, or empty slice if v is not present in s.
// It ignores v present inside quoted runs.
func FindUnquoted(s string, v rune, quote rune) []int {
escaped := false
quoted := false
indexes := make([]int, 0)
quotedIndexes := make([]int, 0)
for i := 0; i < len(s); i++ {
switch rune(s[i]) {
case escape:
escaped = !escaped // escape can escape itself.
case quote:
if escaped {
escaped = false
continue
}
quoted = !quoted
if !quoted {
quotedIndexes = quotedIndexes[:0] // drop possible indices inside quoted segment
}
case v:
escaped = false
if quoted {
quotedIndexes = append(quotedIndexes, i)
} else {
indexes = append(indexes, i)
}
default:
escaped = false
}
}
return append(indexes, quotedIndexes...)
}

View File

@@ -0,0 +1,45 @@
package stringutil
const escape = '\\'
// SplitUnquoted slices s into all substrings separated by sep and returns a slice of
// the substrings between those separators.
//
// If s does not contain sep and sep is not empty, SplitUnquoted returns a
// slice of length 1 whose only element is s.
//
// It ignores sep present inside quoted runs.
func SplitUnquoted(s string, sep rune, quote rune) []string {
return splitUnquoted(s, sep, quote, false)
}
// SplitAfterUnquoted slices s into all substrings after each instance of sep and
// returns a slice of those substrings.
//
// If s does not contain sep and sep is not empty, SplitAfterUnquoted returns
// a slice of length 1 whose only element is s.
//
// It ignores sep present inside quoted runs.
func SplitAfterUnquoted(s string, sep rune, quote rune) []string {
return splitUnquoted(s, sep, quote, true)
}
func splitUnquoted(s string, sep rune, quote rune, preserveSep bool) []string {
ixs := FindUnquoted(s, sep, quote)
if len(ixs) == 0 {
return []string{s}
}
start := 0
result := make([]string, 0, len(ixs)+1)
for _, ix := range ixs {
end := ix
if preserveSep {
end++
}
result = append(result, s[start:end])
start = ix + 1
}
return append(result, s[start:])
}

View File

@@ -0,0 +1,24 @@
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:])
}

View File

@@ -0,0 +1,36 @@
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:]...)
}

107
vendor/github.com/jhillyerd/enmime/match.go generated vendored Normal file
View File

@@ -0,0 +1,107 @@
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
}
}
}

View File

@@ -0,0 +1,513 @@
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"
ctMultipartMixed = "multipart/mixed"
ctMultipartPrefix = "multipart/"
ctTextPrefix = "text/"
ctTextPlain = "text/plain"
// 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(removeTrailingHTMLTags(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.SplitUnquoted(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 := r == '\\'
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 := stringutil.SplitAfterUnquoted(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
}
// removeTrailingHTMLTags removes an unexpected HTML tags at the end of media type.
func removeTrailingHTMLTags(value string) string {
tagStart := 0
closeTags := 0
loop:
for i := len(value) - 1; i > 0; i-- {
c := value[i]
switch c {
case '"':
if closeTags == 0 { // quotes started outside the tag, aborting
break loop
}
case '>':
closeTags++
case '<':
closeTags--
if closeTags == 0 {
tagStart = i
}
}
}
if tagStart != 0 {
return value[0:tagStart]
}
return value
}

29
vendor/github.com/jhillyerd/enmime/options.go generated vendored Normal file
View File

@@ -0,0 +1,29 @@
package enmime
// Option to configure parsing.
type Option interface {
apply(p *Parser)
}
// SkipMalformedParts sets parsing to skip parts that's can't be parsed.
func SkipMalformedParts(s bool) Option {
return skipMalformedPartsOption(s)
}
type skipMalformedPartsOption bool
func (o skipMalformedPartsOption) apply(p *Parser) {
p.skipMalformedParts = bool(o)
}
// MultipartWOBoundaryAsSinglePart if set to true will treat a multi-part messages without boundary parameter as single-part.
// Otherwise, will return error that boundary is not found.
func MultipartWOBoundaryAsSinglePart(a bool) Option {
return multipartWOBoundaryAsSinglePartOption(a)
}
type multipartWOBoundaryAsSinglePartOption bool
func (o multipartWOBoundaryAsSinglePartOption) apply(p *Parser) {
p.multipartWOBoundaryAsSinglePart = bool(o)
}

22
vendor/github.com/jhillyerd/enmime/parser.go generated vendored Normal file
View File

@@ -0,0 +1,22 @@
package enmime
// Parser parses MIME.
// Default parser is a valid one.
type Parser struct {
skipMalformedParts bool
multipartWOBoundaryAsSinglePart bool
}
// defaultParser is a Parser with default configuration.
var defaultParser = Parser{}
// NewParser creates new parser with given options.
func NewParser(ops ...Option) *Parser {
p := Parser{}
for _, o := range ops {
o.apply(&p)
}
return &p
}

445
vendor/github.com/jhillyerd/enmime/part.go generated vendored Normal file
View File

@@ -0,0 +1,445 @@
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.addError(ErrorMalformedBase64, err.Error())
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) {
return defaultParser.ReadParts(r)
}
// ReadParts reads a MIME document from the provided reader and parses it into tree of Part objects.
func (p Parser) 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 detectMultipartMessage(root, p.multipartWOBoundaryAsSinglePart) {
// Content is multipart, parse it.
err = parseParts(root, br, p.skipMalformedParts)
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, skipMalformedParts bool) 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 {
if skipMalformedParts {
parent.addError(ErrorMalformedChildPart, "read header: %s", err.Error())
continue
}
return err
}
// Insert this Part into the MIME tree.
if p.Boundary == "" {
// Content is text or data, decode it.
if err = p.decodeContent(bbr); err != nil {
if skipMalformedParts {
parent.addError(ErrorMalformedChildPart, "decode content: %s", err.Error())
continue
}
return err
}
parent.AddChild(p)
continue
}
parent.AddChild(p)
// Content is another multipart.
if err = parseParts(p, bbr, skipMalformedParts); err != nil {
if skipMalformedParts {
parent.addError(ErrorMalformedChildPart, "parse parts: %s", err.Error())
continue
}
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
}

33
vendor/github.com/jhillyerd/enmime/sender.go generated vendored Normal file
View File

@@ -0,0 +1,33 @@
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)
}

10
vendor/github.com/jhillyerd/enmime/shell.nix generated vendored Normal file
View File

@@ -0,0 +1,10 @@
with import <nixpkgs> {};
stdenv.mkDerivation rec {
name = "env";
env = buildEnv { name = name; paths = buildInputs; };
buildInputs = [
go
golint
];
hardeningDisable = [ "fortify" ];
}