fix: decode RFC 2047 encoded words in mail sender display name
This commit is contained in:
@@ -485,12 +485,58 @@ func partFilename(header textproto.MIMEHeader) string {
|
|||||||
func firstAddressParts(value string) (string, string) {
|
func firstAddressParts(value string) (string, string) {
|
||||||
items, err := netmail.ParseAddressList(value)
|
items, err := netmail.ParseAddressList(value)
|
||||||
if err != nil || len(items) == 0 {
|
if err != nil || len(items) == 0 {
|
||||||
return strings.TrimSpace(value), ""
|
// 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
item := items[0]
|
item := items[0]
|
||||||
return normalizeEmail(item.Address), strings.TrimSpace(item.Name)
|
return normalizeEmail(item.Address), strings.TrimSpace(item.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// decodeMIMEHeader decodes all RFC 2047 encoded words (=?charset?encoding?data?=)
|
||||||
|
// in the given header value. Falls back to the original value on any error.
|
||||||
|
func decodeMIMEHeader(value string) string {
|
||||||
|
if !strings.Contains(value, "=?") {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
decoder := new(mime.WordDecoder)
|
||||||
|
decoded, err := decoder.DecodeHeader(value)
|
||||||
|
if err != nil {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return decoded
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
// Try "Name <email>" pattern
|
||||||
|
if idx := strings.LastIndex(value, "<"); idx > 0 {
|
||||||
|
email := strings.TrimRight(value[idx+1:], ">")
|
||||||
|
name := strings.TrimSpace(strings.Trim(value[:idx], `" `))
|
||||||
|
if strings.Contains(email, "@") {
|
||||||
|
return email, name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Plain email or unknown format
|
||||||
|
if strings.Contains(value, "@") {
|
||||||
|
return value, ""
|
||||||
|
}
|
||||||
|
return value, ""
|
||||||
|
}
|
||||||
|
|
||||||
func addressList(value string) []string {
|
func addressList(value string) []string {
|
||||||
items, err := netmail.ParseAddressList(value)
|
items, err := netmail.ParseAddressList(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -810,10 +810,34 @@ function escapeHtml(value: string) {
|
|||||||
return value.replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char] || char)
|
return value.replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char] || char)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode RFC 2047 encoded words in mail headers (e.g. =?UTF-8?B?5byA5ZSu?=).
|
||||||
|
* Handles Base64 (B) and Quoted-Printable (Q) encoding.
|
||||||
|
*/
|
||||||
|
function decodeMimeHeader(value: string): string {
|
||||||
|
if (!value || !value.includes("=?")) return value
|
||||||
|
return value.replace(/=\?([^?]+)\?([bBqQ])\?([^?]*)\?=/g, (_match, charset, encoding, encoded) => {
|
||||||
|
try {
|
||||||
|
const lowerEncoding = String(encoding).toLowerCase()
|
||||||
|
let decoded: string
|
||||||
|
if (lowerEncoding === "b") {
|
||||||
|
decoded = atob(encoded)
|
||||||
|
} else {
|
||||||
|
decoded = encoded.replace(/_/g, " ").replace(/=([0-9a-fA-F]{2})/g, (_m: string, hex: string) => String.fromCharCode(parseInt(hex, 16)))
|
||||||
|
}
|
||||||
|
const bytes = new Uint8Array(Array.from(decoded, (ch) => ch.charCodeAt(0)))
|
||||||
|
const decoder = new TextDecoder(String(charset).toLowerCase() || "utf-8")
|
||||||
|
return decoder.decode(bytes)
|
||||||
|
} catch {
|
||||||
|
return _match
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function adminSenderDisplayName(message: MailMessage) {
|
function adminSenderDisplayName(message: MailMessage) {
|
||||||
const fromName = message.fromName?.trim()
|
const fromName = decodeMimeHeader(message.fromName?.trim() || "")
|
||||||
if (fromName) return fromName
|
if (fromName) return fromName
|
||||||
const text = message.from.trim()
|
const text = decodeMimeHeader(message.from.trim())
|
||||||
const namedAddress = text.match(/^"?([^"<]+?)"?\s*<[^>]+>$/)
|
const namedAddress = text.match(/^"?([^"<]+?)"?\s*<[^>]+>$/)
|
||||||
const name = namedAddress?.[1]?.trim()
|
const name = namedAddress?.[1]?.trim()
|
||||||
if (name) return name
|
if (name) return name
|
||||||
@@ -822,8 +846,9 @@ function adminSenderDisplayName(message: MailMessage) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function adminSenderTitle(message: MailMessage) {
|
function adminSenderTitle(message: MailMessage) {
|
||||||
const name = message.fromName?.trim()
|
const name = decodeMimeHeader(message.fromName?.trim() || "")
|
||||||
return name ? `${name} <${message.from}>` : message.from
|
const from = decodeMimeHeader(message.from)
|
||||||
|
return name ? `${name} <${from}>` : from
|
||||||
}
|
}
|
||||||
|
|
||||||
function Stat({ icon, label, value }: { icon: React.ReactNode; label: string; value: React.ReactNode }) {
|
function Stat({ icon, label, value }: { icon: React.ReactNode; label: string; value: React.ReactNode }) {
|
||||||
|
|||||||
@@ -1293,6 +1293,31 @@ function MailboxSwitcher({ collapsed, mailboxes, selectedMailbox, onSelect }: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode RFC 2047 encoded words in mail headers (e.g. =?UTF-8?B?5byA5ZSu?=).
|
||||||
|
* Handles Base64 (B) and Quoted-Printable (Q) encoding.
|
||||||
|
* Returns the original string unchanged if no encoded words are found or on error.
|
||||||
|
*/
|
||||||
|
function decodeMimeHeader(value: string): string {
|
||||||
|
if (!value || !value.includes("=?")) return value
|
||||||
|
return value.replace(/=\?([^?]+)\?([bBqQ])\?([^?]*)\?=/g, (_match, charset, encoding, encoded) => {
|
||||||
|
try {
|
||||||
|
const lowerEncoding = String(encoding).toLowerCase()
|
||||||
|
let decoded: string
|
||||||
|
if (lowerEncoding === "b") {
|
||||||
|
decoded = atob(encoded)
|
||||||
|
} else {
|
||||||
|
decoded = encoded.replace(/_/g, " ").replace(/=([0-9a-fA-F]{2})/g, (_m: string, hex: string) => String.fromCharCode(parseInt(hex, 16)))
|
||||||
|
}
|
||||||
|
const bytes = new Uint8Array(Array.from(decoded, (ch) => ch.charCodeAt(0)))
|
||||||
|
const decoder = new TextDecoder(String(charset).toLowerCase() || "utf-8")
|
||||||
|
return decoder.decode(bytes)
|
||||||
|
} catch {
|
||||||
|
return _match
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function cleanAccountName(name: string, email?: string) {
|
function cleanAccountName(name: string, email?: string) {
|
||||||
const value = name.trim()
|
const value = name.trim()
|
||||||
if (!value || (email && value.toLowerCase() === email.toLowerCase())) return email?.split("@")[0] || "用户"
|
if (!value || (email && value.toLowerCase() === email.toLowerCase())) return email?.split("@")[0] || "用户"
|
||||||
@@ -1306,13 +1331,13 @@ function accountInitial(name: string, email?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function senderDisplayName(message: MailMessage) {
|
function senderDisplayName(message: MailMessage) {
|
||||||
const fromName = message.fromName?.trim()
|
const fromName = decodeMimeHeader(message.fromName?.trim() || "")
|
||||||
if (fromName) return fromName
|
if (fromName) return fromName
|
||||||
return displayNameFromAddress(message.from)
|
return displayNameFromAddress(message.from)
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayNameFromAddress(value: string) {
|
function displayNameFromAddress(value: string) {
|
||||||
const text = value.trim()
|
const text = decodeMimeHeader(value.trim())
|
||||||
const namedAddress = text.match(/^"?([^"<]+?)"?\s*<[^>]+>$/)
|
const namedAddress = text.match(/^"?([^"<]+?)"?\s*<[^>]+>$/)
|
||||||
const name = namedAddress?.[1]?.trim()
|
const name = namedAddress?.[1]?.trim()
|
||||||
if (name) return name
|
if (name) return name
|
||||||
@@ -1322,8 +1347,9 @@ function displayNameFromAddress(value: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function senderTitle(message: MailMessage) {
|
function senderTitle(message: MailMessage) {
|
||||||
const name = message.fromName?.trim()
|
const name = decodeMimeHeader(message.fromName?.trim() || "")
|
||||||
return name ? `${name} <${message.from}>` : message.from
|
const from = decodeMimeHeader(message.from)
|
||||||
|
return name ? `${name} <${from}>` : from
|
||||||
}
|
}
|
||||||
|
|
||||||
function MessageRow({
|
function MessageRow({
|
||||||
|
|||||||
Reference in New Issue
Block a user