101 lines
2.2 KiB
Go
101 lines
2.2 KiB
Go
package imap
|
|
|
|
import (
|
|
"bytes"
|
|
"mime"
|
|
"net/mail"
|
|
"regexp"
|
|
"strings"
|
|
"unicode"
|
|
)
|
|
|
|
var (
|
|
htmlTitleRE = regexp.MustCompile(`(?is)<title[^>]*>\s*([^<]+?)\s*</title>`)
|
|
qpHexSeqRE = regexp.MustCompile(`=[0-9A-Fa-f]{2}`)
|
|
)
|
|
|
|
var mimeWordDecoder mime.WordDecoder
|
|
|
|
// RepairSubject decodes RFC 2047 / broken envelope subjects using headers or body fallbacks.
|
|
func RepairSubject(subject string, bodyText, bodyHTML string, raw []byte) string {
|
|
if s := decodeMIMEHeaderValue(subject); !subjectLooksBroken(s) {
|
|
return s
|
|
}
|
|
if len(raw) > 0 {
|
|
if hdr := subjectFromRawMessage(raw); hdr != "" && !subjectLooksBroken(hdr) {
|
|
return hdr
|
|
}
|
|
}
|
|
decodedHTML := decodeBareQuotedPrintableIfNeeded(bodyHTML)
|
|
decodedText := decodeBareQuotedPrintableIfNeeded(bodyText)
|
|
if t := extractSubjectFromHTML(decodedHTML); t != "" {
|
|
return t
|
|
}
|
|
if fallback := subjectFromBodyFallback(decodedText, decodedHTML); fallback != "" {
|
|
return fallback
|
|
}
|
|
return decodeMIMEHeaderValue(subject)
|
|
}
|
|
|
|
func decodeMIMEHeaderValue(s string) string {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
return s
|
|
}
|
|
dec, err := mimeWordDecoder.DecodeHeader(s)
|
|
if err != nil {
|
|
return toValidUTF8(s)
|
|
}
|
|
return toValidUTF8(dec)
|
|
}
|
|
|
|
func subjectFromRawMessage(raw []byte) string {
|
|
msg, err := mail.ReadMessage(bytes.NewReader(raw))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return decodeMIMEHeaderValue(msg.Header.Get("Subject"))
|
|
}
|
|
|
|
func extractSubjectFromHTML(html string) string {
|
|
html = strings.TrimSpace(html)
|
|
if html == "" {
|
|
return ""
|
|
}
|
|
if m := htmlTitleRE.FindStringSubmatch(html); len(m) > 1 {
|
|
t := strings.TrimSpace(m[1])
|
|
if t != "" && !subjectLooksBroken(t) {
|
|
return t
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func subjectFromBodyFallback(text, html string) string {
|
|
plain := strings.TrimSpace(text)
|
|
if plain == "" {
|
|
plain = strings.TrimSpace(stripHTMLForSnippet(html))
|
|
}
|
|
if plain == "" || subjectLooksBroken(plain) {
|
|
return ""
|
|
}
|
|
if idx := strings.IndexAny(plain, ".\n\r"); idx >= 15 && idx <= 100 {
|
|
return truncate(strings.TrimSpace(plain[:idx]), 120)
|
|
}
|
|
return truncate(plain, 120)
|
|
}
|
|
|
|
func subjectLooksBroken(s string) bool {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
return true
|
|
}
|
|
letters := 0
|
|
for _, r := range s {
|
|
if unicode.IsLetter(r) || unicode.IsNumber(r) {
|
|
letters++
|
|
}
|
|
}
|
|
return letters < 2
|
|
}
|