upgrade deps; rewrite smtp session
This commit is contained in:
1
vendor/github.com/jhillyerd/enmime/.envrc
generated
vendored
Normal file
1
vendor/github.com/jhillyerd/enmime/.envrc
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
use flake
|
||||
2
vendor/github.com/jhillyerd/enmime/.gitignore
generated
vendored
2
vendor/github.com/jhillyerd/enmime/.gitignore
generated
vendored
@@ -27,5 +27,7 @@ _testmain.go
|
||||
# vim swp files
|
||||
*.swp
|
||||
|
||||
/.direnv
|
||||
|
||||
cmd/mime-dump/mime-dump
|
||||
cmd/mime-extractor/mime-extractor
|
||||
|
||||
293
vendor/github.com/jhillyerd/enmime/CHANGELOG.md
generated
vendored
293
vendor/github.com/jhillyerd/enmime/CHANGELOG.md
generated
vendored
@@ -1,293 +0,0 @@
|
||||
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.
|
||||
2
vendor/github.com/jhillyerd/enmime/CONTRIBUTING.md
generated
vendored
2
vendor/github.com/jhillyerd/enmime/CONTRIBUTING.md
generated
vendored
@@ -22,7 +22,7 @@ to provide validation and/or guidance on your suggested approach.
|
||||
|
||||
## Making Changes
|
||||
|
||||
Create a topic branch based on our `master` branch.
|
||||
Create a topic branch based on our `main` branch.
|
||||
|
||||
1. Make commits of logical units.
|
||||
2. Add unit tests to exercise your changes.
|
||||
|
||||
6
vendor/github.com/jhillyerd/enmime/README.md
generated
vendored
6
vendor/github.com/jhillyerd/enmime/README.md
generated
vendored
@@ -1,8 +1,8 @@
|
||||
# enmime
|
||||
[][Pkg Docs]
|
||||
[][Build Status]
|
||||
[](https://github.com/jhillyerd/enmime/actions/workflows/build-and-test.yml)
|
||||
[][Go Report Card]
|
||||
[][Coverage Status]
|
||||
[][Coverage Status]
|
||||
|
||||
|
||||
enmime is a MIME encoding and decoding library for Go, focused on generating and
|
||||
@@ -35,7 +35,7 @@ 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
|
||||
[CONTRIBUTING.md]: https://github.com/jhillyerd/enmime/blob/main/CONTRIBUTING.md
|
||||
[Inbucket]: http://www.inbucket.org/
|
||||
[Golang]: http://golang.org/
|
||||
[Go Report Card]: https://goreportcard.com/report/github.com/jhillyerd/enmime
|
||||
|
||||
45
vendor/github.com/jhillyerd/enmime/boundary.go
generated
vendored
45
vendor/github.com/jhillyerd/enmime/boundary.go
generated
vendored
@@ -5,7 +5,6 @@ import (
|
||||
"bytes"
|
||||
stderrors "errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"unicode"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -44,31 +43,31 @@ func newBoundaryReader(reader *bufio.Reader, boundary string) *boundaryReader {
|
||||
|
||||
// Read returns a buffer containing the content up until boundary
|
||||
//
|
||||
// Excerpt from io package on io.Reader implementations:
|
||||
// Excerpt from io package on io.Reader implementations:
|
||||
//
|
||||
// type Reader interface {
|
||||
// Read(p []byte) (n int, err error)
|
||||
// }
|
||||
// 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.
|
||||
// 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.
|
||||
// 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.
|
||||
// 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.
|
||||
@@ -178,7 +177,7 @@ func (b *boundaryReader) Next() (bool, error) {
|
||||
}
|
||||
if b.partsRead > 0 {
|
||||
// Exhaust the current part to prevent errors when moving to the next part.
|
||||
_, _ = io.Copy(ioutil.Discard, b)
|
||||
_, _ = io.Copy(io.Discard, b)
|
||||
}
|
||||
for {
|
||||
var line []byte = nil
|
||||
|
||||
115
vendor/github.com/jhillyerd/enmime/builder.go
generated
vendored
115
vendor/github.com/jhillyerd/enmime/builder.go
generated
vendored
@@ -3,10 +3,12 @@ package enmime
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"math/rand"
|
||||
"mime"
|
||||
"net/mail"
|
||||
"net/textproto"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"time"
|
||||
@@ -21,13 +23,14 @@ import (
|
||||
type MailBuilder struct {
|
||||
to, cc, bcc []mail.Address
|
||||
from mail.Address
|
||||
replyTo mail.Address
|
||||
replyTo []mail.Address
|
||||
subject string
|
||||
date time.Time
|
||||
header textproto.MIMEHeader
|
||||
text, html []byte
|
||||
inlines, attachments []*Part
|
||||
err error
|
||||
randSource rand.Source
|
||||
}
|
||||
|
||||
// Builder returns an empty MailBuilder struct.
|
||||
@@ -35,6 +38,12 @@ func Builder() MailBuilder {
|
||||
return MailBuilder{}
|
||||
}
|
||||
|
||||
// RandSeed sets the seed for random uuid boundary strings.
|
||||
func (p MailBuilder) RandSeed(seed int64) MailBuilder {
|
||||
p.randSource = stringutil.NewLockedSource(seed)
|
||||
return p
|
||||
}
|
||||
|
||||
// Error returns the stored error from a file attachment/inline read or nil.
|
||||
func (p MailBuilder) Error() error {
|
||||
return p.err
|
||||
@@ -46,18 +55,33 @@ func (p MailBuilder) Date(date time.Time) MailBuilder {
|
||||
return p
|
||||
}
|
||||
|
||||
// GetDate returns the stored date.
|
||||
func (p *MailBuilder) GetDate() time.Time {
|
||||
return p.date
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// GetFrom returns the stored from header.
|
||||
func (p *MailBuilder) GetFrom() mail.Address {
|
||||
return p.from
|
||||
}
|
||||
|
||||
// Subject returns a copy of MailBuilder with the specified Subject header.
|
||||
func (p MailBuilder) Subject(subject string) MailBuilder {
|
||||
p.subject = subject
|
||||
return p
|
||||
}
|
||||
|
||||
// GetSubject returns the stored subject header.
|
||||
func (p *MailBuilder) GetSubject() string {
|
||||
return p.subject
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -73,6 +97,13 @@ func (p MailBuilder) ToAddrs(to []mail.Address) MailBuilder {
|
||||
return p
|
||||
}
|
||||
|
||||
// GetTo returns a copy of the stored to addresses.
|
||||
func (p *MailBuilder) GetTo() []mail.Address {
|
||||
var to []mail.Address
|
||||
to = append(to, p.to...)
|
||||
return to
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -88,6 +119,13 @@ func (p MailBuilder) CCAddrs(cc []mail.Address) MailBuilder {
|
||||
return p
|
||||
}
|
||||
|
||||
// GetCC returns a copy of the stored cc addresses.
|
||||
func (p *MailBuilder) GetCC() []mail.Address {
|
||||
var cc []mail.Address
|
||||
cc = append(cc, p.cc...)
|
||||
return cc
|
||||
}
|
||||
|
||||
// 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().
|
||||
@@ -106,13 +144,38 @@ func (p MailBuilder) BCCAddrs(bcc []mail.Address) MailBuilder {
|
||||
return p
|
||||
}
|
||||
|
||||
// GetBCC returns a copy of the stored bcc addresses.
|
||||
func (p *MailBuilder) GetBCC() []mail.Address {
|
||||
var bcc []mail.Address
|
||||
bcc = append(bcc, p.bcc...)
|
||||
return bcc
|
||||
}
|
||||
|
||||
// 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}
|
||||
if len(addr) > 0 {
|
||||
p.replyTo = append(p.replyTo, mail.Address{Name: name, Address: addr})
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// ReplyToAddrs returns a copy of MailBuilder with the new reply to header 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) ReplyToAddrs(replyTo []mail.Address) MailBuilder {
|
||||
p.replyTo = replyTo
|
||||
return p
|
||||
}
|
||||
|
||||
// GetReplyTo returns a copy of the stored replyTo header addresses.
|
||||
func (p *MailBuilder) GetReplyTo() []mail.Address {
|
||||
replyTo := make([]mail.Address, len(p.replyTo))
|
||||
copy(replyTo, p.replyTo)
|
||||
|
||||
return replyTo
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -125,18 +188,37 @@ func (p MailBuilder) Header(name, value string) MailBuilder {
|
||||
return p
|
||||
}
|
||||
|
||||
// GetHeader gets the first value associated with the given header.
|
||||
func (p *MailBuilder) GetHeader(name string) string {
|
||||
return p.header.Get(name)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// GetText returns a copy of the stored text/plain part.
|
||||
func (p *MailBuilder) GetText() []byte {
|
||||
var text []byte
|
||||
text = append(text, p.text...)
|
||||
return text
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// GetHTML returns a copy of the stored text/html part.
|
||||
func (p *MailBuilder) GetHTML() []byte {
|
||||
var html []byte
|
||||
html = append(html, p.html...)
|
||||
return html
|
||||
}
|
||||
|
||||
// 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)
|
||||
@@ -147,6 +229,16 @@ func (p MailBuilder) AddAttachment(b []byte, contentType string, fileName string
|
||||
return p
|
||||
}
|
||||
|
||||
// AddAttachmentWithReader returns a copy of MailBuilder that includes the specified attachment, using an io.Reader to pull the content of the attachment.
|
||||
func (p MailBuilder) AddAttachmentWithReader(r io.Reader, contentType string, fileName string) MailBuilder {
|
||||
part := NewPart(contentType)
|
||||
part.ContentReader = r
|
||||
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.
|
||||
@@ -155,7 +247,7 @@ func (p MailBuilder) AddFileAttachment(path string) MailBuilder {
|
||||
if p.err != nil {
|
||||
return p
|
||||
}
|
||||
b, err := ioutil.ReadFile(path)
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
p.err = err
|
||||
return p
|
||||
@@ -190,7 +282,7 @@ func (p MailBuilder) AddFileInline(path string) MailBuilder {
|
||||
if p.err != nil {
|
||||
return p
|
||||
}
|
||||
b, err := ioutil.ReadFile(path)
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
p.err = err
|
||||
return p
|
||||
@@ -225,7 +317,7 @@ func (p MailBuilder) AddFileOtherPart(path string) MailBuilder {
|
||||
if p.err != nil {
|
||||
return p
|
||||
}
|
||||
b, err := ioutil.ReadFile(path)
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
p.err = err
|
||||
return p
|
||||
@@ -318,8 +410,8 @@ func (p MailBuilder) Build() (*Part, error) {
|
||||
if len(p.cc) > 0 {
|
||||
h.Set("Cc", stringutil.JoinAddress(p.cc))
|
||||
}
|
||||
if p.replyTo.Address != "" {
|
||||
h.Set("Reply-To", p.replyTo.String())
|
||||
if len(p.replyTo) > 0 {
|
||||
h.Set("Reply-To", stringutil.JoinAddress(p.replyTo))
|
||||
}
|
||||
date := p.date
|
||||
if date.IsZero() {
|
||||
@@ -331,6 +423,13 @@ func (p MailBuilder) Build() (*Part, error) {
|
||||
h.Add(k, s)
|
||||
}
|
||||
}
|
||||
if r := p.randSource; r != nil {
|
||||
// Traverse all parts, discard match result.
|
||||
_ = root.DepthMatchAll(func(part *Part) bool {
|
||||
part.randSource = r
|
||||
return false
|
||||
})
|
||||
}
|
||||
return root, nil
|
||||
}
|
||||
|
||||
|
||||
30
vendor/github.com/jhillyerd/enmime/detect.go
generated
vendored
30
vendor/github.com/jhillyerd/enmime/detect.go
generated
vendored
@@ -1,17 +1,16 @@
|
||||
package enmime
|
||||
|
||||
import (
|
||||
"net/textproto"
|
||||
"strings"
|
||||
|
||||
"github.com/jhillyerd/enmime/mediatype"
|
||||
inttp "github.com/jhillyerd/enmime/internal/textproto"
|
||||
)
|
||||
|
||||
// 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)
|
||||
mtype, params, _, err := root.parseMediaType(ctype)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -32,30 +31,30 @@ func detectMultipartMessage(root *Part, multipartWOBoundaryAsSinglepart bool) bo
|
||||
//
|
||||
// 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))
|
||||
// - Content-Disposition: attachment; filename="frog.jpg"
|
||||
// - Content-Disposition: inline; filename="frog.jpg"
|
||||
// - Content-Type: attachment; filename="frog.jpg"
|
||||
func detectAttachmentHeader(root *Part, header inttp.MIMEHeader) bool {
|
||||
mtype, params, _, _ := root.parseMediaType(header.Get(hnContentDisposition))
|
||||
if strings.ToLower(mtype) == cdAttachment ||
|
||||
(strings.ToLower(mtype) == cdInline && len(params) > 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
mtype, _, _, _ = mediatype.Parse(header.Get(hnContentType))
|
||||
mtype, _, _, _ = root.parseMediaType(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 {
|
||||
func detectTextHeader(root *Part, header inttp.MIMEHeader, emptyContentTypeIsText bool) bool {
|
||||
ctype := header.Get(hnContentType)
|
||||
if ctype == "" && emptyContentTypeIsText {
|
||||
return true
|
||||
}
|
||||
|
||||
if mtype, _, _, err := mediatype.Parse(ctype); err == nil {
|
||||
if mtype, _, _, err := root.parseMediaType(ctype); err == nil {
|
||||
switch mtype {
|
||||
case ctTextPlain, ctTextHTML:
|
||||
return true
|
||||
@@ -67,23 +66,24 @@ func detectTextHeader(header textproto.MIMEHeader, emptyContentTypeIsText bool)
|
||||
|
||||
// detectBinaryBody returns true if the mail header defines a binary body.
|
||||
func detectBinaryBody(root *Part) bool {
|
||||
if detectTextHeader(root.Header, true) {
|
||||
header := inttp.MIMEHeader(root.Header) // Use internal header methods.
|
||||
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))
|
||||
mtype, _, _, _ := root.parseMediaType(header.Get(hnContentDisposition))
|
||||
return strings.ToLower(mtype) == cdAttachment
|
||||
}
|
||||
|
||||
isBin := detectAttachmentHeader(root.Header)
|
||||
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, _, _, _ := root.parseMediaType(header.Get(hnContentType))
|
||||
mtype = strings.ToLower(mtype)
|
||||
if mtype != ctTextPlain && mtype != ctTextHTML {
|
||||
return true
|
||||
|
||||
99
vendor/github.com/jhillyerd/enmime/encode.go
generated
vendored
99
vendor/github.com/jhillyerd/enmime/encode.go
generated
vendored
@@ -24,6 +24,14 @@ const (
|
||||
te7Bit transferEncoding = iota
|
||||
teQuoted
|
||||
teBase64
|
||||
teRaw
|
||||
)
|
||||
|
||||
const (
|
||||
base64EncodedLineLen = 76
|
||||
base64DecodedLineLen = base64EncodedLineLen * 3 / 4 // this is ok since lineLen is divisible by 4
|
||||
linesPerChunk = 128
|
||||
readChunkSize = base64DecodedLineLen * linesPerChunk
|
||||
)
|
||||
|
||||
var crnl = []byte{'\r', '\n'}
|
||||
@@ -33,6 +41,15 @@ func (p *Part) Encode(writer io.Writer) error {
|
||||
if p.Header == nil {
|
||||
p.Header = make(textproto.MIMEHeader)
|
||||
}
|
||||
if p.ContentReader != nil {
|
||||
// read some data in order to check whether the content is empty
|
||||
p.Content = make([]byte, readChunkSize)
|
||||
n, err := p.ContentReader.Read(p.Content)
|
||||
if err != nil && err != io.EOF {
|
||||
return err
|
||||
}
|
||||
p.Content = p.Content[:n]
|
||||
}
|
||||
cte := p.setupMIMEHeaders()
|
||||
// Encode this part.
|
||||
b := bufio.NewWriter(writer)
|
||||
@@ -82,12 +99,14 @@ func (p *Part) setupMIMEHeaders() transferEncoding {
|
||||
|
||||
// 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)
|
||||
if p.parser != nil && !p.parser.rawContent {
|
||||
p.Header.Del(hnContentEncoding)
|
||||
}
|
||||
|
||||
cte := te7Bit
|
||||
if len(p.Content) > 0 {
|
||||
cte = teBase64
|
||||
if p.TextContent() {
|
||||
if p.TextContent() && p.ContentReader == nil {
|
||||
cte = selectTransferEncoding(p.Content, false)
|
||||
if p.Charset == "" {
|
||||
p.Charset = utf8
|
||||
@@ -104,7 +123,7 @@ func (p *Part) setupMIMEHeaders() transferEncoding {
|
||||
// Setup headers.
|
||||
if p.FirstChild != nil && p.Boundary == "" {
|
||||
// Multipart, generate random boundary marker.
|
||||
p.Boundary = "enmime-" + stringutil.UUID()
|
||||
p.Boundary = "enmime-" + stringutil.UUID(p.randSource)
|
||||
}
|
||||
if p.ContentID != "" {
|
||||
p.Header.Set(hnContentID, coding.ToIDHeader(p.ContentID))
|
||||
@@ -135,7 +154,7 @@ func (p *Part) setupMIMEHeaders() transferEncoding {
|
||||
param := make(map[string]string)
|
||||
setParamValue(param, hpFilename, fileName)
|
||||
if !p.FileModDate.IsZero() {
|
||||
setParamValue(param, hpModDate, p.FileModDate.Format(time.RFC822))
|
||||
setParamValue(param, hpModDate, p.FileModDate.UTC().Format(time.RFC822))
|
||||
}
|
||||
if mt := mime.FormatMediaType(p.Disposition, param); mt != "" {
|
||||
p.Disposition = mt
|
||||
@@ -151,15 +170,19 @@ func (p *Part) encodeHeader(b *bufio.Writer) error {
|
||||
for k := range p.Header {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
rawContent := p.parser != nil && p.parser.rawContent
|
||||
|
||||
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)
|
||||
if !rawContent {
|
||||
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")
|
||||
@@ -174,11 +197,19 @@ func (p *Part) encodeHeader(b *bufio.Writer) error {
|
||||
|
||||
// encodeContent writes out the content in the selected encoding.
|
||||
func (p *Part) encodeContent(b *bufio.Writer, cte transferEncoding) (err error) {
|
||||
if p.ContentReader != nil {
|
||||
return p.encodeContentFromReader(b)
|
||||
}
|
||||
|
||||
if p.parser != nil && p.parser.rawContent {
|
||||
cte = teRaw
|
||||
}
|
||||
|
||||
switch cte {
|
||||
case teBase64:
|
||||
enc := base64.StdEncoding
|
||||
text := make([]byte, enc.EncodedLen(len(p.Content)))
|
||||
base64.StdEncoding.Encode(text, p.Content)
|
||||
enc.Encode(text, p.Content)
|
||||
// Wrap lines.
|
||||
lineLen := 76
|
||||
for len(text) > 0 {
|
||||
@@ -205,6 +236,54 @@ func (p *Part) encodeContent(b *bufio.Writer, cte transferEncoding) (err error)
|
||||
return err
|
||||
}
|
||||
|
||||
// encodeContentFromReader writes out the content read from the reader using base64 encoding.
|
||||
func (p *Part) encodeContentFromReader(b *bufio.Writer) error {
|
||||
text := make([]byte, base64EncodedLineLen) // a single base64 encoded line
|
||||
enc := base64.StdEncoding
|
||||
|
||||
chunk := make([]byte, readChunkSize) // contains a whole number of lines
|
||||
copy(chunk, p.Content) // copy the data of the initial read that was issued by `Encode`
|
||||
n := len(p.Content)
|
||||
|
||||
for {
|
||||
// call read until we get a full chunk / error
|
||||
for n < len(chunk) {
|
||||
c, err := p.ContentReader.Read(chunk[n:])
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
n += c
|
||||
}
|
||||
|
||||
for i := 0; i < n; i += base64DecodedLineLen {
|
||||
size := n - i
|
||||
if size > base64DecodedLineLen {
|
||||
size = base64DecodedLineLen
|
||||
}
|
||||
|
||||
enc.Encode(text, chunk[i:i+size])
|
||||
if _, err := b.Write(text[:enc.EncodedLen(size)]); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := b.Write(crnl); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if n < len(chunk) {
|
||||
break
|
||||
}
|
||||
|
||||
n = 0
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// selectTransferEncoding scans content for non-ASCII characters and selects 'b' or 'q' encoding.
|
||||
func selectTransferEncoding(content []byte, quoteLineBreaks bool) transferEncoding {
|
||||
if len(content) == 0 {
|
||||
|
||||
10
vendor/github.com/jhillyerd/enmime/enmime.go
generated
vendored
10
vendor/github.com/jhillyerd/enmime/enmime.go
generated
vendored
@@ -2,13 +2,13 @@
|
||||
// included mime/multipart support where possible, but is geared towards parsing MIME encoded
|
||||
// emails.
|
||||
//
|
||||
// Overview
|
||||
// # 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
|
||||
// # 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
|
||||
@@ -22,7 +22,7 @@
|
||||
// DepthMatchFirst() methods to search the Part tree. BreadthMatchAll() and DepthMatchAll() will
|
||||
// collect all Parts matching your criteria.
|
||||
//
|
||||
// Envelope
|
||||
// # Envelope
|
||||
//
|
||||
// ReadEnvelope returns an Envelope struct. Behind the scenes a Part tree is constructed, and then
|
||||
// sorted into the correct fields of the Envelope.
|
||||
@@ -31,7 +31,7 @@
|
||||
// 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
|
||||
// # 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
|
||||
@@ -39,7 +39,7 @@
|
||||
// AddressList(key) will convert the specified address header into a slice of net/mail.Address
|
||||
// values.
|
||||
//
|
||||
// Errors
|
||||
// # 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
|
||||
|
||||
14
vendor/github.com/jhillyerd/enmime/envelope.go
generated
vendored
14
vendor/github.com/jhillyerd/enmime/envelope.go
generated
vendored
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
"github.com/jaytaylor/html2text"
|
||||
"github.com/jhillyerd/enmime/internal/coding"
|
||||
"github.com/jhillyerd/enmime/mediatype"
|
||||
inttp "github.com/jhillyerd/enmime/internal/textproto"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@@ -57,7 +57,7 @@ func (e *Envelope) GetHeaderValues(name string) []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
rawValues := (*e.header)[textproto.CanonicalMIMEHeaderKey(name)]
|
||||
rawValues := (*e.header)[inttp.CanonicalEmailMIMEHeaderKey(name)]
|
||||
values := make([]string, 0, len(rawValues))
|
||||
for _, v := range rawValues {
|
||||
values = append(values, coding.DecodeExtHeader(v))
|
||||
@@ -215,11 +215,7 @@ func (p Parser) EnvelopeFromPart(root *Part) (*Envelope, error) {
|
||||
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])
|
||||
}
|
||||
e.Errors = append(e.Errors, part.Errors...)
|
||||
return false
|
||||
})
|
||||
}
|
||||
@@ -234,7 +230,7 @@ func parseTextOnlyBody(root *Part, e *Envelope) error {
|
||||
var charset string
|
||||
var isHTML bool
|
||||
if ctype := root.Header.Get(hnContentType); ctype != "" {
|
||||
if mediatype, mparams, _, err := mediatype.Parse(ctype); err == nil {
|
||||
if mediatype, mparams, _, err := root.parseMediaType(ctype); err == nil {
|
||||
isHTML = (mediatype == ctTextHTML)
|
||||
if mparams[hpCharset] != "" {
|
||||
charset = mparams[hpCharset]
|
||||
@@ -273,7 +269,7 @@ func parseTextOnlyBody(root *Part, e *Envelope) error {
|
||||
func parseMultiPartBody(root *Part, e *Envelope) error {
|
||||
// Parse top-level multipart
|
||||
ctype := root.Header.Get(hnContentType)
|
||||
mediatype, params, _, err := mediatype.Parse(ctype)
|
||||
mediatype, params, _, err := root.parseMediaType(ctype)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse media type: %v", err)
|
||||
}
|
||||
|
||||
32
vendor/github.com/jhillyerd/enmime/error.go
generated
vendored
32
vendor/github.com/jhillyerd/enmime/error.go
generated
vendored
@@ -27,7 +27,10 @@ const (
|
||||
ErrorMalformedChildPart = "Malformed child part"
|
||||
)
|
||||
|
||||
// MaxPartErrors limits number of part parsing errors, errors after the limit are ignored. 0 means unlimited.
|
||||
// MaxPartErrors limits number of part parsing errors, errors after the limit are ignored.
|
||||
// 0 means unlimited.
|
||||
//
|
||||
// Deprecated: This limit may be set via the `MaxStoredPartErrors` Parser option.
|
||||
var MaxPartErrors = 0
|
||||
|
||||
// Error describes an error encountered while parsing.
|
||||
@@ -71,7 +74,32 @@ func (p *Part) addWarning(name string, detailFmt string, args ...interface{}) {
|
||||
|
||||
// addProblem adds general *Error to the Part error slice.
|
||||
func (p *Part) addProblem(err *Error) {
|
||||
if (MaxPartErrors == 0) || (len(p.Errors) < MaxPartErrors) {
|
||||
maxErrors := MaxPartErrors
|
||||
if p.parser != nil && p.parser.maxStoredPartErrors != nil {
|
||||
// Override global var.
|
||||
maxErrors = *p.parser.maxStoredPartErrors
|
||||
}
|
||||
|
||||
if (maxErrors == 0) || (len(p.Errors) < maxErrors) {
|
||||
p.Errors = append(p.Errors, err)
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorCollector is an interface for collecting errors and warnings during
|
||||
// parsing.
|
||||
type ErrorCollector interface {
|
||||
AddError(name string, detailFmt string, args ...any)
|
||||
AddWarning(name string, detailFmt string, args ...any)
|
||||
}
|
||||
|
||||
type partErrorCollector struct {
|
||||
part *Part
|
||||
}
|
||||
|
||||
func (p *partErrorCollector) AddError(name string, detailFmt string, args ...any) {
|
||||
p.part.addError(name, detailFmt, args...)
|
||||
}
|
||||
|
||||
func (p *partErrorCollector) AddWarning(name string, detailFmt string, args ...any) {
|
||||
p.part.addWarning(name, detailFmt, args...)
|
||||
}
|
||||
|
||||
61
vendor/github.com/jhillyerd/enmime/flake.lock
generated
vendored
Normal file
61
vendor/github.com/jhillyerd/enmime/flake.lock
generated
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1701680307,
|
||||
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1702272962,
|
||||
"narHash": "sha256-D+zHwkwPc6oYQ4G3A1HuadopqRwUY/JkMwHz1YF7j4Q=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e97b3e4186bcadf0ef1b6be22b8558eab1cdeb5d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
17
vendor/github.com/jhillyerd/enmime/flake.nix
generated
vendored
Normal file
17
vendor/github.com/jhillyerd/enmime/flake.nix
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem
|
||||
(system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in
|
||||
{
|
||||
devShell = pkgs.callPackage ./shell.nix { };
|
||||
}
|
||||
);
|
||||
}
|
||||
63
vendor/github.com/jhillyerd/enmime/header.go
generated
vendored
63
vendor/github.com/jhillyerd/enmime/header.go
generated
vendored
@@ -11,7 +11,9 @@ import (
|
||||
|
||||
"github.com/jhillyerd/enmime/internal/coding"
|
||||
"github.com/jhillyerd/enmime/internal/stringutil"
|
||||
inttp "github.com/jhillyerd/enmime/internal/textproto"
|
||||
"github.com/jhillyerd/enmime/mediatype"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@@ -108,22 +110,26 @@ func ParseAddressList(list string) ([]*mail.Address, error) {
|
||||
// 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
|
||||
// - Missing ';' between content-type and media parameters
|
||||
// - Repeating media parameters
|
||||
// - Unquoted values in media parameters containing 'tspecials' characters
|
||||
//
|
||||
// Deprecated: Use mediaType.Parse instead
|
||||
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) {
|
||||
// ReadHeader reads a block of SMTP or MIME headers and returns a
|
||||
// textproto.MIMEHeader. Header parse warnings & errors will be added to
|
||||
// ErrorCollector, io errors will be returned directly.
|
||||
func ReadHeader(r *bufio.Reader, p ErrorCollector) (textproto.MIMEHeader, error) {
|
||||
// buf holds the massaged output for textproto.Reader.ReadMIMEHeader()
|
||||
buf := &bytes.Buffer{}
|
||||
tp := textproto.NewReader(r)
|
||||
tp := inttp.NewReader(r)
|
||||
firstHeader := true
|
||||
line:
|
||||
for {
|
||||
// Pull out each line of the headers as a temporary slice s
|
||||
s, err := tp.ReadLineBytes()
|
||||
@@ -137,30 +143,41 @@ func readHeader(r *bufio.Reader, p *Part) (textproto.MIMEHeader, error) {
|
||||
if firstSpace == 0 {
|
||||
// Starts with space: continuation
|
||||
buf.WriteByte(' ')
|
||||
buf.Write(textproto.TrimBytes(s))
|
||||
buf.Write(inttp.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)
|
||||
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)
|
||||
firstColon = bytes.IndexByte(s, ':')
|
||||
}
|
||||
|
||||
s = textproto.TrimBytes(s)
|
||||
// Behavior change in net/textproto package in Golang 1.20: invalid characters
|
||||
// in header keys are no longer allowed; https://github.com/golang/go/issues/53188
|
||||
for _, c := range s[:firstColon] {
|
||||
if c != ' ' && !inttp.ValidEmailHeaderFieldByte(c) {
|
||||
p.AddError(
|
||||
ErrorMalformedHeader, "Header name %q contains invalid character %q", s, c)
|
||||
continue line
|
||||
}
|
||||
}
|
||||
|
||||
// Contains a colon, treat as a new header line
|
||||
if !firstHeader {
|
||||
// New Header line, end the previous
|
||||
buf.Write([]byte{'\r', '\n'})
|
||||
}
|
||||
|
||||
s = inttp.TrimBytes(s)
|
||||
buf.Write(s)
|
||||
firstHeader = false
|
||||
} else {
|
||||
@@ -169,7 +186,7 @@ func readHeader(r *bufio.Reader, p *Part) (textproto.MIMEHeader, error) {
|
||||
// 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)
|
||||
p.AddWarning(ErrorMalformedHeader, "Continued line %q was not indented", s)
|
||||
} else {
|
||||
// Empty line, finish header parsing
|
||||
buf.Write([]byte{'\r', '\n'})
|
||||
@@ -179,9 +196,15 @@ func readHeader(r *bufio.Reader, p *Part) (textproto.MIMEHeader, error) {
|
||||
}
|
||||
|
||||
buf.Write([]byte{'\r', '\n'})
|
||||
tr := textproto.NewReader(bufio.NewReader(buf))
|
||||
header, err := tr.ReadMIMEHeader()
|
||||
return header, errors.WithStack(err)
|
||||
tr := inttp.NewReader(bufio.NewReader(buf))
|
||||
header, err := tr.ReadEmailMIMEHeader()
|
||||
return textproto.MIMEHeader(header), errors.WithStack(err)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return ReadHeader(r, &partErrorCollector{p})
|
||||
}
|
||||
|
||||
// decodeToUTF8Base64Header decodes a MIME header per RFC 2047, reencoding to =?utf-8b?
|
||||
|
||||
20
vendor/github.com/jhillyerd/enmime/inspect.go
generated
vendored
20
vendor/github.com/jhillyerd/enmime/inspect.go
generated
vendored
@@ -4,9 +4,10 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"net/textproto"
|
||||
|
||||
"github.com/jhillyerd/enmime/internal/coding"
|
||||
"github.com/jhillyerd/enmime/internal/textproto"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@@ -20,12 +21,21 @@ var defaultHeadersList = []string{
|
||||
"Date",
|
||||
}
|
||||
|
||||
// DecodeRFC2047 decodes the given string according to RFC 2047 and returns the
|
||||
// decoded UTF-8 equivalent. If the input is not using RFC 2047 encoding, or the
|
||||
// charset is not recognized, it will return the input unmodified.
|
||||
func DecodeRFC2047(s string) string {
|
||||
return coding.RFC2047Decode(s)
|
||||
}
|
||||
|
||||
// DecodeHeaders returns a limited selection of mime headers for use by user agents
|
||||
// Default header list:
|
||||
// "Date", "Subject", "Sender", "From", "To", "CC" and "BCC"
|
||||
//
|
||||
// "Date", "Subject", "Sender", "From", "To", "CC" and "BCC"
|
||||
//
|
||||
// Additional headers provided will be formatted canonically:
|
||||
// h, err := enmime.DecodeHeaders(b, "content-type", "user-agent")
|
||||
//
|
||||
// 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)))
|
||||
@@ -40,10 +50,10 @@ func DecodeHeaders(b []byte, addtlHeaders ...string) (textproto.MIMEHeader, erro
|
||||
headerList = append(headerList, addtlHeaders...)
|
||||
res := map[string][]string{}
|
||||
for _, header := range headerList {
|
||||
h := textproto.CanonicalMIMEHeaderKey(header)
|
||||
h := textproto.CanonicalEmailMIMEHeaderKey(header)
|
||||
res[h] = make([]string, 0, len(headers[h]))
|
||||
for _, value := range headers[h] {
|
||||
res[h] = append(res[h], coding.RFC2047Decode(value))
|
||||
res[h] = append(res[h], DecodeRFC2047(value))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
3
vendor/github.com/jhillyerd/enmime/internal/coding/charsets.go
generated
vendored
3
vendor/github.com/jhillyerd/enmime/internal/coding/charsets.go
generated
vendored
@@ -4,7 +4,6 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
@@ -307,7 +306,7 @@ func ConvertToUTF8String(charset string, textBytes []byte) (string, error) {
|
||||
}
|
||||
input := bytes.NewReader(textBytes)
|
||||
reader := transform.NewReader(input, csentry.e.NewDecoder())
|
||||
output, err := ioutil.ReadAll(reader)
|
||||
output, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
14
vendor/github.com/jhillyerd/enmime/internal/coding/headerext.go
generated
vendored
14
vendor/github.com/jhillyerd/enmime/internal/coding/headerext.go
generated
vendored
@@ -54,20 +54,20 @@ func RFC2047Decode(s string) string {
|
||||
|
||||
default:
|
||||
if decoded {
|
||||
keyValuePair := strings.SplitAfter(s, "=")
|
||||
if len(keyValuePair) < 2 {
|
||||
key, value, found := strings.Cut(s, "=")
|
||||
if !found {
|
||||
return s
|
||||
}
|
||||
|
||||
// Add quotes as needed.
|
||||
if !strings.HasPrefix(keyValuePair[1], "\"") {
|
||||
keyValuePair[1] = fmt.Sprintf("\"%s", keyValuePair[1])
|
||||
if !strings.HasPrefix(value, "\"") {
|
||||
value = fmt.Sprintf("\"%s", value)
|
||||
}
|
||||
if !strings.HasSuffix(keyValuePair[1], "\"") {
|
||||
keyValuePair[1] = fmt.Sprintf("%s\"", keyValuePair[1])
|
||||
if !strings.HasSuffix(value, "\"") {
|
||||
value = fmt.Sprintf("%s\"", value)
|
||||
}
|
||||
|
||||
return strings.Join(keyValuePair, "")
|
||||
return fmt.Sprintf("%s=%s", key, value)
|
||||
}
|
||||
|
||||
return s
|
||||
|
||||
43
vendor/github.com/jhillyerd/enmime/internal/stringutil/rand_source.go
generated
vendored
Normal file
43
vendor/github.com/jhillyerd/enmime/internal/stringutil/rand_source.go
generated
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
package stringutil
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var globalRandSource rand.Source
|
||||
|
||||
func init() {
|
||||
globalRandSource = NewLockedSource(time.Now().UTC().UnixNano())
|
||||
}
|
||||
|
||||
// NewLockedSource creates a source of randomness using the given seed.
|
||||
func NewLockedSource(seed int64) rand.Source64 {
|
||||
return &lockedSource{
|
||||
s: rand.NewSource(seed).(rand.Source64),
|
||||
}
|
||||
}
|
||||
|
||||
type lockedSource struct {
|
||||
lock sync.Mutex
|
||||
s rand.Source64
|
||||
}
|
||||
|
||||
func (x *lockedSource) Int63() int64 {
|
||||
x.lock.Lock()
|
||||
defer x.lock.Unlock()
|
||||
return x.s.Int63()
|
||||
}
|
||||
|
||||
func (x *lockedSource) Uint64() uint64 {
|
||||
x.lock.Lock()
|
||||
defer x.lock.Unlock()
|
||||
return x.s.Uint64()
|
||||
}
|
||||
|
||||
func (x *lockedSource) Seed(seed int64) {
|
||||
x.lock.Lock()
|
||||
defer x.lock.Unlock()
|
||||
x.s.Seed(seed)
|
||||
}
|
||||
16
vendor/github.com/jhillyerd/enmime/internal/stringutil/uuid.go
generated
vendored
16
vendor/github.com/jhillyerd/enmime/internal/stringutil/uuid.go
generated
vendored
@@ -3,19 +3,15 @@ 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 generates a random UUID according to RFC 4122, using optional rand if supplied
|
||||
func UUID(rs rand.Source) string {
|
||||
uuid := make([]byte, 16)
|
||||
uuidMutex.Lock()
|
||||
_, _ = uuidRand.Read(uuid)
|
||||
uuidMutex.Unlock()
|
||||
if rs == nil {
|
||||
rs = globalRandSource
|
||||
}
|
||||
_, _ = rand.New(rs).Read(uuid)
|
||||
// variant bits; see section 4.1.1
|
||||
uuid[8] = uuid[8]&^0xc0 | 0x80
|
||||
// version 4 (pseudo-random); see section 4.1.3
|
||||
|
||||
56
vendor/github.com/jhillyerd/enmime/internal/textproto/header.go
generated
vendored
Normal file
56
vendor/github.com/jhillyerd/enmime/internal/textproto/header.go
generated
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package textproto
|
||||
|
||||
// A MIMEHeader represents a MIME-style header mapping
|
||||
// keys to sets of values.
|
||||
type MIMEHeader map[string][]string
|
||||
|
||||
// Add adds the key, value pair to the header.
|
||||
// It appends to any existing values associated with key.
|
||||
func (h MIMEHeader) Add(key, value string) {
|
||||
key = CanonicalEmailMIMEHeaderKey(key)
|
||||
h[key] = append(h[key], value)
|
||||
}
|
||||
|
||||
// Set sets the header entries associated with key to
|
||||
// the single element value. It replaces any existing
|
||||
// values associated with key.
|
||||
func (h MIMEHeader) Set(key, value string) {
|
||||
h[CanonicalEmailMIMEHeaderKey(key)] = []string{value}
|
||||
}
|
||||
|
||||
// Get gets the first value associated with the given key.
|
||||
// It is case insensitive; CanonicalMIMEHeaderKey is used
|
||||
// to canonicalize the provided key.
|
||||
// If there are no values associated with the key, Get returns "".
|
||||
// To use non-canonical keys, access the map directly.
|
||||
func (h MIMEHeader) Get(key string) string {
|
||||
if h == nil {
|
||||
return ""
|
||||
}
|
||||
v := h[CanonicalEmailMIMEHeaderKey(key)]
|
||||
if len(v) == 0 {
|
||||
return ""
|
||||
}
|
||||
return v[0]
|
||||
}
|
||||
|
||||
// Values returns all values associated with the given key.
|
||||
// It is case insensitive; CanonicalMIMEHeaderKey is
|
||||
// used to canonicalize the provided key. To use non-canonical
|
||||
// keys, access the map directly.
|
||||
// The returned slice is not a copy.
|
||||
func (h MIMEHeader) Values(key string) []string {
|
||||
if h == nil {
|
||||
return nil
|
||||
}
|
||||
return h[CanonicalEmailMIMEHeaderKey(key)]
|
||||
}
|
||||
|
||||
// Del deletes the values associated with key.
|
||||
func (h MIMEHeader) Del(key string) {
|
||||
delete(h, CanonicalEmailMIMEHeaderKey(key))
|
||||
}
|
||||
118
vendor/github.com/jhillyerd/enmime/internal/textproto/pipeline.go
generated
vendored
Normal file
118
vendor/github.com/jhillyerd/enmime/internal/textproto/pipeline.go
generated
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package textproto
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// A Pipeline manages a pipelined in-order request/response sequence.
|
||||
//
|
||||
// To use a Pipeline p to manage multiple clients on a connection,
|
||||
// each client should run:
|
||||
//
|
||||
// id := p.Next() // take a number
|
||||
//
|
||||
// p.StartRequest(id) // wait for turn to send request
|
||||
// «send request»
|
||||
// p.EndRequest(id) // notify Pipeline that request is sent
|
||||
//
|
||||
// p.StartResponse(id) // wait for turn to read response
|
||||
// «read response»
|
||||
// p.EndResponse(id) // notify Pipeline that response is read
|
||||
//
|
||||
// A pipelined server can use the same calls to ensure that
|
||||
// responses computed in parallel are written in the correct order.
|
||||
type Pipeline struct {
|
||||
mu sync.Mutex
|
||||
id uint
|
||||
request sequencer
|
||||
response sequencer
|
||||
}
|
||||
|
||||
// Next returns the next id for a request/response pair.
|
||||
func (p *Pipeline) Next() uint {
|
||||
p.mu.Lock()
|
||||
id := p.id
|
||||
p.id++
|
||||
p.mu.Unlock()
|
||||
return id
|
||||
}
|
||||
|
||||
// StartRequest blocks until it is time to send (or, if this is a server, receive)
|
||||
// the request with the given id.
|
||||
func (p *Pipeline) StartRequest(id uint) {
|
||||
p.request.Start(id)
|
||||
}
|
||||
|
||||
// EndRequest notifies p that the request with the given id has been sent
|
||||
// (or, if this is a server, received).
|
||||
func (p *Pipeline) EndRequest(id uint) {
|
||||
p.request.End(id)
|
||||
}
|
||||
|
||||
// StartResponse blocks until it is time to receive (or, if this is a server, send)
|
||||
// the request with the given id.
|
||||
func (p *Pipeline) StartResponse(id uint) {
|
||||
p.response.Start(id)
|
||||
}
|
||||
|
||||
// EndResponse notifies p that the response with the given id has been received
|
||||
// (or, if this is a server, sent).
|
||||
func (p *Pipeline) EndResponse(id uint) {
|
||||
p.response.End(id)
|
||||
}
|
||||
|
||||
// A sequencer schedules a sequence of numbered events that must
|
||||
// happen in order, one after the other. The event numbering must start
|
||||
// at 0 and increment without skipping. The event number wraps around
|
||||
// safely as long as there are not 2^32 simultaneous events pending.
|
||||
type sequencer struct {
|
||||
mu sync.Mutex
|
||||
id uint
|
||||
wait map[uint]chan struct{}
|
||||
}
|
||||
|
||||
// Start waits until it is time for the event numbered id to begin.
|
||||
// That is, except for the first event, it waits until End(id-1) has
|
||||
// been called.
|
||||
func (s *sequencer) Start(id uint) {
|
||||
s.mu.Lock()
|
||||
if s.id == id {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
c := make(chan struct{})
|
||||
if s.wait == nil {
|
||||
s.wait = make(map[uint]chan struct{})
|
||||
}
|
||||
s.wait[id] = c
|
||||
s.mu.Unlock()
|
||||
<-c
|
||||
}
|
||||
|
||||
// End notifies the sequencer that the event numbered id has completed,
|
||||
// allowing it to schedule the event numbered id+1. It is a run-time error
|
||||
// to call End with an id that is not the number of the active event.
|
||||
func (s *sequencer) End(id uint) {
|
||||
s.mu.Lock()
|
||||
if s.id != id {
|
||||
s.mu.Unlock()
|
||||
panic("out of sync")
|
||||
}
|
||||
id++
|
||||
s.id = id
|
||||
if s.wait == nil {
|
||||
s.wait = make(map[uint]chan struct{})
|
||||
}
|
||||
c, ok := s.wait[id]
|
||||
if ok {
|
||||
delete(s.wait, id)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
if ok {
|
||||
close(c)
|
||||
}
|
||||
}
|
||||
800
vendor/github.com/jhillyerd/enmime/internal/textproto/reader.go
generated
vendored
Normal file
800
vendor/github.com/jhillyerd/enmime/internal/textproto/reader.go
generated
vendored
Normal file
@@ -0,0 +1,800 @@
|
||||
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package textproto
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/textproto"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// A Reader implements convenience methods for reading requests
|
||||
// or responses from a text protocol network connection.
|
||||
type Reader struct {
|
||||
R *bufio.Reader
|
||||
dot *dotReader
|
||||
buf []byte // a re-usable buffer for readContinuedLineSlice
|
||||
}
|
||||
|
||||
// NewReader returns a new Reader reading from r.
|
||||
//
|
||||
// To avoid denial of service attacks, the provided bufio.Reader
|
||||
// should be reading from an io.LimitReader or similar Reader to bound
|
||||
// the size of responses.
|
||||
func NewReader(r *bufio.Reader) *Reader {
|
||||
return &Reader{R: r}
|
||||
}
|
||||
|
||||
// ReadLine reads a single line from r,
|
||||
// eliding the final \n or \r\n from the returned string.
|
||||
func (r *Reader) ReadLine() (string, error) {
|
||||
line, err := r.readLineSlice()
|
||||
return string(line), err
|
||||
}
|
||||
|
||||
// ReadLineBytes is like ReadLine but returns a []byte instead of a string.
|
||||
func (r *Reader) ReadLineBytes() ([]byte, error) {
|
||||
line, err := r.readLineSlice()
|
||||
if line != nil {
|
||||
buf := make([]byte, len(line))
|
||||
copy(buf, line)
|
||||
line = buf
|
||||
}
|
||||
return line, err
|
||||
}
|
||||
|
||||
func (r *Reader) readLineSlice() ([]byte, error) {
|
||||
r.closeDot()
|
||||
var line []byte
|
||||
for {
|
||||
l, more, err := r.R.ReadLine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Avoid the copy if the first call produced a full line.
|
||||
if line == nil && !more {
|
||||
return l, nil
|
||||
}
|
||||
line = append(line, l...)
|
||||
if !more {
|
||||
break
|
||||
}
|
||||
}
|
||||
return line, nil
|
||||
}
|
||||
|
||||
// ReadContinuedLine reads a possibly continued line from r,
|
||||
// eliding the final trailing ASCII white space.
|
||||
// Lines after the first are considered continuations if they
|
||||
// begin with a space or tab character. In the returned data,
|
||||
// continuation lines are separated from the previous line
|
||||
// only by a single space: the newline and leading white space
|
||||
// are removed.
|
||||
//
|
||||
// For example, consider this input:
|
||||
//
|
||||
// Line 1
|
||||
// continued...
|
||||
// Line 2
|
||||
//
|
||||
// The first call to ReadContinuedLine will return "Line 1 continued..."
|
||||
// and the second will return "Line 2".
|
||||
//
|
||||
// Empty lines are never continued.
|
||||
func (r *Reader) ReadContinuedLine() (string, error) {
|
||||
line, err := r.readContinuedLineSlice(noValidation)
|
||||
return string(line), err
|
||||
}
|
||||
|
||||
// trim returns s with leading and trailing spaces and tabs removed.
|
||||
// It does not assume Unicode or UTF-8.
|
||||
func trim(s []byte) []byte {
|
||||
i := 0
|
||||
for i < len(s) && (s[i] == ' ' || s[i] == '\t') {
|
||||
i++
|
||||
}
|
||||
n := len(s)
|
||||
for n > i && (s[n-1] == ' ' || s[n-1] == '\t') {
|
||||
n--
|
||||
}
|
||||
return s[i:n]
|
||||
}
|
||||
|
||||
// ReadContinuedLineBytes is like ReadContinuedLine but
|
||||
// returns a []byte instead of a string.
|
||||
func (r *Reader) ReadContinuedLineBytes() ([]byte, error) {
|
||||
line, err := r.readContinuedLineSlice(noValidation)
|
||||
if line != nil {
|
||||
buf := make([]byte, len(line))
|
||||
copy(buf, line)
|
||||
line = buf
|
||||
}
|
||||
return line, err
|
||||
}
|
||||
|
||||
// readContinuedLineSlice reads continued lines from the reader buffer,
|
||||
// returning a byte slice with all lines. The validateFirstLine function
|
||||
// is run on the first read line, and if it returns an error then this
|
||||
// error is returned from readContinuedLineSlice.
|
||||
func (r *Reader) readContinuedLineSlice(validateFirstLine func([]byte) error) ([]byte, error) {
|
||||
if validateFirstLine == nil {
|
||||
return nil, fmt.Errorf("missing validateFirstLine func")
|
||||
}
|
||||
|
||||
// Read the first line.
|
||||
line, err := r.readLineSlice()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) == 0 { // blank line - no continuation
|
||||
return line, nil
|
||||
}
|
||||
|
||||
if err := validateFirstLine(line); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Optimistically assume that we have started to buffer the next line
|
||||
// and it starts with an ASCII letter (the next header key), or a blank
|
||||
// line, so we can avoid copying that buffered data around in memory
|
||||
// and skipping over non-existent whitespace.
|
||||
if r.R.Buffered() > 1 {
|
||||
peek, _ := r.R.Peek(2)
|
||||
if len(peek) > 0 && (isASCIILetter(peek[0]) || peek[0] == '\n') ||
|
||||
len(peek) == 2 && peek[0] == '\r' && peek[1] == '\n' {
|
||||
return trim(line), nil
|
||||
}
|
||||
}
|
||||
|
||||
// ReadByte or the next readLineSlice will flush the read buffer;
|
||||
// copy the slice into buf.
|
||||
r.buf = append(r.buf[:0], trim(line)...)
|
||||
|
||||
// Read continuation lines.
|
||||
for r.skipSpace() > 0 {
|
||||
line, err := r.readLineSlice()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
r.buf = append(r.buf, ' ')
|
||||
r.buf = append(r.buf, trim(line)...)
|
||||
}
|
||||
return r.buf, nil
|
||||
}
|
||||
|
||||
// skipSpace skips R over all spaces and returns the number of bytes skipped.
|
||||
func (r *Reader) skipSpace() int {
|
||||
n := 0
|
||||
for {
|
||||
c, err := r.R.ReadByte()
|
||||
if err != nil {
|
||||
// Bufio will keep err until next read.
|
||||
break
|
||||
}
|
||||
if c != ' ' && c != '\t' {
|
||||
_ = r.R.UnreadByte()
|
||||
break
|
||||
}
|
||||
n++
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (r *Reader) readCodeLine(expectCode int) (code int, continued bool, message string, err error) {
|
||||
line, err := r.ReadLine()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return parseCodeLine(line, expectCode)
|
||||
}
|
||||
|
||||
func parseCodeLine(line string, expectCode int) (code int, continued bool, message string, err error) {
|
||||
if len(line) < 4 || line[3] != ' ' && line[3] != '-' {
|
||||
err = textproto.ProtocolError("short response: " + line)
|
||||
return
|
||||
}
|
||||
continued = line[3] == '-'
|
||||
code, err = strconv.Atoi(line[0:3])
|
||||
if err != nil || code < 100 {
|
||||
err = textproto.ProtocolError("invalid response code: " + line)
|
||||
return
|
||||
}
|
||||
message = line[4:]
|
||||
if 1 <= expectCode && expectCode < 10 && code/100 != expectCode ||
|
||||
10 <= expectCode && expectCode < 100 && code/10 != expectCode ||
|
||||
100 <= expectCode && expectCode < 1000 && code != expectCode {
|
||||
err = &textproto.Error{Code: code, Msg: message}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ReadCodeLine reads a response code line of the form
|
||||
//
|
||||
// code message
|
||||
//
|
||||
// where code is a three-digit status code and the message
|
||||
// extends to the rest of the line. An example of such a line is:
|
||||
//
|
||||
// 220 plan9.bell-labs.com ESMTP
|
||||
//
|
||||
// If the prefix of the status does not match the digits in expectCode,
|
||||
// ReadCodeLine returns with err set to &Error{code, message}.
|
||||
// For example, if expectCode is 31, an error will be returned if
|
||||
// the status is not in the range [310,319].
|
||||
//
|
||||
// If the response is multi-line, ReadCodeLine returns an error.
|
||||
//
|
||||
// An expectCode <= 0 disables the check of the status code.
|
||||
func (r *Reader) ReadCodeLine(expectCode int) (code int, message string, err error) {
|
||||
code, continued, message, err := r.readCodeLine(expectCode)
|
||||
if err == nil && continued {
|
||||
err = textproto.ProtocolError("unexpected multi-line response: " + message)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ReadResponse reads a multi-line response of the form:
|
||||
//
|
||||
// code-message line 1
|
||||
// code-message line 2
|
||||
// ...
|
||||
// code message line n
|
||||
//
|
||||
// where code is a three-digit status code. The first line starts with the
|
||||
// code and a hyphen. The response is terminated by a line that starts
|
||||
// with the same code followed by a space. Each line in message is
|
||||
// separated by a newline (\n).
|
||||
//
|
||||
// See page 36 of RFC 959 (https://www.ietf.org/rfc/rfc959.txt) for
|
||||
// details of another form of response accepted:
|
||||
//
|
||||
// code-message line 1
|
||||
// message line 2
|
||||
// ...
|
||||
// code message line n
|
||||
//
|
||||
// If the prefix of the status does not match the digits in expectCode,
|
||||
// ReadResponse returns with err set to &Error{code, message}.
|
||||
// For example, if expectCode is 31, an error will be returned if
|
||||
// the status is not in the range [310,319].
|
||||
//
|
||||
// An expectCode <= 0 disables the check of the status code.
|
||||
func (r *Reader) ReadResponse(expectCode int) (code int, message string, err error) {
|
||||
code, continued, message, err := r.readCodeLine(expectCode)
|
||||
multi := continued
|
||||
for continued {
|
||||
line, err := r.ReadLine()
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
var code2 int
|
||||
var moreMessage string
|
||||
code2, continued, moreMessage, err = parseCodeLine(line, 0)
|
||||
if err != nil || code2 != code {
|
||||
message += "\n" + strings.TrimRight(line, "\r\n")
|
||||
continued = true
|
||||
continue
|
||||
}
|
||||
message += "\n" + moreMessage
|
||||
}
|
||||
if err != nil && multi && message != "" {
|
||||
// replace one line error message with all lines (full message)
|
||||
err = &textproto.Error{Code: code, Msg: message}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DotReader returns a new Reader that satisfies Reads using the
|
||||
// decoded text of a dot-encoded block read from r.
|
||||
// The returned Reader is only valid until the next call
|
||||
// to a method on r.
|
||||
//
|
||||
// Dot encoding is a common framing used for data blocks
|
||||
// in text protocols such as SMTP. The data consists of a sequence
|
||||
// of lines, each of which ends in "\r\n". The sequence itself
|
||||
// ends at a line containing just a dot: ".\r\n". Lines beginning
|
||||
// with a dot are escaped with an additional dot to avoid
|
||||
// looking like the end of the sequence.
|
||||
//
|
||||
// The decoded form returned by the Reader's Read method
|
||||
// rewrites the "\r\n" line endings into the simpler "\n",
|
||||
// removes leading dot escapes if present, and stops with error io.EOF
|
||||
// after consuming (and discarding) the end-of-sequence line.
|
||||
func (r *Reader) DotReader() io.Reader {
|
||||
r.closeDot()
|
||||
r.dot = &dotReader{r: r}
|
||||
return r.dot
|
||||
}
|
||||
|
||||
type dotReader struct {
|
||||
r *Reader
|
||||
state int
|
||||
}
|
||||
|
||||
// Read satisfies reads by decoding dot-encoded data read from d.r.
|
||||
func (d *dotReader) Read(b []byte) (n int, err error) {
|
||||
// Run data through a simple state machine to
|
||||
// elide leading dots, rewrite trailing \r\n into \n,
|
||||
// and detect ending .\r\n line.
|
||||
const (
|
||||
stateBeginLine = iota // beginning of line; initial state; must be zero
|
||||
stateDot // read . at beginning of line
|
||||
stateDotCR // read .\r at beginning of line
|
||||
stateCR // read \r (possibly at end of line)
|
||||
stateData // reading data in middle of line
|
||||
stateEOF // reached .\r\n end marker line
|
||||
)
|
||||
br := d.r.R
|
||||
for n < len(b) && d.state != stateEOF {
|
||||
var c byte
|
||||
c, err = br.ReadByte()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
break
|
||||
}
|
||||
switch d.state {
|
||||
case stateBeginLine:
|
||||
if c == '.' {
|
||||
d.state = stateDot
|
||||
continue
|
||||
}
|
||||
if c == '\r' {
|
||||
d.state = stateCR
|
||||
continue
|
||||
}
|
||||
d.state = stateData
|
||||
|
||||
case stateDot:
|
||||
if c == '\r' {
|
||||
d.state = stateDotCR
|
||||
continue
|
||||
}
|
||||
if c == '\n' {
|
||||
d.state = stateEOF
|
||||
continue
|
||||
}
|
||||
d.state = stateData
|
||||
|
||||
case stateDotCR:
|
||||
if c == '\n' {
|
||||
d.state = stateEOF
|
||||
continue
|
||||
}
|
||||
// Not part of .\r\n.
|
||||
// Consume leading dot and emit saved \r.
|
||||
_ = br.UnreadByte()
|
||||
c = '\r'
|
||||
d.state = stateData
|
||||
|
||||
case stateCR:
|
||||
if c == '\n' {
|
||||
d.state = stateBeginLine
|
||||
break
|
||||
}
|
||||
// Not part of \r\n. Emit saved \r
|
||||
_ = br.UnreadByte()
|
||||
c = '\r'
|
||||
d.state = stateData
|
||||
|
||||
case stateData:
|
||||
if c == '\r' {
|
||||
d.state = stateCR
|
||||
continue
|
||||
}
|
||||
if c == '\n' {
|
||||
d.state = stateBeginLine
|
||||
}
|
||||
}
|
||||
b[n] = c
|
||||
n++
|
||||
}
|
||||
if err == nil && d.state == stateEOF {
|
||||
err = io.EOF
|
||||
}
|
||||
if err != nil && d.r.dot == d {
|
||||
d.r.dot = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// closeDot drains the current DotReader if any,
|
||||
// making sure that it reads until the ending dot line.
|
||||
func (r *Reader) closeDot() {
|
||||
if r.dot == nil {
|
||||
return
|
||||
}
|
||||
buf := make([]byte, 128)
|
||||
for r.dot != nil {
|
||||
// When Read reaches EOF or an error,
|
||||
// it will set r.dot == nil.
|
||||
_, _ = r.dot.Read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
// ReadDotBytes reads a dot-encoding and returns the decoded data.
|
||||
//
|
||||
// See the documentation for the DotReader method for details about dot-encoding.
|
||||
func (r *Reader) ReadDotBytes() ([]byte, error) {
|
||||
return io.ReadAll(r.DotReader())
|
||||
}
|
||||
|
||||
// ReadDotLines reads a dot-encoding and returns a slice
|
||||
// containing the decoded lines, with the final \r\n or \n elided from each.
|
||||
//
|
||||
// See the documentation for the DotReader method for details about dot-encoding.
|
||||
func (r *Reader) ReadDotLines() ([]string, error) {
|
||||
// We could use ReadDotBytes and then Split it,
|
||||
// but reading a line at a time avoids needing a
|
||||
// large contiguous block of memory and is simpler.
|
||||
var v []string
|
||||
var err error
|
||||
for {
|
||||
var line string
|
||||
line, err = r.ReadLine()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Dot by itself marks end; otherwise cut one dot.
|
||||
if len(line) > 0 && line[0] == '.' {
|
||||
if len(line) == 1 {
|
||||
break
|
||||
}
|
||||
line = line[1:]
|
||||
}
|
||||
v = append(v, line)
|
||||
}
|
||||
return v, err
|
||||
}
|
||||
|
||||
var colon = []byte(":")
|
||||
|
||||
// ReadMIMEHeader reads a MIME-style header from r.
|
||||
// The header is a sequence of possibly continued Key: Value lines
|
||||
// ending in a blank line.
|
||||
// The returned map m maps CanonicalMIMEHeaderKey(key) to a
|
||||
// sequence of values in the same order encountered in the input.
|
||||
//
|
||||
// For example, consider this input:
|
||||
//
|
||||
// My-Key: Value 1
|
||||
// Long-Key: Even
|
||||
// Longer Value
|
||||
// My-Key: Value 2
|
||||
//
|
||||
// Given that input, ReadMIMEHeader returns the map:
|
||||
//
|
||||
// map[string][]string{
|
||||
// "My-Key": {"Value 1", "Value 2"},
|
||||
// "Long-Key": {"Even Longer Value"},
|
||||
// }
|
||||
func (r *Reader) ReadMIMEHeader() (MIMEHeader, error) {
|
||||
return readMIMEHeader(r, math.MaxInt64)
|
||||
}
|
||||
|
||||
// readMIMEHeader is a version of ReadMIMEHeader which takes a limit on the header size.
|
||||
// It is called by the mime/multipart package.
|
||||
func readMIMEHeader(r *Reader, lim int64) (MIMEHeader, error) {
|
||||
// Avoid lots of small slice allocations later by allocating one
|
||||
// large one ahead of time which we'll cut up into smaller
|
||||
// slices. If this isn't big enough later, we allocate small ones.
|
||||
var strs []string
|
||||
hint := r.upcomingHeaderNewlines()
|
||||
if hint > 0 {
|
||||
strs = make([]string, hint)
|
||||
}
|
||||
|
||||
m := make(MIMEHeader, hint)
|
||||
|
||||
// The first line cannot start with a leading space.
|
||||
if buf, err := r.R.Peek(1); err == nil && (buf[0] == ' ' || buf[0] == '\t') {
|
||||
line, err := r.readLineSlice()
|
||||
if err != nil {
|
||||
return m, err
|
||||
}
|
||||
return m, textproto.ProtocolError("malformed MIME header initial line: " + string(line))
|
||||
}
|
||||
|
||||
for {
|
||||
kv, err := r.readContinuedLineSlice(mustHaveFieldNameColon)
|
||||
if len(kv) == 0 {
|
||||
return m, err
|
||||
}
|
||||
|
||||
// Key ends at first colon.
|
||||
k, v, ok := bytes.Cut(kv, colon)
|
||||
if !ok {
|
||||
return m, textproto.ProtocolError("malformed MIME header line: " + string(kv))
|
||||
}
|
||||
key, ok := canonicalMIMEHeaderKey(k)
|
||||
if !ok {
|
||||
return m, textproto.ProtocolError("malformed MIME header line: " + string(kv))
|
||||
}
|
||||
for _, c := range v {
|
||||
if !validHeaderValueByte(c) {
|
||||
return m, textproto.ProtocolError("malformed MIME header line: " + string(kv))
|
||||
}
|
||||
}
|
||||
|
||||
// As per RFC 7230 field-name is a token, tokens consist of one or more chars.
|
||||
// We could return a ProtocolError here, but better to be liberal in what we
|
||||
// accept, so if we get an empty key, skip it.
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip initial spaces in value.
|
||||
value := string(bytes.TrimLeft(v, " \t"))
|
||||
|
||||
vv := m[key]
|
||||
if vv == nil {
|
||||
lim -= int64(len(key))
|
||||
lim -= 100 // map entry overhead
|
||||
}
|
||||
lim -= int64(len(value))
|
||||
if lim < 0 {
|
||||
// TODO: This should be a distinguishable error (ErrMessageTooLarge)
|
||||
// to allow mime/multipart to detect it.
|
||||
return m, errors.New("message too large")
|
||||
}
|
||||
if vv == nil && len(strs) > 0 {
|
||||
// More than likely this will be a single-element key.
|
||||
// Most headers aren't multi-valued.
|
||||
// Set the capacity on strs[0] to 1, so any future append
|
||||
// won't extend the slice into the other strings.
|
||||
vv, strs = strs[:1:1], strs[1:]
|
||||
vv[0] = value
|
||||
m[key] = vv
|
||||
} else {
|
||||
m[key] = append(vv, value)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return m, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// noValidation is a no-op validation func for readContinuedLineSlice
|
||||
// that permits any lines.
|
||||
func noValidation(_ []byte) error { return nil }
|
||||
|
||||
// mustHaveFieldNameColon ensures that, per RFC 7230, the
|
||||
// field-name is on a single line, so the first line must
|
||||
// contain a colon.
|
||||
func mustHaveFieldNameColon(line []byte) error {
|
||||
if bytes.IndexByte(line, ':') < 0 {
|
||||
return textproto.ProtocolError(fmt.Sprintf("malformed MIME header: missing colon: %q", line))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var nl = []byte("\n")
|
||||
|
||||
// upcomingHeaderNewlines returns an approximation of the number of newlines
|
||||
// that will be in this header. If it gets confused, it returns 0.
|
||||
func (r *Reader) upcomingHeaderNewlines() (n int) {
|
||||
// Try to determine the 'hint' size.
|
||||
_, _ = r.R.Peek(1) // force a buffer load if empty
|
||||
s := r.R.Buffered()
|
||||
if s == 0 {
|
||||
return
|
||||
}
|
||||
peek, _ := r.R.Peek(s)
|
||||
return bytes.Count(peek, nl)
|
||||
}
|
||||
|
||||
// CanonicalMIMEHeaderKey returns the canonical format of the
|
||||
// MIME header key s. The canonicalization converts the first
|
||||
// letter and any letter following a hyphen to upper case;
|
||||
// the rest are converted to lowercase. For example, the
|
||||
// canonical key for "accept-encoding" is "Accept-Encoding".
|
||||
// MIME header keys are assumed to be ASCII only.
|
||||
// If s contains a space or invalid header field bytes, it is
|
||||
// returned without modifications.
|
||||
func CanonicalMIMEHeaderKey(s string) string {
|
||||
// Quick check for canonical encoding.
|
||||
upper := true
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if !validHeaderFieldByte(c) {
|
||||
return s
|
||||
}
|
||||
if upper && 'a' <= c && c <= 'z' {
|
||||
s, _ = canonicalMIMEHeaderKey([]byte(s))
|
||||
return s
|
||||
}
|
||||
if !upper && 'A' <= c && c <= 'Z' {
|
||||
s, _ = canonicalMIMEHeaderKey([]byte(s))
|
||||
return s
|
||||
}
|
||||
upper = c == '-'
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
const toLower = 'a' - 'A'
|
||||
|
||||
// validHeaderFieldByte reports whether c is a valid byte in a header
|
||||
// field name. RFC 7230 says:
|
||||
//
|
||||
// header-field = field-name ":" OWS field-value OWS
|
||||
// field-name = token
|
||||
// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
|
||||
// "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
|
||||
// token = 1*tchar
|
||||
func validHeaderFieldByte(c byte) bool {
|
||||
// mask is a 128-bit bitmap with 1s for allowed bytes,
|
||||
// so that the byte c can be tested with a shift and an and.
|
||||
// If c >= 128, then 1<<c and 1<<(c-64) will both be zero,
|
||||
// and this function will return false.
|
||||
const mask = 0 |
|
||||
(1<<(10)-1)<<'0' |
|
||||
(1<<(26)-1)<<'a' |
|
||||
(1<<(26)-1)<<'A' |
|
||||
1<<'!' |
|
||||
1<<'#' |
|
||||
1<<'$' |
|
||||
1<<'%' |
|
||||
1<<'&' |
|
||||
1<<'\'' |
|
||||
1<<'*' |
|
||||
1<<'+' |
|
||||
1<<'-' |
|
||||
1<<'.' |
|
||||
1<<'^' |
|
||||
1<<'_' |
|
||||
1<<'`' |
|
||||
1<<'|' |
|
||||
1<<'~'
|
||||
return ((uint64(1)<<c)&(mask&(1<<64-1)) |
|
||||
(uint64(1)<<(c-64))&(mask>>64)) != 0
|
||||
}
|
||||
|
||||
// validHeaderValueByte reports whether c is a valid byte in a header
|
||||
// field value. RFC 7230 says:
|
||||
//
|
||||
// field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
|
||||
// field-vchar = VCHAR / obs-text
|
||||
// obs-text = %x80-FF
|
||||
//
|
||||
// RFC 5234 says:
|
||||
//
|
||||
// HTAB = %x09
|
||||
// SP = %x20
|
||||
// VCHAR = %x21-7E
|
||||
func validHeaderValueByte(c byte) bool {
|
||||
// mask is a 128-bit bitmap with 1s for allowed bytes,
|
||||
// so that the byte c can be tested with a shift and an and.
|
||||
// If c >= 128, then 1<<c and 1<<(c-64) will both be zero.
|
||||
// Since this is the obs-text range, we invert the mask to
|
||||
// create a bitmap with 1s for disallowed bytes.
|
||||
const mask = 0 |
|
||||
(1<<(0x7f-0x21)-1)<<0x21 | // VCHAR: %x21-7E
|
||||
1<<0x20 | // SP: %x20
|
||||
1<<0x09 // HTAB: %x09
|
||||
return ((uint64(1)<<c)&^(mask&(1<<64-1)) |
|
||||
(uint64(1)<<(c-64))&^(mask>>64)) == 0
|
||||
}
|
||||
|
||||
// canonicalMIMEHeaderKey is like CanonicalMIMEHeaderKey but is
|
||||
// allowed to mutate the provided byte slice before returning the
|
||||
// string.
|
||||
//
|
||||
// For invalid inputs (if a contains spaces or non-token bytes), a
|
||||
// is unchanged and a string copy is returned.
|
||||
//
|
||||
// ok is true if the header key contains only valid characters and spaces.
|
||||
// ReadMIMEHeader accepts header keys containing spaces, but does not
|
||||
// canonicalize them.
|
||||
func canonicalMIMEHeaderKey(a []byte) (_ string, ok bool) {
|
||||
// See if a looks like a header key. If not, return it unchanged.
|
||||
noCanon := false
|
||||
for _, c := range a {
|
||||
if validHeaderFieldByte(c) {
|
||||
continue
|
||||
}
|
||||
// Don't canonicalize.
|
||||
if c == ' ' {
|
||||
// We accept invalid headers with a space before the
|
||||
// colon, but must not canonicalize them.
|
||||
// See https://go.dev/issue/34540.
|
||||
noCanon = true
|
||||
continue
|
||||
}
|
||||
return string(a), false
|
||||
}
|
||||
if noCanon {
|
||||
return string(a), true
|
||||
}
|
||||
|
||||
upper := true
|
||||
for i, c := range a {
|
||||
// Canonicalize: first letter upper case
|
||||
// and upper case after each dash.
|
||||
// (Host, User-Agent, If-Modified-Since).
|
||||
// MIME headers are ASCII only, so no Unicode issues.
|
||||
if upper && 'a' <= c && c <= 'z' {
|
||||
c -= toLower
|
||||
} else if !upper && 'A' <= c && c <= 'Z' {
|
||||
c += toLower
|
||||
}
|
||||
a[i] = c
|
||||
upper = c == '-' // for next time
|
||||
}
|
||||
commonHeaderOnce.Do(initCommonHeader)
|
||||
// The compiler recognizes m[string(byteSlice)] as a special
|
||||
// case, so a copy of a's bytes into a new string does not
|
||||
// happen in this map lookup:
|
||||
if v := commonHeader[string(a)]; v != "" {
|
||||
return v, true
|
||||
}
|
||||
return string(a), true
|
||||
}
|
||||
|
||||
// commonHeader interns common header strings.
|
||||
var commonHeader map[string]string
|
||||
|
||||
var commonHeaderOnce sync.Once
|
||||
|
||||
func initCommonHeader() {
|
||||
commonHeader = make(map[string]string)
|
||||
for _, v := range []string{
|
||||
"Accept",
|
||||
"Accept-Charset",
|
||||
"Accept-Encoding",
|
||||
"Accept-Language",
|
||||
"Accept-Ranges",
|
||||
"Cache-Control",
|
||||
"Cc",
|
||||
"Connection",
|
||||
"Content-Id",
|
||||
"Content-Language",
|
||||
"Content-Length",
|
||||
"Content-Transfer-Encoding",
|
||||
"Content-Type",
|
||||
"Cookie",
|
||||
"Date",
|
||||
"Dkim-Signature",
|
||||
"Etag",
|
||||
"Expires",
|
||||
"From",
|
||||
"Host",
|
||||
"If-Modified-Since",
|
||||
"If-None-Match",
|
||||
"In-Reply-To",
|
||||
"Last-Modified",
|
||||
"Location",
|
||||
"Message-Id",
|
||||
"Mime-Version",
|
||||
"Pragma",
|
||||
"Received",
|
||||
"Return-Path",
|
||||
"Server",
|
||||
"Set-Cookie",
|
||||
"Subject",
|
||||
"To",
|
||||
"User-Agent",
|
||||
"Via",
|
||||
"X-Forwarded-For",
|
||||
"X-Imforwards",
|
||||
"X-Powered-By",
|
||||
} {
|
||||
commonHeader[v] = v
|
||||
}
|
||||
}
|
||||
214
vendor/github.com/jhillyerd/enmime/internal/textproto/reader_email.go
generated
vendored
Normal file
214
vendor/github.com/jhillyerd/enmime/internal/textproto/reader_email.go
generated
vendored
Normal file
@@ -0,0 +1,214 @@
|
||||
package textproto
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"math"
|
||||
"net/textproto"
|
||||
)
|
||||
|
||||
// ReadEmailMIMEHeader reads a MIME-style header from r.
|
||||
//
|
||||
// This is a modified version of the stock func that better handles the characters
|
||||
// we must support in email, instead of just HTTP.
|
||||
func (r *Reader) ReadEmailMIMEHeader() (MIMEHeader, error) {
|
||||
return readEmailMIMEHeader(r, math.MaxInt64)
|
||||
}
|
||||
|
||||
func readEmailMIMEHeader(r *Reader, lim int64) (MIMEHeader, error) {
|
||||
// Avoid lots of small slice allocations later by allocating one
|
||||
// large one ahead of time which we'll cut up into smaller
|
||||
// slices. If this isn't big enough later, we allocate small ones.
|
||||
var strs []string
|
||||
hint := r.upcomingHeaderNewlines()
|
||||
if hint > 0 {
|
||||
strs = make([]string, hint)
|
||||
}
|
||||
|
||||
m := make(MIMEHeader, hint)
|
||||
|
||||
// The first line cannot start with a leading space.
|
||||
if buf, err := r.R.Peek(1); err == nil && (buf[0] == ' ' || buf[0] == '\t') {
|
||||
line, err := r.readLineSlice()
|
||||
if err != nil {
|
||||
return m, err
|
||||
}
|
||||
return m, textproto.ProtocolError("malformed MIME header initial line: " + string(line))
|
||||
}
|
||||
|
||||
for {
|
||||
kv, err := r.readContinuedLineSlice(mustHaveFieldNameColon)
|
||||
if len(kv) == 0 {
|
||||
return m, err
|
||||
}
|
||||
|
||||
// Key ends at first colon.
|
||||
k, v, ok := bytes.Cut(kv, colon)
|
||||
if !ok {
|
||||
return m, textproto.ProtocolError("malformed MIME header line: " + string(kv))
|
||||
}
|
||||
key, ok := canonicalEmailMIMEHeaderKey(k)
|
||||
if !ok {
|
||||
return m, textproto.ProtocolError("malformed MIME header line: " + string(kv))
|
||||
}
|
||||
// for _, c := range v {
|
||||
// if !validHeaderValueByte(c) {
|
||||
// return m, ProtocolError("malformed MIME header line: " + string(kv))
|
||||
// }
|
||||
// }
|
||||
|
||||
// As per RFC 7230 field-name is a token, tokens consist of one or more chars.
|
||||
// We could return a ProtocolError here, but better to be liberal in what we
|
||||
// accept, so if we get an empty key, skip it.
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip initial spaces in value.
|
||||
value := string(bytes.TrimLeft(v, " \t"))
|
||||
|
||||
vv := m[key]
|
||||
if vv == nil {
|
||||
lim -= int64(len(key))
|
||||
lim -= 100 // map entry overhead
|
||||
}
|
||||
lim -= int64(len(value))
|
||||
if lim < 0 {
|
||||
// TODO: This should be a distinguishable error (ErrMessageTooLarge)
|
||||
// to allow mime/multipart to detect it.
|
||||
return m, errors.New("message too large")
|
||||
}
|
||||
if vv == nil && len(strs) > 0 {
|
||||
// More than likely this will be a single-element key.
|
||||
// Most headers aren't multi-valued.
|
||||
// Set the capacity on strs[0] to 1, so any future append
|
||||
// won't extend the slice into the other strings.
|
||||
vv, strs = strs[:1:1], strs[1:]
|
||||
vv[0] = value
|
||||
m[key] = vv
|
||||
} else {
|
||||
m[key] = append(vv, value)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return m, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CanonicalEmailMIMEHeaderKey returns the canonical format of the
|
||||
// MIME header key s.
|
||||
//
|
||||
// This is a modified version of the stock func that better handles the characters
|
||||
// we must support in email, instead of just HTTP.
|
||||
func CanonicalEmailMIMEHeaderKey(s string) string {
|
||||
// Quick check for canonical encoding.
|
||||
upper := true
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if !ValidEmailHeaderFieldByte(c) {
|
||||
return s
|
||||
}
|
||||
if upper && 'a' <= c && c <= 'z' {
|
||||
s, _ = canonicalEmailMIMEHeaderKey([]byte(s))
|
||||
return s
|
||||
}
|
||||
if !upper && 'A' <= c && c <= 'Z' {
|
||||
s, _ = canonicalEmailMIMEHeaderKey([]byte(s))
|
||||
return s
|
||||
}
|
||||
upper = c == '-'
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func canonicalEmailMIMEHeaderKey(a []byte) (_ string, ok bool) {
|
||||
noCanon := false
|
||||
for _, c := range a {
|
||||
if ValidEmailHeaderFieldByte(c) {
|
||||
continue
|
||||
}
|
||||
// Don't canonicalize.
|
||||
if c == ' ' {
|
||||
// We accept invalid headers with a space before the
|
||||
// colon, but must not canonicalize them.
|
||||
// See https://go.dev/issue/34540.
|
||||
noCanon = true
|
||||
continue
|
||||
}
|
||||
return string(a), false
|
||||
}
|
||||
if noCanon {
|
||||
return string(a), true
|
||||
}
|
||||
|
||||
upper := true
|
||||
for i, c := range a {
|
||||
// Canonicalize: first letter upper case
|
||||
// and upper case after each dash.
|
||||
// (Host, User-Agent, If-Modified-Since).
|
||||
// MIME headers are ASCII only, so no Unicode issues.
|
||||
if upper && 'a' <= c && c <= 'z' {
|
||||
c -= toLower
|
||||
} else if !upper && 'A' <= c && c <= 'Z' {
|
||||
c += toLower
|
||||
}
|
||||
a[i] = c
|
||||
upper = c == '-' // for next time
|
||||
}
|
||||
commonHeaderOnce.Do(initCommonHeader)
|
||||
// The compiler recognizes m[string(byteSlice)] as a special
|
||||
// case, so a copy of a's bytes into a new string does not
|
||||
// happen in this map lookup:
|
||||
if v := commonHeader[string(a)]; v != "" {
|
||||
return v, true
|
||||
}
|
||||
return string(a), true
|
||||
}
|
||||
|
||||
// ValidEmailHeaderFieldByte Valid characters in email header field.
|
||||
//
|
||||
// According to [RFC 5322](https://www.rfc-editor.org/rfc/rfc5322#section-2.2),
|
||||
//
|
||||
// > A field name MUST be composed of printable US-ASCII characters (i.e.,
|
||||
// > characters that have values between 33 and 126, inclusive), except
|
||||
// > colon.
|
||||
func ValidEmailHeaderFieldByte(c byte) bool {
|
||||
const mask = 0 |
|
||||
(1<<(10)-1)<<'0' |
|
||||
(1<<(26)-1)<<'a' |
|
||||
(1<<(26)-1)<<'A' |
|
||||
1<<'!' |
|
||||
1<<'"' |
|
||||
1<<'#' |
|
||||
1<<'$' |
|
||||
1<<'%' |
|
||||
1<<'&' |
|
||||
1<<'\'' |
|
||||
1<<'(' |
|
||||
1<<')' |
|
||||
1<<'*' |
|
||||
1<<'+' |
|
||||
1<<',' |
|
||||
1<<'-' |
|
||||
1<<'.' |
|
||||
1<<'/' |
|
||||
1<<';' |
|
||||
1<<'<' |
|
||||
1<<'=' |
|
||||
1<<'>' |
|
||||
1<<'?' |
|
||||
1<<'@' |
|
||||
1<<'[' |
|
||||
1<<'\\' |
|
||||
1<<']' |
|
||||
1<<'^' |
|
||||
1<<'_' |
|
||||
1<<'`' |
|
||||
1<<'{' |
|
||||
1<<'|' |
|
||||
1<<'}' |
|
||||
1<<'~'
|
||||
return ((uint64(1)<<c)&(mask&(1<<64-1)) |
|
||||
(uint64(1)<<(c-64))&(mask>>64)) != 0
|
||||
}
|
||||
133
vendor/github.com/jhillyerd/enmime/internal/textproto/textproto.go
generated
vendored
Normal file
133
vendor/github.com/jhillyerd/enmime/internal/textproto/textproto.go
generated
vendored
Normal file
@@ -0,0 +1,133 @@
|
||||
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package textproto implements generic support for text-based request/response
|
||||
// protocols in the style of HTTP, NNTP, and SMTP.
|
||||
//
|
||||
// The package provides:
|
||||
//
|
||||
// Error, which represents a numeric error response from
|
||||
// a server.
|
||||
//
|
||||
// Pipeline, to manage pipelined requests and responses
|
||||
// in a client.
|
||||
//
|
||||
// Reader, to read numeric response code lines,
|
||||
// key: value headers, lines wrapped with leading spaces
|
||||
// on continuation lines, and whole text blocks ending
|
||||
// with a dot on a line by itself.
|
||||
//
|
||||
// Writer, to write dot-encoded text blocks.
|
||||
//
|
||||
// Conn, a convenient packaging of Reader, Writer, and Pipeline for use
|
||||
// with a single network connection.
|
||||
package textproto
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net"
|
||||
)
|
||||
|
||||
// A Conn represents a textual network protocol connection.
|
||||
// It consists of a Reader and Writer to manage I/O
|
||||
// and a Pipeline to sequence concurrent requests on the connection.
|
||||
// These embedded types carry methods with them;
|
||||
// see the documentation of those types for details.
|
||||
type Conn struct {
|
||||
Reader
|
||||
Writer
|
||||
Pipeline
|
||||
conn io.ReadWriteCloser
|
||||
}
|
||||
|
||||
// NewConn returns a new Conn using conn for I/O.
|
||||
func NewConn(conn io.ReadWriteCloser) *Conn {
|
||||
return &Conn{
|
||||
Reader: Reader{R: bufio.NewReader(conn)},
|
||||
Writer: Writer{W: bufio.NewWriter(conn)},
|
||||
conn: conn,
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the connection.
|
||||
func (c *Conn) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
// Dial connects to the given address on the given network using net.Dial
|
||||
// and then returns a new Conn for the connection.
|
||||
func Dial(network, addr string) (*Conn, error) {
|
||||
c, err := net.Dial(network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewConn(c), nil
|
||||
}
|
||||
|
||||
// Cmd is a convenience method that sends a command after
|
||||
// waiting its turn in the pipeline. The command text is the
|
||||
// result of formatting format with args and appending \r\n.
|
||||
// Cmd returns the id of the command, for use with StartResponse and EndResponse.
|
||||
//
|
||||
// For example, a client might run a HELP command that returns a dot-body
|
||||
// by using:
|
||||
//
|
||||
// id, err := c.Cmd("HELP")
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
//
|
||||
// c.StartResponse(id)
|
||||
// defer c.EndResponse(id)
|
||||
//
|
||||
// if _, _, err = c.ReadCodeLine(110); err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// text, err := c.ReadDotBytes()
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// return c.ReadCodeLine(250)
|
||||
func (c *Conn) Cmd(format string, args ...any) (id uint, err error) {
|
||||
id = c.Next()
|
||||
c.StartRequest(id)
|
||||
err = c.PrintfLine(format, args...)
|
||||
c.EndRequest(id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// TrimString returns s without leading and trailing ASCII space.
|
||||
func TrimString(s string) string {
|
||||
for len(s) > 0 && isASCIISpace(s[0]) {
|
||||
s = s[1:]
|
||||
}
|
||||
for len(s) > 0 && isASCIISpace(s[len(s)-1]) {
|
||||
s = s[:len(s)-1]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// TrimBytes returns b without leading and trailing ASCII space.
|
||||
func TrimBytes(b []byte) []byte {
|
||||
for len(b) > 0 && isASCIISpace(b[0]) {
|
||||
b = b[1:]
|
||||
}
|
||||
for len(b) > 0 && isASCIISpace(b[len(b)-1]) {
|
||||
b = b[:len(b)-1]
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func isASCIISpace(b byte) bool {
|
||||
return b == ' ' || b == '\t' || b == '\n' || b == '\r'
|
||||
}
|
||||
|
||||
func isASCIILetter(b byte) bool {
|
||||
b |= 0x20 // make lower case
|
||||
return 'a' <= b && b <= 'z'
|
||||
}
|
||||
119
vendor/github.com/jhillyerd/enmime/internal/textproto/writer.go
generated
vendored
Normal file
119
vendor/github.com/jhillyerd/enmime/internal/textproto/writer.go
generated
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package textproto
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// A Writer implements convenience methods for writing
|
||||
// requests or responses to a text protocol network connection.
|
||||
type Writer struct {
|
||||
W *bufio.Writer
|
||||
dot *dotWriter
|
||||
}
|
||||
|
||||
// NewWriter returns a new Writer writing to w.
|
||||
func NewWriter(w *bufio.Writer) *Writer {
|
||||
return &Writer{W: w}
|
||||
}
|
||||
|
||||
var crnl = []byte{'\r', '\n'}
|
||||
var dotcrnl = []byte{'.', '\r', '\n'}
|
||||
|
||||
// PrintfLine writes the formatted output followed by \r\n.
|
||||
func (w *Writer) PrintfLine(format string, args ...any) error {
|
||||
w.closeDot()
|
||||
fmt.Fprintf(w.W, format, args...)
|
||||
_, _ = w.W.Write(crnl)
|
||||
return w.W.Flush()
|
||||
}
|
||||
|
||||
// DotWriter returns a writer that can be used to write a dot-encoding to w.
|
||||
// It takes care of inserting leading dots when necessary,
|
||||
// translating line-ending \n into \r\n, and adding the final .\r\n line
|
||||
// when the DotWriter is closed. The caller should close the
|
||||
// DotWriter before the next call to a method on w.
|
||||
//
|
||||
// See the documentation for Reader's DotReader method for details about dot-encoding.
|
||||
func (w *Writer) DotWriter() io.WriteCloser {
|
||||
w.closeDot()
|
||||
w.dot = &dotWriter{w: w}
|
||||
return w.dot
|
||||
}
|
||||
|
||||
func (w *Writer) closeDot() {
|
||||
if w.dot != nil {
|
||||
w.dot.Close() // sets w.dot = nil
|
||||
}
|
||||
}
|
||||
|
||||
type dotWriter struct {
|
||||
w *Writer
|
||||
state int
|
||||
}
|
||||
|
||||
const (
|
||||
wstateBegin = iota // initial state; must be zero
|
||||
wstateBeginLine // beginning of line
|
||||
wstateCR // wrote \r (possibly at end of line)
|
||||
wstateData // writing data in middle of line
|
||||
)
|
||||
|
||||
func (d *dotWriter) Write(b []byte) (n int, err error) {
|
||||
bw := d.w.W
|
||||
for n < len(b) {
|
||||
c := b[n]
|
||||
switch d.state {
|
||||
case wstateBegin, wstateBeginLine:
|
||||
d.state = wstateData
|
||||
if c == '.' {
|
||||
// escape leading dot
|
||||
_ = bw.WriteByte('.')
|
||||
}
|
||||
fallthrough
|
||||
|
||||
case wstateData:
|
||||
if c == '\r' {
|
||||
d.state = wstateCR
|
||||
}
|
||||
if c == '\n' {
|
||||
_ = bw.WriteByte('\r')
|
||||
d.state = wstateBeginLine
|
||||
}
|
||||
|
||||
case wstateCR:
|
||||
d.state = wstateData
|
||||
if c == '\n' {
|
||||
d.state = wstateBeginLine
|
||||
}
|
||||
}
|
||||
if err = bw.WriteByte(c); err != nil {
|
||||
break
|
||||
}
|
||||
n++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (d *dotWriter) Close() error {
|
||||
if d.w.dot == d {
|
||||
d.w.dot = nil
|
||||
}
|
||||
bw := d.w.W
|
||||
switch d.state {
|
||||
default:
|
||||
_ = bw.WriteByte('\r')
|
||||
fallthrough
|
||||
case wstateCR:
|
||||
_ = bw.WriteByte('\n')
|
||||
fallthrough
|
||||
case wstateBeginLine:
|
||||
_, _ = bw.Write(dotcrnl)
|
||||
}
|
||||
return bw.Flush()
|
||||
}
|
||||
63
vendor/github.com/jhillyerd/enmime/mediatype/mediatype.go
generated
vendored
63
vendor/github.com/jhillyerd/enmime/mediatype/mediatype.go
generated
vendored
@@ -30,16 +30,25 @@ const (
|
||||
utf8 = "utf-8"
|
||||
)
|
||||
|
||||
type MediaTypeParseOptions struct {
|
||||
StripMediaTypeInvalidCharacters bool
|
||||
}
|
||||
|
||||
// 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
|
||||
// - 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) {
|
||||
return ParseWithOptions(ctype, MediaTypeParseOptions{})
|
||||
}
|
||||
|
||||
// ParseWithOptions parses media-type with additional options controlling the parsing behavior.
|
||||
func ParseWithOptions(ctype string, options MediaTypeParseOptions) (mtype string, params map[string]string, invalidParams []string, err error) {
|
||||
mtype, params, err = mime.ParseMediaType(
|
||||
fixNewlines(fixUnescapedQuotes(fixUnquotedSpecials(fixMangledMediaType(removeTrailingHTMLTags(ctype), ';')))))
|
||||
fixNewlines(fixUnescapedQuotes(fixUnquotedSpecials(fixMangledMediaType(removeTrailingHTMLTags(ctype), ';', options)))))
|
||||
if err != nil {
|
||||
if err.Error() == "mime: no media type" {
|
||||
return "", nil, nil, nil
|
||||
@@ -63,7 +72,7 @@ func Parse(ctype string) (mtype string, params map[string]string, invalidParams
|
||||
|
||||
// fixMangledMediaType is used to insert ; separators into media type strings that lack them, and
|
||||
// remove repeated parameters.
|
||||
func fixMangledMediaType(mtype string, sep rune) string {
|
||||
func fixMangledMediaType(mtype string, sep rune, options MediaTypeParseOptions) string {
|
||||
strsep := string([]rune{sep})
|
||||
if mtype == "" {
|
||||
return ""
|
||||
@@ -84,6 +93,10 @@ func fixMangledMediaType(mtype string, sep rune) string {
|
||||
// The content type is completely missing. Put in a placeholder.
|
||||
p = ctPlaceholder
|
||||
}
|
||||
// Remove invalid characters (specials)
|
||||
if options.StripMediaTypeInvalidCharacters {
|
||||
p = removeTypeSpecials(p)
|
||||
}
|
||||
// Check for missing token after slash.
|
||||
if strings.HasSuffix(p, "/") {
|
||||
switch p {
|
||||
@@ -122,6 +135,12 @@ func fixMangledMediaType(mtype string, sep rune) string {
|
||||
p = coding.RFC2047Decode(p)
|
||||
|
||||
pair := strings.SplitAfter(p, "=")
|
||||
|
||||
if strings.TrimSpace(pair[0]) == "=" {
|
||||
// Ignore unnamed parameters.
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(mtype, strings.TrimSpace(pair[0])) {
|
||||
// Ignore repeated parameters.
|
||||
continue
|
||||
@@ -156,16 +175,24 @@ func fixMangledMediaType(mtype string, sep rune) string {
|
||||
// Content-Type header.
|
||||
//
|
||||
// Given this this header:
|
||||
// `Content-Type: text/calendar; charset=utf-8; method=text/calendar`
|
||||
//
|
||||
// `Content-Type: text/calendar; charset=utf-8; method=text/calendar`
|
||||
//
|
||||
// `consumeParams` should be given this part:
|
||||
// ` charset=utf-8; method=text/calendar`
|
||||
//
|
||||
// ` charset=utf-8; method=text/calendar`
|
||||
//
|
||||
// And returns (first pass):
|
||||
// `consumed = "charset=utf-8;"`
|
||||
// `rest = " method=text/calendar"`
|
||||
//
|
||||
// `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 = ""`
|
||||
//
|
||||
// `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.
|
||||
//
|
||||
@@ -381,8 +408,8 @@ func fixUnquotedSpecials(s string) 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"
|
||||
// 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{}
|
||||
@@ -511,3 +538,11 @@ loop:
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func removeTypeSpecials(value string) string {
|
||||
for _, r := range []string{"(", ")", "<", ">", "@", ",", ":", "\\", "\"", "[", "]", "?", "="} {
|
||||
value = strings.ReplaceAll(value, r, "")
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
61
vendor/github.com/jhillyerd/enmime/options.go
generated
vendored
61
vendor/github.com/jhillyerd/enmime/options.go
generated
vendored
@@ -27,3 +27,64 @@ type multipartWOBoundaryAsSinglePartOption bool
|
||||
func (o multipartWOBoundaryAsSinglePartOption) apply(p *Parser) {
|
||||
p.multipartWOBoundaryAsSinglePart = bool(o)
|
||||
}
|
||||
|
||||
// SetReadPartErrorPolicy sets the given callback function to readPartErrorPolicy.
|
||||
func SetReadPartErrorPolicy(f ReadPartErrorPolicy) Option {
|
||||
return readPartErrorPolicyOption(f)
|
||||
}
|
||||
|
||||
type readPartErrorPolicyOption ReadPartErrorPolicy
|
||||
|
||||
func (o readPartErrorPolicyOption) apply(p *Parser) {
|
||||
p.readPartErrorPolicy = ReadPartErrorPolicy(o)
|
||||
}
|
||||
|
||||
// MaxStoredPartErrors limits number of part parsing errors, errors beyond the limit are discarded.
|
||||
// Zero, the default, means all errors will be kept.
|
||||
func MaxStoredPartErrors(n int) Option {
|
||||
return maxStoredPartErrorsOption(n)
|
||||
}
|
||||
|
||||
type maxStoredPartErrorsOption int
|
||||
|
||||
func (o maxStoredPartErrorsOption) apply(p *Parser) {
|
||||
max := int(o)
|
||||
p.maxStoredPartErrors = &max
|
||||
}
|
||||
|
||||
// RawContent if set to true will not try to decode the CTE and return the raw part content.
|
||||
// Otherwise, will try to automatically decode the CTE.
|
||||
func RawContent(a bool) Option {
|
||||
return rawContentOption(a)
|
||||
}
|
||||
|
||||
type rawContentOption bool
|
||||
|
||||
func (o rawContentOption) apply(p *Parser) {
|
||||
p.rawContent = bool(o)
|
||||
}
|
||||
|
||||
// SetCustomParseMediaType if provided, will be used to parse media type instead of the default ParseMediaType
|
||||
// function. This may be used to parse media type parameters that would otherwise be considered malformed.
|
||||
// By default parsing happens using ParseMediaType
|
||||
func SetCustomParseMediaType(customParseMediaType CustomParseMediaType) Option {
|
||||
return parseMediaTypeOption(customParseMediaType)
|
||||
}
|
||||
|
||||
type parseMediaTypeOption CustomParseMediaType
|
||||
|
||||
func (o parseMediaTypeOption) apply(p *Parser) {
|
||||
p.customParseMediaType = CustomParseMediaType(o)
|
||||
}
|
||||
|
||||
type stripMediaTypeInvalidCharactersOption bool
|
||||
|
||||
func (o stripMediaTypeInvalidCharactersOption) apply(p *Parser) {
|
||||
p.stripMediaTypeInvalidCharacters = bool(o)
|
||||
}
|
||||
|
||||
// StripMediaTypeInvalidCharacters sets stripMediaTypeInvalidCharacters option. If true, invalid characters
|
||||
// will be removed from media type during parsing.
|
||||
func StripMediaTypeInvalidCharacters(stripMediaTypeInvalidCharacters bool) Option {
|
||||
return stripMediaTypeInvalidCharactersOption(stripMediaTypeInvalidCharacters)
|
||||
}
|
||||
|
||||
23
vendor/github.com/jhillyerd/enmime/parser.go
generated
vendored
23
vendor/github.com/jhillyerd/enmime/parser.go
generated
vendored
@@ -1,10 +1,31 @@
|
||||
package enmime
|
||||
|
||||
// ReadPartErrorPolicy allows to recover the buffer (or not) on an error when reading a Part content.
|
||||
//
|
||||
// See AllowCorruptTextPartErrorPolicy for usage.
|
||||
type ReadPartErrorPolicy func(*Part, error) bool
|
||||
|
||||
// AllowCorruptTextPartErrorPolicy recovers partial content from base64.CorruptInputError when content type is text/plain or text/html.
|
||||
func AllowCorruptTextPartErrorPolicy(p *Part, err error) bool {
|
||||
if IsBase64CorruptInputError(err) && (p.ContentType == ctTextHTML || p.ContentType == ctTextPlain) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// CustomParseMediaType parses media type. See ParseMediaType for more details
|
||||
type CustomParseMediaType func(ctype string) (mtype string, params map[string]string, invalidParams []string, err error)
|
||||
|
||||
// Parser parses MIME.
|
||||
// Default parser is a valid one.
|
||||
type Parser struct {
|
||||
skipMalformedParts bool
|
||||
maxStoredPartErrors *int // TODO: Pointer until global var removed.
|
||||
multipartWOBoundaryAsSinglePart bool
|
||||
readPartErrorPolicy ReadPartErrorPolicy
|
||||
skipMalformedParts bool
|
||||
rawContent bool
|
||||
customParseMediaType CustomParseMediaType
|
||||
stripMediaTypeInvalidCharacters bool
|
||||
}
|
||||
|
||||
// defaultParser is a Parser with default configuration.
|
||||
|
||||
115
vendor/github.com/jhillyerd/enmime/part.go
generated
vendored
115
vendor/github.com/jhillyerd/enmime/part.go
generated
vendored
@@ -5,7 +5,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"mime/quotedprintable"
|
||||
"net/textproto"
|
||||
"strconv"
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/gogs/chardet"
|
||||
"github.com/jhillyerd/enmime/internal/coding"
|
||||
inttp "github.com/jhillyerd/enmime/internal/textproto"
|
||||
"github.com/jhillyerd/enmime/mediatype"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@@ -26,11 +27,11 @@ const (
|
||||
// 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.
|
||||
PartID string // PartID labels this part's 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.
|
||||
Header textproto.MIMEHeader // Header for this part.
|
||||
|
||||
Boundary string // Boundary marker used within this part.
|
||||
ContentID string // ContentID header for cid URL scheme.
|
||||
@@ -42,9 +43,14 @@ type Part struct {
|
||||
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.
|
||||
Errors []*Error // Errors encountered while parsing this part.
|
||||
Content []byte // Content after decoding, UTF-8 conversion if applicable.
|
||||
ContentReader io.Reader // Reader interface for pulling the content for encoding.
|
||||
Epilogue []byte // Epilogue contains data following the closing boundary marker.
|
||||
|
||||
parser *Parser // Provides access to parsing options.
|
||||
|
||||
randSource rand.Source // optional rand for uuid boundary generation
|
||||
}
|
||||
|
||||
// NewPart creates a new Part object.
|
||||
@@ -53,6 +59,7 @@ func NewPart(contentType string) *Part {
|
||||
Header: make(textproto.MIMEHeader),
|
||||
ContentType: contentType,
|
||||
ContentTypeParams: make(map[string]string),
|
||||
parser: &defaultParser,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +115,7 @@ func (p *Part) setupHeaders(r *bufio.Reader, defaultContentType string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Header = header
|
||||
p.Header = textproto.MIMEHeader(header)
|
||||
ctype := header.Get(hnContentType)
|
||||
if ctype == "" {
|
||||
if defaultContentType == "" {
|
||||
@@ -118,7 +125,7 @@ func (p *Part) setupHeaders(r *bufio.Reader, defaultContentType string) error {
|
||||
ctype = defaultContentType
|
||||
}
|
||||
// Parse Content-Type header.
|
||||
mtype, mparams, minvalidParams, err := mediatype.Parse(ctype)
|
||||
mtype, mparams, minvalidParams, err := p.parseMediaType(ctype)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -139,8 +146,9 @@ func (p *Part) setupHeaders(r *bufio.Reader, defaultContentType string) error {
|
||||
// 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) {
|
||||
header := inttp.MIMEHeader(p.Header)
|
||||
// Determine content disposition, filename, character set.
|
||||
disposition, dparams, _, err := mediatype.Parse(p.Header.Get(hnContentDisposition))
|
||||
disposition, dparams, _, err := p.parseMediaType(header.Get(hnContentDisposition))
|
||||
if err == nil {
|
||||
// Disposition is optional
|
||||
p.Disposition = disposition
|
||||
@@ -160,11 +168,23 @@ func (p *Part) setupContentHeaders(mediaParams map[string]string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Part) readPartContent(r io.Reader, readPartErrorPolicy ReadPartErrorPolicy) ([]byte, error) {
|
||||
buf, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
if readPartErrorPolicy != nil && readPartErrorPolicy(p, err) {
|
||||
p.addWarning(ErrorMalformedChildPart, "partial content: %s", err.Error())
|
||||
return buf, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
func (p *Part) convertFromDetectedCharset(r io.Reader, readPartErrorPolicy ReadPartErrorPolicy) (io.Reader, error) {
|
||||
// Attempt to detect character set from part content.
|
||||
var cd *chardet.Detector
|
||||
switch p.ContentType {
|
||||
@@ -174,7 +194,7 @@ func (p *Part) convertFromDetectedCharset(r io.Reader) (io.Reader, error) {
|
||||
cd = chardet.NewTextDetector()
|
||||
}
|
||||
|
||||
buf, err := ioutil.ReadAll(r)
|
||||
buf, err := p.readPartContent(r, readPartErrorPolicy)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
@@ -249,13 +269,17 @@ func (p *Part) convertFromStatedCharset(r io.Reader) io.Reader {
|
||||
// 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 {
|
||||
func (p *Part) decodeContent(r io.Reader, readPartErrorPolicy ReadPartErrorPolicy) error {
|
||||
header := inttp.MIMEHeader(p.Header)
|
||||
// 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)
|
||||
encoding := ""
|
||||
if p.parser != nil && !p.parser.rawContent {
|
||||
encoding = header.Get(hnContentEncoding)
|
||||
}
|
||||
validEncoding := true
|
||||
switch strings.ToLower(encoding) {
|
||||
case cteQuotedPrintable:
|
||||
@@ -275,15 +299,15 @@ func (p *Part) decodeContent(r io.Reader) error {
|
||||
encoding)
|
||||
}
|
||||
// Build charset decoding reader.
|
||||
if validEncoding && strings.HasPrefix(p.ContentType, "text/") {
|
||||
if validEncoding && strings.HasPrefix(p.ContentType, "text/") && !p.parser.rawContent {
|
||||
var err error
|
||||
contentReader, err = p.convertFromDetectedCharset(contentReader)
|
||||
contentReader, err = p.convertFromDetectedCharset(contentReader, readPartErrorPolicy)
|
||||
if err != nil {
|
||||
return p.base64CorruptInputCheck(err)
|
||||
}
|
||||
}
|
||||
// Decode and store content.
|
||||
content, err := ioutil.ReadAll(contentReader)
|
||||
content, err := p.readPartContent(contentReader, readPartErrorPolicy)
|
||||
if err != nil {
|
||||
return p.base64CorruptInputCheck(errors.WithStack(err))
|
||||
}
|
||||
@@ -302,18 +326,37 @@ func (p *Part) decodeContent(r io.Reader) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// parses media type using custom or default media type parser
|
||||
func (p *Part) parseMediaType(ctype string) (mtype string, params map[string]string, invalidParams []string, err error) {
|
||||
if p.parser == nil || p.parser.customParseMediaType == nil {
|
||||
return mediatype.ParseWithOptions(ctype, mediatype.MediaTypeParseOptions{StripMediaTypeInvalidCharacters: p.parser.stripMediaTypeInvalidCharacters})
|
||||
}
|
||||
|
||||
return p.parser.customParseMediaType(ctype)
|
||||
}
|
||||
|
||||
// IsBase64CorruptInputError returns true when err is of type base64.CorruptInputError.
|
||||
//
|
||||
// It can be used to create ReadPartErrorPolicy functions.
|
||||
func IsBase64CorruptInputError(err error) bool {
|
||||
switch errors.Cause(err).(type) {
|
||||
case base64.CorruptInputError:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 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:
|
||||
if IsBase64CorruptInputError(err) {
|
||||
p.Content = nil
|
||||
p.addError(ErrorMalformedBase64, err.Error())
|
||||
return nil
|
||||
default:
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Clone returns a clone of the current Part.
|
||||
@@ -350,22 +393,21 @@ func ReadParts(r io.Reader) (*Part, error) {
|
||||
// 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"}
|
||||
root := &Part{PartID: "0", parser: &p}
|
||||
|
||||
// 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 {
|
||||
if err := root.setupHeaders(br, `text/plain; charset="us-ascii"`); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if detectMultipartMessage(root, p.multipartWOBoundaryAsSinglePart) {
|
||||
// Content is multipart, parse it.
|
||||
err = parseParts(root, br, p.skipMalformedParts)
|
||||
if err != nil {
|
||||
if err := parseParts(root, br); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// Content is text or data, decode it.
|
||||
if err := root.decodeContent(br); err != nil {
|
||||
if err := root.decodeContent(br, p.readPartErrorPolicy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -373,7 +415,7 @@ func (p Parser) ReadParts(r io.Reader) (*Part, error) {
|
||||
}
|
||||
|
||||
// parseParts recursively parses a MIME multipart document and sets each Parts PartID.
|
||||
func parseParts(parent *Part, reader *bufio.Reader, skipMalformedParts bool) error {
|
||||
func parseParts(parent *Part, reader *bufio.Reader) error {
|
||||
firstRecursion := parent.Parent == nil
|
||||
// Loop over MIME boundaries.
|
||||
br := newBoundaryReader(reader, parent.Boundary)
|
||||
@@ -389,28 +431,31 @@ func parseParts(parent *Part, reader *bufio.Reader, skipMalformedParts bool) err
|
||||
if !next {
|
||||
break
|
||||
}
|
||||
p := &Part{}
|
||||
|
||||
// Set this Part's PartID, indicating its position within the MIME Part tree.
|
||||
p := &Part{parser: parent.parser}
|
||||
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 {
|
||||
if p.parser.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 {
|
||||
if err = p.decodeContent(bbr, p.parser.readPartErrorPolicy); err != nil {
|
||||
if p.parser.skipMalformedParts {
|
||||
parent.addError(ErrorMalformedChildPart, "decode content: %s", err.Error())
|
||||
continue
|
||||
}
|
||||
@@ -422,20 +467,22 @@ func parseParts(parent *Part, reader *bufio.Reader, skipMalformedParts bool) err
|
||||
|
||||
parent.AddChild(p)
|
||||
// Content is another multipart.
|
||||
if err = parseParts(p, bbr, skipMalformedParts); err != nil {
|
||||
if skipMalformedParts {
|
||||
if err = parseParts(p, bbr); err != nil {
|
||||
if p.parser.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)
|
||||
epilogue, err := io.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 {
|
||||
|
||||
13
vendor/github.com/jhillyerd/enmime/shell.nix
generated
vendored
13
vendor/github.com/jhillyerd/enmime/shell.nix
generated
vendored
@@ -1,10 +1,11 @@
|
||||
with import <nixpkgs> {};
|
||||
stdenv.mkDerivation rec {
|
||||
name = "env";
|
||||
env = buildEnv { name = name; paths = buildInputs; };
|
||||
buildInputs = [
|
||||
go
|
||||
{ pkgs ? import <nixpkgs> { } }:
|
||||
pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
delve
|
||||
go_1_20
|
||||
golint
|
||||
gopls
|
||||
];
|
||||
|
||||
hardeningDisable = [ "fortify" ];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user