add vendoring
This commit is contained in:
24
vendor/github.com/jhillyerd/enmime/.gitattributes
generated
vendored
Normal file
24
vendor/github.com/jhillyerd/enmime/.gitattributes
generated
vendored
Normal 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
31
vendor/github.com/jhillyerd/enmime/.gitignore
generated
vendored
Normal 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
23
vendor/github.com/jhillyerd/enmime/.golangci.yml
generated
vendored
Normal 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
293
vendor/github.com/jhillyerd/enmime/CHANGELOG.md
generated
vendored
Normal 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
37
vendor/github.com/jhillyerd/enmime/CONTRIBUTING.md
generated
vendored
Normal 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
20
vendor/github.com/jhillyerd/enmime/LICENSE
generated
vendored
Normal 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
34
vendor/github.com/jhillyerd/enmime/Makefile
generated
vendored
Normal 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
42
vendor/github.com/jhillyerd/enmime/README.md
generated
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
# enmime
|
||||
[][Pkg Docs]
|
||||
[][Build Status]
|
||||
[][Go Report Card]
|
||||
[][Coverage Status]
|
||||
|
||||
|
||||
enmime is a MIME encoding and decoding library for Go, focused on generating and
|
||||
parsing MIME encoded emails. It is being developed in tandem with the
|
||||
[Inbucket] email service.
|
||||
|
||||
enmime includes a fluent interface builder for generating MIME encoded messages,
|
||||
see the wiki for example [Builder Usage].
|
||||
|
||||
See our [Pkg Docs] for examples and API usage information.
|
||||
|
||||
|
||||
## Development Status
|
||||
|
||||
enmime is near production quality: it works but struggles to parse a small
|
||||
percentage of emails. It's possible the API will evolve slightly before the 1.0
|
||||
release.
|
||||
|
||||
See [CONTRIBUTING.md] for more information.
|
||||
|
||||
|
||||
## About
|
||||
|
||||
enmime is written in [Go][Golang].
|
||||
|
||||
enmime is open source software released under the MIT License. The latest
|
||||
version can be found at https://github.com/jhillyerd/enmime
|
||||
|
||||
|
||||
[Build Status]: https://travis-ci.org/jhillyerd/enmime
|
||||
[Builder Usage]: https://github.com/jhillyerd/enmime/wiki/Builder-Usage
|
||||
[Coverage Status]: https://coveralls.io/github/jhillyerd/enmime
|
||||
[CONTRIBUTING.md]: https://github.com/jhillyerd/enmime/blob/master/CONTRIBUTING.md
|
||||
[Inbucket]: http://www.inbucket.org/
|
||||
[Golang]: http://golang.org/
|
||||
[Go Report Card]: https://goreportcard.com/report/github.com/jhillyerd/enmime
|
||||
[Pkg Docs]: https://pkg.go.dev/github.com/jhillyerd/enmime
|
||||
253
vendor/github.com/jhillyerd/enmime/boundary.go
generated
vendored
Normal file
253
vendor/github.com/jhillyerd/enmime/boundary.go
generated
vendored
Normal 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
371
vendor/github.com/jhillyerd/enmime/builder.go
generated
vendored
Normal 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
94
vendor/github.com/jhillyerd/enmime/detect.go
generated
vendored
Normal 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
238
vendor/github.com/jhillyerd/enmime/encode.go
generated
vendored
Normal 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
57
vendor/github.com/jhillyerd/enmime/enmime.go
generated
vendored
Normal 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
345
vendor/github.com/jhillyerd/enmime/envelope.go
generated
vendored
Normal 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
77
vendor/github.com/jhillyerd/enmime/error.go
generated
vendored
Normal 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
242
vendor/github.com/jhillyerd/enmime/header.go
generated
vendored
Normal 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
74
vendor/github.com/jhillyerd/enmime/inspect.go
generated
vendored
Normal 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
|
||||
}
|
||||
64
vendor/github.com/jhillyerd/enmime/internal/coding/base64.go
generated
vendored
Normal file
64
vendor/github.com/jhillyerd/enmime/internal/coding/base64.go
generated
vendored
Normal 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
|
||||
}
|
||||
339
vendor/github.com/jhillyerd/enmime/internal/coding/charsets.go
generated
vendored
Normal file
339
vendor/github.com/jhillyerd/enmime/internal/coding/charsets.go
generated
vendored
Normal 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 ""
|
||||
}
|
||||
135
vendor/github.com/jhillyerd/enmime/internal/coding/headerext.go
generated
vendored
Normal file
135
vendor/github.com/jhillyerd/enmime/internal/coding/headerext.go
generated
vendored
Normal 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()
|
||||
}
|
||||
26
vendor/github.com/jhillyerd/enmime/internal/coding/idheader.go
generated
vendored
Normal file
26
vendor/github.com/jhillyerd/enmime/internal/coding/idheader.go
generated
vendored
Normal 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) + ">"
|
||||
}
|
||||
161
vendor/github.com/jhillyerd/enmime/internal/coding/quotedprint.go
generated
vendored
Normal file
161
vendor/github.com/jhillyerd/enmime/internal/coding/quotedprint.go
generated
vendored
Normal 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])
|
||||
}
|
||||
88
vendor/github.com/jhillyerd/enmime/internal/stringutil/addr.go
generated
vendored
Normal file
88
vendor/github.com/jhillyerd/enmime/internal/stringutil/addr.go
generated
vendored
Normal 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()
|
||||
}
|
||||
39
vendor/github.com/jhillyerd/enmime/internal/stringutil/find.go
generated
vendored
Normal file
39
vendor/github.com/jhillyerd/enmime/internal/stringutil/find.go
generated
vendored
Normal 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...)
|
||||
}
|
||||
45
vendor/github.com/jhillyerd/enmime/internal/stringutil/split.go
generated
vendored
Normal file
45
vendor/github.com/jhillyerd/enmime/internal/stringutil/split.go
generated
vendored
Normal 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:])
|
||||
}
|
||||
24
vendor/github.com/jhillyerd/enmime/internal/stringutil/uuid.go
generated
vendored
Normal file
24
vendor/github.com/jhillyerd/enmime/internal/stringutil/uuid.go
generated
vendored
Normal 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:])
|
||||
}
|
||||
36
vendor/github.com/jhillyerd/enmime/internal/stringutil/wrap.go
generated
vendored
Normal file
36
vendor/github.com/jhillyerd/enmime/internal/stringutil/wrap.go
generated
vendored
Normal 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
107
vendor/github.com/jhillyerd/enmime/match.go
generated
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
513
vendor/github.com/jhillyerd/enmime/mediatype/mediatype.go
generated
vendored
Normal file
513
vendor/github.com/jhillyerd/enmime/mediatype/mediatype.go
generated
vendored
Normal 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
29
vendor/github.com/jhillyerd/enmime/options.go
generated
vendored
Normal 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
22
vendor/github.com/jhillyerd/enmime/parser.go
generated
vendored
Normal 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
445
vendor/github.com/jhillyerd/enmime/part.go
generated
vendored
Normal 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
33
vendor/github.com/jhillyerd/enmime/sender.go
generated
vendored
Normal 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
10
vendor/github.com/jhillyerd/enmime/shell.nix
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
with import <nixpkgs> {};
|
||||
stdenv.mkDerivation rec {
|
||||
name = "env";
|
||||
env = buildEnv { name = name; paths = buildInputs; };
|
||||
buildInputs = [
|
||||
go
|
||||
golint
|
||||
];
|
||||
hardeningDisable = [ "fortify" ];
|
||||
}
|
||||
Reference in New Issue
Block a user