feat: implement email archiving options and reorganize functionality
This commit is contained in:
+38
-4
@@ -518,12 +518,46 @@ export default function Home() {
|
||||
|
||||
// Find archive mailbox
|
||||
const archiveMailbox = mailboxes.find(m => m.role === "archive" || m.name.toLowerCase() === "archive");
|
||||
if (archiveMailbox) {
|
||||
try {
|
||||
if (!archiveMailbox) return;
|
||||
|
||||
const { archiveMode } = useSettingsStore.getState();
|
||||
|
||||
try {
|
||||
if (archiveMode === 'single') {
|
||||
await moveToMailbox(client, selectedEmail.id, archiveMailbox.id);
|
||||
} catch (error) {
|
||||
console.error("Failed to archive email:", error);
|
||||
} else {
|
||||
// Determine year/month from the email's received date
|
||||
const emailDate = new Date(selectedEmail.receivedAt);
|
||||
const year = emailDate.getFullYear().toString();
|
||||
const month = (emailDate.getMonth() + 1).toString().padStart(2, '0');
|
||||
const archiveId = archiveMailbox.originalId || archiveMailbox.id;
|
||||
|
||||
// Find or create year subfolder under archive
|
||||
let yearMailbox = mailboxes.find(
|
||||
m => m.name === year && m.parentId === archiveId
|
||||
);
|
||||
if (!yearMailbox) {
|
||||
yearMailbox = await client.createMailbox(year, archiveId);
|
||||
await fetchMailboxes(client);
|
||||
}
|
||||
|
||||
if (archiveMode === 'year') {
|
||||
await moveToMailbox(client, selectedEmail.id, yearMailbox.id);
|
||||
} else {
|
||||
// archiveMode === 'month' — find or create month subfolder under year
|
||||
const yearId = yearMailbox.originalId || yearMailbox.id;
|
||||
let monthMailbox = mailboxes.find(
|
||||
m => m.name === month && m.parentId === yearId
|
||||
);
|
||||
if (!monthMailbox) {
|
||||
monthMailbox = await client.createMailbox(month, yearId);
|
||||
await fetchMailboxes(client);
|
||||
}
|
||||
await moveToMailbox(client, selectedEmail.id, monthMailbox.id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to archive email:", error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,13 +3,18 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSettingsStore } from '@/stores/settings-store';
|
||||
import type { ArchiveMode } from '@/stores/settings-store';
|
||||
import { useAuthStore } from '@/stores/auth-store';
|
||||
import { useEmailStore } from '@/stores/email-store';
|
||||
import { SettingsSection, SettingItem, Select, ToggleSwitch } from './settings-section';
|
||||
import { TrustedSendersModal } from '@/components/trusted-senders-modal';
|
||||
import { ChevronRight, AlertTriangle } from 'lucide-react';
|
||||
import { ChevronRight, AlertTriangle, FolderSync, Loader2 } from 'lucide-react';
|
||||
|
||||
export function EmailSettings() {
|
||||
const t = useTranslations('settings.email_behavior');
|
||||
const [showTrustedModal, setShowTrustedModal] = useState(false);
|
||||
const [isReorganizing, setIsReorganizing] = useState(false);
|
||||
const [reorganizeResult, setReorganizeResult] = useState<string | null>(null);
|
||||
|
||||
const {
|
||||
markAsReadDelay,
|
||||
@@ -20,6 +25,7 @@ export function EmailSettings() {
|
||||
externalContentPolicy,
|
||||
mailAttachmentAction,
|
||||
emailAlwaysLightMode,
|
||||
archiveMode,
|
||||
trustedSenders,
|
||||
updateSetting,
|
||||
} = useSettingsStore();
|
||||
@@ -32,6 +38,69 @@ export function EmailSettings() {
|
||||
return t('trusted_senders.count_other', { count });
|
||||
};
|
||||
|
||||
const handleReorganizeArchive = async () => {
|
||||
const { client } = useAuthStore.getState();
|
||||
const { mailboxes, fetchMailboxes } = useEmailStore.getState();
|
||||
if (!client) return;
|
||||
|
||||
const archiveMailbox = mailboxes.find(m => m.role === 'archive' || m.name.toLowerCase() === 'archive');
|
||||
if (!archiveMailbox) return;
|
||||
|
||||
setIsReorganizing(true);
|
||||
setReorganizeResult(null);
|
||||
|
||||
try {
|
||||
const archiveId = archiveMailbox.originalId || archiveMailbox.id;
|
||||
|
||||
// Fetch all emails in the root archive mailbox
|
||||
const emails = await client.getEmailsInMailbox(archiveId);
|
||||
let movedCount = 0;
|
||||
|
||||
for (const email of emails) {
|
||||
const emailDate = new Date(email.receivedAt);
|
||||
const year = emailDate.getFullYear().toString();
|
||||
const month = (emailDate.getMonth() + 1).toString().padStart(2, '0');
|
||||
|
||||
// Re-read mailboxes from store each iteration in case new ones were created
|
||||
let currentMailboxes = useEmailStore.getState().mailboxes;
|
||||
|
||||
// Find or create year subfolder
|
||||
let yearMailbox = currentMailboxes.find(
|
||||
m => m.name === year && m.parentId === archiveId
|
||||
);
|
||||
if (!yearMailbox) {
|
||||
yearMailbox = await client.createMailbox(year, archiveId);
|
||||
await fetchMailboxes(client);
|
||||
currentMailboxes = useEmailStore.getState().mailboxes;
|
||||
}
|
||||
|
||||
if (archiveMode === 'year') {
|
||||
await client.moveEmail(email.id, yearMailbox.id);
|
||||
movedCount++;
|
||||
} else {
|
||||
// month mode
|
||||
const yearId = yearMailbox.originalId || yearMailbox.id;
|
||||
let monthMailbox = currentMailboxes.find(
|
||||
m => m.name === month && m.parentId === yearId
|
||||
);
|
||||
if (!monthMailbox) {
|
||||
monthMailbox = await client.createMailbox(month, yearId);
|
||||
await fetchMailboxes(client);
|
||||
}
|
||||
await client.moveEmail(email.id, monthMailbox.id);
|
||||
movedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
setReorganizeResult(t('archive_mode.reorganize_success', { count: movedCount }));
|
||||
} catch (error) {
|
||||
console.error('Failed to reorganize archive:', error);
|
||||
setReorganizeResult(t('archive_mode.reorganize_error'));
|
||||
} finally {
|
||||
setIsReorganizing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsSection title={t('title')} description={t('description')}>
|
||||
{/* Mark as Read */}
|
||||
@@ -68,6 +137,40 @@ export function EmailSettings() {
|
||||
</div>
|
||||
</SettingItem>
|
||||
|
||||
{/* Archive Mode */}
|
||||
<SettingItem label={t('archive_mode.label')} description={t('archive_mode.description')}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Select
|
||||
value={archiveMode}
|
||||
onChange={(value) => updateSetting('archiveMode', value as ArchiveMode)}
|
||||
options={[
|
||||
{ value: 'single', label: t('archive_mode.single') },
|
||||
{ value: 'year', label: t('archive_mode.year') },
|
||||
{ value: 'month', label: t('archive_mode.month') },
|
||||
]}
|
||||
/>
|
||||
{archiveMode !== 'single' && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<button
|
||||
onClick={handleReorganizeArchive}
|
||||
disabled={isReorganizing}
|
||||
className="flex items-center gap-2 px-3 py-1.5 bg-muted hover:bg-accent rounded-md transition-colors text-sm disabled:opacity-50"
|
||||
>
|
||||
{isReorganizing ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<FolderSync className="w-4 h-4" />
|
||||
)}
|
||||
<span>{t('archive_mode.reorganize')}</span>
|
||||
</button>
|
||||
{reorganizeResult && (
|
||||
<p className="text-xs text-muted-foreground">{reorganizeResult}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SettingItem>
|
||||
|
||||
{/* Permanently Delete Junk */}
|
||||
<SettingItem label={t('permanently_delete_junk.label')} description={t('permanently_delete_junk.description')}>
|
||||
<ToggleSwitch
|
||||
|
||||
@@ -556,6 +556,22 @@ export class JMAPClient {
|
||||
}
|
||||
}
|
||||
|
||||
async getEmailsInMailbox(mailboxId: string): Promise<Email[]> {
|
||||
const allEmails: Email[] = [];
|
||||
let position = 0;
|
||||
const batchSize = 100;
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const { emails, hasMore } = await this.getEmails(mailboxId, undefined, batchSize, position);
|
||||
allEmails.push(...emails);
|
||||
if (!hasMore || emails.length === 0) break;
|
||||
position += emails.length;
|
||||
}
|
||||
|
||||
return allEmails;
|
||||
}
|
||||
|
||||
async getTagCounts(tagIds: string[]): Promise<Record<string, { total: number; unread: number }>> {
|
||||
if (tagIds.length === 0) return {};
|
||||
try {
|
||||
|
||||
@@ -680,6 +680,16 @@
|
||||
"permanent": "Dauerhaft löschen",
|
||||
"warning": "E-Mails werden dauerhaft gelöscht und können nicht wiederhergestellt werden. Diese Aktion ist unwiderruflich."
|
||||
},
|
||||
"archive_mode": {
|
||||
"label": "Nachrichten archivieren in",
|
||||
"description": "Wie E-Mails beim Archivieren organisiert werden",
|
||||
"single": "Einem einzigen Ordner",
|
||||
"year": "Einem Ordner pro Jahr",
|
||||
"month": "Einem Ordner pro Monat",
|
||||
"reorganize": "Bestehendes Archiv umorganisieren",
|
||||
"reorganize_success": "{count, plural, =0 {Keine E-Mails umzuorganisieren} =1 {1 E-Mail umorganisiert} other {# E-Mails umorganisiert}}",
|
||||
"reorganize_error": "Archiv konnte nicht umorganisiert werden"
|
||||
},
|
||||
"permanently_delete_junk": {
|
||||
"label": "Spam dauerhaft löschen",
|
||||
"description": "E-Mails aus dem Spam-Ordner dauerhaft löschen, anstatt sie in den Papierkorb zu verschieben"
|
||||
|
||||
@@ -680,6 +680,16 @@
|
||||
"permanent": "Delete Permanently",
|
||||
"warning": "Emails will be permanently deleted and cannot be recovered. This action is irreversible."
|
||||
},
|
||||
"archive_mode": {
|
||||
"label": "Archive in",
|
||||
"description": "How to organize emails when archiving",
|
||||
"single": "A single folder",
|
||||
"year": "A folder per year",
|
||||
"month": "A folder per month",
|
||||
"reorganize": "Reorganize existing archive",
|
||||
"reorganize_success": "{count, plural, =0 {No emails to reorganize} =1 {1 email reorganized} other {# emails reorganized}}",
|
||||
"reorganize_error": "Failed to reorganize archive"
|
||||
},
|
||||
"permanently_delete_junk": {
|
||||
"label": "Permanently Delete Junk",
|
||||
"description": "Permanently delete emails from the Junk/Spam folder instead of moving them to Trash"
|
||||
|
||||
@@ -680,6 +680,16 @@
|
||||
"permanent": "Eliminar Permanentemente",
|
||||
"warning": "Los correos se eliminarán permanentemente y no se podrán recuperar. Esta acción es irreversible."
|
||||
},
|
||||
"archive_mode": {
|
||||
"label": "Archivar en",
|
||||
"description": "Cómo organizar los correos al archivar",
|
||||
"single": "Una sola carpeta",
|
||||
"year": "Una carpeta por año",
|
||||
"month": "Una carpeta por mes",
|
||||
"reorganize": "Reorganizar archivo existente",
|
||||
"reorganize_success": "{count, plural, =0 {No hay correos para reorganizar} =1 {1 correo reorganizado} other {# correos reorganizados}}",
|
||||
"reorganize_error": "Error al reorganizar el archivo"
|
||||
},
|
||||
"permanently_delete_junk": {
|
||||
"label": "Eliminar spam permanentemente",
|
||||
"description": "Eliminar permanentemente los correos de la carpeta Spam en lugar de moverlos a la Papelera"
|
||||
|
||||
@@ -680,6 +680,16 @@
|
||||
"permanent": "Supprimer définitivement",
|
||||
"warning": "Les emails seront supprimés définitivement et ne pourront pas être récupérés. Cette action est irréversible."
|
||||
},
|
||||
"archive_mode": {
|
||||
"label": "Archiver dans",
|
||||
"description": "Comment organiser les emails lors de l'archivage",
|
||||
"single": "Un seul dossier",
|
||||
"year": "Un dossier par année",
|
||||
"month": "Un dossier par mois",
|
||||
"reorganize": "Réorganiser l'archive existante",
|
||||
"reorganize_success": "{count, plural, =0 {Aucun email à réorganiser} =1 {1 email réorganisé} other {# emails réorganisés}}",
|
||||
"reorganize_error": "Échec de la réorganisation de l'archive"
|
||||
},
|
||||
"permanently_delete_junk": {
|
||||
"label": "Supprimer définitivement les indésirables",
|
||||
"description": "Supprimer définitivement les e-mails du dossier Indésirables au lieu de les déplacer vers la corbeille"
|
||||
|
||||
@@ -680,6 +680,16 @@
|
||||
"permanent": "Elimina definitivamente",
|
||||
"warning": "I messaggi verranno eliminati definitivamente e non potranno essere recuperati. Questa azione è irreversibile."
|
||||
},
|
||||
"archive_mode": {
|
||||
"label": "Archivia in",
|
||||
"description": "Come organizzare le email durante l'archiviazione",
|
||||
"single": "Una singola cartella",
|
||||
"year": "Una cartella per anno",
|
||||
"month": "Una cartella per mese",
|
||||
"reorganize": "Riorganizza archivio esistente",
|
||||
"reorganize_success": "{count, plural, =0 {Nessuna email da riorganizzare} =1 {1 email riorganizzata} other {# email riorganizzate}}",
|
||||
"reorganize_error": "Impossibile riorganizzare l'archivio"
|
||||
},
|
||||
"permanently_delete_junk": {
|
||||
"label": "Elimina spam definitivamente",
|
||||
"description": "Elimina definitivamente i messaggi dalla cartella Spam invece di spostarli nel cestino"
|
||||
|
||||
@@ -680,6 +680,16 @@
|
||||
"permanent": "完全に削除",
|
||||
"warning": "メールは完全に削除され、復元できません。この操作は元に戻せません。"
|
||||
},
|
||||
"archive_mode": {
|
||||
"label": "アーカイブ先",
|
||||
"description": "アーカイブ時のメール整理方法",
|
||||
"single": "単一フォルダ",
|
||||
"year": "年ごとのフォルダ",
|
||||
"month": "月ごとのフォルダ",
|
||||
"reorganize": "既存アーカイブを再整理",
|
||||
"reorganize_success": "{count, plural, =0 {再整理するメールはありません} other {#通のメールを再整理しました}}",
|
||||
"reorganize_error": "アーカイブの再整理に失敗しました"
|
||||
},
|
||||
"permanently_delete_junk": {
|
||||
"label": "迷惑メールを完全に削除",
|
||||
"description": "迷惑メールフォルダのメールをゴミ箱に移動せずに完全に削除する"
|
||||
|
||||
@@ -680,6 +680,16 @@
|
||||
"permanent": "Permanent verwijderen",
|
||||
"warning": "E-mails worden permanent verwijderd en kunnen niet worden hersteld. Deze actie is onomkeerbaar."
|
||||
},
|
||||
"archive_mode": {
|
||||
"label": "Archiveren in",
|
||||
"description": "Hoe e-mails bij archivering worden georganiseerd",
|
||||
"single": "Eén enkele map",
|
||||
"year": "Een map per jaar",
|
||||
"month": "Een map per maand",
|
||||
"reorganize": "Bestaand archief herorganiseren",
|
||||
"reorganize_success": "{count, plural, =0 {Geen e-mails om te herorganiseren} =1 {1 e-mail geherorganiseerd} other {# e-mails geherorganiseerd}}",
|
||||
"reorganize_error": "Archief herorganiseren mislukt"
|
||||
},
|
||||
"permanently_delete_junk": {
|
||||
"label": "Spam permanent verwijderen",
|
||||
"description": "E-mails uit de map Spam permanent verwijderen in plaats van naar de prullenbak te verplaatsen"
|
||||
|
||||
@@ -680,6 +680,16 @@
|
||||
"permanent": "Excluir Permanentemente",
|
||||
"warning": "Os e-mails serão excluídos permanentemente e não poderão ser recuperados. Esta ação é irreversível."
|
||||
},
|
||||
"archive_mode": {
|
||||
"label": "Arquivar em",
|
||||
"description": "Como organizar os e-mails ao arquivar",
|
||||
"single": "Uma única pasta",
|
||||
"year": "Uma pasta por ano",
|
||||
"month": "Uma pasta por mês",
|
||||
"reorganize": "Reorganizar arquivo existente",
|
||||
"reorganize_success": "{count, plural, =0 {Nenhum e-mail para reorganizar} =1 {1 e-mail reorganizado} other {# e-mails reorganizados}}",
|
||||
"reorganize_error": "Falha ao reorganizar o arquivo"
|
||||
},
|
||||
"permanently_delete_junk": {
|
||||
"label": "Excluir spam permanentemente",
|
||||
"description": "Excluir permanentemente e-mails da pasta Spam em vez de movê-los para a Lixeira"
|
||||
|
||||
@@ -30,6 +30,7 @@ export type FirstDayOfWeek = 0 | 1; // 0 = Sunday, 1 = Monday
|
||||
export type ExternalContentPolicy = 'ask' | 'block' | 'allow';
|
||||
export type MailAttachmentAction = 'preview' | 'download';
|
||||
export type ToolbarPosition = 'top' | 'below-subject';
|
||||
export type ArchiveMode = 'single' | 'year' | 'month';
|
||||
|
||||
export interface KeywordDefinition {
|
||||
id: string; // Used as JMAP keyword suffix: $label:<id>
|
||||
@@ -84,6 +85,7 @@ interface SettingsState {
|
||||
externalContentPolicy: ExternalContentPolicy;
|
||||
mailAttachmentAction: MailAttachmentAction;
|
||||
emailAlwaysLightMode: boolean; // Always render email content in light mode
|
||||
archiveMode: ArchiveMode; // How to organize archived emails: single folder, by year, or by year+month
|
||||
|
||||
// Composer
|
||||
autoSaveDraftInterval: number; // milliseconds
|
||||
@@ -166,6 +168,7 @@ const DEFAULT_SETTINGS = {
|
||||
externalContentPolicy: 'ask' as ExternalContentPolicy,
|
||||
mailAttachmentAction: 'preview' as MailAttachmentAction,
|
||||
emailAlwaysLightMode: false,
|
||||
archiveMode: 'single' as ArchiveMode,
|
||||
|
||||
// Composer
|
||||
autoSaveDraftInterval: 60000, // 1 minute
|
||||
@@ -245,6 +248,7 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
emailsPerPage: state.emailsPerPage,
|
||||
externalContentPolicy: state.externalContentPolicy,
|
||||
mailAttachmentAction: state.mailAttachmentAction,
|
||||
archiveMode: state.archiveMode,
|
||||
trustedSenders: state.trustedSenders,
|
||||
autoSaveDraftInterval: state.autoSaveDraftInterval,
|
||||
sendConfirmation: state.sendConfirmation,
|
||||
|
||||
Reference in New Issue
Block a user