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 (
-