go-simple-mail/header.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'
}