chore: handle more encodes & fix idx range
This commit is contained in:
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[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
|
||||
|
||||
@@ -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] || "用户"
|
||||
|
||||
Reference in New Issue
Block a user