chore: handle more encodes & fix idx range
This commit is contained in:
+2
-1
@@ -1,6 +1,6 @@
|
||||
module lanqin-email-api
|
||||
|
||||
go 1.22
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/go-chi/chi/v5 v5.1.0
|
||||
@@ -20,6 +20,7 @@ require (
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
golang.org/x/net v0.26.0 // indirect
|
||||
golang.org/x/sys v0.23.0 // indirect
|
||||
golang.org/x/text v0.38.0 // indirect
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
||||
modernc.org/libc v1.55.3 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
|
||||
@@ -26,13 +26,17 @@ golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
|
||||
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
|
||||
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
|
||||
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
|
||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
|
||||
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
|
||||
|
||||
@@ -17,6 +17,9 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/encoding"
|
||||
"golang.org/x/text/encoding/ianaindex"
|
||||
)
|
||||
|
||||
type maildirMailbox struct {
|
||||
@@ -350,8 +353,7 @@ func (a *App) parseMaildirMessage(raw []byte, fallbackTo string) (storedMessage,
|
||||
if err != nil {
|
||||
return storedMessage{}, nil, err
|
||||
}
|
||||
decoder := new(mime.WordDecoder)
|
||||
subject, _ := decoder.DecodeHeader(m.Header.Get("Subject"))
|
||||
subject := decodeMIMEHeader(m.Header.Get("Subject"))
|
||||
if strings.TrimSpace(subject) == "" {
|
||||
subject = "(no subject)"
|
||||
}
|
||||
@@ -460,42 +462,31 @@ func transferReader(encoding string, r io.Reader) io.Reader {
|
||||
}
|
||||
|
||||
func partFilename(header textproto.MIMEHeader) string {
|
||||
decoder := new(mime.WordDecoder)
|
||||
if _, params, err := mime.ParseMediaType(header.Get("Content-Disposition")); err == nil {
|
||||
if name := strings.TrimSpace(params["filename"]); name != "" {
|
||||
decoded, _ := decoder.DecodeHeader(name)
|
||||
if decoded != "" {
|
||||
name = decoded
|
||||
}
|
||||
return filepath.Base(name)
|
||||
return filepath.Base(decodeMIMEHeader(name))
|
||||
}
|
||||
}
|
||||
if _, params, err := mime.ParseMediaType(header.Get("Content-Type")); err == nil {
|
||||
if name := strings.TrimSpace(params["name"]); name != "" {
|
||||
decoded, _ := decoder.DecodeHeader(name)
|
||||
if decoded != "" {
|
||||
name = decoded
|
||||
}
|
||||
return filepath.Base(name)
|
||||
return filepath.Base(decodeMIMEHeader(name))
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func firstAddressParts(value string) (string, string) {
|
||||
items, err := netmail.ParseAddressList(value)
|
||||
// Proactively decode RFC 2047 encoded words before parsing, so that
|
||||
// non-UTF-8 charsets (e.g. GBK, Shift_JIS) are handled by our
|
||||
// CharsetReader instead of Go's default WordDecoder which only
|
||||
// supports UTF-8 and ISO-8859-1.
|
||||
decoded := decodeMIMEHeader(value)
|
||||
items, err := netmail.ParseAddressList(decoded)
|
||||
if err != nil || len(items) == 0 {
|
||||
// ParseAddressList failed — attempt RFC 2047 decode on the raw header,
|
||||
// then retry parsing. This handles non-standard From headers where
|
||||
// encoded words (e.g. =?UTF-8?B?…?=) cause the initial parse to fail.
|
||||
decoded := decodeMIMEHeader(value)
|
||||
items, err = netmail.ParseAddressList(decoded)
|
||||
if err != nil || len(items) == 0 {
|
||||
// Still unparseable: return the decoded value as the email and
|
||||
// try to extract a display name from the decoded string.
|
||||
email, name := splitNameAndEmail(decoded)
|
||||
return normalizeEmail(email), strings.TrimSpace(name)
|
||||
}
|
||||
// Still unparseable: return the decoded value and try to extract
|
||||
// a display name from the decoded string.
|
||||
email, name := splitNameAndEmail(decoded)
|
||||
return normalizeEmail(email), strings.TrimSpace(name)
|
||||
}
|
||||
item := items[0]
|
||||
return normalizeEmail(item.Address), strings.TrimSpace(item.Name)
|
||||
@@ -503,11 +494,14 @@ func firstAddressParts(value string) (string, string) {
|
||||
|
||||
// decodeMIMEHeader decodes all RFC 2047 encoded words (=?charset?encoding?data?=)
|
||||
// in the given header value. Falls back to the original value on any error.
|
||||
// Supports non-UTF-8 charsets (e.g. GBK, GB2312, Shift_JIS) via x/text.
|
||||
func decodeMIMEHeader(value string) string {
|
||||
if !strings.Contains(value, "=?") {
|
||||
return value
|
||||
}
|
||||
decoder := new(mime.WordDecoder)
|
||||
decoder := &mime.WordDecoder{
|
||||
CharsetReader: charsetReader,
|
||||
}
|
||||
decoded, err := decoder.DecodeHeader(value)
|
||||
if err != nil {
|
||||
return value
|
||||
@@ -515,6 +509,22 @@ func decodeMIMEHeader(value string) string {
|
||||
return decoded
|
||||
}
|
||||
|
||||
// charsetReader converts a non-UTF-8 charset stream into UTF-8 using x/text encodings.
|
||||
func charsetReader(charset string, input io.Reader) (io.Reader, error) {
|
||||
charset = strings.ToLower(strings.TrimSpace(charset))
|
||||
if charset == "utf-8" || charset == "us-ascii" {
|
||||
return input, nil
|
||||
}
|
||||
enc, err := ianaindex.IANA.Encoding(charset)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unsupported charset %q: %w", charset, err)
|
||||
}
|
||||
if enc == encoding.Nop || enc == encoding.Replacement {
|
||||
return nil, fmt.Errorf("unsupported charset %q", charset)
|
||||
}
|
||||
return enc.NewDecoder().Reader(input), nil
|
||||
}
|
||||
|
||||
// splitNameAndEmail attempts to extract a display name and email address from
|
||||
// a string like "Display Name <user@example.com>" or plain "user@example.com".
|
||||
func splitNameAndEmail(value string) (string, string) {
|
||||
@@ -523,7 +533,7 @@ func splitNameAndEmail(value string) (string, string) {
|
||||
return "", ""
|
||||
}
|
||||
// Try "Name <email>" pattern
|
||||
if idx := strings.LastIndex(value, "<"); idx > 0 {
|
||||
if idx := strings.LastIndex(value, "<"); idx >= 0 {
|
||||
email := strings.TrimRight(value[idx+1:], ">")
|
||||
name := strings.TrimSpace(strings.Trim(value[:idx], `" `))
|
||||
if strings.Contains(email, "@") {
|
||||
@@ -539,8 +549,13 @@ func splitNameAndEmail(value string) (string, string) {
|
||||
|
||||
func addressList(value string) []string {
|
||||
items, err := netmail.ParseAddressList(value)
|
||||
if err != nil {
|
||||
return nil
|
||||
if err != nil || len(items) == 0 {
|
||||
// ParseAddressList failed — attempt RFC 2047 decode, then retry.
|
||||
decoded := decodeMIMEHeader(value)
|
||||
items, err = netmail.ParseAddressList(decoded)
|
||||
if err != nil || len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
out := make([]string, 0, len(items))
|
||||
for _, item := range items {
|
||||
|
||||
Reference in New Issue
Block a user