chore: handle more encodes & fix idx range
This commit is contained in:
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
module lanqin-email-api
|
module lanqin-email-api
|
||||||
|
|
||||||
go 1.22
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-chi/chi/v5 v5.1.0
|
github.com/go-chi/chi/v5 v5.1.0
|
||||||
@@ -20,6 +20,7 @@ require (
|
|||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
golang.org/x/net v0.26.0 // indirect
|
golang.org/x/net v0.26.0 // indirect
|
||||||
golang.org/x/sys v0.23.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/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
||||||
modernc.org/libc v1.55.3 // indirect
|
modernc.org/libc v1.55.3 // indirect
|
||||||
modernc.org/mathutil v1.6.0 // 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/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 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
|
||||||
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
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 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
|
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/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 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
|
||||||
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
|
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 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
|
||||||
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||||
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
|
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/text/encoding"
|
||||||
|
"golang.org/x/text/encoding/ianaindex"
|
||||||
)
|
)
|
||||||
|
|
||||||
type maildirMailbox struct {
|
type maildirMailbox struct {
|
||||||
@@ -350,8 +353,7 @@ func (a *App) parseMaildirMessage(raw []byte, fallbackTo string) (storedMessage,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return storedMessage{}, nil, err
|
return storedMessage{}, nil, err
|
||||||
}
|
}
|
||||||
decoder := new(mime.WordDecoder)
|
subject := decodeMIMEHeader(m.Header.Get("Subject"))
|
||||||
subject, _ := decoder.DecodeHeader(m.Header.Get("Subject"))
|
|
||||||
if strings.TrimSpace(subject) == "" {
|
if strings.TrimSpace(subject) == "" {
|
||||||
subject = "(no subject)"
|
subject = "(no subject)"
|
||||||
}
|
}
|
||||||
@@ -460,54 +462,46 @@ func transferReader(encoding string, r io.Reader) io.Reader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func partFilename(header textproto.MIMEHeader) string {
|
func partFilename(header textproto.MIMEHeader) string {
|
||||||
decoder := new(mime.WordDecoder)
|
|
||||||
if _, params, err := mime.ParseMediaType(header.Get("Content-Disposition")); err == nil {
|
if _, params, err := mime.ParseMediaType(header.Get("Content-Disposition")); err == nil {
|
||||||
if name := strings.TrimSpace(params["filename"]); name != "" {
|
if name := strings.TrimSpace(params["filename"]); name != "" {
|
||||||
decoded, _ := decoder.DecodeHeader(name)
|
return filepath.Base(decodeMIMEHeader(name))
|
||||||
if decoded != "" {
|
|
||||||
name = decoded
|
|
||||||
}
|
|
||||||
return filepath.Base(name)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if _, params, err := mime.ParseMediaType(header.Get("Content-Type")); err == nil {
|
if _, params, err := mime.ParseMediaType(header.Get("Content-Type")); err == nil {
|
||||||
if name := strings.TrimSpace(params["name"]); name != "" {
|
if name := strings.TrimSpace(params["name"]); name != "" {
|
||||||
decoded, _ := decoder.DecodeHeader(name)
|
return filepath.Base(decodeMIMEHeader(name))
|
||||||
if decoded != "" {
|
|
||||||
name = decoded
|
|
||||||
}
|
|
||||||
return filepath.Base(name)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func firstAddressParts(value string) (string, string) {
|
func firstAddressParts(value string) (string, string) {
|
||||||
items, err := netmail.ParseAddressList(value)
|
// Proactively decode RFC 2047 encoded words before parsing, so that
|
||||||
if err != nil || len(items) == 0 {
|
// non-UTF-8 charsets (e.g. GBK, Shift_JIS) are handled by our
|
||||||
// ParseAddressList failed — attempt RFC 2047 decode on the raw header,
|
// CharsetReader instead of Go's default WordDecoder which only
|
||||||
// then retry parsing. This handles non-standard From headers where
|
// supports UTF-8 and ISO-8859-1.
|
||||||
// encoded words (e.g. =?UTF-8?B?…?=) cause the initial parse to fail.
|
|
||||||
decoded := decodeMIMEHeader(value)
|
decoded := decodeMIMEHeader(value)
|
||||||
items, err = netmail.ParseAddressList(decoded)
|
items, err := netmail.ParseAddressList(decoded)
|
||||||
if err != nil || len(items) == 0 {
|
if err != nil || len(items) == 0 {
|
||||||
// Still unparseable: return the decoded value as the email and
|
// Still unparseable: return the decoded value and try to extract
|
||||||
// try to extract a display name from the decoded string.
|
// a display name from the decoded string.
|
||||||
email, name := splitNameAndEmail(decoded)
|
email, name := splitNameAndEmail(decoded)
|
||||||
return normalizeEmail(email), strings.TrimSpace(name)
|
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?=)
|
// decodeMIMEHeader decodes all RFC 2047 encoded words (=?charset?encoding?data?=)
|
||||||
// in the given header value. Falls back to the original value on any error.
|
// 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 {
|
func decodeMIMEHeader(value string) string {
|
||||||
if !strings.Contains(value, "=?") {
|
if !strings.Contains(value, "=?") {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
decoder := new(mime.WordDecoder)
|
decoder := &mime.WordDecoder{
|
||||||
|
CharsetReader: charsetReader,
|
||||||
|
}
|
||||||
decoded, err := decoder.DecodeHeader(value)
|
decoded, err := decoder.DecodeHeader(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return value
|
return value
|
||||||
@@ -515,6 +509,22 @@ func decodeMIMEHeader(value string) string {
|
|||||||
return decoded
|
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
|
// splitNameAndEmail attempts to extract a display name and email address from
|
||||||
// a string like "Display Name <user@example.com>" or plain "user@example.com".
|
// a string like "Display Name <user@example.com>" or plain "user@example.com".
|
||||||
func splitNameAndEmail(value string) (string, string) {
|
func splitNameAndEmail(value string) (string, string) {
|
||||||
@@ -523,7 +533,7 @@ func splitNameAndEmail(value string) (string, string) {
|
|||||||
return "", ""
|
return "", ""
|
||||||
}
|
}
|
||||||
// Try "Name <email>" pattern
|
// Try "Name <email>" pattern
|
||||||
if idx := strings.LastIndex(value, "<"); idx > 0 {
|
if idx := strings.LastIndex(value, "<"); idx >= 0 {
|
||||||
email := strings.TrimRight(value[idx+1:], ">")
|
email := strings.TrimRight(value[idx+1:], ">")
|
||||||
name := strings.TrimSpace(strings.Trim(value[:idx], `" `))
|
name := strings.TrimSpace(strings.Trim(value[:idx], `" `))
|
||||||
if strings.Contains(email, "@") {
|
if strings.Contains(email, "@") {
|
||||||
@@ -539,9 +549,14 @@ func splitNameAndEmail(value string) (string, string) {
|
|||||||
|
|
||||||
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 || 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
|
return nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
out := make([]string, 0, len(items))
|
out := make([]string, 0, len(items))
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
out = append(out, normalizeEmail(item.Address))
|
out = append(out, normalizeEmail(item.Address))
|
||||||
|
|||||||
@@ -25,3 +25,59 @@ export function formatBytes(bytes: number) {
|
|||||||
while (size >= 1024 && idx < units.length - 1) { size /= 1024; idx++ }
|
while (size >= 1024 && idx < units.length - 1) { size /= 1024; idx++ }
|
||||||
return `${size.toFixed(idx === 0 ? 0 : 1)} ${units[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 { 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 { 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 { 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 { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
@@ -810,30 +810,6 @@ 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 = decodeMimeHeader(message.fromName?.trim() || "")
|
const fromName = decodeMimeHeader(message.fromName?.trim() || "")
|
||||||
if (fromName) return fromName
|
if (fromName) return fromName
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { useNavigate } from "react-router-dom"
|
|||||||
import type { ImperativePanelHandle } from "react-resizable-panels"
|
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 { 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 { 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 { applyTheme, getInitialTheme } from "@/lib/theme"
|
||||||
import { useDisplayMode } from "@/lib/display-mode"
|
import { useDisplayMode } from "@/lib/display-mode"
|
||||||
import { Button } from "@/components/ui/button"
|
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) {
|
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] || "用户"
|
||||||
|
|||||||
Reference in New Issue
Block a user