upgrade deps; rewrite smtp session

This commit is contained in:
Aine
2024-02-19 22:55:14 +02:00
parent 10213cc7d7
commit a01720da00
277 changed files with 106832 additions and 7641 deletions

1
vendor/github.com/jhillyerd/enmime/.envrc generated vendored Normal file
View File

@@ -0,0 +1 @@
use flake

View File

@@ -27,5 +27,7 @@ _testmain.go
# vim swp files
*.swp
/.direnv
cmd/mime-dump/mime-dump
cmd/mime-extractor/mime-extractor

View File

@@ -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.

View File

@@ -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.

View File

@@ -1,8 +1,8 @@
# enmime
[![PkgGoDev](https://pkg.go.dev/badge/github.com/jhillyerd/enmime)][Pkg Docs]
[![Build Status](https://travis-ci.org/jhillyerd/enmime.svg?branch=master)][Build Status]
[![Build and Test](https://github.com/jhillyerd/enmime/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/jhillyerd/enmime/actions/workflows/build-and-test.yml)
[![Go Report Card](https://goreportcard.com/badge/github.com/jhillyerd/enmime)][Go Report Card]
[![Coverage Status](https://coveralls.io/repos/github/jhillyerd/enmime/badge.svg?branch=master)][Coverage Status]
[![Coverage Status](https://coveralls.io/repos/github/jhillyerd/enmime/badge.svg?branch=main)][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

View File

@@ -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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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
View 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
View 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 { };
}
);
}

View File

@@ -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?

View File

@@ -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))
}
}

View File

@@ -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
}

View File

@@ -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

View 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)
}

View File

@@ -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

View 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))
}

View 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)
}
}

View 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
}
}

View 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
}

View 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'
}

View 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()
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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.

View File

@@ -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 {

View File

@@ -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" ];
}