diff --git a/crates/core/src/account/view.rs b/crates/core/src/account/view.rs
index 5703760..6d9d9c7 100644
--- a/crates/core/src/account/view.rs
+++ b/crates/core/src/account/view.rs
@@ -22,7 +22,7 @@ use serde::{Deserialize, Serialize};
use crate::{
account::{
entity::ImapConfig,
- migration::{AccountModel, AccountType, QuotaWindow},
+ migration::{AccountModel, AccountType, ArchiveRules, QuotaWindow},
since::{DateSince, RelativeDate},
},
users::UserModel,
@@ -58,6 +58,7 @@ pub struct AccountResp {
pub imap_quota_window: Option,
pub auto_download_new_mailboxes: Option,
pub download_schedule: Option,
+ pub archive_rules: Option,
pub deleting: bool,
}
@@ -96,6 +97,7 @@ impl AccountResp {
imap_quota_window: account.imap_quota_window,
auto_download_new_mailboxes: account.auto_download_new_mailboxes,
download_schedule: account.download_schedule,
+ archive_rules: account.archive_rules,
deleting: account.deleting,
}
}
diff --git a/web/src/api/account/api.ts b/web/src/api/account/api.ts
index 735fef4..f3e4eb7 100644
--- a/web/src/api/account/api.ts
+++ b/web/src/api/account/api.ts
@@ -118,6 +118,20 @@ interface DateSelection {
export type QuotaWindow = 'hourly' | 'daily' | 'weekly' | 'monthly'
+
+export interface FilterRule {
+ include: string[];
+ exclude: string[];
+}
+
+export interface ArchiveRules {
+ enabled: boolean;
+ senders: FilterRule;
+ subjects: FilterRule;
+ skip_larger_than?: number;
+ spam_headers: string[];
+}
+
export interface AccountModel {
id: number;
account_type: AccountType;
@@ -145,6 +159,7 @@ export interface AccountModel {
imap_quota_bytes?: number;
auto_download_new_mailboxes?: boolean;
download_schedule?: string;
+ archive_rules?: ArchiveRules;
deleting?: boolean;
}
diff --git a/web/src/components/ui/breadcrumb.tsx b/web/src/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000..6f31b63
--- /dev/null
+++ b/web/src/components/ui/breadcrumb.tsx
@@ -0,0 +1,67 @@
+//
+// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
+//
+// This file is part of the Bichon Email Archiving Project
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import { ChevronRight, Home } from "lucide-react";
+import { Link } from "@tanstack/react-router";
+import { cn } from "@/lib/utils";
+
+export interface BreadcrumbItem {
+ label: string;
+ to?: string;
+}
+
+interface BreadcrumbProps {
+ items: BreadcrumbItem[];
+ className?: string;
+}
+
+export function Breadcrumb({ items, className }: BreadcrumbProps) {
+ return (
+
+
+
+
+ {items.map((item, idx) => {
+ const isLast = idx === items.length - 1;
+ return (
+
+
+ {item.to && !isLast ? (
+
+ {item.label}
+
+ ) : (
+
+ {item.label}
+
+ )}
+
+ );
+ })}
+
+ );
+}
diff --git a/web/src/features/accounts/account-new.tsx b/web/src/features/accounts/account-new.tsx
new file mode 100644
index 0000000..5350329
--- /dev/null
+++ b/web/src/features/accounts/account-new.tsx
@@ -0,0 +1,260 @@
+//
+// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
+//
+// This file is part of the Bichon Email Archiving Project
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import { useCallback, useState } from "react";
+import { useForm, FormProvider } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useTranslation } from "react-i18next";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { useNavigate, Link } from "@tanstack/react-router";
+import { ArrowLeft, Loader2 } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Form } from "@/components/ui/form";
+import { useToast } from "@/hooks/use-toast";
+import { ToastAction } from "@/components/ui/toast";
+import { Breadcrumb } from "@/components/ui/breadcrumb";
+import { FixedHeader } from "@/components/layout/fixed-header";
+import { Main } from "@/components/layout/main";
+import { TabGeneral } from "./components/tab-general";
+import { TabServer } from "./components/tab-server";
+import { TabDownload } from "./components/tab-download";
+import { TabFilters } from "./components/tab-filters";
+import { create_account, autoconfig } from "@/api/account/api";
+import { getAccountSchema, type AccountFormValues } from "./components/schema";
+import type { AxiosError } from "axios";
+
+const defaultValues: AccountFormValues = {
+ login_name: undefined,
+ account_name: undefined,
+ email: '',
+ imap: {
+ host: "",
+ port: 993,
+ encryption: 'Ssl',
+ auth: { auth_type: 'Password', password: undefined },
+ use_proxy: undefined,
+ },
+ enabled: true,
+ use_dangerous: false,
+ date_since: undefined,
+ date_before: undefined,
+ download_interval_min: 60,
+ download_batch_size: 30,
+ max_email_size_bytes: 100 * 1024 * 1024,
+ auto_download_new_mailboxes: true,
+ download_schedule: undefined,
+ archive_rules: undefined,
+};
+
+function SectionHeader({ title, description }: { title: string; description?: string }) {
+ return (
+
+
{title}
+ {description &&
{description}
}
+
+ );
+}
+
+export function AccountNewPage() {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const { toast } = useToast();
+ const queryClient = useQueryClient();
+ const [autoConfigLoading, setAutoConfigLoading] = useState(false);
+
+ const accountSchema = getAccountSchema(false, t);
+ const form = useForm({
+ mode: "onChange",
+ defaultValues,
+ resolver: zodResolver(accountSchema),
+ });
+
+ const createMutation = useMutation({
+ mutationFn: create_account,
+ onSuccess: () => {
+ toast({
+ title: t('accounts.accountCreated'),
+ description: t('accounts.accountCreatedDesc'),
+ action: {t('common.close')} ,
+ });
+ queryClient.invalidateQueries({ queryKey: ['account-list'] });
+ navigate({ to: '/accounts' });
+ },
+ onError: (error: AxiosError) => {
+ const errorMessage =
+ (error.response?.data as { message?: string })?.message ||
+ error.message ||
+ t('accounts.creationFailed');
+ toast({
+ variant: "destructive",
+ title: t('accounts.accountCreationFailed'),
+ description: errorMessage as string,
+ action: {t('common.tryAgain')} ,
+ });
+ },
+ });
+
+ const onSubmit = useCallback(
+ (data: AccountFormValues) => {
+ createMutation.mutate({
+ email: data.email,
+ account_name: data.account_name,
+ login_name: data.login_name,
+ imap: {
+ ...data.imap,
+ auth: {
+ ...data.imap.auth,
+ password: data.imap.auth.auth_type === 'OAuth2' ? undefined : data.imap.auth.password,
+ },
+ },
+ enabled: data.enabled,
+ use_dangerous: data.use_dangerous,
+ date_since: data.date_since,
+ date_before: data.date_before,
+ download_interval_min: data.download_interval_min,
+ download_batch_size: data.download_batch_size,
+ max_email_size_bytes: data.max_email_size_bytes,
+ auto_download_new_mailboxes: data.auto_download_new_mailboxes,
+ download_schedule: data.download_schedule || null,
+ account_type: "IMAP",
+ archive_rules: data.archive_rules || null,
+ });
+ },
+ [createMutation]
+ );
+
+ const handleAutoConfig = async () => {
+ const email = form.getValues('email');
+ if (!email) return;
+ const imap = form.getValues('imap');
+ if (imap.host.trim() !== "" && imap.port > 0) return;
+
+ setAutoConfigLoading(true);
+ try {
+ const result = await autoconfig(email);
+ if (result) {
+ form.setValue('imap.host', result.imap.host);
+ form.setValue('imap.port', result.imap.port);
+ form.setValue('imap.encryption', result.imap.encryption);
+ if (result.oauth2) form.setValue('imap.auth.auth_type', 'OAuth2');
+ }
+ } catch (error) {
+ console.error('Auto-configuration failed:', error);
+ }
+ setAutoConfigLoading(false);
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+ {t('accounts.settings.backToAccounts')}
+
+
+
+
+
+
+
{t('accounts.addAccount')}
+
{t('accounts.addNewEmailAccountHere')}
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/web/src/features/accounts/account-settings-page.tsx b/web/src/features/accounts/account-settings-page.tsx
new file mode 100644
index 0000000..8857fbe
--- /dev/null
+++ b/web/src/features/accounts/account-settings-page.tsx
@@ -0,0 +1,279 @@
+//
+// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
+//
+// This file is part of the Bichon Email Archiving Project
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import { useCallback, useEffect } from "react";
+import { useForm, FormProvider } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useTranslation } from "react-i18next";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { Link } from "@tanstack/react-router";
+import { ArrowLeft } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Form } from "@/components/ui/form";
+import { useToast } from "@/hooks/use-toast";
+import { ToastAction } from "@/components/ui/toast";
+import { Breadcrumb } from "@/components/ui/breadcrumb";
+import { FixedHeader } from "@/components/layout/fixed-header";
+import { Main } from "@/components/layout/main";
+import { TabGeneral } from "./components/tab-general";
+import { TabServer } from "./components/tab-server";
+import { TabDownload } from "./components/tab-download";
+import { TabFilters } from "./components/tab-filters";
+import { update_account, list_accounts, type AccountModel } from "@/api/account/api";
+import { getAccountSchema, type AccountFormValues } from "./components/schema";
+import type { AxiosError } from "axios";
+import { useQuery } from "@tanstack/react-query";
+
+const emptyImap = {
+ host: "",
+ port: 0,
+ encryption: "None" as const,
+ auth: { auth_type: "Password" as const, password: undefined },
+ use_proxy: undefined,
+};
+
+function mapAccountToFormValues(account: AccountModel): AccountFormValues {
+ const imap = { ...(account.imap ?? emptyImap) };
+ imap.auth = { ...imap.auth, password: undefined };
+ if ((imap as any).use_proxy === null) {
+ (imap as any).use_proxy = undefined;
+ }
+
+ return {
+ account_name: account.account_name ?? undefined,
+ login_name: account.login_name ?? undefined,
+ email: account.email,
+ imap,
+ enabled: account.enabled,
+ use_dangerous: account.use_dangerous,
+ date_since: account.date_since ?? undefined,
+ date_before: account.date_before ?? undefined,
+ download_interval_min: account.download_interval_min ?? 60,
+ download_batch_size: account.download_batch_size ?? 30,
+ max_email_size_bytes: account.max_email_size_bytes ?? 100 * 1024 * 1024,
+ auto_download_new_mailboxes: account.auto_download_new_mailboxes ?? true,
+ download_schedule: account.download_schedule ?? undefined,
+ archive_rules: account.archive_rules ?? undefined,
+ };
+}
+
+function SectionHeader({ title, description }: { title: string; description?: string }) {
+ return (
+
+
{title}
+ {description &&
{description}
}
+
+ );
+}
+
+interface AccountSettingsPageProps {
+ accountId: number;
+}
+
+export function AccountSettingsPage({ accountId }: AccountSettingsPageProps) {
+ const { t } = useTranslation();
+ //const navigate = useNavigate();
+ const { toast } = useToast();
+ const queryClient = useQueryClient();
+
+ const { data: accountList } = useQuery({
+ queryKey: ['account-list'],
+ queryFn: list_accounts,
+ });
+
+ const account = accountList?.items?.find((a) => a.id === accountId);
+
+ const accountSchema = getAccountSchema(true, t);
+ const form = useForm({
+ mode: "onChange",
+ defaultValues: account ? mapAccountToFormValues(account) : undefined,
+ resolver: zodResolver(accountSchema),
+ });
+
+ useEffect(() => {
+ if (account) {
+ form.reset(mapAccountToFormValues(account));
+ }
+ }, [account?.id]);
+
+ const updateMutation = useMutation({
+ mutationFn: (data: Record) => update_account(accountId, data),
+ onSuccess: () => {
+ toast({
+ title: t('accounts.settings.saved'),
+ description: t('accounts.settings.savedDesc'),
+ action: {t('common.close')} ,
+ });
+ queryClient.invalidateQueries({ queryKey: ['account-list'] });
+ },
+ onError: (error: AxiosError) => {
+ const errorMessage =
+ (error.response?.data as { message?: string })?.message ||
+ error.message ||
+ t('accounts.updateFailed');
+ toast({
+ variant: "destructive",
+ title: t('accounts.accountUpdateFailed'),
+ description: errorMessage as string,
+ action: {t('common.tryAgain')} ,
+ });
+ },
+ });
+
+ const onSubmit = useCallback(
+ (data: AccountFormValues) => {
+ const payload: Record = {
+ email: data.email,
+ account_name: data.account_name,
+ login_name: data.login_name,
+ imap: {
+ ...data.imap,
+ auth: {
+ ...data.imap.auth,
+ password: data.imap.auth.auth_type === 'OAuth2'
+ ? undefined
+ : (data.imap.auth.password ? data.imap.auth.password : undefined),
+ },
+ },
+ enabled: data.enabled,
+ use_dangerous: data.use_dangerous,
+ date_since: data.date_since,
+ date_before: data.date_before,
+ download_interval_min: data.download_interval_min,
+ download_batch_size: data.download_batch_size,
+ max_email_size_bytes: data.max_email_size_bytes,
+ auto_download_new_mailboxes: data.auto_download_new_mailboxes,
+ download_schedule: data.download_schedule || null,
+ archive_rules: data.archive_rules || null,
+ };
+
+ if (!data.date_since && !data.date_before) {
+ payload.clear_date_range = true;
+ }
+ if (!data.download_schedule && account?.download_schedule) {
+ payload.clear_download_schedule = true;
+ }
+
+ updateMutation.mutate(payload);
+ },
+ [updateMutation, account]
+ );
+
+ if (!account) {
+ return (
+ <>
+
+
+
+ {t('accounts.settings.loading')}
+
+
+ >
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+ {t('accounts.settings.backToAccounts')}
+
+
+
+
+
+
+
{account.email}
+
{t('accounts.updateTheEmailAccountHere')}
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/web/src/features/accounts/components/action-dialog.tsx b/web/src/features/accounts/components/action-dialog.tsx
deleted file mode 100644
index 619a292..0000000
--- a/web/src/features/accounts/components/action-dialog.tsx
+++ /dev/null
@@ -1,368 +0,0 @@
-//
-// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
-//
-// This file is part of the Bichon Email Archiving Project
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import * as React from 'react';
-import { useForm } from 'react-hook-form';
-import { Button } from '@/components/ui/button';
-import { Form } from '@/components/ui/form';
-import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
-import { ScrollArea } from '@/components/ui/scroll-area';
-import { useToast } from '@/hooks/use-toast';
-import Step1 from './step1';
-import Step2 from './step2';
-import Step3 from './step3';
-import Step4 from './step4';
-import { create_account, autoconfig, update_account, AccountModel, ImapConfig } from '@/api/account/api';
-import { useMutation, useQueryClient } from '@tanstack/react-query';
-import { ToastAction } from '@/components/ui/toast';
-import { AxiosError } from 'axios';
-import { useTranslation } from 'react-i18next';
-import { cn } from "@/lib/utils";
-import { getAccountSchema, type AccountFormValues } from './schema';
-
-export type Account = AccountFormValues;
-
-type Step = {
- id: `step-${number}`;
- name: string;
- fields: (keyof Account)[];
-};
-
-export type Steps = [...Step[]];
-
-const getSteps = (t: (key: string) => string): Steps => [
- { id: "step-1", name: t('accounts.steps.emailAddress'), fields: ["email", "account_name"] },
- { id: "step-2", name: t('accounts.steps.imap'), fields: ["imap", "use_dangerous", "login_name"] },
- { id: "step-3", name: t('accounts.steps.syncPreferences'), fields: ["enabled", "date_since", "date_before", "download_interval_min", "download_batch_size", "max_email_size_bytes", "auto_download_new_mailboxes", "download_schedule"] },
- { id: "step-4", name: t('accounts.steps.summary'), fields: [] },
-];
-
-const LAST_STEP = 4;
-
-interface Props {
- currentRow?: AccountModel;
- open: boolean;
- onOpenChange: (open: boolean) => void;
-}
-
-const defaultValues: Account = {
- login_name: undefined,
- account_name: undefined,
- email: '',
- imap: {
- host: "",
- port: 993,
- encryption: 'Ssl',
- auth: {
- auth_type: 'Password',
- password: undefined,
- },
- use_proxy: undefined
- },
- enabled: true,
- use_dangerous: false,
- date_since: undefined,
- date_before: undefined,
- download_interval_min: 60,
- download_batch_size: 30,
- max_email_size_bytes: 100 * 1024 * 1024,
- auto_download_new_mailboxes: true,
- download_schedule: undefined,
-};
-
-const emptyImap: ImapConfig = {
- host: "",
- port: 0,
- encryption: "None",
- auth: { auth_type: "Password", password: undefined },
- use_proxy: undefined,
-};
-
-const mapCurrentRowToFormValues = (currentRow: AccountModel): Account => {
- const imap = { ...(currentRow.imap ?? emptyImap) };
- imap.auth = { ...imap.auth, password: undefined };
- if (imap.use_proxy === null) {
- imap.use_proxy = undefined;
- }
-
- return {
- account_name: currentRow.account_name ?? undefined,
- login_name: currentRow.login_name ?? undefined,
- email: currentRow.email,
- imap,
- enabled: currentRow.enabled,
- use_dangerous: currentRow.use_dangerous,
- date_since: currentRow.date_since ?? undefined,
- date_before: currentRow.date_before ?? undefined,
- download_interval_min: currentRow.download_interval_min ?? 60,
- download_batch_size: currentRow.download_batch_size ?? 30,
- max_email_size_bytes: currentRow.max_email_size_bytes ?? 100 * 1024 * 1024,
- auto_download_new_mailboxes: currentRow.auto_download_new_mailboxes ?? true,
- download_schedule: currentRow.download_schedule ?? undefined,
- };
-};
-
-export function AccountActionDialog({ currentRow, open, onOpenChange }: Props) {
- const { t } = useTranslation();
- const steps = getSteps(t);
- const isEdit = !!currentRow;
- const [currentStep, setCurrentStep] = React.useState(1);
- const { toast } = useToast();
- const [autoConfigLoading, setAutoConfigLoading] = React.useState(false);
-
- const accountSchema = getAccountSchema(isEdit, t);
- const form = useForm({
- mode: "onChange",
- defaultValues: isEdit ? mapCurrentRowToFormValues(currentRow) : defaultValues,
- resolver: zodResolver(accountSchema),
- });
-
- const queryClient = useQueryClient();
-
- const createMutation = useMutation({
- mutationFn: create_account,
- onSuccess: handleSuccess,
- onError: handleError,
- });
-
- const updateMutation = useMutation({
- mutationFn: (data: Record) => update_account(currentRow?.id!, data),
- onSuccess: handleSuccess,
- onError: handleError,
- });
-
- function handleSuccess() {
- toast({
- title: isEdit ? t('accounts.accountUpdated') : t('accounts.accountCreated'),
- description: isEdit ? t('accounts.accountUpdatedDesc') : t('accounts.accountCreatedDesc'),
- action: {t('common.close')} ,
- });
-
- queryClient.invalidateQueries({ queryKey: ['account-list'] });
- form.reset();
- onOpenChange(false);
- }
-
- function handleError(error: AxiosError) {
- const errorMessage =
- (error.response?.data as { message?: string })?.message ||
- error.message ||
- (isEdit ? t('accounts.updateFailed') : t('accounts.creationFailed'));
-
- toast({
- variant: "destructive",
- title: isEdit ? t('accounts.accountUpdateFailed') : t('accounts.accountCreationFailed'),
- description: errorMessage as string,
- action: {t('common.tryAgain')} ,
- });
- console.error(error);
- }
-
- const onSubmit = React.useCallback(
- (data: Account) => {
- const commonData = {
- email: data.email,
- account_name: data.account_name,
- login_name: data.login_name,
- imap: {
- ...data.imap,
- auth: {
- ...data.imap.auth,
- password: data.imap.auth.auth_type === 'OAuth2'
- ? undefined
- : (isEdit && !data.imap.auth.password ? undefined : data.imap.auth.password),
- },
- },
- enabled: data.enabled,
- use_dangerous: data.use_dangerous,
- date_since: data.date_since,
- date_before: data.date_before,
- download_interval_min: data.download_interval_min,
- download_batch_size: data.download_batch_size,
- max_email_size_bytes: data.max_email_size_bytes,
- auto_download_new_mailboxes: data.auto_download_new_mailboxes,
- download_schedule: data.download_schedule || null,
- };
- if (isEdit) {
- const isAllMode = !data.date_since && !data.date_before;
- const clear_download_schedule = !data.download_schedule && currentRow?.download_schedule;
- updateMutation.mutate({
- ...commonData,
- ...(isAllMode ? { clear_date_range: true } : {}),
- ...(clear_download_schedule ? { clear_download_schedule: true } : {})
- });
- } else {
- createMutation.mutate({ ...commonData, account_type: "IMAP" });
- }
- },
- [isEdit, updateMutation, createMutation]
- );
-
- const handleNav = async (index: number) => {
- let isValid = true;
- let failedStep = currentStep;
- for (let i = currentStep - 1; i < index - 1 && isValid; i++) {
- isValid = await form.trigger(steps[i].fields);
- if (!isValid) failedStep = i;
- }
- if (isValid) setCurrentStep(index);
- else setCurrentStep(failedStep);
- };
-
- async function handleContinue() {
- const isValid = await form.trigger(steps[currentStep - 1].fields);
- if (!isValid) return;
-
- if (currentStep === 1) {
- let allValues = form.getValues();
- if (allValues.imap.host.trim() !== "" && allValues.imap.port > 0) {
- handleNav(currentStep + 1);
- return;
- }
- setAutoConfigLoading(true);
- const email = form.getValues('email');
- form.setValue('login_name', email);
- try {
- const result = await autoconfig(email);
- if (result) {
- form.setValue('imap.host', result.imap.host);
- form.setValue('imap.port', result.imap.port);
- form.setValue('imap.encryption', result.imap.encryption);
- if (result.oauth2) form.setValue('imap.auth.auth_type', 'OAuth2');
- }
- } catch (error) {
- console.error('Auto-configuration failed:', error);
- }
- setAutoConfigLoading(false);
- handleNav(currentStep + 1);
- } else {
- handleNav(currentStep + 1);
- }
- }
-
- return (
- {
- if (!state) {
- form.reset();
- setCurrentStep(1);
- }
- onOpenChange(state);
- }}
- >
-
-
-
- {isEdit ? t('accounts.updateAccount') : t('accounts.addAccount')}
-
- {isEdit ? t('accounts.updateAccount') : t('accounts.addAccount')}
- {t('accounts.clickSaveWhenDone')}
-
-
-
-
-
-
- {steps.map((step, index) => (
-
- setCurrentStep(index + 1)}
- >
- {index + 1}
-
- {step.name}
-
- ))}
-
-
-
- {steps.map((step, index) => (
-
-
setCurrentStep(index + 1)}
- >
- {index + 1}
-
-
- {t('accounts.step', { index: index + 1 })}
-
- {step.name}
-
-
-
- ))}
-
-
-
-
-
-
- {currentStep > 1 && (
- setCurrentStep(currentStep - 1)}
- >
- {t('accounts.goBack')}
-
- )}
- {currentStep < LAST_STEP && (
-
- {autoConfigLoading ? t('accounts.autoConfiguring') : t('accounts.continue')}
-
- )}
- {currentStep === LAST_STEP && (
-
- {isEdit ? t('accounts.saveChanges') : t('accounts.submit')}
-
- )}
-
-
-
- );
-}
diff --git a/web/src/features/accounts/components/add-account-dialog.tsx b/web/src/features/accounts/components/add-account-dialog.tsx
deleted file mode 100644
index a26ccb0..0000000
--- a/web/src/features/accounts/components/add-account-dialog.tsx
+++ /dev/null
@@ -1,159 +0,0 @@
-//
-// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
-//
-// This file is part of the Bichon Email Archiving Project
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-//
-
-import * as React from 'react'
-import { Mail, Database } from 'lucide-react'
-import { useTranslation } from 'react-i18next'
-
-import { cn } from '@/lib/utils'
-import { Button } from '@/components/ui/button'
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@/components/ui/dialog'
-import { Label } from '@/components/ui/label'
-import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
-import { useAccountContext } from '../context'
-
-export type AddAccountType = 'IMAP' | 'NoSync'
-
-interface Props {
- open: boolean;
- onOpenChange: (open: boolean) => void;
-}
-
-export function AddAccountDialog({
- open,
- onOpenChange
-}: Props) {
- const { t } = useTranslation()
- const { setOpen } = useAccountContext()
- const [value, setValue] = React.useState('IMAP')
-
- function handleContinue() {
- if (value === 'IMAP') {
- setOpen('add-imap')
- } else {
- setOpen('add-nosync')
- }
- }
-
- return (
-
-
-
-
- {t('accounts.add')}
-
-
-
- {t('accounts.selectAccountType')}
-
-
-
- setValue(v as AddAccountType)}
- className="space-y-4 py-2"
- >
-
-
-
-
-
-
-
-
-
-
- {t('accounts.imapAccount')}
-
-
-
- {t('accounts.imapAccountDescription')}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {t('accounts.noSyncAccount')}
-
-
-
- {t('accounts.noSyncAccountDescription')}
-
-
-
-
-
-
-
- onOpenChange(false)}
- >
- {t('common.cancel')}
-
-
-
- {t('accounts.continue')}
-
-
-
-
- )
-}
\ No newline at end of file
diff --git a/web/src/features/accounts/components/data-table-row-actions.tsx b/web/src/features/accounts/components/data-table-row-actions.tsx
index 6a7ff92..039097e 100644
--- a/web/src/features/accounts/components/data-table-row-actions.tsx
+++ b/web/src/features/accounts/components/data-table-row-actions.tsx
@@ -30,11 +30,12 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { useAccountContext } from '../context'
-import { Mailbox, MessageSquareMore } from 'lucide-react'
+import { Mailbox, MessageSquareMore, Settings } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { useCurrentUser } from '@/hooks/use-current-user'
import { AccountModel, cancel_account_download, start_account_download } from '@/api/account/api'
import { toast } from '@/hooks/use-toast'
+import { useNavigate } from '@tanstack/react-router'
interface DataTableRowActionsProps {
row: Row
@@ -43,6 +44,7 @@ interface DataTableRowActionsProps {
export function DataTableRowActions({ row }: DataTableRowActionsProps) {
const { t } = useTranslation()
const { setOpen, setCurrentRow } = useAccountContext()
+ const navigate = useNavigate()
const account_type = row.original.account_type;
const { require_any_permission } = useCurrentUser()
@@ -103,11 +105,10 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
{hasPermission && {
- setCurrentRow(row.original)
if (account_type === "IMAP") {
- setOpen("edit-imap");
- }
- if (account_type === "NoSync") {
+ navigate({ to: '/accounts/$id/settings', params: { id: String(row.original.id) } });
+ } else {
+ setCurrentRow(row.original)
setOpen("edit-nosync");
}
}}
@@ -117,6 +118,16 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
}
+ {account_type === "IMAP" && hasPermission && {
+ navigate({ to: '/accounts/$id/settings', params: { id: String(row.original.id) } });
+ }}
+ >
+ {t('accounts.settings.settings')}
+
+
+
+ }
{account_type === "IMAP" && hasPermission && {
setCurrentRow(row.original)
diff --git a/web/src/features/accounts/components/pattern-input.tsx b/web/src/features/accounts/components/pattern-input.tsx
new file mode 100644
index 0000000..48aa05e
--- /dev/null
+++ b/web/src/features/accounts/components/pattern-input.tsx
@@ -0,0 +1,90 @@
+//
+// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
+//
+// This file is part of the Bichon Email Archiving Project
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import { Input } from "@/components/ui/input";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Button } from "@/components/ui/button";
+import { X } from "lucide-react";
+import { useTranslation } from "react-i18next";
+import type { MatchType, PatternEntry } from "@/lib/pattern-utils";
+import { isValidRegex } from "@/lib/pattern-utils";
+
+interface PatternInputProps {
+ entry: PatternEntry;
+ onChange: (id: string, entry: Partial) => void;
+ onRemove: (id: string) => void;
+}
+
+export function PatternInput({ entry, onChange, onRemove }: PatternInputProps) {
+ const { t } = useTranslation();
+
+ const matchTypeLabels: Record = {
+ contains: t("accounts.filters.matchType.contains"),
+ starts_with: t("accounts.filters.matchType.startsWith"),
+ ends_with: t("accounts.filters.matchType.endsWith"),
+ is_exactly: t("accounts.filters.matchType.isExactly"),
+ regex: t("accounts.filters.matchType.regex"),
+ };
+
+ const regexValid = entry.matchType === 'regex' ? isValidRegex(entry.value) : true;
+ const showRegexHint = entry.matchType === 'regex' && entry.value && !regexValid;
+
+ return (
+
+
onChange(entry.id, { matchType: v as MatchType })}
+ >
+
+
+
+
+ {Object.entries(matchTypeLabels).map(([value, label]) => (
+ {label}
+ ))}
+
+
+
+ onChange(entry.id, { value: e.target.value })}
+ />
+ {showRegexHint && (
+
+ Invalid regex
+
+ )}
+
+
onRemove(entry.id)}
+ >
+
+
+
+ );
+}
diff --git a/web/src/features/accounts/components/schema.ts b/web/src/features/accounts/components/schema.ts
index 80dc935..35b504f 100644
--- a/web/src/features/accounts/components/schema.ts
+++ b/web/src/features/accounts/components/schema.ts
@@ -67,6 +67,19 @@ const dateSelectionSchema = (t: (key: string) => string) =>
})
.optional()
+const filterRuleSchema = z.object({
+ include: z.array(z.string()),
+ exclude: z.array(z.string()),
+})
+
+const archiveRulesSchema = z.object({
+ enabled: z.boolean(),
+ senders: filterRuleSchema,
+ subjects: filterRuleSchema,
+ skip_larger_than: z.number().int().positive().optional(),
+ spam_headers: z.array(z.string()),
+})
+
export const getAccountSchema = (isEdit: boolean, t: (key: string) => string) =>
z.object({
account_name: z.string().optional(),
@@ -120,6 +133,7 @@ export const getAccountSchema = (isEdit: boolean, t: (key: string) => string) =>
},
{ message: t('validation.invalidCronExpression') }
),
+ archive_rules: archiveRulesSchema.optional(),
})
export type AccountFormValues = z.infer<
diff --git a/web/src/features/accounts/components/step1.tsx b/web/src/features/accounts/components/step1.tsx
deleted file mode 100644
index 949e3f0..0000000
--- a/web/src/features/accounts/components/step1.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-//
-// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
-//
-// This file is part of the Bichon Email Archiving Project
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-
-import { useFormContext } from "react-hook-form";
-import {
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
- FormControl,
- FormDescription,
-} from "@/components/ui/form";
-
-import { Input } from "@/components/ui/input";
-import { Account } from "./action-dialog";
-import { useTranslation } from "react-i18next";
-
-interface StepProps {
- isEdit: boolean;
-}
-
-export default function Step1({ isEdit }: StepProps) {
- const { t } = useTranslation()
- const { control } = useFormContext();
-
- return (
- <>
- {t('accounts.emailAccountRegistration')}
-
- {t('accounts.emailAccountRegistrationDesc')}
-
-
- (
-
-
- {t('accounts.emailAddress')}:
-
-
-
-
-
- {isEdit && (
-
- {t('accounts.emailCannotBeModified')}
-
- )}
-
- )}
- />
- (
-
-
- {t('accounts.name')}:
-
-
-
-
- {t('accounts.optional')}
-
-
- )}
- />
-
- >
- );
-}
\ No newline at end of file
diff --git a/web/src/features/accounts/components/step2.tsx b/web/src/features/accounts/components/step2.tsx
deleted file mode 100644
index edc9242..0000000
--- a/web/src/features/accounts/components/step2.tsx
+++ /dev/null
@@ -1,236 +0,0 @@
-//
-// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
-//
-// This file is part of the Bichon Email Archiving Project
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-
-import {
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
- FormControl,
- FormDescription,
-} from "@/components/ui/form";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import { Input } from "@/components/ui/input";
-import { useFormContext, useWatch } from "react-hook-form";
-import { Account } from "./action-dialog";
-import { PasswordInput } from "@/components/password-input";
-import useProxyList from "@/hooks/use-proxy";
-import { useTranslation } from "react-i18next";
-import { Checkbox } from "@/components/ui/checkbox";
-
-interface StepProps {
- isEdit: boolean;
-}
-
-export default function Step2({ isEdit }: StepProps) {
- const { t } = useTranslation()
- const { control } = useFormContext();
- const { proxyOptions } = useProxyList();
-
- const imapAuthMethod = useWatch({
- control,
- name: "imap.auth.auth_type",
- });
-
- return (
- <>
-
- >
- );
-}
\ No newline at end of file
diff --git a/web/src/features/accounts/components/step3.tsx b/web/src/features/accounts/components/step3.tsx
deleted file mode 100644
index 558ce8a..0000000
--- a/web/src/features/accounts/components/step3.tsx
+++ /dev/null
@@ -1,567 +0,0 @@
-//
-// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
-//
-// This file is part of the Bichon Email Archiving Project
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-import {
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
- FormControl,
- FormDescription,
-} from "@/components/ui/form";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import { Input } from "@/components/ui/input";
-import { useFormContext } from "react-hook-form";
-import { Account } from "./action-dialog";
-import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
-import { Button } from "@/components/ui/button";
-import { format } from "date-fns";
-import { CalendarIcon } from "lucide-react";
-import { Calendar } from "@/components/ui/calendar";
-import { cn, dateFnsLocaleMap } from "@/lib/utils";
-import { useState } from "react";
-import { Checkbox } from "@/components/ui/checkbox";
-import { useTranslation } from "react-i18next";
-import { enUS } from "date-fns/locale";
-import i18n from "@/i18n";
-
-
-type SyncMode = 'all' | 'since_fixed' | 'since_relative' | 'before_relative';
-type ScheduleMode = 'interval' | 'cron';
-type CronMode = 'simple' | 'advanced';
-type CronFrequency = 'daily' | 'weekly' | 'monthly';
-
-interface CronSimpleState {
- frequency: CronFrequency;
- hour: number;
- minute: number;
- dayOfWeek: number;
- dayOfMonth: number;
-}
-
-const DEFAULT_CRON_SIMPLE: CronSimpleState = {
- frequency: 'daily',
- hour: 0,
- minute: 0,
- dayOfWeek: 1,
- dayOfMonth: 1,
-};
-
-function buildCronFromSimple(s: CronSimpleState): string {
- switch (s.frequency) {
- case 'daily':
- return `0 ${s.minute} ${s.hour} * * *`;
- case 'weekly':
- return `0 ${s.minute} ${s.hour} * * ${s.dayOfWeek}`;
- case 'monthly':
- return `0 ${s.minute} ${s.hour} ${s.dayOfMonth} * *`;
- }
-}
-
-function tryParseCronToSimple(cron: string): CronSimpleState | null {
- const fields = cron.trim().split(/\s+/);
- if (fields.length < 6) return null;
-
- const sec = fields[0];
- const min = fields[1];
- const hour = fields[2];
- const dom = fields[3];
- const month = fields[4];
- const dow = fields[5];
-
- if (sec !== '0') return null;
- if (month !== '*') return null;
-
- const minuteVal = parseInt(min, 10);
- const hourVal = parseInt(hour, 10);
- if (isNaN(minuteVal) || isNaN(hourVal)) return null;
-
- if (dom === '*' && dow === '*') {
- return { frequency: 'daily', hour: hourVal, minute: minuteVal, dayOfWeek: 1, dayOfMonth: 1 };
- }
- if (dom === '*') {
- const dowVal = parseInt(dow, 10);
- if (!isNaN(dowVal)) {
- return { frequency: 'weekly', hour: hourVal, minute: minuteVal, dayOfWeek: dowVal, dayOfMonth: 1 };
- }
- }
- if (dow === '*') {
- const domVal = parseInt(dom, 10);
- if (!isNaN(domVal)) {
- return { frequency: 'monthly', hour: hourVal, minute: minuteVal, dayOfWeek: 1, dayOfMonth: domVal };
- }
- }
- return null;
-}
-
-export default function Step3() {
- const { t } = useTranslation();
- const { control, getValues, setValue } = useFormContext();
- const current = getValues();
-
- const [syncMode, setSyncMode] = useState(() => {
- if (current.date_before) return 'before_relative';
- if (current.date_since?.fixed) return 'since_fixed';
- if (current.date_since?.relative) return 'since_relative';
- return 'all';
- });
-
- const [scheduleMode, setScheduleMode] = useState(() => {
- if (current.download_schedule) return 'cron';
- return 'interval';
- });
-
- const [cronMode, setCronMode] = useState(() => {
- if (current.download_schedule && tryParseCronToSimple(current.download_schedule)) {
- return 'simple';
- }
- if (current.download_schedule) return 'advanced';
- return 'simple';
- });
-
- const [cronSimple, setCronSimple] = useState(() => {
- if (current.download_schedule) {
- return tryParseCronToSimple(current.download_schedule) ?? DEFAULT_CRON_SIMPLE;
- }
- return DEFAULT_CRON_SIMPLE;
- });
-
- const updateCronFromSimple = (partial: Partial) => {
- const next = { ...cronSimple, ...partial };
- setCronSimple(next);
- setValue('download_schedule', buildCronFromSimple(next));
- };
-
- const handleModeChange = (mode: SyncMode) => {
- setSyncMode(mode);
-
- setValue("date_since", undefined);
- setValue("date_before", undefined);
-
- if (mode === 'since_fixed') {
- setValue("date_since.fixed", undefined);
- } else if (mode === 'since_relative') {
- setValue("date_since.relative", { value: 1, unit: 'Months' });
- } else if (mode === 'before_relative') {
- setValue("date_before", { value: 1, unit: 'Years' });
- }
- };
-
- const handleScheduleModeChange = (mode: ScheduleMode) => {
- setScheduleMode(mode);
- if (mode === 'interval') {
- setValue("download_schedule", undefined);
- } else {
- setValue("download_interval_min", 60);
- if (cronMode === 'simple') {
- updateCronFromSimple(cronSimple);
- }
- }
- };
-
- return (
-
-
-
- {t('accounts.scheduleMode')}
-
- {t('accounts.scheduleModeDescription')}
-
- handleScheduleModeChange(v as ScheduleMode)}>
-
-
-
-
- {t('accounts.scheduleModeInterval')}
- {t('accounts.scheduleModeCron')}
-
-
-
-
- {scheduleMode === 'interval' ? (
-
(
-
- {t('accounts.downloadInterval')}
-
- field.onChange(parseInt(e.target.value, 10))} />
-
-
-
- {t('accounts.downloadIntervalPlaceholder')}
-
-
- )}
- />
- ) : (
-
-
- {/*
{t('accounts.downloadSchedule')} */}
-
- setCronMode('simple')}
- >
- {t('accounts.cronSimple')}
-
- setCronMode('advanced')}
- >
- {t('accounts.cronAdvanced')}
-
-
-
-
- {cronMode === 'simple' ? (
-
-
- {t('accounts.cronFrequency')}
- updateCronFromSimple({ frequency: v as CronFrequency })}
- >
-
-
-
-
- {t('accounts.cronDaily')}
- {t('accounts.cronWeekly')}
- {t('accounts.cronMonthly')}
-
-
-
-
-
- {t('accounts.cronHour')}
- updateCronFromSimple({ hour: parseInt(v, 10) })}
- >
-
-
-
-
- {Array.from({ length: 24 }, (_, i) => (
-
- {String(i).padStart(2, '0')}
-
- ))}
-
-
-
-
-
:
-
-
- {t('accounts.cronMinute')}
- updateCronFromSimple({ minute: parseInt(v, 10) })}
- >
-
-
-
-
- {[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55].map((m) => (
-
- {String(m).padStart(2, '0')}
-
- ))}
-
-
-
-
- {cronSimple.frequency === 'weekly' && (
-
- {t('accounts.cronDayOfWeek')}
- updateCronFromSimple({ dayOfWeek: parseInt(v, 10) })}
- >
-
-
-
-
- {t('accounts.cronMonday')}
- {t('accounts.cronTuesday')}
- {t('accounts.cronWednesday')}
- {t('accounts.cronThursday')}
- {t('accounts.cronFriday')}
- {t('accounts.cronSaturday')}
- {t('accounts.cronSunday')}
-
-
-
- )}
-
- {cronSimple.frequency === 'monthly' && (
-
- {t('accounts.cronDayOfMonth')}
- updateCronFromSimple({ dayOfMonth: parseInt(v, 10) })}
- >
-
-
-
-
- {Array.from({ length: 28 }, (_, i) => i + 1).map((d) => (
-
- {d}
-
- ))}
-
-
-
- )}
-
-
- = {buildCronFromSimple(cronSimple)}
-
-
- ) : (
-
(
-
-
-
-
-
- {t('accounts.downloadScheduleDescription')}
-
-
-
- )}
- />
- )}
-
- {cronMode === 'simple' && (
- {t('accounts.downloadScheduleDescription')}
- )}
-
-
- {t('accounts.cronTimezoneNote')}
-
-
- )}
- (
-
- {t('accounts.downloadBatchSize')}
-
- field.onChange(parseInt(e.target.value, 10))} />
-
-
-
- {t('accounts.downloadBatchSizeDescription')}
-
-
- )}
- />
- {
- const BYTES_PER_MB = 1024 * 1024;
-
- return (
-
- {t('accounts.maxEmailSizeBytes')}
-
-
- {
- const parsed = parseInt(e.target.value, 10);
- field.onChange(isNaN(parsed) ? parsed : parsed * BYTES_PER_MB);
- }}
- />
- MB
-
-
-
-
- {t('accounts.maxEmailSizeBytesDescription')}
-
-
- );
- }}
- />
-
-
-
-
(
-
-
-
-
-
- {t('accounts.enabled')}
- {t('accounts.enabledDescription')}
-
-
- )}
- />
-
-
-
-
- {t('accounts.downloadScope')}
-
- {t('accounts.downloadScopeDescription')}
-
- handleModeChange(v as SyncMode)}>
-
-
-
-
- {t('accounts.downloadAll')}
- {t('accounts.sinceFixed')}
- {t('accounts.sinceRelative')}
- {t('accounts.beforeRelative')}
-
-
-
-
-
-
-
-
- (
-
-
-
-
-
- {t('accounts.autoDownloadNewMailboxes')}
- {t('accounts.autoDownloadNewMailboxesDescription')}
-
-
- )}
- />
-
-
-
- );
-}
\ No newline at end of file
diff --git a/web/src/features/accounts/components/step4.tsx b/web/src/features/accounts/components/step4.tsx
deleted file mode 100644
index adcb128..0000000
--- a/web/src/features/accounts/components/step4.tsx
+++ /dev/null
@@ -1,188 +0,0 @@
-//
-// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
-//
-// This file is part of the Bichon Email Archiving Project
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-
-import { useFormContext } from "react-hook-form";
-import { Account } from "./action-dialog";
-import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "@/components/ui/accordion";
-import { useTranslation } from "react-i18next";
-import useProxyList from "@/hooks/use-proxy";
-
-export default function Step4() {
- const { t } = useTranslation();
- const { getValues } = useFormContext();
- const { getUrlById } = useProxyList();
- const summaryData = getValues();
-
-
- const sinceText = (() => {
- if (summaryData.date_since?.fixed) {
- return summaryData.date_since.fixed;
- }
-
- if (summaryData.date_since?.relative?.value) {
- return `${t('accounts.sinceRelativeValue', {
- value: summaryData.date_since!.relative!.value,
- unit: t(`accounts.${summaryData.date_since!.relative!.unit!.toLowerCase()}`)
- })}`;
- }
-
- return t('accounts.syncAll');
- })();
-
- const hasSince = !!summaryData.date_since;
- const hasBefore = !!summaryData.date_before?.value;
-
- return (
-
-
-
- {t('accounts.email')}:
- {summaryData.email}
-
-
-
- {t('accounts.name')}:
- {summaryData.account_name ?? t('accounts.notAvailable')}
-
-
-
- {t('accounts.login_name')}:
- {summaryData.login_name ?? t('accounts.notAvailable')}
-
-
-
- {t('accounts.imap')}:
-
-
-
-
-
- {t('accounts.host')}:
- {summaryData.imap.host}
-
-
- {t('accounts.port')}:
- {summaryData.imap.port}
-
-
- {t('accounts.encryption')}:
- {summaryData.imap.encryption}
-
-
- {t('accounts.useDangerous')}:
- {`${summaryData.use_dangerous}`}
-
-
- {t('accounts.authType')}:
- {summaryData.imap.auth.auth_type}
-
- {summaryData.imap.auth.auth_type === 'Password' && (
-
- {t('accounts.password')}:
- {summaryData.imap.auth.password}
-
- )}
-
- {t('accounts.useProxyField')}:
-
- {(() => {
- if (!summaryData.imap.use_proxy) {
- return t('accounts.useNoProxy');
- }
- const proxyUrl = getUrlById(summaryData.imap.use_proxy);
- return proxyUrl || `${t('common.yes')} (${summaryData.imap.use_proxy})`;
- })()}
-
-
-
-
-
-
-
-
-
-
- {t('accounts.downloadScope')}:
-
-
-
- {hasSince && (
-
-
- {t('accounts.sinceFixed')}:
-
- {sinceText}
-
- )}
-
- {hasBefore && (
-
-
- {t('accounts.beforeRelative')}:
-
-
- {t('accounts.beforeRelativeValue', {
- value: summaryData.date_before!.value,
- unit: t(`accounts.${summaryData.date_before!.unit!.toLowerCase()}`)
- })}
-
-
- )}
-
- {!hasSince && !hasBefore && (
-
- {t('accounts.downloadAll')}
-
- )}
-
-
-
-
-
- {t('accounts.downloadInterval')}:
- {summaryData.download_interval_min} {t('accounts.minutes')}
-
-
-
- {t('accounts.downloadBatchSize')}:
- {summaryData.download_batch_size}
-
-
-
- {t('accounts.maxEmailSizeBytes')}:
- {summaryData.max_email_size_bytes ? `${(summaryData.max_email_size_bytes / 1024 / 1024).toFixed(0)} MB` : t('accounts.maxEmailSizeBytesUnlimited')}
-
-
-
- {t('accounts.downloadSchedule')}:
- {summaryData.download_schedule || t('accounts.notAvailable')}
-
-
-
- {t('accounts.autoDownloadNewMailboxes')}:
- {summaryData.auto_download_new_mailboxes ? t('common.yes') : t('common.no')}
-
-
-
- );
-}
diff --git a/web/src/features/accounts/components/tab-download.tsx b/web/src/features/accounts/components/tab-download.tsx
new file mode 100644
index 0000000..eea2290
--- /dev/null
+++ b/web/src/features/accounts/components/tab-download.tsx
@@ -0,0 +1,564 @@
+//
+// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
+//
+// This file is part of the Bichon Email Archiving Project
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import { useState } from "react";
+import { useFormContext } from "react-hook-form";
+import { useTranslation } from "react-i18next";
+import { format } from "date-fns";
+import { CalendarIcon } from "lucide-react";
+import { enUS } from "date-fns/locale";
+import {
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+ FormControl,
+ FormDescription,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { Checkbox } from "@/components/ui/checkbox";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Button } from "@/components/ui/button";
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
+import { Calendar } from "@/components/ui/calendar";
+import { cn, dateFnsLocaleMap } from "@/lib/utils";
+import i18n from "@/i18n";
+import { AccountFormValues } from "./schema";
+
+type SyncMode = 'all' | 'since_fixed' | 'since_relative' | 'before_relative';
+type ScheduleMode = 'interval' | 'cron';
+type CronMode = 'simple' | 'advanced';
+type CronFrequency = 'daily' | 'weekly' | 'monthly';
+
+interface CronSimpleState {
+ frequency: CronFrequency;
+ hour: number;
+ minute: number;
+ dayOfWeek: number;
+ dayOfMonth: number;
+}
+
+const DEFAULT_CRON_SIMPLE: CronSimpleState = {
+ frequency: 'daily',
+ hour: 0,
+ minute: 0,
+ dayOfWeek: 1,
+ dayOfMonth: 1,
+};
+
+function buildCronFromSimple(s: CronSimpleState): string {
+ switch (s.frequency) {
+ case 'daily':
+ return `0 ${s.minute} ${s.hour} * * *`;
+ case 'weekly':
+ return `0 ${s.minute} ${s.hour} * * ${s.dayOfWeek}`;
+ case 'monthly':
+ return `0 ${s.minute} ${s.hour} ${s.dayOfMonth} * *`;
+ }
+}
+
+function tryParseCronToSimple(cron: string): CronSimpleState | null {
+ const fields = cron.trim().split(/\s+/);
+ if (fields.length < 6) return null;
+ const sec = fields[0];
+ const min = fields[1];
+ const hour = fields[2];
+ const dom = fields[3];
+ const month = fields[4];
+ const dow = fields[5];
+ if (sec !== '0') return null;
+ if (month !== '*') return null;
+ const minuteVal = parseInt(min, 10);
+ const hourVal = parseInt(hour, 10);
+ if (isNaN(minuteVal) || isNaN(hourVal)) return null;
+ if (dom === '*' && dow === '*') {
+ return { frequency: 'daily', hour: hourVal, minute: minuteVal, dayOfWeek: 1, dayOfMonth: 1 };
+ }
+ if (dom === '*') {
+ const dowVal = parseInt(dow, 10);
+ if (!isNaN(dowVal)) {
+ return { frequency: 'weekly', hour: hourVal, minute: minuteVal, dayOfWeek: dowVal, dayOfMonth: 1 };
+ }
+ }
+ if (dow === '*') {
+ const domVal = parseInt(dom, 10);
+ if (!isNaN(domVal)) {
+ return { frequency: 'monthly', hour: hourVal, minute: minuteVal, dayOfWeek: 1, dayOfMonth: domVal };
+ }
+ }
+ return null;
+}
+
+export function TabDownload() {
+ const { t } = useTranslation();
+ const { control, getValues, setValue } = useFormContext();
+ const current = getValues();
+
+ const [syncMode, setSyncMode] = useState(() => {
+ if (current.date_before) return 'before_relative';
+ if (current.date_since?.fixed) return 'since_fixed';
+ if (current.date_since?.relative) return 'since_relative';
+ return 'all';
+ });
+
+ const [scheduleMode, setScheduleMode] = useState(() => {
+ if (current.download_schedule) return 'cron';
+ return 'interval';
+ });
+
+ const [cronMode, setCronMode] = useState(() => {
+ if (current.download_schedule && tryParseCronToSimple(current.download_schedule)) {
+ return 'simple';
+ }
+ if (current.download_schedule) return 'advanced';
+ return 'simple';
+ });
+
+ const [cronSimple, setCronSimple] = useState(() => {
+ if (current.download_schedule) {
+ return tryParseCronToSimple(current.download_schedule) ?? DEFAULT_CRON_SIMPLE;
+ }
+ return DEFAULT_CRON_SIMPLE;
+ });
+
+ const updateCronFromSimple = (partial: Partial) => {
+ const next = { ...cronSimple, ...partial };
+ setCronSimple(next);
+ setValue('download_schedule', buildCronFromSimple(next));
+ };
+
+ const handleModeChange = (mode: SyncMode) => {
+ setSyncMode(mode);
+ setValue("date_since", undefined);
+ setValue("date_before", undefined);
+ if (mode === 'since_fixed') {
+ setValue("date_since.fixed", undefined);
+ } else if (mode === 'since_relative') {
+ setValue("date_since.relative", { value: 1, unit: 'Months' });
+ } else if (mode === 'before_relative') {
+ setValue("date_before", { value: 1, unit: 'Years' });
+ }
+ };
+
+ const handleScheduleModeChange = (mode: ScheduleMode) => {
+ setScheduleMode(mode);
+ if (mode === 'interval') {
+ setValue("download_schedule", undefined);
+ } else {
+ setValue("download_interval_min", 60);
+ if (cronMode === 'simple') {
+ updateCronFromSimple(cronSimple);
+ }
+ }
+ };
+
+ const BYTES_PER_MB = 1024 * 1024;
+
+ return (
+
+ {/* Schedule */}
+
+
+ {t('accounts.settings.schedule')}
+
+
+
+ {t('accounts.scheduleMode')}
+ {t('accounts.scheduleModeDescription')}
+ handleScheduleModeChange(v as ScheduleMode)}>
+
+
+
+
+ {t('accounts.scheduleModeInterval')}
+ {t('accounts.scheduleModeCron')}
+
+
+
+
+
+ {scheduleMode === 'interval' && (
+ (
+
+ {t('accounts.downloadInterval')}*
+
+ field.onChange(parseInt(e.target.value, 10))} />
+
+ {t('accounts.downloadIntervalPlaceholder')}
+
+
+ )}
+ />
+ )}
+
+
+ {scheduleMode === 'cron' && (
+
+
+
+ setCronMode('simple')}
+ >
+ {t('accounts.cronSimple')}
+
+ setCronMode('advanced')}
+ >
+ {t('accounts.cronAdvanced')}
+
+
+
+
+ {cronMode === 'simple' ? (
+ <>
+
+
+ {t('accounts.cronFrequency')}
+ updateCronFromSimple({ frequency: v as CronFrequency })}
+ >
+
+
+
+
+ {t('accounts.cronDaily')}
+ {t('accounts.cronWeekly')}
+ {t('accounts.cronMonthly')}
+
+
+
+
+
+ {t('accounts.cronHour')}
+ updateCronFromSimple({ hour: parseInt(v, 10) })}
+ >
+
+
+
+
+ {Array.from({ length: 24 }, (_, i) => (
+
+ {String(i).padStart(2, '0')}
+
+ ))}
+
+
+
+
+ :
+
+
+ {t('accounts.cronMinute')}
+ updateCronFromSimple({ minute: parseInt(v, 10) })}
+ >
+
+
+
+
+ {[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55].map((m) => (
+
+ {String(m).padStart(2, '0')}
+
+ ))}
+
+
+
+
+ {cronSimple.frequency === 'weekly' && (
+
+ {t('accounts.cronDayOfWeek')}
+ updateCronFromSimple({ dayOfWeek: parseInt(v, 10) })}
+ >
+
+
+
+
+ {t('accounts.cronMonday')}
+ {t('accounts.cronTuesday')}
+ {t('accounts.cronWednesday')}
+ {t('accounts.cronThursday')}
+ {t('accounts.cronFriday')}
+ {t('accounts.cronSaturday')}
+ {t('accounts.cronSunday')}
+
+
+
+ )}
+
+ {cronSimple.frequency === 'monthly' && (
+
+ {t('accounts.cronDayOfMonth')}
+ updateCronFromSimple({ dayOfMonth: parseInt(v, 10) })}
+ >
+
+
+
+
+ {Array.from({ length: 31 }, (_, i) => i + 1).map((d) => (
+ {d}
+ ))}
+
+
+
+ )}
+
+
+
+ {cronSimple.frequency === 'daily' && t('accounts.cronSummaryDaily', { hour: String(cronSimple.hour).padStart(2, '0'), minute: String(cronSimple.minute).padStart(2, '0') })}
+ {cronSimple.frequency === 'weekly' && (() => {
+ const dayNames = ['cronSunday', 'cronMonday', 'cronTuesday', 'cronWednesday', 'cronThursday', 'cronFriday', 'cronSaturday'];
+ return t('accounts.cronSummaryWeekly', { hour: String(cronSimple.hour).padStart(2, '0'), minute: String(cronSimple.minute).padStart(2, '0'), day: t(`accounts.${dayNames[cronSimple.dayOfWeek]}`) });
+ })()}
+ {cronSimple.frequency === 'monthly' && t('accounts.cronSummaryMonthly', { hour: String(cronSimple.hour).padStart(2, '0'), minute: String(cronSimple.minute).padStart(2, '0'), day: cronSimple.dayOfMonth })}
+ {cronSimple.frequency === 'monthly' && cronSimple.dayOfMonth > 28 && (
+ {t('accounts.cronMonthAlignNote')}
+ )}
+
+ >
+ ) : (
+
(
+
+
+
+
+ {t('accounts.downloadScheduleDescription')}
+
+
+ )}
+ />
+ )}
+
+
+ {t('accounts.cronTimezoneNote')}
+
+
+ )}
+
+
+
+
+ {/* Batch & Size */}
+
+
+ {t('accounts.settings.performance')}
+
+
+
+
+
+
+ {/* Download Scope */}
+
+
+ {t('accounts.settings.scope')}
+
+
+
+ {t('accounts.downloadScope')}
+ {t('accounts.downloadScopeDescription')}
+ handleModeChange(v as SyncMode)}>
+
+
+
+
+ {t('accounts.downloadAll')}
+ {t('accounts.sinceFixed')}
+ {t('accounts.sinceRelative')}
+ {t('accounts.beforeRelative')}
+
+
+
+ {syncMode === 'all' && t('accounts.downloadAllDesc')}
+ {syncMode === 'since_fixed' && t('accounts.sinceFixedDesc')}
+ {syncMode === 'since_relative' && t('accounts.sinceRelativeDesc')}
+ {syncMode === 'before_relative' && t('accounts.beforeRelativeDesc')}
+
+
+
+
+
+
+
+
+
(
+
+
+
+
+
+ {t('accounts.autoDownloadNewMailboxes')}
+ {t('accounts.autoDownloadNewMailboxesDescription')}
+
+
+ )}
+ />
+
+ );
+}
diff --git a/web/src/features/accounts/components/tab-filters.tsx b/web/src/features/accounts/components/tab-filters.tsx
new file mode 100644
index 0000000..7d75673
--- /dev/null
+++ b/web/src/features/accounts/components/tab-filters.tsx
@@ -0,0 +1,514 @@
+//
+// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
+//
+// This file is part of the Bichon Email Archiving Project
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import { useFormContext, useWatch } from "react-hook-form";
+import { useTranslation } from "react-i18next";
+import {
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+ FormControl,
+ FormDescription,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Button } from "@/components/ui/button";
+import { Plus, HelpCircle } from "lucide-react";
+import { PatternInput } from "./pattern-input";
+import type { PatternEntry } from "@/lib/pattern-utils";
+import { newPatternId, simplePatternToRegex } from "@/lib/pattern-utils";
+import { useState } from "react";
+import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
+import { AccountFormValues } from "./schema";
+
+const SUGGESTED_SPAM_HEADERS = [
+ 'X-Spam-Flag',
+ 'X-Spam',
+ 'X-Spam-Status',
+ 'X-Barracuda-Spam-Status',
+ 'X-Barracuda-Spam-Flag',
+ 'X-MS-Exchange-Organization-SCL',
+];
+
+function toPatternEntries(patterns: string[]): PatternEntry[] {
+ return patterns.map((p) => ({
+ id: newPatternId(),
+ matchType: 'regex' as const,
+ value: p,
+ }));
+}
+
+function patternsToRegexList(entries: PatternEntry[]): string[] {
+ return entries
+ .filter((e) => e.value.trim() !== '')
+ .map((e) => simplePatternToRegex(e.matchType, e.value));
+}
+
+export function TabFilters() {
+ const { t } = useTranslation();
+ const { control, setValue } = useFormContext();
+ const archiveRules = useWatch({ control, name: 'archive_rules' });
+
+ const enabled = archiveRules?.enabled ?? false;
+ const sendersInclude = archiveRules?.senders?.include ?? [];
+ const sendersExclude = archiveRules?.senders?.exclude ?? [];
+ const subjectsInclude = archiveRules?.subjects?.include ?? [];
+ const subjectsExclude = archiveRules?.subjects?.exclude ?? [];
+ const spamHeaders = archiveRules?.spam_headers ?? [];
+
+ const [senderIncludeEntries, setSenderIncludeEntries] = useState(
+ () => toPatternEntries(sendersInclude)
+ );
+ const [senderExcludeEntries, setSenderExcludeEntries] = useState(
+ () => toPatternEntries(sendersExclude)
+ );
+ const [subjectIncludeEntries, setSubjectIncludeEntries] = useState(
+ () => toPatternEntries(subjectsInclude)
+ );
+ const [subjectExcludeEntries, setSubjectExcludeEntries] = useState(
+ () => toPatternEntries(subjectsExclude)
+ );
+
+ // Resync local state when form values change externally (e.g. after form.reset)
+ const [lastSyncKey, setLastSyncKey] = useState('');
+ const syncKey = JSON.stringify({ sendersInclude, sendersExclude, subjectsInclude, subjectsExclude });
+ if (syncKey !== lastSyncKey) {
+ setLastSyncKey(syncKey);
+ setSenderIncludeEntries(toPatternEntries(sendersInclude));
+ setSenderExcludeEntries(toPatternEntries(sendersExclude));
+ setSubjectIncludeEntries(toPatternEntries(subjectsInclude));
+ setSubjectExcludeEntries(toPatternEntries(subjectsExclude));
+ }
+
+ const syncToForm = (
+ includeEntries: PatternEntry[],
+ excludeEntries: PatternEntry[],
+ fieldPrefix: string
+ ) => {
+ const include = patternsToRegexList(includeEntries);
+ const exclude = patternsToRegexList(excludeEntries);
+ setValue(`${fieldPrefix}.include` as any, include);
+ setValue(`${fieldPrefix}.exclude` as any, exclude);
+ };
+
+ const addEntry = (
+ side: 'include' | 'exclude',
+ includeEntries: PatternEntry[],
+ excludeEntries: PatternEntry[],
+ setIncludeEntries: React.Dispatch>,
+ setExcludeEntries: React.Dispatch>,
+ fieldPrefix: string
+ ) => {
+ const newEntry: PatternEntry = { id: newPatternId(), matchType: 'contains', value: '' };
+ const newInclude = side === 'include' ? [...includeEntries, newEntry] : includeEntries;
+ const newExclude = side === 'exclude' ? [...excludeEntries, newEntry] : excludeEntries;
+ setIncludeEntries(newInclude);
+ setExcludeEntries(newExclude);
+ syncToForm(newInclude, newExclude, fieldPrefix);
+ };
+
+ const updateEntry = (
+ id: string,
+ partial: Partial,
+ side: 'include' | 'exclude',
+ includeEntries: PatternEntry[],
+ excludeEntries: PatternEntry[],
+ setIncludeEntries: React.Dispatch>,
+ setExcludeEntries: React.Dispatch>,
+ fieldPrefix: string
+ ) => {
+ if (side === 'include') {
+ const updated = includeEntries.map((e) => (e.id === id ? { ...e, ...partial } : e));
+ setIncludeEntries(updated);
+ syncToForm(updated, excludeEntries, fieldPrefix);
+ } else {
+ const updated = excludeEntries.map((e) => (e.id === id ? { ...e, ...partial } : e));
+ setExcludeEntries(updated);
+ syncToForm(includeEntries, updated, fieldPrefix);
+ }
+ };
+
+ const removeEntry = (
+ id: string,
+ side: 'include' | 'exclude',
+ includeEntries: PatternEntry[],
+ excludeEntries: PatternEntry[],
+ setIncludeEntries: React.Dispatch>,
+ setExcludeEntries: React.Dispatch>,
+ fieldPrefix: string
+ ) => {
+ if (side === 'include') {
+ const filtered = includeEntries.filter((e) => e.id !== id);
+ setIncludeEntries(filtered);
+ syncToForm(filtered, excludeEntries, fieldPrefix);
+ } else {
+ const filtered = excludeEntries.filter((e) => e.id !== id);
+ setExcludeEntries(filtered);
+ syncToForm(includeEntries, filtered, fieldPrefix);
+ }
+ };
+
+ const handleEnableChange = (checked: boolean) => {
+ if (checked) {
+ setValue('archive_rules', {
+ enabled: true,
+ senders: { include: [], exclude: [] },
+ subjects: { include: [], exclude: [] },
+ skip_larger_than: undefined,
+ spam_headers: [],
+ });
+ } else {
+ setValue('archive_rules', undefined);
+ }
+ };
+
+ const addSpamHeader = (header: string) => {
+ if (!spamHeaders.includes(header)) {
+ setValue('archive_rules.spam_headers', [...spamHeaders, header]);
+ }
+ };
+
+ const removeSpamHeader = (header: string) => {
+ setValue('archive_rules.spam_headers', spamHeaders.filter((h) => h !== header));
+ };
+
+ const [newSpamHeader, setNewSpamHeader] = useState('');
+
+ const handleAddCustomSpamHeader = () => {
+ const trimmed = newSpamHeader.trim();
+ if (trimmed && !spamHeaders.includes(trimmed)) {
+ setValue('archive_rules.spam_headers', [...spamHeaders, trimmed]);
+ setNewSpamHeader('');
+ }
+ };
+
+ const BYTES_PER_MB = 1024 * 1024;
+
+ return (
+
+ {/* Master Switch */}
+
+
+
+
+
+
+ {t('accounts.filters.enableFiltering')}
+
+ {t('accounts.filters.enableFilteringDesc')}
+
+
+
+
+
+ {enabled && (
+ <>
+ {/* Sender Filters */}
+
+
+
{t('accounts.filters.senderFilter')}
+
+
+
+
+
+ {t('accounts.filters.senderFilterHelp')}
+
+
+
+
+
+
+
+ {t('accounts.filters.include')}
+
+ {senderIncludeEntries.length === 0 ? (
+
+ {t('accounts.filters.noIncludePatterns')}
+
+ ) : (
+
+ {senderIncludeEntries.map((entry) => (
+
+ updateEntry(id, partial, 'include', senderIncludeEntries, senderExcludeEntries, setSenderIncludeEntries, setSenderExcludeEntries, 'archive_rules.senders')
+ }
+ onRemove={(id) =>
+ removeEntry(id, 'include', senderIncludeEntries, senderExcludeEntries, setSenderIncludeEntries, setSenderExcludeEntries, 'archive_rules.senders')
+ }
+ />
+ ))}
+
+ )}
+
addEntry('include', senderIncludeEntries, senderExcludeEntries, setSenderIncludeEntries, setSenderExcludeEntries, 'archive_rules.senders')}
+ >
+
+ {t('accounts.filters.addPattern')}
+
+
+
+
+
+ {t('accounts.filters.exclude')}
+
+ {senderExcludeEntries.length === 0 ? (
+
+ {t('accounts.filters.noExcludePatterns')}
+
+ ) : (
+
+ {senderExcludeEntries.map((entry) => (
+
+ updateEntry(id, partial, 'exclude', senderIncludeEntries, senderExcludeEntries, setSenderIncludeEntries, setSenderExcludeEntries, 'archive_rules.senders')
+ }
+ onRemove={(id) =>
+ removeEntry(id, 'exclude', senderIncludeEntries, senderExcludeEntries, setSenderIncludeEntries, setSenderExcludeEntries, 'archive_rules.senders')
+ }
+ />
+ ))}
+
+ )}
+
addEntry('exclude', senderIncludeEntries, senderExcludeEntries, setSenderIncludeEntries, setSenderExcludeEntries, 'archive_rules.senders')}
+ >
+
+ {t('accounts.filters.addPattern')}
+
+
+
+
+
+ {/* Subject Filters */}
+
+
+
{t('accounts.filters.subjectFilter')}
+
+
+
+
+
+ {t('accounts.filters.subjectFilterHelp')}
+
+
+
+
+
+
+ {t('accounts.filters.include')}
+
+ {subjectIncludeEntries.length === 0 ? (
+
+ {t('accounts.filters.noIncludePatterns')}
+
+ ) : (
+
+ {subjectIncludeEntries.map((entry) => (
+
+ updateEntry(id, partial, 'include', subjectIncludeEntries, subjectExcludeEntries, setSubjectIncludeEntries, setSubjectExcludeEntries, 'archive_rules.subjects')
+ }
+ onRemove={(id) =>
+ removeEntry(id, 'include', subjectIncludeEntries, subjectExcludeEntries, setSubjectIncludeEntries, setSubjectExcludeEntries, 'archive_rules.subjects')
+ }
+ />
+ ))}
+
+ )}
+
addEntry('include', subjectIncludeEntries, subjectExcludeEntries, setSubjectIncludeEntries, setSubjectExcludeEntries, 'archive_rules.subjects')}
+ >
+
+ {t('accounts.filters.addPattern')}
+
+
+
+
+
+ {t('accounts.filters.exclude')}
+
+ {subjectExcludeEntries.length === 0 ? (
+
+ {t('accounts.filters.noExcludePatterns')}
+
+ ) : (
+
+ {subjectExcludeEntries.map((entry) => (
+
+ updateEntry(id, partial, 'exclude', subjectIncludeEntries, subjectExcludeEntries, setSubjectIncludeEntries, setSubjectExcludeEntries, 'archive_rules.subjects')
+ }
+ onRemove={(id) =>
+ removeEntry(id, 'exclude', subjectIncludeEntries, subjectExcludeEntries, setSubjectIncludeEntries, setSubjectExcludeEntries, 'archive_rules.subjects')
+ }
+ />
+ ))}
+
+ )}
+
addEntry('exclude', subjectIncludeEntries, subjectExcludeEntries, setSubjectIncludeEntries, setSubjectExcludeEntries, 'archive_rules.subjects')}
+ >
+
+ {t('accounts.filters.addPattern')}
+
+
+
+
+
+ {/* Size Limit */}
+
+
+
{t('accounts.filters.sizeLimit')}
+
+
(
+
+ {t('accounts.filters.skipLargerThan')}
+
+
+ {
+ const parsed = parseInt(e.target.value, 10);
+ field.onChange(isNaN(parsed) ? undefined : parsed * BYTES_PER_MB);
+ }}
+ />
+ MB
+
+
+ {t('accounts.filters.sizeLimitDesc')}
+
+
+ )}
+ />
+
+
+ {/* Spam Headers */}
+
+
+
{t('accounts.filters.spamHeaders')}
+
+
+
+
+
+ {t('accounts.filters.spamHeadersHelp')}
+
+
+
+
+ {spamHeaders.length > 0 ? (
+ spamHeaders.map((header) => (
+
+
+ {header}
+
+
removeSpamHeader(header)}
+ >
+ ✕
+
+
+ ))
+ ) : (
+
+ {t('accounts.filters.noSpamHeaders')}
+
+ )}
+
+
+
+
setNewSpamHeader(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ handleAddCustomSpamHeader();
+ }
+ }}
+ />
+
+
+ {t('accounts.filters.addHeader')}
+
+
+
+
+
{t('accounts.filters.suggestions')}
+
+ {SUGGESTED_SPAM_HEADERS.filter((h) => !spamHeaders.includes(h)).map((header) => (
+ addSpamHeader(header)}
+ >
+ + {header}
+
+ ))}
+
+
+
+ >
+ )}
+
+ );
+}
diff --git a/web/src/features/accounts/components/tab-general.tsx b/web/src/features/accounts/components/tab-general.tsx
new file mode 100644
index 0000000..2741d0d
--- /dev/null
+++ b/web/src/features/accounts/components/tab-general.tsx
@@ -0,0 +1,91 @@
+//
+// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
+//
+// This file is part of the Bichon Email Archiving Project
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import { useFormContext } from "react-hook-form";
+import { useTranslation } from "react-i18next";
+import {
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+ FormControl,
+ FormDescription,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { Checkbox } from "@/components/ui/checkbox";
+import { AccountFormValues } from "./schema";
+
+interface TabGeneralProps {
+ isEdit?: boolean;
+}
+
+export function TabGeneral({ isEdit }: TabGeneralProps) {
+ const { t } = useTranslation();
+ const { control } = useFormContext();
+
+ return (
+
+ );
+}
diff --git a/web/src/features/accounts/components/tab-server.tsx b/web/src/features/accounts/components/tab-server.tsx
new file mode 100644
index 0000000..44f9121
--- /dev/null
+++ b/web/src/features/accounts/components/tab-server.tsx
@@ -0,0 +1,180 @@
+//
+// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
+//
+// This file is part of the Bichon Email Archiving Project
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import { useFormContext } from "react-hook-form";
+import { useTranslation } from "react-i18next";
+import {
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+ FormControl,
+ FormDescription,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { Checkbox } from "@/components/ui/checkbox";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { PasswordInput } from "@/components/password-input";
+import { AccountFormValues } from "./schema";
+
+interface TabServerProps {
+ isEdit?: boolean;
+}
+
+export function TabServer({ isEdit }: TabServerProps) {
+ const { t } = useTranslation();
+ const { control, watch } = useFormContext();
+ const authType = watch('imap.auth.auth_type');
+
+ return (
+
+ );
+}
diff --git a/web/src/features/accounts/context/index.tsx b/web/src/features/accounts/context/index.tsx
index b0b93fd..e381d09 100644
--- a/web/src/features/accounts/context/index.tsx
+++ b/web/src/features/accounts/context/index.tsx
@@ -21,10 +21,7 @@ import { AccountModel } from '@/api/account/api';
import React from 'react'
export type AccountDialogType =
- | 'add'
- | 'add-imap'
| 'add-nosync'
- | 'edit-imap'
| 'edit-nosync'
| 'delete'
| 'detail'
diff --git a/web/src/features/accounts/index.tsx b/web/src/features/accounts/index.tsx
index d5f34e1..d80c51a 100644
--- a/web/src/features/accounts/index.tsx
+++ b/web/src/features/accounts/index.tsx
@@ -19,16 +19,16 @@
import { useState } from 'react'
import useDialogState from '@/hooks/use-dialog-state'
+import { useNavigate } from '@tanstack/react-router'
import { Button } from '@/components/ui/button'
import { Main } from '@/components/layout/main'
-import { AccountActionDialog } from './components/action-dialog'
import { useColumns } from './components/columns'
import { AccountDeleteDialog } from './components/delete-dialog'
import { AccountTable } from './components/table'
import AccountProvider, {
type AccountDialogType,
} from './context'
-import { Plus } from 'lucide-react'
+import { Mail, Database } from 'lucide-react'
import Logo from '@/assets/logo.svg'
import { AccountDetailDrawer } from './components/account-detail'
import { AccountModel, list_accounts } from '@/api/account/api'
@@ -42,10 +42,10 @@ import { NoSyncAccountDialog } from './components/nosync-dialog'
import { useTranslation } from 'react-i18next'
import { AccountAccessAssignmentDialog } from './components/access-assignment-dialog'
import { useCurrentUser } from '@/hooks/use-current-user'
-import { AddAccountDialog } from './components/add-account-dialog'
export default function Accounts() {
const { t } = useTranslation()
+ const navigate = useNavigate()
const columns = useColumns()
// Dialog states
const [currentRow, setCurrentRow] = useState(null)
@@ -77,15 +77,14 @@ export default function Accounts() {
{require_any_permission(['system:root', 'account:create']) &&
-
-
setOpen("add")}
- className="border-r-0"
- >
-
- {t('accounts.add')}
-
-
+
navigate({ to: '/accounts/new' })}>
+
+ {t('accounts.imapAccount')}
+
+
setOpen("add-nosync")}>
+
+ {t('accounts.noSyncAccount')}
+
}
@@ -107,8 +106,13 @@ export default function Accounts() {
{t('accounts.noAccountConfigurationsDesc')}
- setOpen("add")}>
- {t('accounts.add')}
+ navigate({ to: '/accounts/new' })}>
+
+ {t('accounts.imapAccount')}
+
+ setOpen('add-nosync')}>
+
+ {t('accounts.noSyncAccount')}
@@ -117,16 +121,6 @@ export default function Accounts() {
- setOpen('add')} />
-
- setOpen('add-imap')}
- />
- {
- setOpen('edit-imap')
- setTimeout(() => {
- setCurrentRow(null)
- }, 500)
- }}
- currentRow={currentRow}
- />
.
+
+export type MatchType = 'contains' | 'starts_with' | 'ends_with' | 'is_exactly' | 'regex';
+
+export interface PatternEntry {
+ id: string;
+ matchType: MatchType;
+ value: string;
+}
+
+function escapeRegex(str: string): string {
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
+
+export function simplePatternToRegex(matchType: MatchType, value: string): string {
+ switch (matchType) {
+ case 'contains':
+ return `.*${escapeRegex(value)}.*`;
+ case 'starts_with':
+ return `^${escapeRegex(value)}.*`;
+ case 'ends_with':
+ return `.*${escapeRegex(value)}$`;
+ case 'is_exactly':
+ return `^${escapeRegex(value)}$`;
+ case 'regex':
+ return value;
+ }
+}
+
+export function isValidRegex(pattern: string): boolean {
+ try {
+ new RegExp(pattern);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+let _idCounter = 0;
+export function newPatternId(): string {
+ return `pat_${++_idCounter}_${Date.now()}`;
+}
diff --git a/web/src/locales/ar.json b/web/src/locales/ar.json
index 997dcf8..f3d69db 100644
--- a/web/src/locales/ar.json
+++ b/web/src/locales/ar.json
@@ -99,10 +99,12 @@
"areYouSureYouWantTo": "هل أنت متأكد من أنك تريد {{action}} هذا الحساب؟",
"auth": "المصادقة",
"authType": "نوع_المصادقة",
- "autoConfiguring": "جارٍ التكوين التلقائي...",
+ "autoConfiguring": "جاري التكوين التلقائي…",
+ "autoDiscover": "اكتشاف إعدادات الخادم تلقائيًا",
"autoDownloadNewMailboxes": "إضافة المجلدات الجديدة تلقائيًا",
"autoDownloadNewMailboxesDescription": "إضافة المجلدات الجديدة المكتشفة تلقائيًا إلى قائمة التنزيل.",
"beforeRelative": "تنزيل رسائل البريد الإلكتروني القديمة فقط",
+ "beforeRelativeDesc": "تنزيل الرسائل الأقدم من الفترة الزمنية المحددة فقط. يتحرك تاريخ القطع تلقائياً مع مرور الوقت — مفيد لأرشفة الرسائل القديمة تدriجياً.",
"beforeRelativeValue": "تنزيل رسائل البريد الإلكتروني قبل {{value}} {{unit}} مضت",
"cancelDownload": "إلغاء التنزيل",
"cancelFailed": "فشل إلغاء مهمة التنزيل",
@@ -124,9 +126,13 @@
"cronHour": "الساعة",
"cronMinute": "الدقيقة",
"cronMonday": "الإثنين",
+ "cronMonthAlignNote": "ملاحظة: إذا كان الشهر أقصر، فسيتم التنفيذ في اليوم الأخير من الشهر.",
"cronMonthly": "شهرياً",
"cronSaturday": "السبت",
"cronSimple": "تعبير بسيط",
+ "cronSummaryDaily": "يومياً عند الساعة {{hour}}:{{minute}}",
+ "cronSummaryMonthly": "في اليوم {{day}} من كل شهر عند الساعة {{hour}}:{{minute}}",
+ "cronSummaryWeekly": "كل يوم {{day}} عند الساعة {{hour}}:{{minute}}",
"cronSunday": "الأحد",
"cronThursday": "الخميس",
"cronTimezoneNote": "جميع الأوقات بالتوقيت المحلي للخادم.",
@@ -145,6 +151,7 @@
"disableAccount": "تعطيل الحساب",
"disabled": "معطل",
"downloadAll": "تنزيل جميع رسائل البريد الإلكتروني",
+ "downloadAllDesc": "سيتم تنزيل جميع رسائل البريد الإلكتروني في كل صندوق بريد محدد، من أول رسالة إلى أحدثها. الأفضل للأرشفة الكاملة.",
"downloadBatchSize": "حجم دفعة التنزيل",
"downloadBatchSizeDescription": "عدد الرسائل التي يتم جلبها لكل طلب IMAP",
"downloadCancelled": "تم إلغاء المهمة",
@@ -176,6 +183,35 @@
"enterValue": "الرجاء إدخال قيمة",
"everyMinutes": "كل {{minutes}} دقيقة",
"field": "الحقل",
+ "filters": {
+ "addHeader": "إضافة رأس",
+ "addPattern": "إضافة نمط",
+ "enableFiltering": "تفعيل تصفية المحتوى",
+ "enableFilteringDesc": "عند التفعيل، سيتم أرشفة الرسائل التي تطابق القواعد أدناه فقط. عند التعطيل، يتم أرشفة جميع الرسائل.",
+ "exclude": "استبعاد",
+ "include": "تضمين",
+ "matchType": {
+ "contains": "يحتوي على",
+ "endsWith": "ينتهي بـ",
+ "isExactly": "يطابق تماماً",
+ "regex": "تعبير نمطي (Regex)",
+ "startsWith": "يبدأ بـ"
+ },
+ "noExcludePatterns": "لا توجد أنماط استبعاد — لن يتم استبعاد أي شيء.",
+ "noIncludePatterns": "لا توجد أنماط تضمين — سيتم تضمين كل شيء.",
+ "noLimit": "بلا حد",
+ "noSpamHeaders": "لم يتم تكوين رؤوس البريد المزعج.",
+ "senderFilter": "فلتر المرسل",
+ "senderFilterHelp": "تصفية الرسائل بناءً على عنوان بريد المرسل. تحدد أنماط التضمين المرسِلين المقبولين، بينما ترفض أنماط الاستبعاد من يطابقها.",
+ "sizeLimit": "حد الحجم",
+ "sizeLimitDesc": "سيتم تخطي الرسائل التي تتجاوز هذا الحجم أثناء الأرشفة. اتركه فارغاً لعدم وضع حد.",
+ "skipLargerThan": "تخطى الرسائل الأكبر من",
+ "spamHeaders": "رؤوس البريد المزعج",
+ "spamHeadersHelp": "سيتم تخطي الرسائل التي تحتوي على أي من هذه الرؤوس بقيمة 'yes' أو 'true'. تظهر أدناه بعض الرؤوس الشائعة لكشف البريد المزعج.",
+ "subjectFilter": "فلتر الموضوع",
+ "subjectFilterHelp": "تصفية الرسائل بناءً على سطر الموضوع. يعمل بنفس طريقة تصفية المرسل.",
+ "suggestions": "اقتراحات (اضغط للإضافة):"
+ },
"fixed": "ثابت",
"folderSync": {
"autoSelectDescendants": "تحديد العناصر الفرعية تلقائيًا",
@@ -284,9 +320,25 @@
"selectUnit": "اختر وحدة",
"selectedMailboxes": "صناديق البريد المحددة",
"serverConfiguration": "تكوين الخادم (IMAP)",
+ "settings": {
+ "download": "تنزيل",
+ "downloadDesc": "تكوين وقت وكيفية جلب رسائل البريد الإلكتروني من الخادم.",
+ "filters": "الفلاتر",
+ "filtersDesc": "التحكم في الرسائل التي يتم أرشفتها. عند تعطيل الفلترة، يتم حفظ جميع الرسائل.",
+ "general": "عام",
+ "generalDesc": "معلومات الحساب الأساسية والحالة.",
+ "newAccount": "حساب جديد",
+ "performance": "الأداء",
+ "schedule": "جدول المزامنة",
+ "scope": "نطاق المزامنة",
+ "server": "الخادم",
+ "serverDesc": "إعدادات اتصال IMAP والمصادقة."
+ },
"since": "منذ",
"sinceFixed": "منذ تاريخ محدد",
+ "sinceFixedDesc": "تنزيل الرسائل المستلمة بعد التاريخ المحدد فقط، وتجاهل ما قبله.",
"sinceRelative": "تنزيل رسائل البريد الإلكتروني الأخيرة فقط",
+ "sinceRelativeDesc": "تنزيل رسائل الفترة الأخيرة فقط (مثل آخر 3 أشهر). يتحرك تاريخ البدء تلقائياً مع مرور الوقت.",
"sinceRelativeValue": "تنزيل رسائل البريد الإلكتروني من آخر",
"startDownload": "بدء التنزيل",
"state": "الحالة",
diff --git a/web/src/locales/da.json b/web/src/locales/da.json
index a505556..6fdbd00 100644
--- a/web/src/locales/da.json
+++ b/web/src/locales/da.json
@@ -99,10 +99,12 @@
"areYouSureYouWantTo": "Er du sikker på, at du vil {{action}} denne konto?",
"auth": "Godkendelse",
"authType": "godkendelsestype",
- "autoConfiguring": "Konfigurerer automatisk...",
+ "autoConfiguring": "Konfigurerer automatisk…",
+ "autoDiscover": "Find serverindstillinger automatisk",
"autoDownloadNewMailboxes": "Tilføj automatisk nye mapper",
"autoDownloadNewMailboxesDescription": "Føj automatisk nye mapper til downloadlisten.",
"beforeRelative": "Download kun gamle e-mails",
+ "beforeRelativeDesc": "Download kun e-mails, der er ældre end den angivne periode. Skæringsdatoen flyttes automatisk – nyttigt til gradvis arkivering af gamle e-mails.",
"beforeRelativeValue": "Download e-mails før {{value}} {{unit}} siden",
"cancelDownload": "Annuller download",
"cancelFailed": "Kunne ikke annullere download-opgave",
@@ -124,9 +126,13 @@
"cronHour": "Time",
"cronMinute": "Minut",
"cronMonday": "Mandag",
+ "cronMonthAlignNote": "Bemærk: Hvis måneden er kortere, udføres den på månedens sidste dag.",
"cronMonthly": "Månedligt",
"cronSaturday": "Lørdag",
"cronSimple": "Simpelt udtryk",
+ "cronSummaryDaily": "Hver dag kl. {{hour}}:{{minute}}",
+ "cronSummaryMonthly": "Den {{day}}. i hver måned kl. {{hour}}:{{minute}}",
+ "cronSummaryWeekly": "Hver {{day}} kl. {{hour}}:{{minute}}",
"cronSunday": "Søndag",
"cronThursday": "Torsdag",
"cronTimezoneNote": "Alle tider er serverens lokale tid.",
@@ -145,6 +151,7 @@
"disableAccount": "Deaktivér konto",
"disabled": "Deaktiveret",
"downloadAll": "Download alle e-mails",
+ "downloadAllDesc": "Alle e-mails i alle valgte postkasser vil blive downloadet. Bedst til komplet arkivering.",
"downloadBatchSize": "Download batchstørrelse",
"downloadBatchSizeDescription": "Antal beskeder hentet pr. IMAP-anmodning",
"downloadCancelled": "Opgave annulleret",
@@ -176,6 +183,35 @@
"enterValue": "Indtast en værdi",
"everyMinutes": "hvert {{minutes}} minut",
"field": "Felt",
+ "filters": {
+ "addHeader": "Tilføj header",
+ "addPattern": "Tilføj mønster",
+ "enableFiltering": "Aktiver indholdsfiltrering",
+ "enableFilteringDesc": "Når den er aktiveret, vil kun e-mails, der matcher reglerne nedenfor, blive arkiveret. Når den er deaktiveret, arkiveres alle e-mails.",
+ "exclude": "Ekskluder",
+ "include": "Inkluder",
+ "matchType": {
+ "contains": "Indeholder",
+ "endsWith": "Ender med",
+ "isExactly": "Er præcis",
+ "regex": "Regulært udtryk (Regex)",
+ "startsWith": "Begynder med"
+ },
+ "noExcludePatterns": "Ingen ekskluderingsmønstre – intet er ekskluderet.",
+ "noIncludePatterns": "Ingen inkluderingsmønstre – alt er inkluderet.",
+ "noLimit": "Ingen begrænsning",
+ "noSpamHeaders": "Ingen spamheadere konfigureret.",
+ "senderFilter": "Afsenderfilter",
+ "senderFilterHelp": "Filtrer e-mails baseret på afsenderens adresse. Inkluderingsmønstre tillader afsendere, mens ekskluderingsmønstre afviser dem.",
+ "sizeLimit": "Størrelsesbegrænsning",
+ "sizeLimitDesc": "E-mails, der er større end denne størrelse, vil blive sprunget over under arkivering. Lad feltet være tomt for ingen begrænsning.",
+ "skipLargerThan": "Spring e-mails over, der er større end",
+ "spamHeaders": "Spamheadere",
+ "spamHeadersHelp": "E-mails med en af disse headere sat til 'yes' eller 'true' vil blive sprunget over. Almindelige spamheadere er foreslået nedenfor.",
+ "subjectFilter": "Emnefilter",
+ "subjectFilterHelp": "Filtrer e-mails baseret på emnelinjen. Fungerer på samme måde som afsenderfiltrering.",
+ "suggestions": "Forslag (klik for at tilføje):"
+ },
"fixed": "Fast",
"folderSync": {
"autoSelectDescendants": "Vælg efterkommere automatisk",
@@ -284,9 +320,25 @@
"selectUnit": "Vælg en enhed",
"selectedMailboxes": "Valgte postkasser",
"serverConfiguration": "Serverkonfiguration (IMAP)",
+ "settings": {
+ "download": "Download",
+ "downloadDesc": "Konfigurer, hvornår og hvordan e-mails hentes fra serveren.",
+ "filters": "Filtre",
+ "filtersDesc": "Styr, hvilke e-mails der arkiveres. Når filtrering er deaktiveret, gemmes alle e-mails.",
+ "general": "Generelt",
+ "generalDesc": "Generelle kontooplysninger og status.",
+ "newAccount": "Ny konto",
+ "performance": "Ydeevne",
+ "schedule": "Tidsplan",
+ "scope": "Omfang",
+ "server": "Server",
+ "serverDesc": "IMAP-forbindelsesindstillinger og godkendelse."
+ },
"since": "siden",
"sinceFixed": "Siden specifik dato",
+ "sinceFixedDesc": "Download kun e-mails modtaget efter den valgte dato. E-mails før denne dato ignoreres.",
"sinceRelative": "Download kun seneste e-mails",
+ "sinceRelativeDesc": "Download kun e-mails fra den seneste periode (f.eks. de seneste 3 måneder). Startdatoen flyttes automatisk fremad.",
"sinceRelativeValue": "Download e-mails fra de sidste",
"startDownload": "Start download",
"state": "Tilstand",
diff --git a/web/src/locales/de.json b/web/src/locales/de.json
index 42080f2..0c0f42e 100644
--- a/web/src/locales/de.json
+++ b/web/src/locales/de.json
@@ -99,10 +99,12 @@
"areYouSureYouWantTo": "Möchten Sie dieses Konto wirklich {{action}}?",
"auth": "Authentifizierung",
"authType": "Authentifizierungstyp",
- "autoConfiguring": "Automatische Konfiguration läuft...",
+ "autoConfiguring": "Automatische Konfiguration…",
+ "autoDiscover": "Servereinstellungen automatisch erkennen",
"autoDownloadNewMailboxes": "Neue Ordner automatisch hinzufügen",
"autoDownloadNewMailboxesDescription": "Neu entdeckte Ordner automatisch zur Download-Liste hinzufügen.",
"beforeRelative": "Nur alte E-Mails herunterladen",
+ "beforeRelativeDesc": "Nur E-Mails herunterladen, die älter als der angegebene Zeitraum sind. Das Stichtagsdatum verschiebt sich automatisch – ideal für die schrittweise Archivierung.",
"beforeRelativeValue": "E-Mails von vor {{value}} {{unit}} herunterladen",
"cancelDownload": "Download abbrechen",
"cancelFailed": "Download-Aufgabe konnte nicht abgebrochen werden",
@@ -124,9 +126,13 @@
"cronHour": "Stunde",
"cronMinute": "Minute",
"cronMonday": "Montag",
+ "cronMonthAlignNote": "Hinweis: Bei kürzeren Monaten erfolgt die Ausführung am letzten Tag des Monats.",
"cronMonthly": "Monatlich",
"cronSaturday": "Samstag",
"cronSimple": "Einfacher Ausdruck",
+ "cronSummaryDaily": "Täglich um {{hour}}:{{minute}} Uhr",
+ "cronSummaryMonthly": "Jeden {{day}}. des Monats um {{hour}}:{{minute}} Uhr",
+ "cronSummaryWeekly": "Wöchentlich, jeden {{day}} um {{hour}}:{{minute}} Uhr",
"cronSunday": "Sonntag",
"cronThursday": "Donnerstag",
"cronTimezoneNote": "Alle Zeiten nutzen Server-Ortszeit.",
@@ -145,6 +151,7 @@
"disableAccount": "Konto deaktivieren",
"disabled": "Deaktiviert",
"downloadAll": "Alle E-Mails herunterladen",
+ "downloadAllDesc": "Alle E-Mails in jedem ausgewählten Postfach werden heruntergeladen. Bestens geeignet für eine vollständige Archivierung.",
"downloadBatchSize": "Download-Batch-Größe",
"downloadBatchSizeDescription": "Anzahl der pro IMAP-Anfrage abgerufenen Nachrichten",
"downloadCancelled": "Aufgabe abgebrochen",
@@ -176,6 +183,35 @@
"enterValue": "Wert eingeben",
"everyMinutes": "alle {{minutes}} Minuten",
"field": "Feld",
+ "filters": {
+ "addHeader": "Header hinzufügen",
+ "addPattern": "Muster hinzufügen",
+ "enableFiltering": "Inhaltsfilterung aktivieren",
+ "enableFilteringDesc": "Wenn aktiviert, werden nur E-Mails archiviert, die den folgenden Regeln entsprechen. Wenn deaktiviert, werden alle E-Mails archiviert.",
+ "exclude": "Ausschließen",
+ "include": "Einschließen",
+ "matchType": {
+ "contains": "Enthält",
+ "endsWith": "Endet mit",
+ "isExactly": "Ist genau",
+ "regex": "Regulärer Ausdruck (Regex)",
+ "startsWith": "Beginnt mit"
+ },
+ "noExcludePatterns": "Keine Ausschlussmuster – nichts wird ausgeschlossen.",
+ "noIncludePatterns": "Keine Einschlussmuster – alles wird eingeschlossen.",
+ "noLimit": "Kein Limit",
+ "noSpamHeaders": "Keine Spam-Header konfiguriert.",
+ "senderFilter": "Absenderfilter",
+ "senderFilterHelp": "Filtert E-Mails nach Absenderadresse. Einschlussmuster lassen Absender zu, Ausschlussmuster weisen übereinstimmende Absender ab.",
+ "sizeLimit": "Größenbeschränkung",
+ "sizeLimitDesc": "E-Mails, die diese Größe überschreiten, werden bei der Archivierung übersprungen. Leer lassen für unbegrenzt.",
+ "skipLargerThan": "E-Mails überspringen größer als",
+ "spamHeaders": "Spam-Header",
+ "spamHeadersHelp": "E-Mails, bei denen einer dieser Header auf 'yes' oder 'true' gesetzt ist, werden übersprungen. Häufige Spam-Header sind unten angegeben.",
+ "subjectFilter": "Betreffsfilter",
+ "subjectFilterHelp": "Filtert E-Mails nach Betreffzeile. Funktioniert genauso wie die Absenderfilterung.",
+ "suggestions": "Vorschläge (zum Hinzufügen anklicken):"
+ },
"fixed": "Fest",
"folderSync": {
"autoSelectDescendants": "Unterelemente automatisch auswählen",
@@ -284,9 +320,25 @@
"selectUnit": "Einheit auswählen",
"selectedMailboxes": "Ausgewählte Postfächer",
"serverConfiguration": "Serverkonfiguration (IMAP)",
+ "settings": {
+ "download": "Herunterladen",
+ "downloadDesc": "Konfigurieren, wann und wie E-Mails vom Server abgerufen werden.",
+ "filters": "Filter",
+ "filtersDesc": "Steuern Sie, welche E-Mails archiviert werden. Wenn die Filterung deaktiviert ist, werden alle E-Mails gespeichert.",
+ "general": "Allgemein",
+ "generalDesc": "Basis-Kontoinformationen und Status.",
+ "newAccount": "Neues Konto",
+ "performance": "Leistung",
+ "schedule": "Zeitplan",
+ "scope": "Zeitraum",
+ "server": "Server",
+ "serverDesc": "IMAP-Verbindungseinstellungen und Authentifizierung."
+ },
"since": "seit",
"sinceFixed": "Seit einem bestimmten Datum",
+ "sinceFixedDesc": "Nur E-Mails herunterladen, die nach dem gewählten Datum empfangen wurden. Ältere E-Mails werden ignoriert.",
"sinceRelative": "Nur aktuelle E-Mails herunterladen",
+ "sinceRelativeDesc": "Nur E-Mails aus dem jüngsten Zeitraum herunterladen (z. B. letzte 3 Monate). Das Startdatum verschiebt sich automatisch.",
"sinceRelativeValue": "E-Mails der letzten Zeit herunterladen",
"startDownload": "Download starten",
"state": "Zustand",
diff --git a/web/src/locales/en.json b/web/src/locales/en.json
index 2f13a09..64a29f6 100644
--- a/web/src/locales/en.json
+++ b/web/src/locales/en.json
@@ -98,11 +98,14 @@
"allMailSkipped": "Selected standard folders. 'All Mail' was skipped to avoid duplicates.",
"areYouSureYouWantTo": "Are you sure you want to {{action}} this account?",
"auth": "Auth",
+ "authPassword": "Password",
"authType": "auth_type",
"autoConfiguring": "Auto-configuring...",
+ "autoDiscover": "Auto-discover Server Settings",
"autoDownloadNewMailboxes": "Auto-add new mailboxes",
"autoDownloadNewMailboxesDescription": "Automatically add newly discovered folders to the download list.",
"beforeRelative": "Download Old Emails Only",
+ "beforeRelativeDesc": "Only download emails older than the specified time period. The cutoff date automatically moves over time — useful for gradually archiving old emails while skipping recent ones.",
"beforeRelativeValue": "Download emails before {{value}} {{unit}} ago",
"cancelDownload": "Cancel download",
"cancelFailed": "Failed to cancel download task",
@@ -114,19 +117,24 @@
"clickSaveWhenDone": "Click save when you're done.",
"continue": "Continue",
"createdAt": "Created At",
+ "creating": "Creating...",
"creationFailed": "Creation failed, please try again later",
"cronAdvanced": "Advanced Expression",
"cronDaily": "Daily",
- "cronDayOfMonth": "Day of Week",
+ "cronDayOfMonth": "Day of Month",
"cronDayOfWeek": "Day of Week",
"cronFrequency": "Frequency",
"cronFriday": "Friday",
"cronHour": "Hour",
"cronMinute": "Minute",
"cronMonday": "Monday",
+ "cronMonthAlignNote": "Note: If the month is shorter, it will execute on the last day of the month.",
"cronMonthly": "Monthly",
"cronSaturday": "Saturday",
"cronSimple": "Simple Expression",
+ "cronSummaryDaily": "Every day at {{hour}}:{{minute}}",
+ "cronSummaryMonthly": "On day {{day}} of every month at {{hour}}:{{minute}}",
+ "cronSummaryWeekly": "Every {{day}} at {{hour}}:{{minute}}",
"cronSunday": "Sunday",
"cronThursday": "Thursday",
"cronTimezoneNote": "All times use server local timezone.",
@@ -145,6 +153,7 @@
"disableAccount": "Disable account",
"disabled": "Disabled",
"downloadAll": "Download All Emails",
+ "downloadAllDesc": "All emails in every selected mailbox will be downloaded, from the very first to the latest. Best for complete archiving.",
"downloadBatchSize": "Download batch size",
"downloadBatchSizeDescription": "Number of messages fetched per IMAP request",
"downloadCancelled": "Task cancelled",
@@ -176,6 +185,35 @@
"enterValue": "Please enter a value",
"everyMinutes": "every {{minutes}} minutes",
"field": "Field",
+ "filters": {
+ "addHeader": "Add Header",
+ "addPattern": "Add pattern",
+ "enableFiltering": "Enable content filtering",
+ "enableFilteringDesc": "When enabled, only emails matching the rules below will be archived. When disabled, all emails are archived.",
+ "exclude": "Exclude",
+ "include": "Include",
+ "matchType": {
+ "contains": "Contains",
+ "endsWith": "Ends with",
+ "isExactly": "Is exactly",
+ "regex": "Regular expression (Regex)",
+ "startsWith": "Starts with"
+ },
+ "noExcludePatterns": "No exclude patterns — nothing is excluded.",
+ "noIncludePatterns": "No include patterns — everything is included.",
+ "noLimit": "No limit",
+ "noSpamHeaders": "No spam headers configured.",
+ "senderFilter": "Sender Filter",
+ "senderFilterHelp": "Filter emails based on the sender's email address. Include patterns determine which senders pass; exclude patterns reject matching senders.",
+ "sizeLimit": "Size Limit",
+ "sizeLimitDesc": "Emails larger than this size will be skipped during archiving. Leave empty for no size limit.",
+ "skipLargerThan": "Skip emails larger than",
+ "spamHeaders": "Spam Headers",
+ "spamHeadersHelp": "Emails with any of these headers set to 'yes' or 'true' will be skipped. Common spam detection headers are suggested below.",
+ "subjectFilter": "Subject Filter",
+ "subjectFilterHelp": "Filter emails based on the subject line. Works the same way as sender filtering.",
+ "suggestions": "Suggestions (click to add):"
+ },
"fixed": "Fixed",
"folderSync": {
"autoSelectDescendants": "Auto select descendants",
@@ -284,9 +322,33 @@
"selectUnit": "Select a unit",
"selectedMailboxes": "Selected Mailboxes",
"serverConfiguration": "Server Configuration (IMAP)",
+ "settings": {
+ "backToAccounts": "Back to Accounts",
+ "download": "Download",
+ "downloadDesc": "Configure when and how emails are fetched from the server.",
+ "filters": "Filters",
+ "filtersDesc": "Control which emails are archived. When filtering is disabled, all emails are saved.",
+ "general": "General",
+ "generalDesc": "Basic account information and status.",
+ "loading": "Loading account...",
+ "newAccount": "New Account",
+ "performance": "Performance",
+ "reset": "Reset",
+ "save": "Save",
+ "saved": "Saved",
+ "savedDesc": "Settings have been saved successfully.",
+ "saving": "Saving...",
+ "schedule": "Schedule",
+ "scope": "Scope",
+ "server": "Server",
+ "serverDesc": "IMAP connection settings and authentication.",
+ "settings": "Settings"
+ },
"since": "since",
"sinceFixed": "Since Specific Date",
+ "sinceFixedDesc": "Only download emails received after the selected date. Emails before that date will be ignored.",
"sinceRelative": "Download Recent Emails Only",
+ "sinceRelativeDesc": "Only download emails from the recent period (e.g. last 3 months). The start date automatically moves forward over time.",
"sinceRelativeValue": "Download emails from the last",
"startDownload": "Start download",
"state": "State",
diff --git a/web/src/locales/es.json b/web/src/locales/es.json
index 3755cf3..99e59ac 100644
--- a/web/src/locales/es.json
+++ b/web/src/locales/es.json
@@ -99,10 +99,12 @@
"areYouSureYouWantTo": "¿Está seguro de que desea {{action}} esta cuenta?",
"auth": "Autenticación",
"authType": "tipo de autenticación",
- "autoConfiguring": "Autoconfigurando...",
+ "autoConfiguring": "Configurando automáticamente…",
+ "autoDiscover": "Detectar automáticamente la configuración del servidor",
"autoDownloadNewMailboxes": "Añadir automáticamente nuevas carpetas",
"autoDownloadNewMailboxesDescription": "Añadir automáticamente las nuevas carpetas a la lista de descarga.",
"beforeRelative": "Descargar solo correos antiguos",
+ "beforeRelativeDesc": "Solo descargar correos anteriores al período especificado. La fecha límite avanza automáticamente, ideal para archivar correos viejos gradualmente.",
"beforeRelativeValue": "Descargar correos de hace {{value}} {{unit}}",
"cancelDownload": "Cancelar descarga",
"cancelFailed": "Error al cancelar la tarea de descarga",
@@ -124,9 +126,13 @@
"cronHour": "Hora",
"cronMinute": "Minuto",
"cronMonday": "Lunes",
+ "cronMonthAlignNote": "Nota: Si el mes es más corto, se ejecutará el último día del mes.",
"cronMonthly": "Mensual",
"cronSaturday": "Sábado",
"cronSimple": "Expresión simple",
+ "cronSummaryDaily": "Todos los días a las {{hour}}:{{minute}}",
+ "cronSummaryMonthly": "El día {{day}} de cada mes a las {{hour}}:{{minute}}",
+ "cronSummaryWeekly": "Todos los {{day}} a las {{hour}}:{{minute}}",
"cronSunday": "Domingo",
"cronThursday": "Jueves",
"cronTimezoneNote": "Horas en zona horaria del servidor.",
@@ -145,6 +151,7 @@
"disableAccount": "Desactivar cuenta",
"disabled": "Deshabilitado",
"downloadAll": "Descargar todos los correos",
+ "downloadAllDesc": "Se descargarán todos los correos de cada buzón seleccionado. Ideal para un archivado completo.",
"downloadBatchSize": "Tamaño del lote de descarga",
"downloadBatchSizeDescription": "Número de mensajes obtenidos por solicitud IMAP",
"downloadCancelled": "Tarea cancelada",
@@ -176,6 +183,35 @@
"enterValue": "Introduce valor",
"everyMinutes": "cada {{minutes}} minutos",
"field": "Campo",
+ "filters": {
+ "addHeader": "Añadir encabezado",
+ "addPattern": "Añadir patrón",
+ "enableFiltering": "Activar filtrado de contenido",
+ "enableFilteringDesc": "Si está activado, solo se archivarán los correos que cumplan las siguientes reglas. Si está desactivado, se archivarán todos.",
+ "exclude": "Excluir",
+ "include": "Incluir",
+ "matchType": {
+ "contains": "Contiene",
+ "endsWith": "Termina en",
+ "isExactly": "Es exactamente",
+ "regex": "Expresión regular (Regex)",
+ "startsWith": "Empieza por"
+ },
+ "noExcludePatterns": "Sin patrones de exclusión: no se excluye nada.",
+ "noIncludePatterns": "Sin patrones de inclusión: se incluye todo.",
+ "noLimit": "Sin límite",
+ "noSpamHeaders": "No hay encabezados de spam configurados.",
+ "senderFilter": "Filtro de remitente",
+ "senderFilterHelp": "Filtra correos por dirección del remitente. Los patrones de inclusión permiten el paso; los de exclusión rechazan coincidencias.",
+ "sizeLimit": "Límite de tamaño",
+ "sizeLimitDesc": "Los correos que superen este tamaño se omitirán al archivar. Déjelo vacío si no desea un límite.",
+ "skipLargerThan": "Omitir correos mayores de",
+ "spamHeaders": "Encabezados de spam",
+ "spamHeadersHelp": "Se omitirán los correos con cualquiera de estos encabezados establecidos en 'yes' o 'true'. Abajo se sugieren encabezados comunes de detección de spam.",
+ "subjectFilter": "Filtro de asunto",
+ "subjectFilterHelp": "Filtra correos por la línea de asunto. Funciona de la misma manera que el filtrado de remitentes.",
+ "suggestions": "Sugerencias (haz clic para añadir):"
+ },
"fixed": "Fija",
"folderSync": {
"autoSelectDescendants": "Seleccionar automáticamente los descendientes",
@@ -284,9 +320,25 @@
"selectUnit": "Seleccionar unidad",
"selectedMailboxes": "Buzones seleccionados",
"serverConfiguration": "Configuración del servidor (IMAP)",
+ "settings": {
+ "download": "Descarga",
+ "downloadDesc": "Configure cuándo y cómo se obtienen los correos del servidor.",
+ "filters": "Filtros",
+ "filtersDesc": "Controle qué correos se archivan. Si el filtrado está desactivado, se guardarán todos.",
+ "general": "General",
+ "generalDesc": "Información básica de la cuenta y estado.",
+ "newAccount": "Nueva cuenta",
+ "performance": "Rendimiento",
+ "schedule": "Planificación",
+ "scope": "Alcance",
+ "server": "Servidor",
+ "serverDesc": "Configuración de conexión IMAP y autenticación."
+ },
"since": "desde",
"sinceFixed": "Desde una fecha específica",
+ "sinceFixedDesc": "Solo descargar correos posteriores a la fecha seleccionada. Los anteriores serán ignorados.",
"sinceRelative": "Descargar solo correos recientes",
+ "sinceRelativeDesc": "Solo descargar correos del período reciente (ej. últimos 3 meses). La fecha de inicio avanza automáticamente.",
"sinceRelativeValue": "Descargar correos de los últimos",
"startDownload": "Iniciar descarga",
"state": "Estado",
diff --git a/web/src/locales/fi.json b/web/src/locales/fi.json
index 722b6dd..dbe9725 100644
--- a/web/src/locales/fi.json
+++ b/web/src/locales/fi.json
@@ -99,10 +99,12 @@
"areYouSureYouWantTo": "Haluatko varmasti {{action}} tämän tilin?",
"auth": "Todennus",
"authType": "todennustyyppi",
- "autoConfiguring": "Automaattinen määritys...",
+ "autoConfiguring": "Määritetään automaattisesti…",
+ "autoDiscover": "Hae palvelinasetukset automaattisesti",
"autoDownloadNewMailboxes": "Lisää uudet kansiot automaattisesti",
"autoDownloadNewMailboxesDescription": "Lisää uudet löydetyt kansiot automaattisesti latausluetteloon.",
"beforeRelative": "Lataa vain vanhat sähköpostit",
+ "beforeRelativeDesc": "Lataa vain määritettyä ajanjaksoa vanhemmat sähköpostit. Takaraja siirtyy automaattisesti – hyödyllinen vanhojen sähköpostien asteittaiseen arkistointiin.",
"beforeRelativeValue": "Lataa sähköpostit {{value}} {{unit}} sitten",
"cancelDownload": "Peruuta lataus",
"cancelFailed": "Lataustehtävän peruuttaminen epäonnistui",
@@ -124,9 +126,13 @@
"cronHour": "Tunti",
"cronMinute": "Minuutti",
"cronMonday": "Maanantai",
+ "cronMonthAlignNote": "Huomautus: Jos kuukausi on lyhyempi, suoritus tapahtuu kuukauden viimeisenä päivänä.",
"cronMonthly": "Kuukausittain",
"cronSaturday": "Lauantai",
"cronSimple": "Yksinkertainen lauseke",
+ "cronSummaryDaily": "Joka päivä klo {{hour}}:{{minute}}",
+ "cronSummaryMonthly": "Joka kuukauden {{day}}. päivä klo {{hour}}:{{minute}}",
+ "cronSummaryWeekly": "Joka {{day}} klo {{hour}}:{{minute}}",
"cronSunday": "Sunnuntai",
"cronThursday": "Torstai",
"cronTimezoneNote": "Kaikki ajat palvelimen paikallista aikaa.",
@@ -145,6 +151,7 @@
"disableAccount": "Poista tili käytöstä",
"disabled": "Poistettu käytöstä",
"downloadAll": "Lataa kaikki sähköpostit",
+ "downloadAllDesc": "Kaikki sähköpostit valituista postilaatikoista ladataan. Paras täydelliseen arkistointiin.",
"downloadBatchSize": "Latauserän koko",
"downloadBatchSizeDescription": "IMAP-pyyntöä kohden noudettujen viestien määrä",
"downloadCancelled": "Tehtävä peruutettu",
@@ -176,6 +183,35 @@
"enterValue": "Syötä arvo",
"everyMinutes": "joka {{minutes}} minuutti",
"field": "Kenttä",
+ "filters": {
+ "addHeader": "Lisää otsake",
+ "addPattern": "Lisää kuvio",
+ "enableFiltering": "Ota sisällön suodatus käyttöön",
+ "enableFilteringDesc": "Kun käytössä, vain alla olevia sääntöjä vastaavat sähköpostit arkistoidaan. Kun poissa käytöstä, kaikki sähköpostit arkistoidaan.",
+ "exclude": "Poissulje",
+ "include": "Sisällytä",
+ "matchType": {
+ "contains": "Sisältää",
+ "endsWith": "Päättyy merkkeihin",
+ "isExactly": "On täsmälleen",
+ "regex": "Säännöllinen lauseke (Regex)",
+ "startsWith": "Alkaa merkeillä"
+ },
+ "noExcludePatterns": "Ei poissulkemiskuvioita – mitään ei poissuljeta.",
+ "noIncludePatterns": "Ei sisällytyskuvioita – kaikki sisällytetään.",
+ "noLimit": "Ei rajoitusta",
+ "noSpamHeaders": "Spam-otsakkeita ei ole määritetty.",
+ "senderFilter": "Lähettäjän suodatin",
+ "senderFilterHelp": "Suodata sähköpostit lähettäjän osoitteen perusteun. Sisällytyskuviot sallivat lähettäjät, poissulkemiskuviot hylkäävät ne.",
+ "sizeLimit": "Koko-rajoitus",
+ "sizeLimitDesc": "Tätä suuremmat sähköpostit ohitetaan arkistoinnin aikana. Jätä tyhjäksi, jos et halua kokorajoitusta.",
+ "skipLargerThan": "Ohita sähköpostit, jotka ovat suurempia kuin",
+ "spamHeaders": "Spam-otsakkeet",
+ "spamHeadersHelp": "Sähköpostit, joiden otsakkeena on 'yes' tai 'true', ohitetaan. Yleisimmät roskapostin tunnistusotsakkeet näkyvät alla.",
+ "subjectFilter": "Aiheen suodatin",
+ "subjectFilterHelp": "Suodata sähköpostit aiherivin perusteella. Toimii samalla tavalla kuin lähettäjän suodatus.",
+ "suggestions": "Ehdotukset (klikkaa lisätäksesi):"
+ },
"fixed": "Kiinteä",
"folderSync": {
"autoSelectDescendants": "Valitse alisolmut automaattisesti",
@@ -284,9 +320,25 @@
"selectUnit": "Valitse yksikkö",
"selectedMailboxes": "Valitut postilaatikot",
"serverConfiguration": "Palvelinmääritys (IMAP)",
+ "settings": {
+ "download": "Lataus",
+ "downloadDesc": "Määritä, milloin ja miten sähköpostit haetaan palvelimelta.",
+ "filters": "Suodattimet",
+ "filtersDesc": "Hallitse, mitkä sähköpostit arkistoidaan. Kun suodatus on poissa päältä, kaikki sähköpostit tallennetaan.",
+ "general": "Yleiset",
+ "generalDesc": "Tilin perustiedot ja tila.",
+ "newAccount": "Uusi tili",
+ "performance": "Suorituskyky",
+ "schedule": "Aikataulu",
+ "scope": "Laajuus",
+ "server": "Palvelin",
+ "serverDesc": "IMAP-yhteysasetukset ja todennus."
+ },
"since": "alkaen",
"sinceFixed": "Tietystä päivämäärästä lähtien",
+ "sinceFixedDesc": "Lataa vain valitun päivämäärän jälkeen saapuneet sähköpostit. Sitä edeltävät sähköpostit ohitetaan.",
"sinceRelative": "Lataa vain viimeisimmät sähköpostit",
+ "sinceRelativeDesc": "Lataa vain viimeaikaiset sähköpostit (esim. viimeiset 3 kuukautta). Aloituspäivämäärä siirtyy automaattisesti eteenpäin.",
"sinceRelativeValue": "Lataa sähköpostit viimeisimmiltä",
"startDownload": "Aloita lataus",
"state": "Tila",
diff --git a/web/src/locales/fr.json b/web/src/locales/fr.json
index fdf7cd9..92cc4ce 100644
--- a/web/src/locales/fr.json
+++ b/web/src/locales/fr.json
@@ -99,10 +99,12 @@
"areYouSureYouWantTo": "Êtes-vous sûr de vouloir {{action}} ce compte ?",
"auth": "Auth.",
"authType": "type_auth",
- "autoConfiguring": "Configuration automatique...",
+ "autoConfiguring": "Configuration automatique…",
+ "autoDiscover": "Détection automatique des paramètres du serveur",
"autoDownloadNewMailboxes": "Ajouter automatiquement les nouveaux dossiers",
"autoDownloadNewMailboxesDescription": "Ajouter automatiquement les nouveaux dossiers à la liste de téléchargement.",
"beforeRelative": "Télécharger uniquement les anciens e-mails",
+ "beforeRelativeDesc": "Télécharger uniquement les e-mails antérieurs à la période spécifiée. La date limite s'ajuste automatiquement, idéal pour archiver progressivement les anciens e-mails.",
"beforeRelativeValue": "Télécharger les e-mails d'il y a {{value}} {{unit}}",
"cancelDownload": "Annuler le téléchargement",
"cancelFailed": "Échec de l'annulation de la tâche de téléchargement",
@@ -124,9 +126,13 @@
"cronHour": "Heure",
"cronMinute": "Minute",
"cronMonday": "Lundi",
+ "cronMonthAlignNote": "Remarque : Si le mois est plus court, l'exécution aura lieu le dernier jour du mois.",
"cronMonthly": "Chaque mois",
"cronSaturday": "Samedi",
"cronSimple": "Expression simple",
+ "cronSummaryDaily": "Chaque jour à {{hour}}:{{minute}}",
+ "cronSummaryMonthly": "Le {{day}} de chaque mois à {{hour}}:{{minute}}",
+ "cronSummaryWeekly": "Chaque {{day}} à {{hour}}:{{minute}}",
"cronSunday": "Dimanche",
"cronThursday": "Jeudi",
"cronTimezoneNote": "Heures au fuseau horaire du serveur.",
@@ -145,6 +151,7 @@
"disableAccount": "Désactiver le compte",
"disabled": "Désactivé",
"downloadAll": "Télécharger tous les e-mails",
+ "downloadAllDesc": "Tous les e-mails des boîtes aux lettres sélectionnées seront téléchargés. Idéal pour un archivage complet.",
"downloadBatchSize": "Taille du lot de téléchargement",
"downloadBatchSizeDescription": "Nombre de messages récupérés par requête IMAP",
"downloadCancelled": "Tâche annulée",
@@ -176,6 +183,35 @@
"enterValue": "Entrez une valeur",
"everyMinutes": "toutes les {{minutes}} minutes",
"field": "Champ",
+ "filters": {
+ "addHeader": "Ajouter un en-tête",
+ "addPattern": "Ajouter un motif",
+ "enableFiltering": "Activer le filtrage du contenu",
+ "enableFilteringDesc": "Lorsqu'il est activé, seuls les e-mails correspondant aux règles ci-dessous seront archivés. S'il est désactivé, tous les e-mails seront archivés.",
+ "exclude": "Exclure",
+ "include": "Inclure",
+ "matchType": {
+ "contains": "Contient",
+ "endsWith": "Se termine par",
+ "isExactly": "Est exactement",
+ "regex": "Expression régulière (Regex)",
+ "startsWith": "Commence par"
+ },
+ "noExcludePatterns": "Aucun motif d'exclusion — rien n'est exclu.",
+ "noIncludePatterns": "Aucun motif d'inclusion — tout est inclus.",
+ "noLimit": "Sans limite",
+ "noSpamHeaders": "Aucun en-tête de spam configuré.",
+ "senderFilter": "Filtre d'expéditeur",
+ "senderFilterHelp": "Filtrez par adresse de l'expéditeur. Les motifs d'inclusion autorisent les expéditeurs ; les motifs d'exclusion les rejettent.",
+ "sizeLimit": "Limite de taille",
+ "sizeLimitDesc": "Les e-mails dépassant cette taille seront ignorés lors de l'archivage. Laissez vide pour ne pas fixer de limite.",
+ "skipLargerThan": "Ignorer les e-mails supérieurs à",
+ "spamHeaders": "En-têtes de spam",
+ "spamHeadersHelp": "Les e-mails contenant l'un de ces en-têtes défini sur 'yes' ou 'true' seront ignorés. Des en-têtes de détection de spam courants sont suggérés ci-dessous.",
+ "subjectFilter": "Filtre d'objet",
+ "subjectFilterHelp": "Filtrez les e-mails en fonction de l'objet. Fonctionne de la même manière que le filtrage des expéditeurs.",
+ "suggestions": "Suggestions (cliquer pour ajouter) :"
+ },
"fixed": "Fixe",
"folderSync": {
"autoSelectDescendants": "Sélectionner automatiquement les descendants",
@@ -284,9 +320,25 @@
"selectUnit": "Sélectionner une unité",
"selectedMailboxes": "Boîtes mail sélectionnées",
"serverConfiguration": "Configuration du Serveur (IMAP)",
+ "settings": {
+ "download": "Téléchargement",
+ "downloadDesc": "Configurer quand et comment les e-mails sont récupérés depuis le serveur.",
+ "filters": "Filtres",
+ "filtersDesc": "Contrôlez quels e-mails sont archivés. Si le filtrage est désactivé, tous les e-mails seront enregistrés.",
+ "general": "Général",
+ "generalDesc": "Informations de base sur le compte et statut.",
+ "newAccount": "Nouveau compte",
+ "performance": "Performances",
+ "schedule": "Planification",
+ "scope": "Période",
+ "server": "Serveur",
+ "serverDesc": "Paramètres de connexion IMAP et authentification."
+ },
"since": "depuis",
"sinceFixed": "Depuis une date spécifique",
+ "sinceFixedDesc": "Télécharger uniquement les e-mails reçus après la date sélectionnée. Les e-mails antérieurs seront ignorés.",
"sinceRelative": "Télécharger uniquement les e-mails récents",
+ "sinceRelativeDesc": "Télécharger uniquement les e-mails récents (ex. 3 derniers mois). La date de début avance automatiquement.",
"sinceRelativeValue": "Télécharger les e-mails des derniers",
"startDownload": "Lancer le téléchargement",
"state": "État",
diff --git a/web/src/locales/it.json b/web/src/locales/it.json
index 3b64a37..6579e19 100644
--- a/web/src/locales/it.json
+++ b/web/src/locales/it.json
@@ -99,10 +99,12 @@
"areYouSureYouWantTo": "Sei sicuro di voler {{action}} questo account?",
"auth": "Autenticazione",
"authType": "tipo_autenticazione",
- "autoConfiguring": "Configurazione automatica...",
+ "autoConfiguring": "Configurazione automatica…",
+ "autoDiscover": "Rilevamento automatico impostazioni server",
"autoDownloadNewMailboxes": "Aggiungi automaticamente nuove cartelle",
"autoDownloadNewMailboxesDescription": "Aggiungi automaticamente le nuove cartelle all'elenco di download.",
"beforeRelative": "Scarica solo le vecchie email",
+ "beforeRelativeDesc": "Scarica solo le email antecedenti al periodo specificato. La data limite si aggiorna automaticamente, utile per archiviare gradualmente le vecchie email.",
"beforeRelativeValue": "Scarica email da {{value}} {{unit}} fa",
"cancelDownload": "Annulla download",
"cancelFailed": "Annullamento attività di download non riuscito",
@@ -124,9 +126,13 @@
"cronHour": "Ora",
"cronMinute": "Minuto",
"cronMonday": "Lunedì",
+ "cronMonthAlignNote": "Nota: Se il mese è più breve, verrà eseguito l'ultimo giorno del mese.",
"cronMonthly": "Ogni mese",
"cronSaturday": "Sabato",
"cronSimple": "Espressione semplice",
+ "cronSummaryDaily": "Ogni giorno alle {{hour}}:{{minute}}",
+ "cronSummaryMonthly": "Il giorno {{day}} di ogni mese alle {{hour}}:{{minute}}",
+ "cronSummaryWeekly": "Ogni {{day}} alle {{hour}}:{{minute}}",
"cronSunday": "Domenica",
"cronThursday": "Giovedì",
"cronTimezoneNote": "Orari nel fuso orario del server.",
@@ -145,6 +151,7 @@
"disableAccount": "Disattiva account",
"disabled": "Disabilitato",
"downloadAll": "Scarica tutte le email",
+ "downloadAllDesc": "Tutte le email in ogni casella di posta selezionata verranno scaricate. Ideale per un archivio completo.",
"downloadBatchSize": "Dimensione del lotto di download",
"downloadBatchSizeDescription": "Numero di messaggi recuperati per richiesta IMAP",
"downloadCancelled": "Attività annullata",
@@ -176,6 +183,35 @@
"enterValue": "Inserisci un valore",
"everyMinutes": "ogni {{minutes}} minuti",
"field": "Campo",
+ "filters": {
+ "addHeader": "Aggiungi intestazione",
+ "addPattern": "Aggiungi pattern",
+ "enableFiltering": "Attiva il filtraggio dei contenuti",
+ "enableFilteringDesc": "Se attivato, verranno archiviate solo le email che soddisfano le regole sottostanti. Se disattivato, verranno archiviate tutte le email.",
+ "exclude": "Escludi",
+ "include": "Includi",
+ "matchType": {
+ "contains": "Contiene",
+ "endsWith": "Termina con",
+ "isExactly": "È esattamente",
+ "regex": "Espressione regolare (Regex)",
+ "startsWith": "Inizia con"
+ },
+ "noExcludePatterns": "Nessun pattern di esclusione — nulla è escluso.",
+ "noIncludePatterns": "Nessun pattern di inclusione — tutto è incluso.",
+ "noLimit": "Nessun limite",
+ "noSpamHeaders": "Nessuna intestazione spam configurata.",
+ "senderFilter": "Filtro mittente",
+ "senderFilterHelp": "Filtra le email in base all'indirizzo del mittente. I pattern di inclusione approvano i mittenti, quelli di esclusione li rifiutano.",
+ "sizeLimit": "Limite di dimensione",
+ "sizeLimitDesc": "Le email che superano questa dimensione verranno ignorate durante l'archiviazione. Lascia vuoto per nessun limite.",
+ "skipLargerThan": "Salta le email più grandi di",
+ "spamHeaders": "Intestazioni spam",
+ "spamHeadersHelp": "Le email con una di queste intestazioni impostata su 'yes' o 'true' verranno ignorate. Di seguito sono suggerite le intestazioni spam più comuni.",
+ "subjectFilter": "Filtro oggetto",
+ "subjectFilterHelp": "Filtra le email in base all'oggetto. Funziona allo stesso modo del filtraggio del mittente.",
+ "suggestions": "Suggerimenti (clicca per aggiungere):"
+ },
"fixed": "Fissa",
"folderSync": {
"autoSelectDescendants": "Seleziona automaticamente i discendenti",
@@ -284,9 +320,25 @@
"selectUnit": "Seleziona un'unità",
"selectedMailboxes": "Caselle selezionate",
"serverConfiguration": "Configurazione Server (IMAP)",
+ "settings": {
+ "download": "Download",
+ "downloadDesc": "Configura quando e come le email vengono scaricate dal server.",
+ "filters": "Filtri",
+ "filtersDesc": "Controlla quali email archiviare. Quando il filtraggio è disattivato, vengono salvate tutte le email.",
+ "general": "Generale",
+ "generalDesc": "Informazioni di base sull'account e stato.",
+ "newAccount": "Nuovo account",
+ "performance": "Prestazioni",
+ "schedule": "Pianificazione",
+ "scope": "Ambito",
+ "server": "Server",
+ "serverDesc": "Impostazioni di connessione IMAP e autenticazione."
+ },
"since": "da",
"sinceFixed": "Da una data specifica",
+ "sinceFixedDesc": "Scarica solo le email ricevute dopo la date selezionata. Le email precedenti saranno ignorate.",
"sinceRelative": "Scarica solo le email recenti",
+ "sinceRelativeDesc": "Scarica solo le email del periodo recente (es. ultimi 3 mesi). La data di inizio si aggiorna automaticamente.",
"sinceRelativeValue": "Scarica email degli ultimi",
"startDownload": "Avvia download",
"state": "Stato",
diff --git a/web/src/locales/jp.json b/web/src/locales/jp.json
index 30dc60d..aae12e1 100644
--- a/web/src/locales/jp.json
+++ b/web/src/locales/jp.json
@@ -99,10 +99,12 @@
"areYouSureYouWantTo": "本当にこのアカウントを{{action}}しますか?",
"auth": "認証",
"authType": "認証タイプ",
- "autoConfiguring": "自動設定中...",
+ "autoConfiguring": "自動設定中…",
+ "autoDiscover": "サーバー設定の自動検出",
"autoDownloadNewMailboxes": "新着フォルダーの自動追加",
"autoDownloadNewMailboxesDescription": "新しく見つかったフォルダーを自動的にダウンロード一覧に追加します。",
"beforeRelative": "古いメールのみダウンロード",
+ "beforeRelativeDesc": "指定した期間より古いメールのみをダウンロードします。基準日は自動的に推移し、古いメールの段階的なアーカイブに役立ちます。",
"beforeRelativeValue": "{{value}} {{unit}}前より古いメールをダウンロード",
"cancelDownload": "ダウンロードをキャンセル",
"cancelFailed": "ダウンロードタスクのキャンセルに失敗しました",
@@ -124,9 +126,13 @@
"cronHour": "时",
"cronMinute": "分",
"cronMonday": "月曜日",
+ "cronMonthAlignNote": "注意:指定した日付がその月にない場合は、月末の最終日に実行されます。",
"cronMonthly": "毎月",
"cronSaturday": "土曜日",
"cronSimple": "簡易式",
+ "cronSummaryDaily": "毎日 {{hour}}:{{minute}}",
+ "cronSummaryMonthly": "毎月 {{day}} 日 {{hour}}:{{minute}}",
+ "cronSummaryWeekly": "毎週{{day}} {{hour}}:{{minute}}",
"cronSunday": "日曜日",
"cronThursday": "木曜日",
"cronTimezoneNote": "時間はサーバーの現地時間です。",
@@ -145,6 +151,7 @@
"disableAccount": "アカウントを無効化",
"disabled": "無効",
"downloadAll": "すべてのメールをダウンロード",
+ "downloadAllDesc": "選択されたメールボックスの全メールを最初から最新までダウンロードします。完全なアーカイブに最適です。",
"downloadBatchSize": "ダウンロードバッチサイズ",
"downloadBatchSizeDescription": "IMAPリクエストごとに取得されるメッセージ数",
"downloadCancelled": "タスクをキャンセルしました",
@@ -176,6 +183,35 @@
"enterValue": "値を入力してください",
"everyMinutes": "{{minutes}}分ごと",
"field": "フィールド",
+ "filters": {
+ "addHeader": "ヘッダーを追加",
+ "addPattern": "パターンを追加",
+ "enableFiltering": "コンテンツフィルターを有効にする",
+ "enableFilteringDesc": "有効にすると、以下のルールに一致するメールのみがアーカイブされます。無効にすると、すべてのメールがアーカイブされます。",
+ "exclude": "除外する",
+ "include": "含める",
+ "matchType": {
+ "contains": "含む",
+ "endsWith": "後方一致",
+ "isExactly": "完全一致",
+ "regex": "正規表現 (Regex)",
+ "startsWith": "前方一致"
+ },
+ "noExcludePatterns": "除外するパターンがありません — 何も除外されません。",
+ "noIncludePatterns": "含めるパターンがありません — すべて対象になります。",
+ "noLimit": "制限なし",
+ "noSpamHeaders": "スパムヘッダーが設定されていません。",
+ "senderFilter": "送信者フィルター",
+ "senderFilterHelp": "送信者のメールアドレスでフィルターします。一致する「含めるパターン」は許可され、「除外するパターン」は拒否されます。",
+ "sizeLimit": "サイズ制限",
+ "sizeLimitDesc": "このサイズを超えるメールはアーカイブ時にスキップされます。制限しない場合は空欄にしてください。",
+ "skipLargerThan": "次より大きいサイズをスキップ:",
+ "spamHeaders": "スパムヘッダー",
+ "spamHeadersHelp": "これらのヘッダーのいずれかが 'yes' または 'true' に設定されているメールはスキップされます。以下に一般的なスパム検出ヘッダーを提案しています。",
+ "subjectFilter": "件名フィルター",
+ "subjectFilterHelp": "メールの件名でフィルターします。送信者フィルターと同じ仕組みで動作します。",
+ "suggestions": "推奨設定(クリックで追加):"
+ },
"fixed": "固定",
"folderSync": {
"autoSelectDescendants": "子項目を自動選択",
@@ -284,9 +320,25 @@
"selectUnit": "単位を選択",
"selectedMailboxes": "選択されたメールボックス",
"serverConfiguration": "サーバー設定 (IMAP)",
+ "settings": {
+ "download": "ダウンロード設定",
+ "downloadDesc": "サーバーからメールを取得するタイミングと方法を設定します。",
+ "filters": "フィルター",
+ "filtersDesc": "アーカイブ対象のメールを制御します。フィルターを無効にすると、すべてのメールが保存されます。",
+ "general": "基本情報",
+ "generalDesc": "アカウントの基本情報とステータス。",
+ "newAccount": "新規アカウント",
+ "performance": "パフォーマンス",
+ "schedule": "時間計画",
+ "scope": "同期対象期間",
+ "server": "サーバー設定",
+ "serverDesc": "IMAP接続設定と認証。"
+ },
"since": "以降",
"sinceFixed": "指定した日付以降",
+ "sinceFixedDesc": "選択した日付以降に受信したメールのみをダウンロードし、それ以前のメールは無視します。",
"sinceRelative": "最近のメールのみダウンロード",
+ "sinceRelativeDesc": "直近の期間(例:過去3ヶ月)のメールのみをダウンロードします。開始日は時間経過に伴い自動的に更新されます。",
"sinceRelativeValue": "直近の期間のメールをダウンロード",
"startDownload": "ダウンロードを開始",
"state": "状態",
diff --git a/web/src/locales/ko.json b/web/src/locales/ko.json
index 170b4de..11be0ed 100644
--- a/web/src/locales/ko.json
+++ b/web/src/locales/ko.json
@@ -99,10 +99,12 @@
"areYouSureYouWantTo": "이 계정을 정말 {{action}}하시겠습니까?",
"auth": "인증",
"authType": "인증 유형",
- "autoConfiguring": "자동 구성 중...",
+ "autoConfiguring": "자동 설정 중…",
+ "autoDiscover": "서버 설정 자동 검색",
"autoDownloadNewMailboxes": "새 폴더 자동 추가",
"autoDownloadNewMailboxesDescription": "새로 발견된 폴더를 다운로드 목록에 자동으로 추가합니다.",
"beforeRelative": "이전 이메일만 다운로드",
+ "beforeRelativeDesc": "지정한 기간보다 오래된 이메일만 다운로드합니다. 기준 날짜는 자동으로 이동하며, 오래된 이메일을 순차적으로 보관할 때 유용합니다.",
"beforeRelativeValue": "{{value}} {{unit}} 전의 이메일 다운로드",
"cancelDownload": "다운로드 취소",
"cancelFailed": "다운로드 작업 취소 실패",
@@ -124,9 +126,13 @@
"cronHour": "시",
"cronMinute": "분",
"cronMonday": "월요일",
+ "cronMonthAlignNote": "참고: 해당 날짜가 없는 짧은 달의 경우, 해당 월의 마지막 날에 실행됩니다.",
"cronMonthly": "매월",
"cronSaturday": "토요일",
"cronSimple": "간단한 표현식",
+ "cronSummaryDaily": "매일 {{hour}}:{{minute}}",
+ "cronSummaryMonthly": "매월 {{day}}일 {{hour}}:{{minute}}",
+ "cronSummaryWeekly": "매주 {{day}} {{hour}}:{{minute}}",
"cronSunday": "일요일",
"cronThursday": "목요일",
"cronTimezoneNote": "모든 시간은 서버 현지 시간 기준입니다.",
@@ -145,6 +151,7 @@
"disableAccount": "계정 비활성화",
"disabled": "비활성화",
"downloadAll": "모든 이메일 다운로드",
+ "downloadAllDesc": "선택한 사서함의 모든 이메일을 처음부터 끝까지 다운로드합니다. 전체 보관에 가장 적합합니다.",
"downloadBatchSize": "다운로드 일괄 처리 크기",
"downloadBatchSizeDescription": "IMAP 요청당 가져온 메시지 수",
"downloadCancelled": "작업 취소됨",
@@ -176,6 +183,35 @@
"enterValue": "값 입력",
"everyMinutes": "매 {{minutes}}분",
"field": "필드",
+ "filters": {
+ "addHeader": "헤더 추가",
+ "addPattern": "패턴 추가",
+ "enableFiltering": "콘텐츠 필터링 활성화",
+ "enableFilteringDesc": "활성화하면 아래 규칙과 일치하는 이메일만 보관됩니다. 비활성화하면 모든 이메일이 보관됩니다.",
+ "exclude": "제외",
+ "include": "포함",
+ "matchType": {
+ "contains": "포함",
+ "endsWith": "끝 문자열",
+ "isExactly": "정확히 일치",
+ "regex": "정규 표현식 (Regex)",
+ "startsWith": "시작 문자열"
+ },
+ "noExcludePatterns": "제외 패턴 없음 — 제외되는 항목이 없습니다.",
+ "noIncludePatterns": "포함 패턴 없음 — 모든 항목이 포함됩니다.",
+ "noLimit": "제한 없음",
+ "noSpamHeaders": "설정된 스팸 헤더가 없습니다.",
+ "senderFilter": "발신자 필터",
+ "senderFilterHelp": "발신자 주소 기준 필터입니다. 포함 패턴에 일치하면 허용되고, 제외 패턴에 일치하면 차단됩니다.",
+ "sizeLimit": "용량 제한",
+ "sizeLimitDesc": "이 크기를 초과하는 이메일은 보관 시 건너뜁니다. 제한하지 않으려면 비워 두세요.",
+ "skipLargerThan": "다음 용량보다 큰 이메일 건너뛰기:",
+ "spamHeaders": "스팸 헤더",
+ "spamHeadersHelp": "이러한 헤더 중 하나라도 'yes' 또는 'true'로 설정된 이메일은 건너뜁니다. 자주 사용되는 스팸 감지 헤더가 아래에 제안되어 있습니다.",
+ "subjectFilter": "제목 필터",
+ "subjectFilterHelp": "이메일 제목 기준 필터입니다. 발신자 필터링과 동일한 방식으로 작동합니다.",
+ "suggestions": "추천 항목 (클릭하여 추가):"
+ },
"fixed": "고정",
"folderSync": {
"autoSelectDescendants": "하위 항목 자동 선택",
@@ -284,9 +320,25 @@
"selectUnit": "단위 선택",
"selectedMailboxes": "선택된 메일함",
"serverConfiguration": "서버 구성 (IMAP)",
+ "settings": {
+ "download": "다운로드 설정",
+ "downloadDesc": "서버에서 이메일을 가져오는 시기와 방법을 설정합니다.",
+ "filters": "필터",
+ "filtersDesc": "보관할 이메일을 제어합니다. 필터링을 비활성화하면 모든 이메일이 저장됩니다.",
+ "general": "기본 정보",
+ "generalDesc": "기본 계정 정보 및 상태입니다.",
+ "newAccount": "새 계정",
+ "performance": "성능",
+ "schedule": "시간 계획",
+ "scope": "동기화 범위",
+ "server": "서버 설정",
+ "serverDesc": "IMAP 연결 설정 및 인증입니다."
+ },
"since": "이후",
"sinceFixed": "특정 날짜 이후",
+ "sinceFixedDesc": "선택한 날짜 이후에 수신된 이메일만 다운로드하며, 그 이전 이메일은 무시합니다.",
"sinceRelative": "최근 이메일만 다운로드",
+ "sinceRelativeDesc": "최근 기간(예: 지난 3개월)의 이메일만 다운로드합니다. 시작 날짜는 시간이 지남에 따라 자동으로 이동합니다.",
"sinceRelativeValue": "최근 기간의 이메일 다운로드",
"startDownload": "다운로드 시작",
"state": "상태",
diff --git a/web/src/locales/nl.json b/web/src/locales/nl.json
index 6e2c196..c9fb6dd 100644
--- a/web/src/locales/nl.json
+++ b/web/src/locales/nl.json
@@ -99,10 +99,12 @@
"areYouSureYouWantTo": "Weet je zeker dat je dit account wilt {{action}}?",
"auth": "Authenticatie",
"authType": "authenticatie_type",
- "autoConfiguring": "Automatisch configureren...",
+ "autoConfiguring": "Automatisch configureren…",
+ "autoDiscover": "Serverinstellingen automatisch detecteren",
"autoDownloadNewMailboxes": "Nieuwe mappen automatisch importeren",
"autoDownloadNewMailboxesDescription": "Voeg automatisch nieuw ontdekte mappen toe aan de downloadlijst.",
"beforeRelative": "Download alleen oude e-mails",
+ "beforeRelativeDesc": "Download alleen e-mails die ouder zijn dan de opgegeven periode. De limietdatum verschuift automatisch – handig voor het geleidelijk archiveren van oude e-mails.",
"beforeRelativeValue": "Download e-mails van {{value}} {{unit}} geleden",
"cancelDownload": "Download annuleren",
"cancelFailed": "Downloadtaak annuleren mislukt",
@@ -124,9 +126,13 @@
"cronHour": "Uur",
"cronMinute": "Minuut",
"cronMonday": "Maandag",
+ "cronMonthAlignNote": "Opmerking: Als de maand korter is, wordt dit op de laatste dag van de maand uitgevoerd.",
"cronMonthly": "Maandelijks",
"cronSaturday": "Zaterdag",
"cronSimple": "Simpele expressie",
+ "cronSummaryDaily": "Elke dag om {{hour}}:{{minute}}",
+ "cronSummaryMonthly": "Op dag {{day}} van elke maand om {{hour}}:{{minute}}",
+ "cronSummaryWeekly": "Elke {{day}} om {{hour}}:{{minute}}",
"cronSunday": "Zondag",
"cronThursday": "Donderdag",
"cronTimezoneNote": "Alle tijden zijn server-lokale tijd.",
@@ -145,6 +151,7 @@
"disableAccount": "Account uitschakelen",
"disabled": "Uitgeschakeld",
"downloadAll": "Download alle e-mails",
+ "downloadAllDesc": "Alle e-mails in elke geselecteerde mailbox worden gedownload. Best voor volledige archivering.",
"downloadBatchSize": "Download batchgrootte",
"downloadBatchSizeDescription": "Aantal berichten opgehaald per IMAP-verzoek",
"downloadCancelled": "Taak geannuleerd",
@@ -176,6 +183,35 @@
"enterValue": "Voer een waarde in",
"everyMinutes": "elke {{minutes}} minuten",
"field": "Veld",
+ "filters": {
+ "addHeader": "Header toevoegen",
+ "addPattern": "Patroon toevoegen",
+ "enableFiltering": "Inhoudsfiltering inschakelen",
+ "enableFilteringDesc": "Indien ingeschakeld, worden alleen e-mails die aan de onderstaande regels voldoen gearchiveerd. Indien uitgeschakeld, worden alle e-mails gearchiveerd.",
+ "exclude": "Exclusief",
+ "include": "Inclusief",
+ "matchType": {
+ "contains": "Bevat",
+ "endsWith": "Eindigt met",
+ "isExactly": "Is exact",
+ "regex": "Reguliere expressie (Regex)",
+ "startsWith": "Begint met"
+ },
+ "noExcludePatterns": "Geen exclusiepatronen — niets is uitgesloten.",
+ "noIncludePatterns": "Geen inclusiepatronen — alles is inbegrepen.",
+ "noLimit": "Geen limiet",
+ "noSpamHeaders": "Geen spamheaders geconfigureerd.",
+ "senderFilter": "Afzenderfilter",
+ "senderFilterHelp": "Filter e-mails op afzender. Inclusiepatronen laten afzenders door, exclusiepatronen weigeren overeenkomende afzenders.",
+ "sizeLimit": "Groottebeperking",
+ "sizeLimitDesc": "E-mails die groter zijn dan deze omvang worden overgeslagen tijdens het archiveren. Laat leeg voor geen limiet.",
+ "skipLargerThan": "E-mails overslaan die groter zijn dan",
+ "spamHeaders": "Spamheaders",
+ "spamHeadersHelp": "E-mails waarbij een van deze headers is ingesteld na 'yes' of 'true' worden overgeslagen. Algemene spamheaders worden hieronder gesuggereerd.",
+ "subjectFilter": "Onderwerpfilter",
+ "subjectFilterHelp": "Filter e-mails op basis van de onderwerpregel. Werkt op dezelfde manier als afzenderfiltering.",
+ "suggestions": "Suggesties (klik om toe te voegen):"
+ },
"fixed": "Vast",
"folderSync": {
"autoSelectDescendants": "Automatisch onderliggende items selecteren",
@@ -284,9 +320,25 @@
"selectUnit": "Selecteer een eenheid",
"selectedMailboxes": "Geselecteerde mailboxen",
"serverConfiguration": "Serverconfiguratie (IMAP)",
+ "settings": {
+ "download": "Downloaden",
+ "downloadDesc": "Configureren wanneer en hoe e-mails van de server worden opgehaald.",
+ "filters": "Filters",
+ "filtersDesc": "Bepaal welke e-mails worden gearchiveerd. Als filtering is uitgeschakeld, worden alle e-mails opgeslagen.",
+ "general": "Algemeen",
+ "generalDesc": "Basisaccountinformatie en status.",
+ "newAccount": "Nieuw account",
+ "performance": "Prestaties",
+ "schedule": "Tijdschema",
+ "scope": "Bereik",
+ "server": "Server",
+ "serverDesc": "IMAP-verbindinginstellingen en authenticatie."
+ },
"since": "sinds",
"sinceFixed": "Sinds een specifieke datum",
+ "sinceFixedDesc": "Download alleen e-mails ontvangen na de geselecteerde datum. E-mails van vóór die datum worden genegeerd.",
"sinceRelative": "Download alleen recente e-mails",
+ "sinceRelativeDesc": "Download alleen e-mails uit de afgelopen periode (bijv. laatste 3 maanden). De startdatum verschuift automatisch mee.",
"sinceRelativeValue": "Download e-mails van de laatste",
"startDownload": "Download starten",
"state": "Status",
diff --git a/web/src/locales/no.json b/web/src/locales/no.json
index f865256..09dbd90 100644
--- a/web/src/locales/no.json
+++ b/web/src/locales/no.json
@@ -99,10 +99,12 @@
"areYouSureYouWantTo": "Er du sikker på at du vil {{action}} denne kontoen?",
"auth": "Autentisering",
"authType": "autentiseringstype",
- "autoConfiguring": "Konfigurerer automatisk...",
+ "autoConfiguring": "Konfigurerer automatisk…",
+ "autoDiscover": "Finn serverinnstillinger automatisk",
"autoDownloadNewMailboxes": "Legg til nye mapper automatisk",
"autoDownloadNewMailboxesDescription": "Legg automatisk til nye mapper i nedlastingslisten.",
"beforeRelative": "Last ned kun gamle e-poster",
+ "beforeRelativeDesc": "Last bare ned e-poster som er eldre enn den angitte perioden. Skjæringsdatoen flyttes automatisk – nyttig for gradvis arkivering.",
"beforeRelativeValue": "Last ned e-poster fra {{value}} {{unit}} siden",
"cancelDownload": "Avbryt nedlasting",
"cancelFailed": "Kunne ikke avbryte nedlastingsoppgave",
@@ -124,9 +126,13 @@
"cronHour": "Time",
"cronMinute": "Minutt",
"cronMonday": "Mandag",
+ "cronMonthAlignNote": "Merk: Hvis måneden er kortere, vil den bli utført på månedens siste dag.",
"cronMonthly": "Månedlig",
"cronSaturday": "Lørdag",
"cronSimple": "Enkelt uttrykk",
+ "cronSummaryDaily": "Hver dag kl. {{hour}}:{{minute}}",
+ "cronSummaryMonthly": "Den {{day}}. i hver måned kl. {{hour}}:{{minute}}",
+ "cronSummaryWeekly": "Hver {{day}} kl. {{hour}}:{{minute}}",
"cronSunday": "Søndag",
"cronThursday": "Torsdag",
"cronTimezoneNote": "Alle klokkeslett er serverens lokaltid.",
@@ -145,6 +151,7 @@
"disableAccount": "Deaktiver konto",
"disabled": "Deaktivert",
"downloadAll": "Last ned alle e-poster",
+ "downloadAllDesc": "Alle e-poster i hver valgte postkasse vil bli lastet ned. Best for komplett arkivering.",
"downloadBatchSize": "Nedlastingsbatchstørrelse",
"downloadBatchSizeDescription": "Antall meldinger hentet per IMAP-forespørsel",
"downloadCancelled": "Oppgave avbrutt",
@@ -176,6 +183,35 @@
"enterValue": "Vennligst skriv inn en verdi",
"everyMinutes": "hvert {{minutes}} minutt",
"field": "Felt",
+ "filters": {
+ "addHeader": "Legg til header",
+ "addPattern": "Legg til mønster",
+ "enableFiltering": "Aktiver innholdsfiltrering",
+ "enableFilteringDesc": "Når aktivert, vil bare e-poster som samsvarer med reglene nedenfor bli arkivert. Når deaktivert, arkiveres alle e-poster.",
+ "exclude": "Ekskluder",
+ "include": "Inkluder",
+ "matchType": {
+ "contains": "Inneholder",
+ "endsWith": "Ender med",
+ "isExactly": "Er nøyaktig",
+ "regex": "Regulært uttrykk (Regex)",
+ "startsWith": "Starter med"
+ },
+ "noExcludePatterns": "Ingen ekskluderingsmønstre – ingenting er ekskludert.",
+ "noIncludePatterns": "Ingen inkluderingsmønstre – alt er inkludert.",
+ "noLimit": "Ingen grense",
+ "noSpamHeaders": "Ingen spam-headere er konfigurert.",
+ "senderFilter": "Afsenderfilter",
+ "senderFilterHelp": "Filtrer e-poster basert på avsenderadresse. Inkluderingsmønstre tillater avsendere, ekskluderingsmønstre avviser dem.",
+ "sizeLimit": "Størrelsesbegrensning",
+ "sizeLimitDesc": "E-poster større enn denne størrelsen vil bli hoppet over under arkivering. La stå tom for ingen grense.",
+ "skipLargerThan": "Hopp over e-poster større enn",
+ "spamHeaders": "Spam-headere",
+ "spamHeadersHelp": "E-poster med en av disse headerne satt til 'yes' eller 'true' vil bli hoppet over. Vanlige spam-headere er foreslått nedenfor.",
+ "subjectFilter": "Emnefilter",
+ "subjectFilterHelp": "Filtrer e-poster basert på emnelinjen. Fungerer på samme måte som avsenderfiltrering.",
+ "suggestions": "Forslag (klikk for å legge til):"
+ },
"fixed": "Fast",
"folderSync": {
"autoSelectDescendants": "Velg etterkommere automatisk",
@@ -284,9 +320,25 @@
"selectUnit": "Velg en enhet",
"selectedMailboxes": "Valgte postbokser",
"serverConfiguration": "Serverkonfigurasjon (IMAP)",
+ "settings": {
+ "download": "Nedlasting",
+ "downloadDesc": "Konfigurer når og hvordan e-poster hentes fra serveren.",
+ "filters": "Filtre",
+ "filtersDesc": "Styr hvilke e-poster som arkiveres. Når filtrering er deaktivert, lagres alle e-poster.",
+ "general": "Generelt",
+ "generalDesc": "Grunnleggende kontoinformasjon og status.",
+ "newAccount": "Ny konto",
+ "performance": "Ytelse",
+ "schedule": "Tidsplan",
+ "scope": "Omfang",
+ "server": "Server",
+ "serverDesc": "IMAP-tilkoblingsinnstillinger og autentisering."
+ },
"since": "siden",
"sinceFixed": "Siden spesifikk dato",
+ "sinceFixedDesc": "Last bare ned e-poster mottatt etter valgt dato. E-poster før denne datoen ignoreres.",
"sinceRelative": "Last ned kun nylige e-poster",
+ "sinceRelativeDesc": "Last bare ned e-poster fra den siste perioden (f.eks. siste 3 måneder). Startdatoen flyttes automatisk fremover.",
"sinceRelativeValue": "Last ned e-poster fra de siste",
"startDownload": "Start nedlasting",
"state": "Tilstand",
diff --git a/web/src/locales/pl.json b/web/src/locales/pl.json
index cb8a90a..93e1b9c 100644
--- a/web/src/locales/pl.json
+++ b/web/src/locales/pl.json
@@ -99,10 +99,12 @@
"areYouSureYouWantTo": "Czy na pewno chcesz {{action}} dla tego konta?",
"auth": "Auth",
"authType": "auth_type",
- "autoConfiguring": "Auto konfiguracja...",
+ "autoConfiguring": "Automatyczna konfiguracja…",
+ "autoDiscover": "Automatyczne wykrywanie ustawień serwera",
"autoDownloadNewMailboxes": "Automatycznie dodawaj nowe foldery",
"autoDownloadNewMailboxesDescription": "Automatycznie dodawaj nowo wykryte foldery do listy pobierania.",
"beforeRelative": "Pobierz tylko stare e-maile",
+ "beforeRelativeDesc": "Pobieraj tylko wiadomości starsze niż określony czas. Data graniczna automatycznie przesuwa się w czasie – przydatne do stopniowej archiwizacji starych wiadomości.",
"beforeRelativeValue": "Pobierz e-maile sprzed {{value}} {{unit}}",
"cancelDownload": "Anuluj pobieranie",
"cancelFailed": "Nie udało się anulować zadania pobierania",
@@ -124,9 +126,13 @@
"cronHour": "Godzina",
"cronMinute": "Minuta",
"cronMonday": "Poniedziałek",
+ "cronMonthAlignNote": "Uwaga: Jeśli miesiąc jest krótszy, wykonanie nastąpi w ostatnim dniu miesiąca.",
"cronMonthly": "Co miesiąc",
"cronSaturday": "Sobota",
"cronSimple": "Proste wyrażenie",
+ "cronSummaryDaily": "Codziennie o {{hour}}:{{minute}}",
+ "cronSummaryMonthly": "Każdego {{day}}. dnia miesiąca o {{hour}}:{{minute}}",
+ "cronSummaryWeekly": "W każdy(ą) {{day}} o {{hour}}:{{minute}}",
"cronSunday": "Niedziela",
"cronThursday": "Czwartek",
"cronTimezoneNote": "Czas według lokalnej strefy serwera.",
@@ -145,6 +151,7 @@
"disableAccount": "Wyłącz konto",
"disabled": "Wyłączone",
"downloadAll": "Pobierz wszystkie e-maile",
+ "downloadAllDesc": "Wszystkie wiadomości w wybranych skrzynkach zostaną pobrane. Najlepsze do kompletnej archiwizacji.",
"downloadBatchSize": "Rozmiar partii pobierania",
"downloadBatchSizeDescription": "Liczba wiadomości pobieranych na żądanie IMAP",
"downloadCancelled": "Zadanie anulowane",
@@ -176,6 +183,35 @@
"enterValue": "Wpisz wartość",
"everyMinutes": "co {{minutes}} minut",
"field": "Pole",
+ "filters": {
+ "addHeader": "Dodaj nagłówek",
+ "addPattern": "Dodaj regułę",
+ "enableFiltering": "Włącz filtrowanie zawartości",
+ "enableFilteringDesc": "Po włączeniu archiwizowane będą tylko wiadomości spełniające poniższe reguły. Po wyłączeniu archiwizowane są wszystkie wiadomości.",
+ "exclude": "Wyklucz",
+ "include": "Uwzględnij",
+ "matchType": {
+ "contains": "Zawiera",
+ "endsWith": "Kończy się na",
+ "isExactly": "Równa się",
+ "regex": "Wyrażenie regularne (Regex)",
+ "startsWith": "Zaczyna się od"
+ },
+ "noExcludePatterns": "Brak reguł wykluczania — nic nie zostanie wykluczone.",
+ "noIncludePatterns": "Brak reguł uwzględniania — wszystko zostanie uwzględnione.",
+ "noLimit": "Bez limitu",
+ "noSpamHeaders": "Nie skonfigurowano nagłówków spamu.",
+ "senderFilter": "Filtr nadawcy",
+ "senderFilterHelp": "Filtruj wiadomości według adresu nadawcy. Reguły uwzględniania przepuszczają nadawców, reguły wykluczania ich odrzucają.",
+ "sizeLimit": "Limit rozmiaru",
+ "sizeLimitDesc": "Wiadomości przekraczające ten rozmiar zostaną pominięte podczas archiwizacji. Pozostaw puste, aby nie nakładać limitu.",
+ "skipLargerThan": "Pomiń wiadomości większe niż",
+ "spamHeaders": "Nagłówki spamu",
+ "spamHeadersHelp": "Wiadomości z dowolnym z tych nagłówków ustawionym na 'yes' lub 'true' zostaną pominięte. Poniżej sugerowane są popularne nagłówki spamu.",
+ "subjectFilter": "Filtr tematu",
+ "subjectFilterHelp": "Filtruj wiadomości według tematu. Działa w ten sam sposób, co filtrowanie nadawców.",
+ "suggestions": "Sugerowane (kliknij, aby dodać):"
+ },
"fixed": "Dokładnie",
"folderSync": {
"autoSelectDescendants": "Automatyczny wybór dzieci (podrzędnych)",
@@ -284,9 +320,25 @@
"selectUnit": "Zaznacz jednostki",
"selectedMailboxes": "Wybrane skrzynki",
"serverConfiguration": "Konfiguracja serwera (IMAP)",
+ "settings": {
+ "download": "Pobieranie",
+ "downloadDesc": "Konfiguruj, kiedy i jak wiadomości e-mail są pobierane z serwera.",
+ "filters": "Filtry",
+ "filtersDesc": "Kontroluj, które wiadomości są archiwizowane. Gdy filtrowanie jest wyłączone, zapisywane są wszystkie wiadomości.",
+ "general": "Ogólne",
+ "generalDesc": "Podstawowe informacje o koncie i jego status.",
+ "newAccount": "Nowe konto",
+ "performance": "Wydajność",
+ "schedule": "Harmonogram",
+ "scope": "Zakres",
+ "server": "Serwer",
+ "serverDesc": "Ustawienia połączenia IMAP i uwierzytelnianie."
+ },
"since": "od",
"sinceFixed": "Od konkretnej daty",
+ "sinceFixedDesc": "Pobieraj tylko wiadomości odebrane po wybranej dacie. Wiadomości sprzed tej daty będą ignorowane.",
"sinceRelative": "Pobierz tylko ostatnie e-maile",
+ "sinceRelativeDesc": "Pobieraj tylko wiadomości z ostatniego okresu (np. ostatnie 3 miesiące). Data początkowa automatycznie przesuwa się w czasie.",
"sinceRelativeValue": "Pobierz e-maile z ostatnich",
"startDownload": "Uruchom pobieranie",
"state": "Status",
diff --git a/web/src/locales/pt.json b/web/src/locales/pt.json
index c18de84..af17c7a 100644
--- a/web/src/locales/pt.json
+++ b/web/src/locales/pt.json
@@ -99,10 +99,12 @@
"areYouSureYouWantTo": "Tem certeza de que deseja {{action}} esta conta?",
"auth": "Autenticação",
"authType": "Tipo de Autenticação",
- "autoConfiguring": "Configurando Automaticamente...",
+ "autoConfiguring": "Configurando automaticamente…",
+ "autoDiscover": "Autodetectar configurações do servidor",
"autoDownloadNewMailboxes": "Adicionar automaticamente novas pastas",
"autoDownloadNewMailboxesDescription": "Adicionar automaticamente novas pastas à lista de download.",
"beforeRelative": "Baixar apenas e-mails antigos",
+ "beforeRelativeDesc": "Baixar apenas e-mails anteriores ao período especificado. A data limite avança automaticamente com o tempo — útil para arquivar e-mails antigos gradualmente.",
"beforeRelativeValue": "Baixar e-mails de {{value}} {{unit}} atrás",
"cancelDownload": "Cancelar download",
"cancelFailed": "Falha ao cancelar tarefa de download",
@@ -124,9 +126,13 @@
"cronHour": "Hora",
"cronMinute": "Minuto",
"cronMonday": "Segunda-feira",
+ "cronMonthAlignNote": "Nota: Se o mês for mais curto, a execução ocorrerá no último dia do mês.",
"cronMonthly": "Mensalmente",
"cronSaturday": "Sábado",
"cronSimple": "Expressão simples",
+ "cronSummaryDaily": "Todos os dias às {{hour}}:{{minute}}",
+ "cronSummaryMonthly": "No dia {{day}} de cada mês às {{hour}}:{{minute}}",
+ "cronSummaryWeekly": "Toda(o) {{day}} às {{hour}}:{{minute}}",
"cronSunday": "Domingo",
"cronThursday": "Quinta-feira",
"cronTimezoneNote": "Horários no fuso horário do servidor.",
@@ -145,6 +151,7 @@
"disableAccount": "Desativar conta",
"disabled": "Desativado",
"downloadAll": "Baixar todos os e-mails",
+ "downloadAllDesc": "Todos os e-mails de cada caixa de correio selecionada serão baixados. Ideal para arquivamento completo.",
"downloadBatchSize": "Tamanho do lote de download",
"downloadBatchSizeDescription": "Número de mensagens recuperadas por solicitação IMAP",
"downloadCancelled": "Tarefa cancelada",
@@ -176,6 +183,35 @@
"enterValue": "Insira o Valor",
"everyMinutes": "A cada {{minutes}} minutos",
"field": "Campo",
+ "filters": {
+ "addHeader": "Adicionar cabeçalho",
+ "addPattern": "Adicionar padrão",
+ "enableFiltering": "Ativar filtragem de conteúdo",
+ "enableFilteringDesc": "Quando ativado, apenas os e-mails que correspondem às regras abaixo serão arquivados. Quando desativado, todos os e-mails são arquivados.",
+ "exclude": "Excluir",
+ "include": "Incluir",
+ "matchType": {
+ "contains": "Contém",
+ "endsWith": "Termina com",
+ "isExactly": "É exatamente",
+ "regex": "Expressão regular (Regex)",
+ "startsWith": "Começa com"
+ },
+ "noExcludePatterns": "Sem padrões de exclusão — nada está excluído.",
+ "noIncludePatterns": "Sem padrões de inclusão — tudo está incluído.",
+ "noLimit": "Sem limite",
+ "noSpamHeaders": "Nenhum cabeçalho de spam configurado.",
+ "senderFilter": "Filtro de remetente",
+ "senderFilterHelp": "Filtre e-mails pelo endereço do remetente. Padrões de inclusão aprovam os remetentes; padrões de exclusão rejeitam os correspondentes.",
+ "sizeLimit": "Limite de tamanho",
+ "sizeLimitDesc": "E-mails maiores que este tamanho serão ignorados durante o arquivamento. Deixe em branco para não limitar.",
+ "skipLargerThan": "Ignorar e-mails maiores que",
+ "spamHeaders": "Cabeçalhos de spam",
+ "spamHeadersHelp": "E-mails com qualquer um desses cabeçalhos definidos como 'yes' ou 'true' serão ignorados. Cabeçalhos comuns de detecção de spam são sugeridos abaixo.",
+ "subjectFilter": "Filtro de assunto",
+ "subjectFilterHelp": "Filtre e-mails com base na linha de assunto. Funciona da mesma forma que a filtragem de remetentes.",
+ "suggestions": "Sugestões (clique para adicionar):"
+ },
"fixed": "Fixo",
"folderSync": {
"autoSelectDescendants": "Selecionar automaticamente os descendentes",
@@ -284,9 +320,25 @@
"selectUnit": "Selecionar Unidade",
"selectedMailboxes": "Caixas de correio selecionadas",
"serverConfiguration": "Configuração do Servidor (IMAP)",
+ "settings": {
+ "download": "Download",
+ "downloadDesc": "Configure quando e como os e-mails são buscados no servidor.",
+ "filters": "Filtros",
+ "filtersDesc": "Controle quais e-mails serão arquivados. Quando a filtragem está desativada, todos os e-mails são salvos.",
+ "general": "Geral",
+ "generalDesc": "Informações básicas da conta e status.",
+ "newAccount": "Nova conta",
+ "performance": "Desempenho",
+ "schedule": "Cronograma",
+ "scope": "Escopo",
+ "server": "Servidor",
+ "serverDesc": "Configurações de conexão IMAP e autenticação."
+ },
"since": "Desde",
"sinceFixed": "Desde uma data específica",
+ "sinceFixedDesc": "Baixar apenas e-mails recebidos após a data selecionada. E-mails anteriores serão ignorados.",
"sinceRelative": "Baixar apenas e-mails recentes",
+ "sinceRelativeDesc": "Baixar apenas e-mails do período recente (ex: últimos 3 meses). A data de início avança automaticamente com o tempo.",
"sinceRelativeValue": "Baixar e-mails dos últimos",
"startDownload": "Iniciar download",
"state": "Estado",
diff --git a/web/src/locales/ru.json b/web/src/locales/ru.json
index 96bebff..292df30 100644
--- a/web/src/locales/ru.json
+++ b/web/src/locales/ru.json
@@ -99,10 +99,12 @@
"areYouSureYouWantTo": "Вы уверены, что хотите {{action}} этот аккаунт?",
"auth": "Авторизация",
"authType": "тип_авторизации",
- "autoConfiguring": "Автонастройка...",
+ "autoConfiguring": "Автоматическая настройка…",
+ "autoDiscover": "Автоопределение настроек сервера",
"autoDownloadNewMailboxes": "Автодобавление новых папок",
"autoDownloadNewMailboxesDescription": "Автоматически добавлять новые папки в список загрузки.",
"beforeRelative": "Скачать только старые письма",
+ "beforeRelativeDesc": "Скачивать только письма старше указанного периода. Дата отсечки автоматически сдвигается со временем — полезно для постепенного архивирования старых писем.",
"beforeRelativeValue": "Скачать письма за {{value}} {{unit}} назад",
"cancelDownload": "Отменить загрузку",
"cancelFailed": "Не удалось отменить задачу загрузки",
@@ -124,9 +126,13 @@
"cronHour": "Час",
"cronMinute": "Минута",
"cronMonday": "Понедельник",
+ "cronMonthAlignNote": "Примечание: Если в месяце меньше дней, выполнение произойдет в последний день месяца.",
"cronMonthly": "Ежемесячно",
"cronSaturday": "Суббота",
"cronSimple": "Простое выражение",
+ "cronSummaryDaily": "Каждый день в {{hour}}:{{minute}}",
+ "cronSummaryMonthly": "{{day}}-го числа каждого месяца в {{hour}}:{{minute}}",
+ "cronSummaryWeekly": "Каждую(ый) {{day}} в {{hour}}:{{minute}}",
"cronSunday": "Воскресенье",
"cronThursday": "Четверг",
"cronTimezoneNote": "Время по местному часовому поясу сервера.",
@@ -145,6 +151,7 @@
"disableAccount": "Отключить аккаунт",
"disabled": "Отключено",
"downloadAll": "Скачать все письма",
+ "downloadAllDesc": "Все письма в выбранных ящиках будут скачаны от первых до последних. Идеально для полного архивирования.",
"downloadBatchSize": "Размер пакета загрузки",
"downloadBatchSizeDescription": "Количество сообщений, получаемых за один IMAP-запрос",
"downloadCancelled": "Задача отменена",
@@ -176,6 +183,35 @@
"enterValue": "Пожалуйста, введите значение",
"everyMinutes": "каждые {{minutes}} мин.",
"field": "Поле",
+ "filters": {
+ "addHeader": "Добавить заголовок",
+ "addPattern": "Добавить шаблон",
+ "enableFiltering": "Включить фильтрацию контента",
+ "enableFilteringDesc": "Если включено, будут архивироваться только письма, соответствующие правилам ниже. Если отключено — архивируются все письма.",
+ "exclude": "Исключить",
+ "include": "Включить",
+ "matchType": {
+ "contains": "Содержит",
+ "endsWith": "Заканчивается на",
+ "isExactly": "Точное совпадение",
+ "regex": "Регулярное выражение (Regex)",
+ "startsWith": "Начинается с"
+ },
+ "noExcludePatterns": "Нет шаблонов исключения — ничто не исключено.",
+ "noIncludePatterns": "Нет шаблонов включения — включено всё.",
+ "noLimit": "Без ограничений",
+ "noSpamHeaders": "Заголовки спама не настроены.",
+ "senderFilter": "Фильтр отправителей",
+ "senderFilterHelp": "Фильтрация по адресу отправителя. Шаблоны включения пропускают отправителей, а шаблоны исключения — отклоняют.",
+ "sizeLimit": "Лимит по размеру",
+ "sizeLimitDesc": "Письма крупнее этого размера будут пропущены при архивации. Оставьте пустым, чтобы не ограничивать размер.",
+ "skipLargerThan": "Пропускать письма крупнее",
+ "spamHeaders": "Заголовки спама",
+ "spamHeadersHelp": "Письма, у которых любой из этих заголовков имеет значение 'yes' или 'true', будут пропущены. Ниже приведены популярные заголовки для обнаружения спама.",
+ "subjectFilter": "Фильтр тем",
+ "subjectFilterHelp": "Фильтрация по теме письма. Работает так же, как и фильтрация отправителей.",
+ "suggestions": "Рекомендации (нажмите, чтобы добавить):"
+ },
"fixed": "Фиксированная",
"folderSync": {
"autoSelectDescendants": "Автоматически выбирать дочерние элементы",
@@ -284,9 +320,25 @@
"selectUnit": "Выберите единицу",
"selectedMailboxes": "Выбранные почтовые ящики",
"serverConfiguration": "Конфигурация сервера (IMAP)",
+ "settings": {
+ "download": "Скачивание",
+ "downloadDesc": "Настройка времени и способа получения писем с сервера.",
+ "filters": "Фильтры",
+ "filtersDesc": "Управляйте тем, какие письма архивировать. Если фильтрация отключена, будут сохраняться все письма.",
+ "general": "Общие",
+ "generalDesc": "Основная информация об аккаунте и его статус.",
+ "newAccount": "Новый аккаунт",
+ "performance": "Производительность",
+ "schedule": "Расписание",
+ "scope": "Период синхронизации",
+ "server": "Сервер",
+ "serverDesc": "Настройки подключения IMAP и аутентификация."
+ },
"since": "с",
"sinceFixed": "С определенной даты",
+ "sinceFixedDesc": "Скачивать только письма, полученные после указанной даты. Более ранние письма будут проигнорированы.",
"sinceRelative": "Скачать только недавние письма",
+ "sinceRelativeDesc": "Скачивать письма только за последний период (напр., за 3 месяца). Дата начала автоматически сдвигается со временем.",
"sinceRelativeValue": "Скачать письма за последние",
"startDownload": "Запустить загрузку",
"state": "Состояние",
diff --git a/web/src/locales/sv.json b/web/src/locales/sv.json
index c0975d6..4bd022f 100644
--- a/web/src/locales/sv.json
+++ b/web/src/locales/sv.json
@@ -99,10 +99,12 @@
"areYouSureYouWantTo": "Är du säker på att du vill {{action}} detta konto?",
"auth": "Auth",
"authType": "auth_typ",
- "autoConfiguring": "Konfigurerar automatiskt...",
+ "autoConfiguring": "Konfigurerar automatiskt…",
+ "autoDiscover": "Hitta serverinställningar automatiskt",
"autoDownloadNewMailboxes": "Lägg till nya mappar automatiskt",
"autoDownloadNewMailboxesDescription": "Lägg automatiskt till nya mappar i hämtningslistan.",
"beforeRelative": "Ladda ner endast gamla e-postmeddelanden",
+ "beforeRelativeDesc": "Ladda endast ner e-postmeddelanden som är äldre än den angivna perioden. Brytdatumet flyttas automatiskt – användbart för att gradvis arkivera gamla e-postmeddelanden.",
"beforeRelativeValue": "Ladda ner e-post från {{value}} {{unit}} sedan",
"cancelDownload": "Avbryt hämtning",
"cancelFailed": "Misslyckades med att avbryta hämtningsuppgift",
@@ -124,9 +126,13 @@
"cronHour": "Timme",
"cronMinute": "Minut",
"cronMonday": "Måndag",
+ "cronMonthAlignNote": "Obs: Om månaden är kortare kommer det att utföras på månadens sista dag.",
"cronMonthly": "Månadsvis",
"cronSaturday": "Lördag",
"cronSimple": "Enkelt uttryck",
+ "cronSummaryDaily": "Varje dag kl. {{hour}}:{{minute}}",
+ "cronSummaryMonthly": "Den {{day}}:e i varje månad kl. {{hour}}:{{minute}}",
+ "cronSummaryWeekly": "Varje {{day}} kl. {{hour}}:{{minute}}",
"cronSunday": "Söndag",
"cronThursday": "Torsdag",
"cronTimezoneNote": "Alla tider visas i serverns lokaltid.",
@@ -145,6 +151,7 @@
"disableAccount": "Inaktivera konto",
"disabled": "Inaktiverad",
"downloadAll": "Ladda ner alla e-postmeddelanden",
+ "downloadAllDesc": "Alla e-postmeddelanden i varje vald brevlåda kommer att laddas ner. Bäst för komplett arkivering.",
"downloadBatchSize": "Batchstorlek för nedladdning",
"downloadBatchSizeDescription": "Antal meddelanden som hämtas per IMAP-begäran",
"downloadCancelled": "Uppgiften avbruten",
@@ -176,6 +183,35 @@
"enterValue": "Ange ett värde",
"everyMinutes": "varje {{minutes}} minut",
"field": "Fält",
+ "filters": {
+ "addHeader": "Lägg till rubrik",
+ "addPattern": "Lägg till mönster",
+ "enableFiltering": "Aktivera innehållsfiltrering",
+ "enableFilteringDesc": "När den er aktiverad kommer endast e-postmeddelanden som matchar reglerna nedan att arkiveras. När den är inaktiverad arkiveras alla e-postmeddelanden.",
+ "exclude": "Exkludera",
+ "include": "Inkludera",
+ "matchType": {
+ "contains": "Innehåller",
+ "endsWith": "Slutar med",
+ "isExactly": "Är exakt",
+ "regex": "Reguljärt uttryck (Regex)",
+ "startsWith": "Börjar med"
+ },
+ "noExcludePatterns": "Inga exkluderingsmönster – inget exkluderas.",
+ "noIncludePatterns": "Inga inkluderingsmönster – allt inkluderas.",
+ "noLimit": "Ingen gräns",
+ "noSpamHeaders": "Inga spamrubriker har konfigurerats.",
+ "senderFilter": "Afsendarefilter",
+ "senderFilterHelp": "Filtrera e-post baserat på avsändaradress. Inkluderingsmönster godkänner avsändare, exkluderingsmönster avvisar dem.",
+ "sizeLimit": "Storleksgräns",
+ "sizeLimitDesc": "E-postmeddelanden som är större än denna storlek kommer att hoppas över vid arkivering. Lämna tomt för ingen gräns.",
+ "skipLargerThan": "Hoppa över e-postmeddelanden större än",
+ "spamHeaders": "Spamrubriker",
+ "spamHeadersHelp": "E-postmeddelanden där någon av dessa rubriker är satt till 'yes' eller 'true' kommer att hoppas över. Vanliga spamrubriker föreslås nedan.",
+ "subjectFilter": "Ämnesfilter",
+ "subjectFilterHelp": "Filtrera e-post baserat på ämnesraden. Fungerar på samma sätt som avsändarfiltrering.",
+ "suggestions": "Förslag (klicka för att lägga till):"
+ },
"fixed": "Fast",
"folderSync": {
"autoSelectDescendants": "Välj underordnade automatiskt",
@@ -284,9 +320,25 @@
"selectUnit": "Välj en enhet",
"selectedMailboxes": "Valda brevlådor",
"serverConfiguration": "Serverkonfiguration (IMAP)",
+ "settings": {
+ "download": "Ladda ner",
+ "downloadDesc": "Konfigurera när och hur e-postmeddelanden hämtas från serveren.",
+ "filters": "Filter",
+ "filtersDesc": "Styr vilka e-postmeddelanden som arkiveras. När filtrering är inaktiverad sparas alla e-postmeddelanden.",
+ "general": "Allmänt",
+ "generalDesc": "Grundläggande kontoinformation och status.",
+ "newAccount": "Nytt konto",
+ "performance": "Prestanda",
+ "schedule": "Tidsplan",
+ "scope": "Omfång",
+ "server": "Server",
+ "serverDesc": "IMAP-anslutningsinställningar och autentisering."
+ },
"since": "sedan",
"sinceFixed": "Sedan specifikt datum",
+ "sinceFixedDesc": "Ladda endast ner e-postmeddelanden som tagits emot efter det valda datumet. Tidigare e-postmeddelanden ignoreras.",
"sinceRelative": "Ladda ner endast senaste e-postmeddelanden",
+ "sinceRelativeDesc": "Ladda endast ner e-postmeddelanden från den senaste perioden (t.ex. senaste 3 månaderna). Startdatumet flyttas automatiskt framåt.",
"sinceRelativeValue": "Ladda ner e-post från de senaste",
"startDownload": "Starta hämtning",
"state": "Tillstånd",
diff --git a/web/src/locales/zh-tw.json b/web/src/locales/zh-tw.json
index c733ac5..19c5375 100644
--- a/web/src/locales/zh-tw.json
+++ b/web/src/locales/zh-tw.json
@@ -99,10 +99,12 @@
"areYouSureYouWantTo": "你確定要{{action}}此帳戶嗎?",
"auth": "驗證",
"authType": "驗證類型",
- "autoConfiguring": "正在自動設定...",
+ "autoConfiguring": "正在自動設定…",
+ "autoDiscover": "自動偵測伺服器設定",
"autoDownloadNewMailboxes": "自動添加新郵件夾",
"autoDownloadNewMailboxesDescription": "自動將新發現的郵件夾添加到下載列表中。",
"beforeRelative": "僅下載舊郵件",
+ "beforeRelativeDesc": "僅下載早於指定時間段的舊郵件。截止日期會隨時間自動向後推移,適合逐步封存舊郵件並跳過新郵件。",
"beforeRelativeValue": "下載 {{value}} {{unit}} 之前的郵件",
"cancelDownload": "取消下載",
"cancelFailed": "取消下載任務失敗",
@@ -124,9 +126,13 @@
"cronHour": "小時",
"cronMinute": "分鐘",
"cronMonday": "星期一",
+ "cronMonthAlignNote": "注意:若當月天數不足,將在該月的最後一天執行。",
"cronMonthly": "每月",
"cronSaturday": "星期六",
"cronSimple": "简易表達式",
+ "cronSummaryDaily": "每天 {{hour}}:{{minute}}",
+ "cronSummaryMonthly": "每月 {{day}} 日 {{hour}}:{{minute}}",
+ "cronSummaryWeekly": "每周{{day}} {{hour}}:{{minute}}",
"cronSunday": "星期日",
"cronThursday": "星期四",
"cronTimezoneNote": "所有時間均使用伺服器在地時區。",
@@ -145,6 +151,7 @@
"disableAccount": "停用帳戶",
"disabled": "已停用",
"downloadAll": "下載所有郵件",
+ "downloadAllDesc": "將下載選取信箱中的所有郵件(從第一封到最新一封)。最適合完整封存。",
"downloadBatchSize": "下載批量大小",
"downloadBatchSizeDescription": "每個 IMAP 請求獲取的郵件數量",
"downloadCancelled": "下載任務已取消",
@@ -176,6 +183,35 @@
"enterValue": "輸入值",
"everyMinutes": "每 {{minutes}} 分鐘",
"field": "欄位",
+ "filters": {
+ "addHeader": "新增標頭",
+ "addPattern": "新增規則",
+ "enableFiltering": "启用內容篩選",
+ "enableFilteringDesc": "開啟後,僅封存符合下方規則的郵件;關閉後,將封存所有郵件。",
+ "exclude": "排除",
+ "include": "包含",
+ "matchType": {
+ "contains": "包含",
+ "endsWith": "結尾為",
+ "isExactly": "完全符合",
+ "regex": "正規表示式",
+ "startsWith": "開頭為"
+ },
+ "noExcludePatterns": "無排除規則 — 無內容被排除。",
+ "noIncludePatterns": "無包含規則 — 所有內容均將被包含。",
+ "noLimit": "不限制",
+ "noSpamHeaders": "未設定垃圾郵件標頭。",
+ "senderFilter": "寄件者篩選",
+ "senderFilterHelp": "基於寄件者電子郵件地址進行篩選。符合「包含規則」的允許通過,符合「排除規則」的則予以拒絕。",
+ "sizeLimit": "大小限制",
+ "sizeLimitDesc": "超過此大小的郵件在封存時將被跳過。留空則表示不限制大小。",
+ "skipLargerThan": "跳過大小超過以下值的郵件",
+ "spamHeaders": "垃圾郵件標頭",
+ "spamHeadersHelp": "若郵件包含以下任一標頭且值為 'yes' 或 'true',將被自動跳過。下方列出了常用的垃圾郵件偵測標記。",
+ "subjectFilter": "主旨篩選",
+ "subjectFilterHelp": "基於郵件主旨進行篩選。運作原理與寄件者篩選相同。",
+ "suggestions": "▼ 建議標記(點擊新增):"
+ },
"fixed": "固定",
"folderSync": {
"autoSelectDescendants": "自動選取子項",
@@ -284,9 +320,25 @@
"selectUnit": "選擇單位",
"selectedMailboxes": "已選擇的郵件夾",
"serverConfiguration": "伺服器設定 (IMAP)",
+ "settings": {
+ "download": "下載設定",
+ "downloadDesc": "設定從伺服器獲取與收取郵件的时间与方式。",
+ "filters": "篩選器",
+ "filtersDesc": "控制哪些郵件需要封存。關閉篩選時,將儲存所有郵件。",
+ "general": "基本資訊",
+ "generalDesc": "基本帳戶資訊与狀態。",
+ "newAccount": "建立新帳戶",
+ "performance": "效能",
+ "schedule": "時間排程",
+ "scope": "下載範圍",
+ "server": "伺服器設定",
+ "serverDesc": "IMAP 連線設定與驗證。"
+ },
"since": "自",
"sinceFixed": "自特定日期起",
+ "sinceFixedDesc": "僅下載指定日期之後收到的郵件,此前的郵件將被忽略。",
"sinceRelative": "僅下載最近郵件",
+ "sinceRelativeDesc": "僅下載最近一段時間(如過去 3 個月)的郵件。開始日期會隨時間推移自動向前滾動。",
"sinceRelativeValue": "下載最近一段時期的郵件",
"startDownload": "啟動下載",
"state": "狀態",
diff --git a/web/src/locales/zh.json b/web/src/locales/zh.json
index fdcda37..b468399 100644
--- a/web/src/locales/zh.json
+++ b/web/src/locales/zh.json
@@ -98,11 +98,14 @@
"allMailSkipped": "已选择标准文件夹。已跳过'所有邮件'以避免重复。",
"areYouSureYouWantTo": "你确定要{{action}}此账户吗?",
"auth": "认证",
+ "authPassword": "密码",
"authType": "认证类型",
- "autoConfiguring": "自动配置中...",
+ "autoConfiguring": "正在自动配置…",
+ "autoDiscover": "自动检测服务器设置",
"autoDownloadNewMailboxes": "自动添加新邮件夹",
"autoDownloadNewMailboxesDescription": "自动将新发现的邮件夹添加到下载列表中。",
"beforeRelative": "仅下载旧邮件",
+ "beforeRelativeDesc": "仅下载早于指定时间段的旧邮件。截止日期会随时间自动向后推移,适合逐步归档旧邮件并跳过新邮件。",
"beforeRelativeValue": "下载 {{value}} {{unit}} 之前的邮件",
"cancelDownload": "取消下载",
"cancelFailed": "取消下载任务失败",
@@ -114,19 +117,24 @@
"clickSaveWhenDone": "完成后请点击保存。",
"continue": "继续",
"createdAt": "创建时间",
+ "creating": "创建中...",
"creationFailed": "创建失败,请稍后重试",
"cronAdvanced": "高级表达式",
"cronDaily": "每天",
- "cronDayOfMonth": "星期",
+ "cronDayOfMonth": "几号",
"cronDayOfWeek": "星期",
"cronFrequency": "频率",
"cronFriday": "星期五",
"cronHour": "小时",
"cronMinute": "分钟",
"cronMonday": "星期一",
+ "cronMonthAlignNote": "注意:若当月天数不足,将在该月的最后一天执行。",
"cronMonthly": "每月",
"cronSaturday": "星期六",
"cronSimple": "简易表达式",
+ "cronSummaryDaily": "每天 {{hour}}:{{minute}}",
+ "cronSummaryMonthly": "每月 {{day}} 日 {{hour}}:{{minute}}",
+ "cronSummaryWeekly": "每周{{day}} {{hour}}:{{minute}}",
"cronSunday": "星期日",
"cronThursday": "星期四",
"cronTimezoneNote": "所有时间均使用服务器本地时区。",
@@ -145,6 +153,7 @@
"disableAccount": "禁用账户",
"disabled": "已禁用",
"downloadAll": "下载所有邮件",
+ "downloadAllDesc": "将下载选中邮箱中的所有邮件(从第一封到最新一封)。最适合完整归档。",
"downloadBatchSize": "下载批量大小",
"downloadBatchSizeDescription": "每个 IMAP 请求获取的邮件数量",
"downloadCancelled": "下载任务已取消",
@@ -176,6 +185,35 @@
"enterValue": "请输入一个值",
"everyMinutes": "每 {{minutes}} 分钟",
"field": "字段",
+ "filters": {
+ "addHeader": "添加头字段",
+ "addPattern": "添加规则",
+ "enableFiltering": "启用内容过滤",
+ "enableFilteringDesc": "开启后,仅归档符合下方规则的邮件;关闭后,将归档所有邮件。",
+ "exclude": "排除",
+ "include": "包含",
+ "matchType": {
+ "contains": "包含",
+ "endsWith": "结尾为",
+ "isExactly": "完全匹配",
+ "regex": "正则表达式",
+ "startsWith": "开头为"
+ },
+ "noExcludePatterns": "无排除规则 — 无内容被排除。",
+ "noIncludePatterns": "无包含规则 — 所有内容均将被包含。",
+ "noLimit": "不限制",
+ "noSpamHeaders": "未配置垃圾邮件头字段。",
+ "senderFilter": "发件人过滤",
+ "senderFilterHelp": "基于发件人邮箱地址进行过滤。符合“包含规则”的允许通过,符合“排除规则”的则予以拒绝。",
+ "sizeLimit": "大小限制",
+ "sizeLimitDesc": "超过此大小的邮件在归档时将被跳过。留空则表示不限制大小。",
+ "skipLargerThan": "跳过大小超过以下值的邮件",
+ "spamHeaders": "垃圾邮件头字段",
+ "spamHeadersHelp": "若邮件包含以下任一头字段且值为 'yes' 或 'true',将被自动跳过。下方列出了常用的垃圾邮件检测标志。",
+ "subjectFilter": "主题过滤",
+ "subjectFilterHelp": "基于邮件主题进行过滤。工作原理与发件人过滤相同。",
+ "suggestions": "推荐配置(点击添加):"
+ },
"fixed": "固定",
"folderSync": {
"autoSelectDescendants": "自动选择子项",
@@ -284,9 +322,33 @@
"selectUnit": "选择单位",
"selectedMailboxes": "已选择的邮件夹",
"serverConfiguration": "服务器配置 (IMAP)",
+ "settings": {
+ "backToAccounts": "返回账户列表",
+ "download": "下载设置",
+ "downloadDesc": "配置从服务器获取和收取邮件的时间与方式。",
+ "filters": "过滤器",
+ "filtersDesc": "控制哪些邮件需要归档。关闭过滤时,将保存所有邮件。",
+ "general": "基本信息",
+ "generalDesc": "基本账户信息与状态。",
+ "loading": "加载账户中...",
+ "newAccount": "新建账户",
+ "performance": "性能",
+ "reset": "重置",
+ "save": "保存",
+ "saved": "已保存",
+ "savedDesc": "设置已成功保存。",
+ "saving": "保存中...",
+ "schedule": "时间计划",
+ "scope": "下载范围",
+ "server": "服务器设置",
+ "serverDesc": "IMAP 连接设置与身份验证。",
+ "settings": "设置"
+ },
"since": "自",
"sinceFixed": "自特定日期起",
+ "sinceFixedDesc": "仅下载指定日期之后收到的邮件,此前的邮件将被忽略。",
"sinceRelative": "仅下载最近邮件",
+ "sinceRelativeDesc": "仅下载最近一段时间(如过去 3 个月)的邮件。开始日期会随时间推移自动向后滚动。",
"sinceRelativeValue": "下载最近一段时期的邮件",
"startDownload": "启动下载",
"state": "状态",
diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts
index 64f735d..55516b5 100644
--- a/web/src/routeTree.gen.ts
+++ b/web/src/routeTree.gen.ts
@@ -76,6 +76,12 @@ const AuthenticatedSettingsApiTokensLazyImport = createFileRoute(
const AuthenticatedSettingsAccessLazyImport = createFileRoute(
'/_authenticated/settings/access',
)()
+const AuthenticatedAccountsNewLazyImport = createFileRoute(
+ '/_authenticated/accounts/new',
+)()
+const AuthenticatedAccountsIdSettingsLazyImport = createFileRoute(
+ '/_authenticated/accounts/$id/settings',
+)()
// Create/Update Routes
@@ -317,6 +323,26 @@ const AuthenticatedSettingsAccessLazyRoute =
import('./routes/_authenticated/settings/access.lazy').then((d) => d.Route),
)
+const AuthenticatedAccountsNewLazyRoute =
+ AuthenticatedAccountsNewLazyImport.update({
+ id: '/accounts/new',
+ path: '/accounts/new',
+ getParentRoute: () => AuthenticatedRouteRoute,
+ } as any).lazy(() =>
+ import('./routes/_authenticated/accounts/new.lazy').then((d) => d.Route),
+ )
+
+const AuthenticatedAccountsIdSettingsLazyRoute =
+ AuthenticatedAccountsIdSettingsLazyImport.update({
+ id: '/accounts/$id/settings',
+ path: '/accounts/$id/settings',
+ getParentRoute: () => AuthenticatedRouteRoute,
+ } as any).lazy(() =>
+ import('./routes/_authenticated/accounts/$id.settings.lazy').then(
+ (d) => d.Route,
+ ),
+ )
+
// Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' {
@@ -398,6 +424,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthenticatedIndexImport
parentRoute: typeof AuthenticatedRouteImport
}
+ '/_authenticated/accounts/new': {
+ id: '/_authenticated/accounts/new'
+ path: '/accounts/new'
+ fullPath: '/accounts/new'
+ preLoaderRoute: typeof AuthenticatedAccountsNewLazyImport
+ parentRoute: typeof AuthenticatedRouteImport
+ }
'/_authenticated/settings/access': {
id: '/_authenticated/settings/access'
path: '/access'
@@ -517,6 +550,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthenticatedUsersIndexLazyImport
parentRoute: typeof AuthenticatedUsersRouteLazyImport
}
+ '/_authenticated/accounts/$id/settings': {
+ id: '/_authenticated/accounts/$id/settings'
+ path: '/accounts/$id/settings'
+ fullPath: '/accounts/$id/settings'
+ preLoaderRoute: typeof AuthenticatedAccountsIdSettingsLazyImport
+ parentRoute: typeof AuthenticatedRouteImport
+ }
}
}
@@ -574,6 +614,7 @@ interface AuthenticatedRouteRouteChildren {
AuthenticatedSettingsRouteLazyRoute: typeof AuthenticatedSettingsRouteLazyRouteWithChildren
AuthenticatedUsersRouteLazyRoute: typeof AuthenticatedUsersRouteLazyRouteWithChildren
AuthenticatedIndexRoute: typeof AuthenticatedIndexRoute
+ AuthenticatedAccountsNewLazyRoute: typeof AuthenticatedAccountsNewLazyRoute
AuthenticatedAttachmentIndexRoute: typeof AuthenticatedAttachmentIndexRoute
AuthenticatedImportIndexRoute: typeof AuthenticatedImportIndexRoute
AuthenticatedSearchIndexRoute: typeof AuthenticatedSearchIndexRoute
@@ -581,6 +622,7 @@ interface AuthenticatedRouteRouteChildren {
AuthenticatedApiDocsIndexLazyRoute: typeof AuthenticatedApiDocsIndexLazyRoute
AuthenticatedOauth2ResultIndexLazyRoute: typeof AuthenticatedOauth2ResultIndexLazyRoute
AuthenticatedOauth2IndexLazyRoute: typeof AuthenticatedOauth2IndexLazyRoute
+ AuthenticatedAccountsIdSettingsLazyRoute: typeof AuthenticatedAccountsIdSettingsLazyRoute
}
const AuthenticatedRouteRouteChildren: AuthenticatedRouteRouteChildren = {
@@ -589,6 +631,7 @@ const AuthenticatedRouteRouteChildren: AuthenticatedRouteRouteChildren = {
AuthenticatedUsersRouteLazyRoute:
AuthenticatedUsersRouteLazyRouteWithChildren,
AuthenticatedIndexRoute: AuthenticatedIndexRoute,
+ AuthenticatedAccountsNewLazyRoute: AuthenticatedAccountsNewLazyRoute,
AuthenticatedAttachmentIndexRoute: AuthenticatedAttachmentIndexRoute,
AuthenticatedImportIndexRoute: AuthenticatedImportIndexRoute,
AuthenticatedSearchIndexRoute: AuthenticatedSearchIndexRoute,
@@ -597,6 +640,8 @@ const AuthenticatedRouteRouteChildren: AuthenticatedRouteRouteChildren = {
AuthenticatedOauth2ResultIndexLazyRoute:
AuthenticatedOauth2ResultIndexLazyRoute,
AuthenticatedOauth2IndexLazyRoute: AuthenticatedOauth2IndexLazyRoute,
+ AuthenticatedAccountsIdSettingsLazyRoute:
+ AuthenticatedAccountsIdSettingsLazyRoute,
}
const AuthenticatedRouteRouteWithChildren =
@@ -613,6 +658,7 @@ export interface FileRoutesByFullPath {
'/404': typeof errors404LazyRoute
'/503': typeof errors503LazyRoute
'/': typeof AuthenticatedIndexRoute
+ '/accounts/new': typeof AuthenticatedAccountsNewLazyRoute
'/settings/access': typeof AuthenticatedSettingsAccessLazyRoute
'/settings/api-tokens': typeof AuthenticatedSettingsApiTokensLazyRoute
'/settings/appearance': typeof AuthenticatedSettingsAppearanceLazyRoute
@@ -630,6 +676,7 @@ export interface FileRoutesByFullPath {
'/oauth2': typeof AuthenticatedOauth2IndexLazyRoute
'/settings/': typeof AuthenticatedSettingsIndexLazyRoute
'/users/': typeof AuthenticatedUsersIndexLazyRoute
+ '/accounts/$id/settings': typeof AuthenticatedAccountsIdSettingsLazyRoute
}
export interface FileRoutesByTo {
@@ -640,6 +687,7 @@ export interface FileRoutesByTo {
'/404': typeof errors404LazyRoute
'/503': typeof errors503LazyRoute
'/': typeof AuthenticatedIndexRoute
+ '/accounts/new': typeof AuthenticatedAccountsNewLazyRoute
'/settings/access': typeof AuthenticatedSettingsAccessLazyRoute
'/settings/api-tokens': typeof AuthenticatedSettingsApiTokensLazyRoute
'/settings/appearance': typeof AuthenticatedSettingsAppearanceLazyRoute
@@ -657,6 +705,7 @@ export interface FileRoutesByTo {
'/oauth2': typeof AuthenticatedOauth2IndexLazyRoute
'/settings': typeof AuthenticatedSettingsIndexLazyRoute
'/users': typeof AuthenticatedUsersIndexLazyRoute
+ '/accounts/$id/settings': typeof AuthenticatedAccountsIdSettingsLazyRoute
}
export interface FileRoutesById {
@@ -672,6 +721,7 @@ export interface FileRoutesById {
'/(errors)/500': typeof errors500LazyRoute
'/(errors)/503': typeof errors503LazyRoute
'/_authenticated/': typeof AuthenticatedIndexRoute
+ '/_authenticated/accounts/new': typeof AuthenticatedAccountsNewLazyRoute
'/_authenticated/settings/access': typeof AuthenticatedSettingsAccessLazyRoute
'/_authenticated/settings/api-tokens': typeof AuthenticatedSettingsApiTokensLazyRoute
'/_authenticated/settings/appearance': typeof AuthenticatedSettingsAppearanceLazyRoute
@@ -689,6 +739,7 @@ export interface FileRoutesById {
'/_authenticated/oauth2/': typeof AuthenticatedOauth2IndexLazyRoute
'/_authenticated/settings/': typeof AuthenticatedSettingsIndexLazyRoute
'/_authenticated/users/': typeof AuthenticatedUsersIndexLazyRoute
+ '/_authenticated/accounts/$id/settings': typeof AuthenticatedAccountsIdSettingsLazyRoute
}
export interface FileRouteTypes {
@@ -704,6 +755,7 @@ export interface FileRouteTypes {
| '/404'
| '/503'
| '/'
+ | '/accounts/new'
| '/settings/access'
| '/settings/api-tokens'
| '/settings/appearance'
@@ -721,6 +773,7 @@ export interface FileRouteTypes {
| '/oauth2'
| '/settings/'
| '/users/'
+ | '/accounts/$id/settings'
fileRoutesByTo: FileRoutesByTo
to:
| '/500'
@@ -730,6 +783,7 @@ export interface FileRouteTypes {
| '/404'
| '/503'
| '/'
+ | '/accounts/new'
| '/settings/access'
| '/settings/api-tokens'
| '/settings/appearance'
@@ -747,6 +801,7 @@ export interface FileRouteTypes {
| '/oauth2'
| '/settings'
| '/users'
+ | '/accounts/$id/settings'
id:
| '__root__'
| '/_authenticated'
@@ -760,6 +815,7 @@ export interface FileRouteTypes {
| '/(errors)/500'
| '/(errors)/503'
| '/_authenticated/'
+ | '/_authenticated/accounts/new'
| '/_authenticated/settings/access'
| '/_authenticated/settings/api-tokens'
| '/_authenticated/settings/appearance'
@@ -777,6 +833,7 @@ export interface FileRouteTypes {
| '/_authenticated/oauth2/'
| '/_authenticated/settings/'
| '/_authenticated/users/'
+ | '/_authenticated/accounts/$id/settings'
fileRoutesById: FileRoutesById
}
@@ -828,13 +885,15 @@ export const routeTree = rootRoute
"/_authenticated/settings",
"/_authenticated/users",
"/_authenticated/",
+ "/_authenticated/accounts/new",
"/_authenticated/attachment/",
"/_authenticated/import/",
"/_authenticated/search/",
"/_authenticated/accounts/",
"/_authenticated/api-docs/",
"/_authenticated/oauth2-result/",
- "/_authenticated/oauth2/"
+ "/_authenticated/oauth2/",
+ "/_authenticated/accounts/$id/settings"
]
},
"/(auth)/500": {
@@ -884,6 +943,10 @@ export const routeTree = rootRoute
"filePath": "_authenticated/index.tsx",
"parent": "/_authenticated"
},
+ "/_authenticated/accounts/new": {
+ "filePath": "_authenticated/accounts/new.lazy.tsx",
+ "parent": "/_authenticated"
+ },
"/_authenticated/settings/access": {
"filePath": "_authenticated/settings/access.lazy.tsx",
"parent": "/_authenticated/settings"
@@ -951,6 +1014,10 @@ export const routeTree = rootRoute
"/_authenticated/users/": {
"filePath": "_authenticated/users/index.lazy.tsx",
"parent": "/_authenticated/users"
+ },
+ "/_authenticated/accounts/$id/settings": {
+ "filePath": "_authenticated/accounts/$id.settings.lazy.tsx",
+ "parent": "/_authenticated"
}
}
}
diff --git a/web/src/routes/_authenticated/accounts/$id.settings.lazy.tsx b/web/src/routes/_authenticated/accounts/$id.settings.lazy.tsx
new file mode 100644
index 0000000..d42de2d
--- /dev/null
+++ b/web/src/routes/_authenticated/accounts/$id.settings.lazy.tsx
@@ -0,0 +1,29 @@
+//
+// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
+//
+// This file is part of the Bichon Email Archiving Project
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import { createLazyFileRoute } from '@tanstack/react-router'
+import { AccountSettingsPage } from '@/features/accounts/account-settings-page'
+
+export const Route = createLazyFileRoute('/_authenticated/accounts/$id/settings')({
+ component: AccountSettingsWrapper,
+})
+
+function AccountSettingsWrapper() {
+ const { id } = Route.useParams()
+ return
+}
diff --git a/web/src/routes/_authenticated/accounts/new.lazy.tsx b/web/src/routes/_authenticated/accounts/new.lazy.tsx
new file mode 100644
index 0000000..fed8def
--- /dev/null
+++ b/web/src/routes/_authenticated/accounts/new.lazy.tsx
@@ -0,0 +1,24 @@
+//
+// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
+//
+// This file is part of the Bichon Email Archiving Project
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import { createLazyFileRoute } from '@tanstack/react-router'
+import { AccountNewPage } from '@/features/accounts/account-new'
+
+export const Route = createLazyFileRoute('/_authenticated/accounts/new')({
+ component: AccountNewPage,
+})