198 lines
4.7 KiB
Go
198 lines
4.7 KiB
Go
|
// headers.go implements "Q" encoding as specified by RFC 2047.
|
||
|
//Modified from https://github.com/joegrasse/mime to use with Go Simple Mail
|
||
|
|
||
|
package mail
|
||
|
|
||
|
import (
|
||
|
"bufio"
|
||
|
"bytes"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"strings"
|
||
|
"unicode/utf8"
|
||
|
)
|
||
|
|
||
|
type encoder struct {
|
||
|
w *bufio.Writer
|
||
|
charset string
|
||
|
usedChars int
|
||
|
}
|
||
|
|
||
|
// newEncoder returns a new mime header encoder that writes to w. The c
|
||
|
// parameter specifies the name of the character set of the text that will be
|
||
|
// encoded. The u parameter indicates how many characters have been used
|
||
|
// already.
|
||
|
func newEncoder(w io.Writer, c string, u int) *encoder {
|
||
|
return &encoder{bufio.NewWriter(w), strings.ToUpper(c), u}
|
||
|
}
|
||
|
|
||
|
// encode encodes p using the "Q" encoding and writes it to the underlying
|
||
|
// io.Writer. It limits line length to 75 characters.
|
||
|
func (e *encoder) encode(p []byte) (n int, err error) {
|
||
|
var output bytes.Buffer
|
||
|
allPrintable := true
|
||
|
|
||
|
// some lines we encode end in "
|
||
|
//maxLineLength := 75 - 1
|
||
|
maxLineLength := 76
|
||
|
|
||
|
// prevent header injection
|
||
|
p = secureHeader(p)
|
||
|
|
||
|
// check to see if we have all printable characters
|
||
|
for _, c := range p {
|
||
|
if !isVchar(c) && !isWSP(c) {
|
||
|
allPrintable = false
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// all characters are printable. just do line folding
|
||
|
if allPrintable {
|
||
|
text := string(p)
|
||
|
words := strings.Split(text, " ")
|
||
|
|
||
|
lineBuffer := ""
|
||
|
firstWord := true
|
||
|
|
||
|
// split the line where necessary
|
||
|
for _, word := range words {
|
||
|
/*fmt.Println("Current Line:",lineBuffer)
|
||
|
fmt.Println("Here: Max:", maxLineLength ,"Buffer Length:", len(lineBuffer), "Used Chars:", e.usedChars, "Length Encoded Char:",len(word))
|
||
|
fmt.Println("----------")*/
|
||
|
|
||
|
newWord := ""
|
||
|
if !firstWord {
|
||
|
newWord += " "
|
||
|
}
|
||
|
newWord += word
|
||
|
|
||
|
// check line length
|
||
|
if (e.usedChars+len(lineBuffer)+len(newWord) /*+len(" ")+len(word)*/) > maxLineLength && (lineBuffer != "" || e.usedChars != 0) {
|
||
|
output.WriteString(lineBuffer + "\r\n")
|
||
|
|
||
|
// first word on newline needs a space in front
|
||
|
if !firstWord {
|
||
|
lineBuffer = ""
|
||
|
} else {
|
||
|
lineBuffer = " "
|
||
|
}
|
||
|
|
||
|
//firstLine = false
|
||
|
//firstWord = true
|
||
|
// reset since not on the first line anymore
|
||
|
e.usedChars = 0
|
||
|
}
|
||
|
|
||
|
/*if !firstWord {
|
||
|
lineBuffer += " "
|
||
|
}*/
|
||
|
|
||
|
lineBuffer += newWord /*word*/
|
||
|
|
||
|
firstWord = false
|
||
|
|
||
|
// reset since not on the first line anymore
|
||
|
/*if !firstLine {
|
||
|
e.usedChars = 0
|
||
|
}*/
|
||
|
}
|
||
|
|
||
|
output.WriteString(lineBuffer)
|
||
|
|
||
|
} else {
|
||
|
firstLine := true
|
||
|
|
||
|
// A single encoded word can not be longer than 75 characters
|
||
|
if e.usedChars == 0 {
|
||
|
maxLineLength = 75
|
||
|
}
|
||
|
|
||
|
wordBegin := "=?" + e.charset + "?Q?"
|
||
|
wordEnd := "?="
|
||
|
|
||
|
lineBuffer := wordBegin
|
||
|
|
||
|
for i := 0; i < len(p); {
|
||
|
// encode the character
|
||
|
encodedChar, runeLength := encode(p, i)
|
||
|
|
||
|
/*fmt.Println("Current Line:",lineBuffer)
|
||
|
fmt.Println("Here: Max:", maxLineLength ,"Buffer Length:", len(lineBuffer), "Used Chars:", e.usedChars, "Length Encoded Char:",len(encodedChar))
|
||
|
fmt.Println("----------")*/
|
||
|
|
||
|
// Check line length
|
||
|
if len(lineBuffer)+e.usedChars+len(encodedChar) > (maxLineLength - len(wordEnd)) {
|
||
|
output.WriteString(lineBuffer + wordEnd + "\r\n")
|
||
|
lineBuffer = " " + wordBegin
|
||
|
firstLine = false
|
||
|
}
|
||
|
|
||
|
lineBuffer += encodedChar
|
||
|
|
||
|
i = i + runeLength
|
||
|
|
||
|
// reset since not on the first line anymore
|
||
|
if !firstLine {
|
||
|
e.usedChars = 0
|
||
|
maxLineLength = 76
|
||
|
}
|
||
|
}
|
||
|
|
||
|
output.WriteString(lineBuffer + wordEnd)
|
||
|
}
|
||
|
|
||
|
e.w.Write(output.Bytes())
|
||
|
e.w.Flush()
|
||
|
n = output.Len()
|
||
|
|
||
|
return n, nil
|
||
|
}
|
||
|
|
||
|
// encode takes a string and position in that string and encodes one utf-8
|
||
|
// character. It then returns the encoded string and number of runes in the
|
||
|
// character.
|
||
|
func encode(text []byte, i int) (encodedString string, runeLength int) {
|
||
|
started := false
|
||
|
|
||
|
for ; i < len(text) && (!utf8.RuneStart(text[i]) || !started); i++ {
|
||
|
switch c := text[i]; {
|
||
|
case c == ' ':
|
||
|
encodedString += "_"
|
||
|
case isVchar(c) && c != '=' && c != '?' && c != '_':
|
||
|
encodedString += string(c)
|
||
|
default:
|
||
|
encodedString += fmt.Sprintf("=%02X", c)
|
||
|
}
|
||
|
|
||
|
runeLength++
|
||
|
|
||
|
started = true
|
||
|
}
|
||
|
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// secureHeader removes all unnecessary values to prevent
|
||
|
// header injection
|
||
|
func secureHeader(text []byte) []byte {
|
||
|
secureValue := strings.TrimSpace(string(text))
|
||
|
secureValue = strings.Replace(secureValue, "\r", "", -1)
|
||
|
secureValue = strings.Replace(secureValue, "\n", "", -1)
|
||
|
secureValue = strings.Replace(secureValue, "\t", "", -1)
|
||
|
|
||
|
return []byte(secureValue)
|
||
|
}
|
||
|
|
||
|
// isVchar returns true if c is an RFC 5322 VCHAR character.
|
||
|
func isVchar(c byte) bool {
|
||
|
// Visible (printing) characters.
|
||
|
return '!' <= c && c <= '~'
|
||
|
}
|
||
|
|
||
|
// isWSP returns true if c is a WSP (white space).
|
||
|
// WSP is a space or horizontal tab (RFC5234 Appendix B).
|
||
|
func isWSP(c byte) bool {
|
||
|
return c == ' ' || c == '\t'
|
||
|
}
|