318 lines
7.6 KiB
Go
318 lines
7.6 KiB
Go
package enmime
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/base64"
|
|
"io"
|
|
"mime"
|
|
"mime/quotedprintable"
|
|
"net/textproto"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/jhillyerd/enmime/internal/coding"
|
|
"github.com/jhillyerd/enmime/internal/stringutil"
|
|
)
|
|
|
|
// b64Percent determines the percent of non-ASCII characters enmime will tolerate before switching
|
|
// from quoted-printable to base64 encoding.
|
|
const b64Percent = 20
|
|
|
|
type transferEncoding byte
|
|
|
|
const (
|
|
te7Bit transferEncoding = iota
|
|
teQuoted
|
|
teBase64
|
|
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'}
|
|
|
|
// Encode writes this Part and all its children to the specified writer in MIME format.
|
|
func (p *Part) Encode(writer io.Writer) error {
|
|
if p.Header == nil {
|
|
p.Header = make(textproto.MIMEHeader)
|
|
}
|
|
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)
|
|
if err := p.encodeHeader(b); err != nil {
|
|
return err
|
|
}
|
|
if len(p.Content) > 0 {
|
|
if _, err := b.Write(crnl); err != nil {
|
|
return err
|
|
}
|
|
if err := p.encodeContent(b, cte); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if p.FirstChild == nil {
|
|
return b.Flush()
|
|
}
|
|
// Encode children.
|
|
endMarker := []byte("\r\n--" + p.Boundary + "--")
|
|
marker := endMarker[:len(endMarker)-2]
|
|
c := p.FirstChild
|
|
for c != nil {
|
|
if _, err := b.Write(marker); err != nil {
|
|
return err
|
|
}
|
|
if _, err := b.Write(crnl); err != nil {
|
|
return err
|
|
}
|
|
if err := c.Encode(b); err != nil {
|
|
return err
|
|
}
|
|
c = c.NextSibling
|
|
}
|
|
if _, err := b.Write(endMarker); err != nil {
|
|
return err
|
|
}
|
|
if _, err := b.Write(crnl); err != nil {
|
|
return err
|
|
}
|
|
return b.Flush()
|
|
}
|
|
|
|
// setupMIMEHeaders determines content transfer encoding, generates a boundary string if required,
|
|
// then sets the Content-Type (type, charset, filename, boundary) and Content-Disposition headers.
|
|
func (p *Part) setupMIMEHeaders() transferEncoding {
|
|
// Determine content transfer encoding.
|
|
|
|
// If we are encoding a part that previously had content-transfer-encoding set, unset it so
|
|
// the correct encoding detection can be done below.
|
|
if p.parser != nil && !p.parser.rawContent {
|
|
p.Header.Del(hnContentEncoding)
|
|
}
|
|
|
|
cte := te7Bit
|
|
if len(p.Content) > 0 {
|
|
cte = teBase64
|
|
if p.TextContent() && p.ContentReader == nil {
|
|
cte = selectTransferEncoding(p.Content, false)
|
|
if p.Charset == "" {
|
|
p.Charset = utf8
|
|
}
|
|
}
|
|
// RFC 2045: 7bit is assumed if CTE header not present.
|
|
switch cte {
|
|
case teBase64:
|
|
p.Header.Set(hnContentEncoding, cteBase64)
|
|
case teQuoted:
|
|
p.Header.Set(hnContentEncoding, cteQuotedPrintable)
|
|
}
|
|
}
|
|
// Setup headers.
|
|
if p.FirstChild != nil && p.Boundary == "" {
|
|
// Multipart, generate random boundary marker.
|
|
p.Boundary = "enmime-" + stringutil.UUID(p.randSource)
|
|
}
|
|
if p.ContentID != "" {
|
|
p.Header.Set(hnContentID, coding.ToIDHeader(p.ContentID))
|
|
}
|
|
fileName := p.FileName
|
|
switch selectTransferEncoding([]byte(p.FileName), true) {
|
|
case teBase64:
|
|
fileName = mime.BEncoding.Encode(utf8, p.FileName)
|
|
case teQuoted:
|
|
fileName = mime.QEncoding.Encode(utf8, p.FileName)
|
|
}
|
|
if p.ContentType != "" {
|
|
// Build content type header.
|
|
param := make(map[string]string)
|
|
for k, v := range p.ContentTypeParams {
|
|
param[k] = v
|
|
}
|
|
setParamValue(param, hpCharset, p.Charset)
|
|
setParamValue(param, hpName, fileName)
|
|
setParamValue(param, hpBoundary, p.Boundary)
|
|
if mt := mime.FormatMediaType(p.ContentType, param); mt != "" {
|
|
p.ContentType = mt
|
|
}
|
|
p.Header.Set(hnContentType, p.ContentType)
|
|
}
|
|
if p.Disposition != "" {
|
|
// Build disposition header.
|
|
param := make(map[string]string)
|
|
setParamValue(param, hpFilename, fileName)
|
|
if !p.FileModDate.IsZero() {
|
|
setParamValue(param, hpModDate, p.FileModDate.UTC().Format(time.RFC822))
|
|
}
|
|
if mt := mime.FormatMediaType(p.Disposition, param); mt != "" {
|
|
p.Disposition = mt
|
|
}
|
|
p.Header.Set(hnContentDisposition, p.Disposition)
|
|
}
|
|
return cte
|
|
}
|
|
|
|
// encodeHeader writes out a sorted list of headers.
|
|
func (p *Part) encodeHeader(b *bufio.Writer) error {
|
|
keys := make([]string, 0, len(p.Header))
|
|
for k := range p.Header {
|
|
keys = append(keys, k)
|
|
}
|
|
rawContent := p.parser != nil && p.parser.rawContent
|
|
|
|
sort.Strings(keys)
|
|
for _, k := range keys {
|
|
for _, v := range p.Header[k] {
|
|
encv := 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")
|
|
wb[len(k)+1] = ' '
|
|
if _, err := b.Write(wb); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// encodeContent writes out the content in the selected encoding.
|
|
func (p *Part) encodeContent(b *bufio.Writer, cte transferEncoding) (err error) {
|
|
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)))
|
|
enc.Encode(text, p.Content)
|
|
// Wrap lines.
|
|
lineLen := 76
|
|
for len(text) > 0 {
|
|
if lineLen > len(text) {
|
|
lineLen = len(text)
|
|
}
|
|
if _, err = b.Write(text[:lineLen]); err != nil {
|
|
return err
|
|
}
|
|
if _, err := b.Write(crnl); err != nil {
|
|
return err
|
|
}
|
|
text = text[lineLen:]
|
|
}
|
|
case teQuoted:
|
|
qp := quotedprintable.NewWriter(b)
|
|
if _, err = qp.Write(p.Content); err != nil {
|
|
return err
|
|
}
|
|
err = qp.Close()
|
|
default:
|
|
_, err = b.Write(p.Content)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// 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 {
|
|
return te7Bit
|
|
}
|
|
// Binary chars remaining before we choose b64 encoding.
|
|
threshold := b64Percent * len(content) / 100
|
|
bincount := 0
|
|
for _, b := range content {
|
|
if (b < ' ' || '~' < b) && b != '\t' {
|
|
if !quoteLineBreaks && (b == '\r' || b == '\n') {
|
|
continue
|
|
}
|
|
bincount++
|
|
if bincount >= threshold {
|
|
return teBase64
|
|
}
|
|
}
|
|
}
|
|
if bincount == 0 {
|
|
return te7Bit
|
|
}
|
|
return teQuoted
|
|
}
|
|
|
|
// setParamValue will ignore empty values
|
|
func setParamValue(p map[string]string, k, v string) {
|
|
if v != "" {
|
|
p[k] = v
|
|
}
|
|
}
|