diff --git a/app/plugins/builtins/mail/services.py b/app/plugins/builtins/mail/services.py index 75f8a91..60abb21 100644 --- a/app/plugins/builtins/mail/services.py +++ b/app/plugins/builtins/mail/services.py @@ -707,11 +707,22 @@ async def imap_sync_account( else: body_text = decoded + from email.header import decode_header, make_header + + def _decode_mime_header(value: str) -> str: + """Decode MIME encoded-words (=?UTF-8?Q?...?=) to readable text.""" + if not value: + return "" + try: + return str(make_header(decode_header(value))) + except Exception: + return value + message_id = msg.get("Message-ID", make_msgid()) - subject = msg.get("Subject", "") - from_addr = msg.get("From", "") - to_addrs = msg.get("To", "") - cc_addrs = msg.get("Cc", "") + subject = _decode_mime_header(msg.get("Subject", "")) + from_addr = _decode_mime_header(msg.get("From", "")) + to_addrs = _decode_mime_header(msg.get("To", "")) + cc_addrs = _decode_mime_header(msg.get("Cc", "")) refs = msg.get("References", "") in_reply_to = msg.get("In-Reply-To") date_str = msg.get("Date", "") diff --git a/frontend/src/api/mail.ts b/frontend/src/api/mail.ts index 3794cf9..6449cb2 100644 --- a/frontend/src/api/mail.ts +++ b/frontend/src/api/mail.ts @@ -381,8 +381,40 @@ export function testConnection(accountId: string): Promise<{ success: boolean; m return apiPost<{ success: boolean; message: string }>(`/mail/accounts/${accountId}/test-connection`); } -export function triggerSync(accountId: string): Promise<{ success: boolean; synced_count: number }> { - return apiPost<{ success: boolean; synced_count: number }>(`/mail/accounts/${accountId}/sync`); +export function triggerSync(accountId: string): Promise<{ synced: number; error?: string }> { + return apiPost<{ synced: number; error?: string }>(`/mail/accounts/${accountId}/sync`); +} + +/** + * Decode MIME encoded-words (=?UTF-8?Q?...?=) to readable text. + * Used for mail subjects/from names that were stored before backend decoding was added. + */ +export function decodeMimeHeader(value: string | undefined | null): string { + if (!value) return ''; + // Check if value contains MIME encoded-words + if (!value.includes('=?')) return value; + try { + // Decode all encoded-words in the string + return value.replace(/=\?([^?]+)\?([BQ])\?([^?]*)\?=/gi, (_match, _charset, encoding, encoded) => { + try { + if (encoding.toUpperCase() === 'B') { + // Base64 decode + const decoded = atob(encoded); + return decodeURIComponent(escape(decoded)); + } else { + // Q encoding: replace _ with space, then decode =XX hex sequences + const qDecoded = encoded.replace(/_/g, ' ').replace(/=([0-9A-F]{2})/gi, (_m, hex) => { + return String.fromCharCode(parseInt(hex, 16)); + }); + return decodeURIComponent(escape(qDecoded)); + } + } catch { + return _match; + } + }); + } catch { + return value; + } } // ─── Folders ──────────────────────────────────────────────────────────────── diff --git a/frontend/src/components/mail/MailDetail.tsx b/frontend/src/components/mail/MailDetail.tsx index c70b04d..7043a92 100644 --- a/frontend/src/components/mail/MailDetail.tsx +++ b/frontend/src/components/mail/MailDetail.tsx @@ -7,6 +7,7 @@ import React, { useMemo, useRef, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import type { Mail, MailAttachment } from '@/api/mail'; +import { decodeMimeHeader } from '@/api/mail'; import { Button } from '@/components/ui/Button'; import { EmptyState } from '@/components/ui/EmptyState'; @@ -97,7 +98,7 @@ export function MailDetail({ {/* Headers */}
-

{mail.subject || t('mail.noSubject')}

+

{decodeMimeHeader(mail.subject) || t('mail.noSubject')}

{mail.labels && mail.labels.length > 0 && (
{mail.labels.map((label) => ( @@ -111,7 +112,7 @@ export function MailDetail({
{t('mail.from')}: - {mail.from_name ? `${mail.from_name} <${mail.from_address}>` : mail.from_address} + {decodeMimeHeader(mail.from_name) ? `${decodeMimeHeader(mail.from_name)} <${mail.from_address}>` : mail.from_address}
{t('mail.to')}: diff --git a/frontend/src/components/mail/MailList.tsx b/frontend/src/components/mail/MailList.tsx index 97541ea..26d9608 100644 --- a/frontend/src/components/mail/MailList.tsx +++ b/frontend/src/components/mail/MailList.tsx @@ -7,6 +7,7 @@ import React from 'react'; import clsx from 'clsx'; import { useTranslation } from 'react-i18next'; import type { Mail } from '@/api/mail'; +import { decodeMimeHeader } from '@/api/mail'; import { EmptyState } from '@/components/ui/EmptyState'; import { Pagination } from '@/components/ui/Pagination'; @@ -89,9 +90,9 @@ export function MailList({ } return ( -
+
{/* Select-all header */} -
+
{/* Sort header */} {onSortChange && ( -
+
@@ -140,7 +141,8 @@ export function MailList({
)} -
    + {/* Mail list — scrolls, pagination stays fixed */} +
      {mails.map((mail) => (
    • @@ -182,12 +184,12 @@ export function MailList({
      - {mail.from_name || mail.from_address} + {decodeMimeHeader(mail.from_name) || mail.from_address} {formatDate(mail.date)}

      - {mail.subject || t('mail.noSubject')} + {decodeMimeHeader(mail.subject) || t('mail.noSubject')}

      {mail.labels && mail.labels.length > 0 && (
      @@ -206,13 +208,15 @@ export function MailList({ ))}
    {totalPages > 1 && ( - +
    + +
    )}
); diff --git a/frontend/src/components/ui/Pagination.tsx b/frontend/src/components/ui/Pagination.tsx index 211b1ec..9ee5300 100644 --- a/frontend/src/components/ui/Pagination.tsx +++ b/frontend/src/components/ui/Pagination.tsx @@ -30,18 +30,18 @@ export function Pagination({ currentPage, totalPages, total, pageSize, onPageCha if (totalPages <= 1) return null; return ( -