chore: handle more encodes & fix idx range

This commit is contained in:
killerprojecte
2026-06-19 22:15:00 +08:00
parent f0c175e7f6
commit d312076d4b
6 changed files with 108 additions and 81 deletions
+56
View File
@@ -25,3 +25,59 @@ export function formatBytes(bytes: number) {
while (size >= 1024 && idx < units.length - 1) { size /= 1024; idx++ }
return `${size.toFixed(idx === 0 ? 0 : 1)} ${units[idx]}`
}
/**
* Normalize charset aliases to canonical names supported by the browser's TextDecoder.
* Only contains aliases that differ from their canonical name — standard names like
* "gb18030", "big5", "euc-kr" are passed through directly to TextDecoder.
*/
function normalizeCharset(charset: string): string {
const c = charset.toLowerCase().trim()
const aliases: Record<string, string> = {
"gb2312": "gbk",
"x-gbk": "gbk",
"euc-cn": "gbk",
"hz-gb-2312": "gbk",
"shift-jis": "shift_jis",
"sjis": "shift_jis",
"windows-31j": "shift_jis",
"ks_c_5601-1987": "euc-kr",
"ksc5601": "euc-kr",
"windows-949": "euc-kr",
"iso-8859-1": "windows-1252",
}
const mapped = aliases[c]
if (mapped) return mapped
// cpXXX / cpXXX windows code pages: cp936→windows-936, cp943→windows-943, etc.
if (/^cp\d+$/.test(c)) return "windows-" + c.slice(2)
return c
}
/**
* Decode RFC 2047 encoded words in mail headers (e.g. =?UTF-8?B?5byA5ZSu?=).
* Handles Base64 (B) and Quoted-Printable (Q) encoding.
* Supports non-UTF-8 charsets (e.g. GBK, GB2312, Shift_JIS) via charset alias normalization.
* Returns the original string unchanged if no encoded words are found or on error.
*/
export 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") {
const sanitized = encoded.replace(/\s+/g, "")
const padded = sanitized + "=".repeat((4 - (sanitized.length % 4)) % 4)
decoded = atob(padded)
} 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 normalized = normalizeCharset(charset) || "utf-8"
const decoder = new TextDecoder(normalized)
return decoder.decode(bytes)
} catch {
return _match
}
})
}
+1 -25
View File
@@ -4,7 +4,7 @@ import { useSearchParams } from "react-router-dom"
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { ArrowRight, CheckCircle2, Circle, Copy, Globe2, Mailbox, MoreHorizontal, Plus, RefreshCcw, Search, ShieldCheck, Trash2, Users } from "lucide-react"
import { api, AdminUser, Alias, DNSRecord, Domain, Mailbox as MailboxType, MailMessage, MailTemplate, SystemSettings } from "@/lib/api"
import { cn, formatBytes, formatDate } from "@/lib/utils"
import { cn, decodeMimeHeader, formatBytes, formatDate } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
@@ -810,30 +810,6 @@ function escapeHtml(value: string) {
return value.replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[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) {
const fromName = decodeMimeHeader(message.fromName?.trim() || "")
if (fromName) return fromName
+1 -26
View File
@@ -14,7 +14,7 @@ import { useNavigate } from "react-router-dom"
import type { ImperativePanelHandle } from "react-resizable-panels"
import { AlignCenter, AlignLeft, AlignRight, Archive, ArrowLeft, Bold, Calendar, Check, ChevronDown, ChevronsUpDown, Clock3, Code2, Copy, Ellipsis, Eraser, Eye, FileText, Forward, Highlighter, Image, Inbox, IndentDecrease, IndentIncrease, Italic, Link, List, ListOrdered, Mail, MailCheck, Moon, PanelLeftClose, PanelLeftOpen, Paperclip, PencilLine, Plus, Quote, Redo2, RefreshCcw, Reply, Search, Send, Settings, Signature, SlidersHorizontal, Smile, Star, Strikethrough, Sun, Tag, Trash2, Type, Underline, Undo2, X } from "lucide-react"
import { api, Mailbox, MailFolder, MailLabel, MailMessage, SendPayload, DraftPayload, ScheduledSend } from "@/lib/api"
import { cn, formatBytes, formatDate, formatDateTime } from "@/lib/utils"
import { cn, decodeMimeHeader, formatBytes, formatDate, formatDateTime } from "@/lib/utils"
import { applyTheme, getInitialTheme } from "@/lib/theme"
import { useDisplayMode } from "@/lib/display-mode"
import { Button } from "@/components/ui/button"
@@ -1293,31 +1293,6 @@ 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) {
const value = name.trim()
if (!value || (email && value.toLowerCase() === email.toLowerCase())) return email?.split("@")[0] || "用户"