mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-08 17:23:23 +03:00
214 lines
4.2 KiB
Go
214 lines
4.2 KiB
Go
package email
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"unicode"
|
|
)
|
|
|
|
const maxLineLength = 78
|
|
const continuePrefix = " "
|
|
const addressSeparator = ", "
|
|
|
|
type Composer struct {
|
|
isClosed bool
|
|
content strings.Builder
|
|
}
|
|
|
|
func NewComposer() *Composer {
|
|
return &Composer{}
|
|
}
|
|
|
|
type Address struct {
|
|
Name string
|
|
Email string
|
|
}
|
|
|
|
func (c *Composer) AddAddressHeader(name string, addresses []Address) {
|
|
c.content.WriteString(genAddressHeader(name, addresses, maxLineLength))
|
|
c.content.WriteString("\n")
|
|
}
|
|
|
|
func genAddressHeader(name string, addresses []Address, maxLength int) string {
|
|
hl := &headerLine{
|
|
maxLineLength: maxLength,
|
|
continuePrefix: continuePrefix,
|
|
}
|
|
|
|
hl.Write(name)
|
|
hl.Write(": ")
|
|
|
|
for i, addr := range addresses {
|
|
var email string
|
|
if i < len(addresses)-1 {
|
|
email = fmt.Sprintf("<%s>%s", addr.Email, addressSeparator)
|
|
} else {
|
|
email = fmt.Sprintf("<%s>", addr.Email)
|
|
}
|
|
writeHeaderQ(hl, addr.Name)
|
|
writeHeaderAtom(hl, " ")
|
|
writeHeaderAtom(hl, email)
|
|
}
|
|
hl.EndLine()
|
|
return hl.String()
|
|
}
|
|
|
|
func (c *Composer) AddHeader(name, value string) {
|
|
if isPrintableASCII(value) && len(value)+len(name)+len(": ") < maxLineLength {
|
|
c.AddHeaderRaw(name, value)
|
|
return
|
|
}
|
|
|
|
c.content.WriteString(genHeader(name, value, maxLineLength))
|
|
c.content.WriteString("\n")
|
|
}
|
|
|
|
func genHeader(name, value string, maxLength int) string {
|
|
// add content as raw header when it is printable ASCII and shorter than maxLineLength
|
|
hl := &headerLine{
|
|
maxLineLength: maxLength,
|
|
continuePrefix: continuePrefix,
|
|
}
|
|
|
|
hl.Write(name)
|
|
hl.Write(": ")
|
|
writeHeaderQ(hl, value)
|
|
hl.EndLine()
|
|
return hl.String()
|
|
}
|
|
|
|
const qEncStart = "=?utf-8?q?"
|
|
const qEncEnd = "?="
|
|
|
|
type headerLine struct {
|
|
buffer strings.Builder
|
|
line strings.Builder
|
|
maxLineLength int
|
|
continuePrefix string
|
|
}
|
|
|
|
func (h *headerLine) FitsLine(length int) bool {
|
|
return h.line.Len()+len(h.continuePrefix)+length+2 < h.maxLineLength
|
|
}
|
|
|
|
func (h *headerLine) Write(str string) {
|
|
h.line.WriteString(str)
|
|
}
|
|
|
|
func (h *headerLine) EndLineWith(str string) {
|
|
h.line.WriteString(str)
|
|
h.EndLine()
|
|
}
|
|
|
|
func (h *headerLine) EndLine() {
|
|
if h.line.Len() == 0 {
|
|
return
|
|
}
|
|
|
|
if h.buffer.Len() != 0 {
|
|
h.buffer.WriteString("\n")
|
|
h.buffer.WriteString(h.continuePrefix)
|
|
}
|
|
h.buffer.WriteString(h.line.String())
|
|
h.line.Reset()
|
|
}
|
|
|
|
func (h *headerLine) String() string {
|
|
return h.buffer.String()
|
|
}
|
|
|
|
func writeHeaderQ(header *headerLine, value string) {
|
|
|
|
// current line does not fit event the first character - do \n
|
|
if !header.FitsLine(len(qEncStart) + len(convertRunes(value[0:1])[0]) + len(qEncEnd)) {
|
|
header.EndLineWith("")
|
|
}
|
|
|
|
header.Write(qEncStart)
|
|
|
|
for _, token := range convertRunes(value) {
|
|
if header.FitsLine(len(token) + len(qEncEnd)) {
|
|
header.Write(token)
|
|
} else {
|
|
header.EndLineWith(qEncEnd)
|
|
header.Write(qEncStart)
|
|
header.Write(token)
|
|
}
|
|
}
|
|
|
|
header.Write(qEncEnd)
|
|
}
|
|
|
|
func writeHeaderAtom(header *headerLine, value string) {
|
|
if !header.FitsLine(len(value)) {
|
|
header.EndLine()
|
|
}
|
|
header.Write(value)
|
|
}
|
|
|
|
func (c *Composer) AddHeaderRaw(name, value string) {
|
|
if c.isClosed {
|
|
panic("composer had already written body!")
|
|
}
|
|
header := fmt.Sprintf("%s: %s\n", name, value)
|
|
c.content.WriteString(header)
|
|
}
|
|
|
|
func (c *Composer) Body(body string) {
|
|
c.content.WriteString("\n")
|
|
c.content.WriteString(body)
|
|
c.isClosed = true
|
|
}
|
|
|
|
func (c *Composer) String() string {
|
|
return c.content.String()
|
|
}
|
|
|
|
func convertRunes(str string) []string {
|
|
var enc = make([]string, 0, len(str))
|
|
for _, r := range []rune(str) {
|
|
if r == ' ' {
|
|
enc = append(enc, "_")
|
|
} else if isPrintableASCIIRune(r) &&
|
|
r != '=' &&
|
|
r != '?' &&
|
|
r != '_' {
|
|
enc = append(enc, string(r))
|
|
} else {
|
|
enc = append(enc, string(toHex([]byte(string(r)))))
|
|
}
|
|
}
|
|
return enc
|
|
}
|
|
|
|
func toHex(in []byte) []byte {
|
|
enc := make([]byte, 0, len(in)*2)
|
|
for _, b := range in {
|
|
enc = append(enc, '=')
|
|
enc = append(enc, hex(b/16))
|
|
enc = append(enc, hex(b%16))
|
|
}
|
|
return enc
|
|
}
|
|
|
|
func hex(n byte) byte {
|
|
if n > 9 {
|
|
return n + (65 - 10)
|
|
} else {
|
|
return n + 48
|
|
}
|
|
}
|
|
|
|
func isPrintableASCII(str string) bool {
|
|
for _, r := range []rune(str) {
|
|
if !unicode.IsPrint(r) || r >= unicode.MaxASCII {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isPrintableASCIIRune(r rune) bool {
|
|
return r > 31 && r < 127
|
|
}
|