mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-03 07:48:34 +02:00
feat: Improve account setup UI and add post-download email filtering
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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 (
|
||||
<nav
|
||||
aria-label="Breadcrumb"
|
||||
className={cn("flex items-center gap-1.5 text-sm text-muted-foreground", className)}
|
||||
>
|
||||
<Link
|
||||
to="/"
|
||||
className="flex items-center gap-1 hover:text-foreground transition-colors"
|
||||
>
|
||||
<Home className="h-3.5 w-3.5" />
|
||||
</Link>
|
||||
{items.map((item, idx) => {
|
||||
const isLast = idx === items.length - 1;
|
||||
return (
|
||||
<div key={idx} className="flex items-center gap-1.5">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
{item.to && !isLast ? (
|
||||
<Link
|
||||
to={item.to}
|
||||
className="hover:text-foreground transition-colors"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
) : (
|
||||
<span className={cn(isLast && "text-foreground font-medium")}>
|
||||
{item.label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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 (
|
||||
<div className="pb-3">
|
||||
<h3 className="text-base font-semibold">{title}</h3>
|
||||
{description && <p className="text-sm text-muted-foreground mt-0.5">{description}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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<AccountFormValues>({
|
||||
mode: "onChange",
|
||||
defaultValues,
|
||||
resolver: zodResolver(accountSchema),
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: create_account,
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: t('accounts.accountCreated'),
|
||||
description: t('accounts.accountCreatedDesc'),
|
||||
action: <ToastAction altText={t('common.close')}>{t('common.close')}</ToastAction>,
|
||||
});
|
||||
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: <ToastAction altText={t('common.tryAgain')}>{t('common.tryAgain')}</ToastAction>,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
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 (
|
||||
<>
|
||||
<FixedHeader />
|
||||
<Main>
|
||||
<div className="mx-auto w-full max-w-[46rem] px-4 py-6">
|
||||
<div className="mb-6 space-y-3">
|
||||
<Link
|
||||
to="/accounts"
|
||||
className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
{t('accounts.settings.backToAccounts')}
|
||||
</Link>
|
||||
<Breadcrumb items={[
|
||||
{ label: t('accounts.title'), to: '/accounts' },
|
||||
{ label: t('accounts.settings.newAccount') },
|
||||
]} />
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border shadow-sm bg-card p-6 md:p-8">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-bold">{t('accounts.addAccount')}</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">{t('accounts.addNewEmailAccountHere')}</p>
|
||||
</div>
|
||||
|
||||
<FormProvider {...form}>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-10">
|
||||
<section>
|
||||
<SectionHeader
|
||||
title={t('accounts.settings.general')}
|
||||
description={t('accounts.settings.generalDesc')}
|
||||
/>
|
||||
<TabGeneral />
|
||||
</section>
|
||||
|
||||
<hr />
|
||||
|
||||
<section>
|
||||
<SectionHeader
|
||||
title={t('accounts.settings.server')}
|
||||
description={t('accounts.settings.serverDesc')}
|
||||
/>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={autoConfigLoading}
|
||||
onClick={handleAutoConfig}
|
||||
>
|
||||
{autoConfigLoading && <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />}
|
||||
{autoConfigLoading ? t('accounts.autoConfiguring') : t('accounts.autoDiscover')}
|
||||
</Button>
|
||||
</div>
|
||||
<TabServer />
|
||||
</section>
|
||||
|
||||
<hr />
|
||||
|
||||
<section>
|
||||
<SectionHeader
|
||||
title={t('accounts.settings.download')}
|
||||
description={t('accounts.settings.downloadDesc')}
|
||||
/>
|
||||
<TabDownload />
|
||||
</section>
|
||||
|
||||
<hr />
|
||||
|
||||
<section>
|
||||
<SectionHeader
|
||||
title={t('accounts.settings.filters')}
|
||||
description={t('accounts.settings.filtersDesc')}
|
||||
/>
|
||||
<TabFilters />
|
||||
</section>
|
||||
|
||||
<div className="flex items-center justify-between pt-4 border-t">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => navigate({ to: '/accounts' })}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button type="submit" size="lg" disabled={createMutation.isPending}>
|
||||
{createMutation.isPending ? t('accounts.creating') : t('accounts.submit')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
</div>
|
||||
</Main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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 (
|
||||
<div className="pb-3">
|
||||
<h3 className="text-base font-semibold">{title}</h3>
|
||||
{description && <p className="text-sm text-muted-foreground mt-0.5">{description}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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<AccountFormValues>({
|
||||
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<string, any>) => update_account(accountId, data),
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: t('accounts.settings.saved'),
|
||||
description: t('accounts.settings.savedDesc'),
|
||||
action: <ToastAction altText={t('common.close')}>{t('common.close')}</ToastAction>,
|
||||
});
|
||||
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: <ToastAction altText={t('common.tryAgain')}>{t('common.tryAgain')}</ToastAction>,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(data: AccountFormValues) => {
|
||||
const payload: Record<string, any> = {
|
||||
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 (
|
||||
<>
|
||||
<FixedHeader />
|
||||
<Main>
|
||||
<div className="mx-auto w-full max-w-[46rem] px-4 py-12 text-center text-muted-foreground">
|
||||
{t('accounts.settings.loading')}
|
||||
</div>
|
||||
</Main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FixedHeader />
|
||||
<Main>
|
||||
<div className="mx-auto w-full max-w-[46rem] px-4 py-6">
|
||||
<div className="mb-6 space-y-3">
|
||||
<Link
|
||||
to="/accounts"
|
||||
className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
{t('accounts.settings.backToAccounts')}
|
||||
</Link>
|
||||
<Breadcrumb items={[
|
||||
{ label: t('accounts.title'), to: '/accounts' },
|
||||
{ label: account.email },
|
||||
{ label: t('accounts.settings.settings') },
|
||||
]} />
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border shadow-sm bg-card p-6 md:p-8">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-bold">{account.email}</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">{t('accounts.updateTheEmailAccountHere')}</p>
|
||||
</div>
|
||||
|
||||
<FormProvider {...form}>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-10">
|
||||
<section>
|
||||
<SectionHeader
|
||||
title={t('accounts.settings.general')}
|
||||
description={t('accounts.settings.generalDesc')}
|
||||
/>
|
||||
<TabGeneral isEdit />
|
||||
</section>
|
||||
|
||||
<hr />
|
||||
|
||||
<section>
|
||||
<SectionHeader
|
||||
title={t('accounts.settings.server')}
|
||||
description={t('accounts.settings.serverDesc')}
|
||||
/>
|
||||
<TabServer isEdit />
|
||||
</section>
|
||||
|
||||
<hr />
|
||||
|
||||
<section>
|
||||
<SectionHeader
|
||||
title={t('accounts.settings.download')}
|
||||
description={t('accounts.settings.downloadDesc')}
|
||||
/>
|
||||
<TabDownload />
|
||||
</section>
|
||||
|
||||
<hr />
|
||||
|
||||
<section>
|
||||
<SectionHeader
|
||||
title={t('accounts.settings.filters')}
|
||||
description={t('accounts.settings.filtersDesc')}
|
||||
/>
|
||||
<TabFilters />
|
||||
</section>
|
||||
|
||||
<div className="flex items-center justify-between pt-4 border-t">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (account) form.reset(mapAccountToFormValues(account));
|
||||
}}
|
||||
>
|
||||
{t('accounts.settings.reset')}
|
||||
</Button>
|
||||
<Button type="submit" size="lg" disabled={updateMutation.isPending}>
|
||||
{updateMutation.isPending ? t('accounts.settings.saving') : t('accounts.saveChanges')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
</div>
|
||||
</Main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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<Account>({
|
||||
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<string, any>) => 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: <ToastAction altText={t('common.close')}>{t('common.close')}</ToastAction>,
|
||||
});
|
||||
|
||||
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: <ToastAction altText={t('common.tryAgain')}>{t('common.tryAgain')}</ToastAction>,
|
||||
});
|
||||
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 (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(state) => {
|
||||
if (!state) {
|
||||
form.reset();
|
||||
setCurrentStep(1);
|
||||
}
|
||||
onOpenChange(state);
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-[95vw] md:max-w-5xl w-full p-0 overflow-hidden flex flex-col h-[50rem]">
|
||||
<div className="p-6 pb-2 flex-shrink-0">
|
||||
<DialogHeader className="text-left">
|
||||
<DialogTitle>{isEdit ? t('accounts.updateAccount') : t('accounts.addAccount')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isEdit ? t('accounts.updateAccount') : t('accounts.addAccount')}
|
||||
{t('accounts.clickSaveWhenDone')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row flex-1 min-h-0 overflow-hidden border-y">
|
||||
<div className="md:hidden flex px-6 py-2 space-x-2 overflow-x-auto border-b flex-shrink-0 bg-background/50">
|
||||
{steps.map((step, index) => (
|
||||
<div key={step.id} className="flex flex-col items-center flex-shrink-0 min-w-[70px]">
|
||||
<Button
|
||||
variant={currentStep === index + 1 ? "default" : "secondary"}
|
||||
className="size-8 rounded-full font-bold p-0"
|
||||
disabled={currentStep === index + 1}
|
||||
onClick={() => setCurrentStep(index + 1)}
|
||||
>
|
||||
{index + 1}
|
||||
</Button>
|
||||
<span className="text-[10px] mt-1 text-muted-foreground line-clamp-1">{step.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="hidden md:block w-[240px] flex-shrink-0 px-8 py-4 border-r overflow-y-auto">
|
||||
{steps.map((step, index) => (
|
||||
<div className="mb-8 flex items-center" key={step.id}>
|
||||
<Button
|
||||
variant={currentStep === index + 1 ? "default" : "secondary"}
|
||||
className="size-9 rounded-full text-sm font-bold"
|
||||
disabled={currentStep === index + 1}
|
||||
onClick={() => setCurrentStep(index + 1)}
|
||||
>
|
||||
{index + 1}
|
||||
</Button>
|
||||
<div className="flex flex-col items-baseline uppercase ml-4">
|
||||
<span className="text-[10px] text-muted-foreground">{t('accounts.step', { index: index + 1 })}</span>
|
||||
<span className={cn("font-bold text-sm tracking-wider", currentStep === index + 1 ? "text-foreground" : "text-muted-foreground")}>
|
||||
{step.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0 relative">
|
||||
<ScrollArea className="h-full w-full">
|
||||
<div className="p-6 md:p-6 lg:p-8">
|
||||
<Form {...form}>
|
||||
<form id="account-register-form" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
{currentStep === 1 && <Step1 isEdit={isEdit} />}
|
||||
{currentStep === 2 && <Step2 isEdit={isEdit} />}
|
||||
{currentStep === 3 && <Step3 />}
|
||||
{currentStep === 4 && <Step4 />}
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="p-4 md:p-6 bg-background flex flex-row sm:justify-end gap-2 flex-shrink-0">
|
||||
{currentStep > 1 && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="flex-1 sm:flex-none"
|
||||
onClick={() => setCurrentStep(currentStep - 1)}
|
||||
>
|
||||
{t('accounts.goBack')}
|
||||
</Button>
|
||||
)}
|
||||
{currentStep < LAST_STEP && (
|
||||
<Button
|
||||
type="button"
|
||||
className="flex-1 sm:flex-none px-8"
|
||||
onClick={handleContinue}
|
||||
>
|
||||
{autoConfigLoading ? t('accounts.autoConfiguring') : t('accounts.continue')}
|
||||
</Button>
|
||||
)}
|
||||
{currentStep === LAST_STEP && (
|
||||
<Button
|
||||
type="submit"
|
||||
form="account-register-form"
|
||||
className="flex-1 sm:flex-none px-10"
|
||||
>
|
||||
{isEdit ? t('accounts.saveChanges') : t('accounts.submit')}
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
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<AddAccountType>('IMAP')
|
||||
|
||||
function handleContinue() {
|
||||
if (value === 'IMAP') {
|
||||
setOpen('add-imap')
|
||||
} else {
|
||||
setOpen('add-nosync')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-xl">
|
||||
<DialogHeader className="text-left">
|
||||
<DialogTitle>
|
||||
{t('accounts.add')}
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
{t('accounts.selectAccountType')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<RadioGroup
|
||||
value={value}
|
||||
onValueChange={(v) => setValue(v as AddAccountType)}
|
||||
className="space-y-4 py-2"
|
||||
>
|
||||
<Label
|
||||
htmlFor="imap-account"
|
||||
className={cn(
|
||||
'flex cursor-pointer items-start gap-4 rounded-2xl border p-5 transition-all',
|
||||
value === 'IMAP'
|
||||
? 'border-primary bg-muted/50'
|
||||
: 'hover:bg-muted/30'
|
||||
)}
|
||||
>
|
||||
<RadioGroupItem
|
||||
value="IMAP"
|
||||
id="imap-account"
|
||||
className="mt-1"
|
||||
/>
|
||||
|
||||
<div className="flex flex-1 gap-4">
|
||||
<div className="rounded-xl border p-2">
|
||||
<Mail className="h-5 w-5" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="font-medium">
|
||||
{t('accounts.imapAccount')}
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t('accounts.imapAccountDescription')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
<Label
|
||||
htmlFor="nosync-account"
|
||||
className={cn(
|
||||
'flex cursor-pointer items-start gap-4 rounded-2xl border p-5 transition-all',
|
||||
value === 'NoSync'
|
||||
? 'border-primary bg-muted/50'
|
||||
: 'hover:bg-muted/30'
|
||||
)}
|
||||
>
|
||||
<RadioGroupItem
|
||||
value="NoSync"
|
||||
id="nosync-account"
|
||||
className="mt-1"
|
||||
/>
|
||||
|
||||
<div className="flex flex-1 gap-4">
|
||||
<div className="rounded-xl border p-2">
|
||||
<Database className="h-5 w-5" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="font-medium">
|
||||
{t('accounts.noSyncAccount')}
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t('accounts.noSyncAccountDescription')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Label>
|
||||
</RadioGroup>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
|
||||
<Button onClick={handleContinue}>
|
||||
{t('accounts.continue')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -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<AccountModel>
|
||||
@@ -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) {
|
||||
<DropdownMenuContent align='end' className='w-[220px]'>
|
||||
{hasPermission && <DropdownMenuItem
|
||||
onClick={() => {
|
||||
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) {
|
||||
<IconEdit size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>}
|
||||
{account_type === "IMAP" && hasPermission && <DropdownMenuItem
|
||||
onClick={() => {
|
||||
navigate({ to: '/accounts/$id/settings', params: { id: String(row.original.id) } });
|
||||
}}
|
||||
>
|
||||
{t('accounts.settings.settings')}
|
||||
<DropdownMenuShortcut>
|
||||
<Settings size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>}
|
||||
{account_type === "IMAP" && hasPermission && <DropdownMenuItem
|
||||
onClick={() => {
|
||||
setCurrentRow(row.original)
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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<PatternEntry>) => void;
|
||||
onRemove: (id: string) => void;
|
||||
}
|
||||
|
||||
export function PatternInput({ entry, onChange, onRemove }: PatternInputProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const matchTypeLabels: Record<MatchType, string> = {
|
||||
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 (
|
||||
<div className="flex items-center gap-2 group">
|
||||
<Select
|
||||
value={entry.matchType}
|
||||
onValueChange={(v) => onChange(entry.id, { matchType: v as MatchType })}
|
||||
>
|
||||
<SelectTrigger className="w-[130px] h-9 text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(matchTypeLabels).map(([value, label]) => (
|
||||
<SelectItem key={value} value={value}>{label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex-1 relative">
|
||||
<Input
|
||||
className="h-9 text-sm"
|
||||
value={entry.value}
|
||||
onChange={(e) => onChange(entry.id, { value: e.target.value })}
|
||||
/>
|
||||
{showRegexHint && (
|
||||
<span className="text-[10px] text-destructive absolute -bottom-4 left-0">
|
||||
Invalid regex
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 shrink-0 opacity-50 group-hover:opacity-100 transition-opacity"
|
||||
onClick={() => onRemove(entry.id)}
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
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<Account>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="my-3 md:mt-8">{t('accounts.emailAccountRegistration')}</h1>
|
||||
<p className="mb-5 md:mb-8">
|
||||
{t('accounts.emailAccountRegistrationDesc')}
|
||||
</p>
|
||||
<div className="space-y-8">
|
||||
<FormField
|
||||
control={control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center justify-between">
|
||||
{t('accounts.emailAddress')}:
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t('accounts.emailPlaceholder')} disabled={isEdit} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
{isEdit && (
|
||||
<FormDescription>
|
||||
{t('accounts.emailCannotBeModified')}
|
||||
</FormDescription>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={control}
|
||||
name="account_name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center justify-between">
|
||||
{t('accounts.name')}:
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t('accounts.namePlaceholder')} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>{t('accounts.optional')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
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<Account>();
|
||||
const { proxyOptions } = useProxyList();
|
||||
|
||||
const imapAuthMethod = useWatch({
|
||||
control,
|
||||
name: "imap.auth.auth_type",
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-8">
|
||||
<FormField
|
||||
control={control}
|
||||
name="imap.host"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center justify-between">
|
||||
{t('accounts.imapHost')}:
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t('accounts.imapHostPlaceholder')} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={control}
|
||||
name="imap.port"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center justify-between">
|
||||
{t('accounts.imapPort')}:
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" placeholder={t('accounts.imapPortPlaceholder')} {...field} onChange={(e) => field.onChange(parseInt(e.target.value, 10))} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={control}
|
||||
name="imap.encryption"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('accounts.imapEncryption')}:</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t('accounts.selectEncryptionMethod')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="Ssl">Ssl</SelectItem>
|
||||
<SelectItem value="StartTls">StartTls</SelectItem>
|
||||
<SelectItem value="None">None</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
{t('accounts.chooseEncryptionMethod')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={control}
|
||||
name="use_dangerous"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col items-start gap-y-1">
|
||||
<FormLabel>{t('accounts.useDangerous')}:</FormLabel>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
className="mt-2"
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('accounts.useDangerousDescription')}</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={control}
|
||||
name="login_name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center justify-between">
|
||||
{t('accounts.login_name')}:
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t('accounts.namePlaceholder')} {...field} disabled={isEdit} />
|
||||
</FormControl>
|
||||
<FormDescription>{t('accounts.nameDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={control}
|
||||
name="imap.auth.auth_type"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('accounts.imapAuthMethod')}:</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t('accounts.selectAuthMethod')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="OAuth2">OAuth2</SelectItem>
|
||||
<SelectItem value="Password">Password</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
{t('accounts.chooseAuthMethod')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{imapAuthMethod === "Password" && (
|
||||
<FormField
|
||||
control={control}
|
||||
name="imap.auth.password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center justify-between">
|
||||
{t('accounts.imapPassword')}:
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput placeholder={isEdit ? t('accounts.leaveEmptyToKeepPassword') : t('accounts.enterPassword')} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
{isEdit && (
|
||||
<FormDescription>
|
||||
{t('accounts.leaveEmptyToKeepExisting')}
|
||||
</FormDescription>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<FormField
|
||||
control={control}
|
||||
name='imap.use_proxy'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center justify-between">{t('accounts.useProxy')} ({t('accounts.optional')}):</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
onValueChange={(val) => {
|
||||
field.onChange(val === 'none' ? undefined : Number(val))
|
||||
}}
|
||||
defaultValue={field.value?.toString()}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t('accounts.selectProxy')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem key="none" value="none">
|
||||
{t('accounts.useNoProxy')}
|
||||
</SelectItem>
|
||||
{proxyOptions && proxyOptions.length > 0 && (
|
||||
proxyOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value.toString()}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormDescription className='flex-1'>
|
||||
{t('accounts.imapProxy')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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<Account>();
|
||||
const current = getValues();
|
||||
|
||||
const [syncMode, setSyncMode] = useState<SyncMode>(() => {
|
||||
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<ScheduleMode>(() => {
|
||||
if (current.download_schedule) return 'cron';
|
||||
return 'interval';
|
||||
});
|
||||
|
||||
const [cronMode, setCronMode] = useState<CronMode>(() => {
|
||||
if (current.download_schedule && tryParseCronToSimple(current.download_schedule)) {
|
||||
return 'simple';
|
||||
}
|
||||
if (current.download_schedule) return 'advanced';
|
||||
return 'simple';
|
||||
});
|
||||
|
||||
const [cronSimple, setCronSimple] = useState<CronSimpleState>(() => {
|
||||
if (current.download_schedule) {
|
||||
return tryParseCronToSimple(current.download_schedule) ?? DEFAULT_CRON_SIMPLE;
|
||||
}
|
||||
return DEFAULT_CRON_SIMPLE;
|
||||
});
|
||||
|
||||
const updateCronFromSimple = (partial: Partial<CronSimpleState>) => {
|
||||
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 (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-4">
|
||||
<FormItem>
|
||||
<FormLabel className="text-base font-semibold">{t('accounts.scheduleMode')}</FormLabel>
|
||||
<FormDescription>
|
||||
{t('accounts.scheduleModeDescription')}
|
||||
</FormDescription>
|
||||
<Select value={scheduleMode} onValueChange={(v) => handleScheduleModeChange(v as ScheduleMode)}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="interval">{t('accounts.scheduleModeInterval')}</SelectItem>
|
||||
<SelectItem value="cron">{t('accounts.scheduleModeCron')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{scheduleMode === 'interval' ? (
|
||||
<FormField
|
||||
control={control}
|
||||
name="download_interval_min"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('accounts.downloadInterval')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" {...field} onChange={(e) => field.onChange(parseInt(e.target.value, 10))} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t('accounts.downloadIntervalPlaceholder')}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* <FormLabel className="text-sm font-medium">{t('accounts.downloadSchedule')}</FormLabel> */}
|
||||
<div className="flex items-center rounded-md border text-xs">
|
||||
<button
|
||||
type="button"
|
||||
className={`px-2 py-1 rounded-l-md ${cronMode === 'simple' ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:text-foreground'}`}
|
||||
onClick={() => setCronMode('simple')}
|
||||
>
|
||||
{t('accounts.cronSimple')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`px-2 py-1 rounded-r-md ${cronMode === 'advanced' ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:text-foreground'}`}
|
||||
onClick={() => setCronMode('advanced')}
|
||||
>
|
||||
{t('accounts.cronAdvanced')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{cronMode === 'simple' ? (
|
||||
<div className="flex flex-wrap items-end gap-3">
|
||||
<FormItem className="w-[140px]">
|
||||
<FormLabel className="text-xs">{t('accounts.cronFrequency')}</FormLabel>
|
||||
<Select
|
||||
value={cronSimple.frequency}
|
||||
onValueChange={(v) => updateCronFromSimple({ frequency: v as CronFrequency })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="daily">{t('accounts.cronDaily')}</SelectItem>
|
||||
<SelectItem value="weekly">{t('accounts.cronWeekly')}</SelectItem>
|
||||
<SelectItem value="monthly">{t('accounts.cronMonthly')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItem>
|
||||
|
||||
<FormItem className="w-[80px]">
|
||||
<FormLabel className="text-xs">{t('accounts.cronHour')}</FormLabel>
|
||||
<Select
|
||||
value={String(cronSimple.hour)}
|
||||
onValueChange={(v) => updateCronFromSimple({ hour: parseInt(v, 10) })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Array.from({ length: 24 }, (_, i) => (
|
||||
<SelectItem key={i} value={String(i)}>
|
||||
{String(i).padStart(2, '0')}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItem>
|
||||
|
||||
<span className="text-muted-foreground pb-2">:</span>
|
||||
|
||||
<FormItem className="w-[80px]">
|
||||
<FormLabel className="text-xs">{t('accounts.cronMinute')}</FormLabel>
|
||||
<Select
|
||||
value={String(cronSimple.minute)}
|
||||
onValueChange={(v) => updateCronFromSimple({ minute: parseInt(v, 10) })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55].map((m) => (
|
||||
<SelectItem key={m} value={String(m)}>
|
||||
{String(m).padStart(2, '0')}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItem>
|
||||
|
||||
{cronSimple.frequency === 'weekly' && (
|
||||
<FormItem className="w-[140px]">
|
||||
<FormLabel className="text-xs">{t('accounts.cronDayOfWeek')}</FormLabel>
|
||||
<Select
|
||||
value={String(cronSimple.dayOfWeek)}
|
||||
onValueChange={(v) => updateCronFromSimple({ dayOfWeek: parseInt(v, 10) })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">{t('accounts.cronMonday')}</SelectItem>
|
||||
<SelectItem value="2">{t('accounts.cronTuesday')}</SelectItem>
|
||||
<SelectItem value="3">{t('accounts.cronWednesday')}</SelectItem>
|
||||
<SelectItem value="4">{t('accounts.cronThursday')}</SelectItem>
|
||||
<SelectItem value="5">{t('accounts.cronFriday')}</SelectItem>
|
||||
<SelectItem value="6">{t('accounts.cronSaturday')}</SelectItem>
|
||||
<SelectItem value="0">{t('accounts.cronSunday')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
{cronSimple.frequency === 'monthly' && (
|
||||
<FormItem className="w-[90px]">
|
||||
<FormLabel className="text-xs">{t('accounts.cronDayOfMonth')}</FormLabel>
|
||||
<Select
|
||||
value={String(cronSimple.dayOfMonth)}
|
||||
onValueChange={(v) => updateCronFromSimple({ dayOfMonth: parseInt(v, 10) })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-[200px]">
|
||||
{Array.from({ length: 28 }, (_, i) => i + 1).map((d) => (
|
||||
<SelectItem key={d} value={String(d)}>
|
||||
{d}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
<div className="text-xs text-muted-foreground pb-2 font-mono">
|
||||
= {buildCronFromSimple(cronSimple)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<FormField
|
||||
control={control}
|
||||
name="download_schedule"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
placeholder={t('accounts.downloadSchedulePlaceholder')}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('accounts.downloadScheduleDescription')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{cronMode === 'simple' && (
|
||||
<FormDescription>{t('accounts.downloadScheduleDescription')}</FormDescription>
|
||||
)}
|
||||
|
||||
<div className="text-xs text-muted-foreground mt-1 flex items-center gap-1">
|
||||
<span>{t('accounts.cronTimezoneNote')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<FormField
|
||||
control={control}
|
||||
name="download_batch_size"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('accounts.downloadBatchSize')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" {...field} onChange={(e) => field.onChange(parseInt(e.target.value, 10))} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t('accounts.downloadBatchSizeDescription')}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={control}
|
||||
name="max_email_size_bytes"
|
||||
render={({ field }) => {
|
||||
const BYTES_PER_MB = 1024 * 1024;
|
||||
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>{t('accounts.maxEmailSizeBytes')}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={t('accounts.maxEmailSizeBytesPlaceholder')}
|
||||
className="flex-1"
|
||||
value={field.value ? field.value / BYTES_PER_MB : ''}
|
||||
onChange={(e) => {
|
||||
const parsed = parseInt(e.target.value, 10);
|
||||
field.onChange(isNaN(parsed) ? parsed : parsed * BYTES_PER_MB);
|
||||
}}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground whitespace-nowrap">MB</span>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t('accounts.maxEmailSizeBytesDescription')}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={control}
|
||||
name="enabled"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4 shadow-sm">
|
||||
<FormControl>
|
||||
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>{t('accounts.enabled')}</FormLabel>
|
||||
<FormDescription>{t('accounts.enabledDescription')}</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<hr className="my-4" />
|
||||
<div className="space-y-4">
|
||||
<FormItem>
|
||||
<FormLabel className="text-base font-semibold">{t('accounts.downloadScope')}</FormLabel>
|
||||
<FormDescription>
|
||||
{t('accounts.downloadScopeDescription')}
|
||||
</FormDescription>
|
||||
<Select value={syncMode} onValueChange={(v) => handleModeChange(v as SyncMode)}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder={t('accounts.selectMode')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">{t('accounts.downloadAll')}</SelectItem>
|
||||
<SelectItem value="since_fixed">{t('accounts.sinceFixed')}</SelectItem>
|
||||
<SelectItem value="since_relative">{t('accounts.sinceRelative')}</SelectItem>
|
||||
<SelectItem value="before_relative">{t('accounts.beforeRelative')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<div className="pl-2 border-l-2 border-primary/20 space-y-4 pt-2">
|
||||
{syncMode === 'since_fixed' && (
|
||||
<FormField
|
||||
control={control}
|
||||
name="date_since.fixed"
|
||||
render={({ field }) => {
|
||||
const currentLang = i18n.language.toLowerCase().replace('_', '-');
|
||||
const dateLocale = dateFnsLocaleMap[currentLang] || enUS;
|
||||
return <FormItem className="flex flex-col">
|
||||
<FormLabel>{t('accounts.selectDate')}</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn("w-[440px] pl-3 text-left font-normal", !field.value && "text-muted-foreground")}
|
||||
>
|
||||
{field.value ? format(new Date(field.value), "PPP", { locale: dateLocale }) : <span>{t('accounts.selectDate')}</span>}
|
||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={field.value ? new Date(field.value) : undefined}
|
||||
onSelect={(date) => field.onChange(date?.toLocaleDateString('en-CA'))}
|
||||
disabled={(date) => date > new Date() || date < new Date("1900-01-01")}
|
||||
locale={dateLocale}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>;
|
||||
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(syncMode === 'since_relative' || syncMode === 'before_relative') && (
|
||||
<div className="flex flex-row items-end gap-4 animate-in fade-in slide-in-from-left-2">
|
||||
<FormField
|
||||
control={control}
|
||||
name={syncMode === 'since_relative' ? "date_since.relative.value" : "date_before.value"}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1 max-w-[150px]">
|
||||
<FormLabel>{t('accounts.duration', 'Duration')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" {...field} onChange={(e) => field.onChange(parseInt(e.target.value, 10))} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={control}
|
||||
name={syncMode === 'since_relative' ? "date_since.relative.unit" : "date_before.unit"}
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-[180px]">
|
||||
<FormLabel>{t('accounts.unit', 'Unit')}</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t('accounts.selectUnit')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="Days">{t('accounts.days')}</SelectItem>
|
||||
<SelectItem value="Months">{t('accounts.months')}</SelectItem>
|
||||
<SelectItem value="Years">{t('accounts.years')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="my-4" />
|
||||
|
||||
<FormField
|
||||
control={control}
|
||||
name="auto_download_new_mailboxes"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4 shadow-sm">
|
||||
<FormControl>
|
||||
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>{t('accounts.autoDownloadNewMailboxes')}</FormLabel>
|
||||
<FormDescription>{t('accounts.autoDownloadNewMailboxesDescription')}</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<hr className="my-4" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
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<Account>();
|
||||
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 (
|
||||
<div className="rounded-xl">
|
||||
<Accordion type="multiple" defaultValue={[
|
||||
'email', 'account_name', 'login_name', 'imap', 'date_since',
|
||||
'max_email_size_bytes', 'sync_interval', 'sync_scope',
|
||||
'sync_batch_size', 'download_schedule'
|
||||
]}>
|
||||
<AccordionItem key="email" value="email">
|
||||
<AccordionTrigger className="font-medium capitalize text-gray-600">{t('accounts.email')}:</AccordionTrigger>
|
||||
<AccordionContent>{summaryData.email}</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem key="account_name" value="account_name">
|
||||
<AccordionTrigger className="font-medium capitalize text-gray-600">{t('accounts.name')}:</AccordionTrigger>
|
||||
<AccordionContent>{summaryData.account_name ?? t('accounts.notAvailable')}</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem key="login_name" value="login_name">
|
||||
<AccordionTrigger className="font-medium capitalize text-gray-600">{t('accounts.login_name')}:</AccordionTrigger>
|
||||
<AccordionContent>{summaryData.login_name ?? t('accounts.notAvailable')}</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem key="imap" value="imap">
|
||||
<AccordionTrigger className="font-medium capitalize text-gray-600">{t('accounts.imap')}:</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y">
|
||||
<tbody className="divide-y">
|
||||
<tr>
|
||||
<td className="px-6 py-2 whitespace-nowrap text-sm font-medium text-gray-600">{t('accounts.host')}:</td>
|
||||
<td className="px-6 py-2 whitespace-nowrap text-sm">{summaryData.imap.host}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-6 py-2 whitespace-nowrap text-sm font-medium text-gray-600">{t('accounts.port')}:</td>
|
||||
<td className="px-6 py-2 whitespace-nowrap text-sm">{summaryData.imap.port}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-6 py-2 whitespace-nowrap text-sm font-medium text-gray-600">{t('accounts.encryption')}:</td>
|
||||
<td className="px-6 py-2 whitespace-nowrap text-sm">{summaryData.imap.encryption}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-6 py-2 whitespace-nowrap text-sm font-medium text-gray-600">{t('accounts.useDangerous')}:</td>
|
||||
<td className="px-6 py-2 whitespace-nowrap text-sm">{`${summaryData.use_dangerous}`}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-6 py-2 whitespace-nowrap text-sm font-medium text-gray-600">{t('accounts.authType')}:</td>
|
||||
<td className="px-6 py-2 whitespace-nowrap text-sm">{summaryData.imap.auth.auth_type}</td>
|
||||
</tr>
|
||||
{summaryData.imap.auth.auth_type === 'Password' && (
|
||||
<tr>
|
||||
<td className="px-6 py-2 whitespace-nowrap text-sm font-medium text-gray-600">{t('accounts.password')}:</td>
|
||||
<td className="px-6 py-2 whitespace-nowrap text-sm break-words">{summaryData.imap.auth.password}</td>
|
||||
</tr>
|
||||
)}
|
||||
<tr>
|
||||
<td className="px-6 py-2 whitespace-nowrap text-sm font-medium text-gray-600">{t('accounts.useProxyField')}:</td>
|
||||
<td className="px-6 py-2 whitespace-nowrap text-sm">
|
||||
{(() => {
|
||||
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})`;
|
||||
})()}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem key="sync_scope" value="sync_scope">
|
||||
<AccordionTrigger className="font-medium capitalize text-gray-600">
|
||||
{t('accounts.downloadScope')}:
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent className="space-y-3">
|
||||
{hasSince && (
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t('accounts.sinceFixed')}:
|
||||
</span>
|
||||
<span className="text-sm">{sinceText}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasBefore && (
|
||||
<div className="flex flex-col border-t pt-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t('accounts.beforeRelative')}:
|
||||
</span>
|
||||
<span className="text-sm">
|
||||
{t('accounts.beforeRelativeValue', {
|
||||
value: summaryData.date_before!.value,
|
||||
unit: t(`accounts.${summaryData.date_before!.unit!.toLowerCase()}`)
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hasSince && !hasBefore && (
|
||||
<span className="text-sm">
|
||||
{t('accounts.downloadAll')}
|
||||
</span>
|
||||
)}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
|
||||
<AccordionItem key="sync_interval" value="sync_interval">
|
||||
<AccordionTrigger className="font-medium capitalize text-gray-600">{t('accounts.downloadInterval')}:</AccordionTrigger>
|
||||
<AccordionContent>{summaryData.download_interval_min} {t('accounts.minutes')}</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem key="sync_batch_size" value="sync_batch_size">
|
||||
<AccordionTrigger className="font-medium capitalize text-gray-600">{t('accounts.downloadBatchSize')}:</AccordionTrigger>
|
||||
<AccordionContent>{summaryData.download_batch_size}</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem key="max_email_size_bytes" value="max_email_size_bytes">
|
||||
<AccordionTrigger className="font-medium capitalize text-gray-600">{t('accounts.maxEmailSizeBytes')}:</AccordionTrigger>
|
||||
<AccordionContent>{summaryData.max_email_size_bytes ? `${(summaryData.max_email_size_bytes / 1024 / 1024).toFixed(0)} MB` : t('accounts.maxEmailSizeBytesUnlimited')}</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem key="download_schedule" value="download_schedule">
|
||||
<AccordionTrigger className="font-medium capitalize text-gray-600">{t('accounts.downloadSchedule')}:</AccordionTrigger>
|
||||
<AccordionContent>{summaryData.download_schedule || t('accounts.notAvailable')}</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem key="auto_download_new_mailboxes" value="auto_download_new_mailboxes">
|
||||
<AccordionTrigger className="font-medium capitalize text-gray-600">{t('accounts.autoDownloadNewMailboxes')}:</AccordionTrigger>
|
||||
<AccordionContent>{summaryData.auto_download_new_mailboxes ? t('common.yes') : t('common.no')}</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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<AccountFormValues>();
|
||||
const current = getValues();
|
||||
|
||||
const [syncMode, setSyncMode] = useState<SyncMode>(() => {
|
||||
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<ScheduleMode>(() => {
|
||||
if (current.download_schedule) return 'cron';
|
||||
return 'interval';
|
||||
});
|
||||
|
||||
const [cronMode, setCronMode] = useState<CronMode>(() => {
|
||||
if (current.download_schedule && tryParseCronToSimple(current.download_schedule)) {
|
||||
return 'simple';
|
||||
}
|
||||
if (current.download_schedule) return 'advanced';
|
||||
return 'simple';
|
||||
});
|
||||
|
||||
const [cronSimple, setCronSimple] = useState<CronSimpleState>(() => {
|
||||
if (current.download_schedule) {
|
||||
return tryParseCronToSimple(current.download_schedule) ?? DEFAULT_CRON_SIMPLE;
|
||||
}
|
||||
return DEFAULT_CRON_SIMPLE;
|
||||
});
|
||||
|
||||
const updateCronFromSimple = (partial: Partial<CronSimpleState>) => {
|
||||
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 (
|
||||
<div className="space-y-8">
|
||||
{/* Schedule */}
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{t('accounts.settings.schedule')}
|
||||
</h4>
|
||||
|
||||
<FormItem>
|
||||
<FormLabel>{t('accounts.scheduleMode')}</FormLabel>
|
||||
<FormDescription>{t('accounts.scheduleModeDescription')}</FormDescription>
|
||||
<Select value={scheduleMode} onValueChange={(v) => handleScheduleModeChange(v as ScheduleMode)}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="interval">{t('accounts.scheduleModeInterval')}</SelectItem>
|
||||
<SelectItem value="cron">{t('accounts.scheduleModeCron')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItem>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{scheduleMode === 'interval' && (
|
||||
<FormField
|
||||
control={control}
|
||||
name="download_interval_min"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('accounts.downloadInterval')}<span className="text-red-500 align-super text-xs">*</span></FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" {...field} onChange={(e) => field.onChange(parseInt(e.target.value, 10))} />
|
||||
</FormControl>
|
||||
<FormDescription>{t('accounts.downloadIntervalPlaceholder')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{scheduleMode === 'cron' && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center rounded-md border text-xs">
|
||||
<button
|
||||
type="button"
|
||||
className={`px-2 py-1 rounded-l-md ${cronMode === 'simple' ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:text-foreground'}`}
|
||||
onClick={() => setCronMode('simple')}
|
||||
>
|
||||
{t('accounts.cronSimple')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`px-2 py-1 rounded-r-md ${cronMode === 'advanced' ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:text-foreground'}`}
|
||||
onClick={() => setCronMode('advanced')}
|
||||
>
|
||||
{t('accounts.cronAdvanced')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{cronMode === 'simple' ? (
|
||||
<>
|
||||
<div className="flex items-end gap-2 overflow-x-auto pb-1">
|
||||
<FormItem className="w-[120px] shrink-0">
|
||||
<FormLabel className="text-sm">{t('accounts.cronFrequency')}</FormLabel>
|
||||
<Select
|
||||
value={cronSimple.frequency}
|
||||
onValueChange={(v) => updateCronFromSimple({ frequency: v as CronFrequency })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="daily">{t('accounts.cronDaily')}</SelectItem>
|
||||
<SelectItem value="weekly">{t('accounts.cronWeekly')}</SelectItem>
|
||||
<SelectItem value="monthly">{t('accounts.cronMonthly')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItem>
|
||||
|
||||
<FormItem className="w-[72px] shrink-0">
|
||||
<FormLabel className="text-sm">{t('accounts.cronHour')}</FormLabel>
|
||||
<Select
|
||||
value={String(cronSimple.hour)}
|
||||
onValueChange={(v) => updateCronFromSimple({ hour: parseInt(v, 10) })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Array.from({ length: 24 }, (_, i) => (
|
||||
<SelectItem key={i} value={String(i)}>
|
||||
{String(i).padStart(2, '0')}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItem>
|
||||
|
||||
<span className="text-muted-foreground font-medium pb-2 shrink-0">:</span>
|
||||
|
||||
<FormItem className="w-[72px] shrink-0">
|
||||
<FormLabel className="text-sm">{t('accounts.cronMinute')}</FormLabel>
|
||||
<Select
|
||||
value={String(cronSimple.minute)}
|
||||
onValueChange={(v) => updateCronFromSimple({ minute: parseInt(v, 10) })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55].map((m) => (
|
||||
<SelectItem key={m} value={String(m)}>
|
||||
{String(m).padStart(2, '0')}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItem>
|
||||
|
||||
{cronSimple.frequency === 'weekly' && (
|
||||
<FormItem className="w-[120px] shrink-0">
|
||||
<FormLabel className="text-sm">{t('accounts.cronDayOfWeek')}</FormLabel>
|
||||
<Select
|
||||
value={String(cronSimple.dayOfWeek)}
|
||||
onValueChange={(v) => updateCronFromSimple({ dayOfWeek: parseInt(v, 10) })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">{t('accounts.cronMonday')}</SelectItem>
|
||||
<SelectItem value="2">{t('accounts.cronTuesday')}</SelectItem>
|
||||
<SelectItem value="3">{t('accounts.cronWednesday')}</SelectItem>
|
||||
<SelectItem value="4">{t('accounts.cronThursday')}</SelectItem>
|
||||
<SelectItem value="5">{t('accounts.cronFriday')}</SelectItem>
|
||||
<SelectItem value="6">{t('accounts.cronSaturday')}</SelectItem>
|
||||
<SelectItem value="0">{t('accounts.cronSunday')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
{cronSimple.frequency === 'monthly' && (
|
||||
<FormItem className="w-[80px] shrink-0">
|
||||
<FormLabel className="text-sm">{t('accounts.cronDayOfMonth')}</FormLabel>
|
||||
<Select
|
||||
value={String(cronSimple.dayOfMonth)}
|
||||
onValueChange={(v) => updateCronFromSimple({ dayOfMonth: parseInt(v, 10) })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-[200px]">
|
||||
{Array.from({ length: 31 }, (_, i) => i + 1).map((d) => (
|
||||
<SelectItem key={d} value={String(d)}>{d}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItem>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{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 && (
|
||||
<span className="block text-xs text-yellow-600 mt-0.5">{t('accounts.cronMonthAlignNote')}</span>
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<FormField
|
||||
control={control}
|
||||
name="download_schedule"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
placeholder={t('accounts.downloadSchedulePlaceholder')}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t('accounts.downloadScheduleDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="text-xs text-muted-foreground mt-1 flex items-center gap-1">
|
||||
<span>{t('accounts.cronTimezoneNote')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
{/* Batch & Size */}
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{t('accounts.settings.performance')}
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={control}
|
||||
name="download_batch_size"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('accounts.downloadBatchSize')}<span className="text-red-500 align-super text-xs">*</span></FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" {...field} onChange={(e) => field.onChange(parseInt(e.target.value, 10))} />
|
||||
</FormControl>
|
||||
<FormDescription>{t('accounts.downloadBatchSizeDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={control}
|
||||
name="max_email_size_bytes"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('accounts.maxEmailSizeBytes')}<span className="text-red-500 align-super text-xs">*</span></FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={t('accounts.maxEmailSizeBytesPlaceholder')}
|
||||
className="flex-1"
|
||||
value={field.value ? field.value / BYTES_PER_MB : ''}
|
||||
onChange={(e) => {
|
||||
const parsed = parseInt(e.target.value, 10);
|
||||
field.onChange(isNaN(parsed) ? parsed : parsed * BYTES_PER_MB);
|
||||
}}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground whitespace-nowrap">MB</span>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormDescription>{t('accounts.maxEmailSizeBytesDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
{/* Download Scope */}
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{t('accounts.settings.scope')}
|
||||
</h4>
|
||||
|
||||
<FormItem>
|
||||
<FormLabel>{t('accounts.downloadScope')}</FormLabel>
|
||||
<FormDescription>{t('accounts.downloadScopeDescription')}</FormDescription>
|
||||
<Select value={syncMode} onValueChange={(v) => handleModeChange(v as SyncMode)}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">{t('accounts.downloadAll')}</SelectItem>
|
||||
<SelectItem value="since_fixed">{t('accounts.sinceFixed')}</SelectItem>
|
||||
<SelectItem value="since_relative">{t('accounts.sinceRelative')}</SelectItem>
|
||||
<SelectItem value="before_relative">{t('accounts.beforeRelative')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription className="mt-2">
|
||||
{syncMode === 'all' && t('accounts.downloadAllDesc')}
|
||||
{syncMode === 'since_fixed' && t('accounts.sinceFixedDesc')}
|
||||
{syncMode === 'since_relative' && t('accounts.sinceRelativeDesc')}
|
||||
{syncMode === 'before_relative' && t('accounts.beforeRelativeDesc')}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
|
||||
<div className="pl-2 border-l-2 border-primary/20 space-y-4 pt-2">
|
||||
{syncMode === 'since_fixed' && (
|
||||
<FormField
|
||||
control={control}
|
||||
name="date_since.fixed"
|
||||
render={({ field }) => {
|
||||
const currentLang = i18n.language.toLowerCase().replace('_', '-');
|
||||
const dateLocale = dateFnsLocaleMap[currentLang] || enUS;
|
||||
return (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>{t('accounts.selectDate')}<span className="text-red-500 align-super text-xs">*</span></FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn("w-[440px] pl-3 text-left font-normal", !field.value && "text-muted-foreground")}
|
||||
>
|
||||
{field.value ? format(new Date(field.value), "PPP", { locale: dateLocale }) : <span>{t('accounts.selectDate')}</span>}
|
||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={field.value ? new Date(field.value) : undefined}
|
||||
onSelect={(date) => field.onChange(date?.toLocaleDateString('en-CA'))}
|
||||
disabled={(date) => date > new Date() || date < new Date("1900-01-01")}
|
||||
locale={dateLocale}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(syncMode === 'since_relative' || syncMode === 'before_relative') && (
|
||||
<div className="flex flex-row items-end gap-4">
|
||||
<FormField
|
||||
control={control}
|
||||
name={syncMode === 'since_relative' ? "date_since.relative.value" : "date_before.value"}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1 max-w-[150px]">
|
||||
<FormLabel>{t('accounts.duration')}<span className="text-red-500 align-super text-xs">*</span></FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" {...field} onChange={(e) => field.onChange(parseInt(e.target.value, 10))} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={control}
|
||||
name={syncMode === 'since_relative' ? "date_since.relative.unit" : "date_before.unit"}
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-[180px]">
|
||||
<FormLabel>{t('accounts.unit', 'Unit')}<span className="text-red-500 align-super text-xs">*</span></FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="Days">{t('accounts.days')}</SelectItem>
|
||||
<SelectItem value="Months">{t('accounts.months')}</SelectItem>
|
||||
<SelectItem value="Years">{t('accounts.years')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<FormField
|
||||
control={control}
|
||||
name="auto_download_new_mailboxes"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
|
||||
<FormControl>
|
||||
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>{t('accounts.autoDownloadNewMailboxes')}</FormLabel>
|
||||
<FormDescription>{t('accounts.autoDownloadNewMailboxesDescription')}</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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<AccountFormValues>();
|
||||
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<PatternEntry[]>(
|
||||
() => toPatternEntries(sendersInclude)
|
||||
);
|
||||
const [senderExcludeEntries, setSenderExcludeEntries] = useState<PatternEntry[]>(
|
||||
() => toPatternEntries(sendersExclude)
|
||||
);
|
||||
const [subjectIncludeEntries, setSubjectIncludeEntries] = useState<PatternEntry[]>(
|
||||
() => toPatternEntries(subjectsInclude)
|
||||
);
|
||||
const [subjectExcludeEntries, setSubjectExcludeEntries] = useState<PatternEntry[]>(
|
||||
() => toPatternEntries(subjectsExclude)
|
||||
);
|
||||
|
||||
// Resync local state when form values change externally (e.g. after form.reset)
|
||||
const [lastSyncKey, setLastSyncKey] = useState<string>('');
|
||||
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<React.SetStateAction<PatternEntry[]>>,
|
||||
setExcludeEntries: React.Dispatch<React.SetStateAction<PatternEntry[]>>,
|
||||
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<PatternEntry>,
|
||||
side: 'include' | 'exclude',
|
||||
includeEntries: PatternEntry[],
|
||||
excludeEntries: PatternEntry[],
|
||||
setIncludeEntries: React.Dispatch<React.SetStateAction<PatternEntry[]>>,
|
||||
setExcludeEntries: React.Dispatch<React.SetStateAction<PatternEntry[]>>,
|
||||
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<React.SetStateAction<PatternEntry[]>>,
|
||||
setExcludeEntries: React.Dispatch<React.SetStateAction<PatternEntry[]>>,
|
||||
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 (
|
||||
<div className="space-y-8">
|
||||
{/* Master Switch */}
|
||||
<div className="rounded-md border p-5 space-y-2 bg-muted/30">
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<Checkbox checked={enabled} onCheckedChange={handleEnableChange} />
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>{t('accounts.filters.enableFiltering')}</FormLabel>
|
||||
<FormDescription>
|
||||
{t('accounts.filters.enableFilteringDesc')}
|
||||
</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
{enabled && (
|
||||
<>
|
||||
{/* Sender Filters */}
|
||||
<div className="space-y-4 rounded-md border p-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="text-sm font-semibold">{t('accounts.filters.senderFilter')}</h4>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t('accounts.filters.senderFilterHelp')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<p className="text-xs font-medium text-muted-foreground mb-2 uppercase tracking-wider">
|
||||
{t('accounts.filters.include')}
|
||||
</p>
|
||||
{senderIncludeEntries.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground italic">
|
||||
{t('accounts.filters.noIncludePatterns')}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{senderIncludeEntries.map((entry) => (
|
||||
<PatternInput
|
||||
key={entry.id}
|
||||
entry={entry}
|
||||
onChange={(id, partial) =>
|
||||
updateEntry(id, partial, 'include', senderIncludeEntries, senderExcludeEntries, setSenderIncludeEntries, setSenderExcludeEntries, 'archive_rules.senders')
|
||||
}
|
||||
onRemove={(id) =>
|
||||
removeEntry(id, 'include', senderIncludeEntries, senderExcludeEntries, setSenderIncludeEntries, setSenderExcludeEntries, 'archive_rules.senders')
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
type="button"
|
||||
size="sm"
|
||||
className="mt-2 h-8 text-xs"
|
||||
onClick={() => addEntry('include', senderIncludeEntries, senderExcludeEntries, setSenderIncludeEntries, setSenderExcludeEntries, 'archive_rules.senders')}
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
{t('accounts.filters.addPattern')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-xs font-medium text-muted-foreground mb-2 uppercase tracking-wider">
|
||||
{t('accounts.filters.exclude')}
|
||||
</p>
|
||||
{senderExcludeEntries.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground italic">
|
||||
{t('accounts.filters.noExcludePatterns')}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{senderExcludeEntries.map((entry) => (
|
||||
<PatternInput
|
||||
key={entry.id}
|
||||
entry={entry}
|
||||
onChange={(id, partial) =>
|
||||
updateEntry(id, partial, 'exclude', senderIncludeEntries, senderExcludeEntries, setSenderIncludeEntries, setSenderExcludeEntries, 'archive_rules.senders')
|
||||
}
|
||||
onRemove={(id) =>
|
||||
removeEntry(id, 'exclude', senderIncludeEntries, senderExcludeEntries, setSenderIncludeEntries, setSenderExcludeEntries, 'archive_rules.senders')
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
type="button"
|
||||
size="sm"
|
||||
className="mt-2 h-8 text-xs"
|
||||
onClick={() => addEntry('exclude', senderIncludeEntries, senderExcludeEntries, setSenderIncludeEntries, setSenderExcludeEntries, 'archive_rules.senders')}
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
{t('accounts.filters.addPattern')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subject Filters */}
|
||||
<div className="space-y-4 rounded-md border p-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="text-sm font-semibold">{t('accounts.filters.subjectFilter')}</h4>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t('accounts.filters.subjectFilterHelp')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<p className="text-xs font-medium text-muted-foreground mb-2 uppercase tracking-wider">
|
||||
{t('accounts.filters.include')}
|
||||
</p>
|
||||
{subjectIncludeEntries.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground italic">
|
||||
{t('accounts.filters.noIncludePatterns')}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{subjectIncludeEntries.map((entry) => (
|
||||
<PatternInput
|
||||
key={entry.id}
|
||||
entry={entry}
|
||||
onChange={(id, partial) =>
|
||||
updateEntry(id, partial, 'include', subjectIncludeEntries, subjectExcludeEntries, setSubjectIncludeEntries, setSubjectExcludeEntries, 'archive_rules.subjects')
|
||||
}
|
||||
onRemove={(id) =>
|
||||
removeEntry(id, 'include', subjectIncludeEntries, subjectExcludeEntries, setSubjectIncludeEntries, setSubjectExcludeEntries, 'archive_rules.subjects')
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
type="button"
|
||||
size="sm"
|
||||
className="mt-2 h-8 text-xs"
|
||||
onClick={() => addEntry('include', subjectIncludeEntries, subjectExcludeEntries, setSubjectIncludeEntries, setSubjectExcludeEntries, 'archive_rules.subjects')}
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
{t('accounts.filters.addPattern')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-xs font-medium text-muted-foreground mb-2 uppercase tracking-wider">
|
||||
{t('accounts.filters.exclude')}
|
||||
</p>
|
||||
{subjectExcludeEntries.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground italic">
|
||||
{t('accounts.filters.noExcludePatterns')}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{subjectExcludeEntries.map((entry) => (
|
||||
<PatternInput
|
||||
key={entry.id}
|
||||
entry={entry}
|
||||
onChange={(id, partial) =>
|
||||
updateEntry(id, partial, 'exclude', subjectIncludeEntries, subjectExcludeEntries, setSubjectIncludeEntries, setSubjectExcludeEntries, 'archive_rules.subjects')
|
||||
}
|
||||
onRemove={(id) =>
|
||||
removeEntry(id, 'exclude', subjectIncludeEntries, subjectExcludeEntries, setSubjectIncludeEntries, setSubjectExcludeEntries, 'archive_rules.subjects')
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
type="button"
|
||||
size="sm"
|
||||
className="mt-2 h-8 text-xs"
|
||||
onClick={() => addEntry('exclude', subjectIncludeEntries, subjectExcludeEntries, setSubjectIncludeEntries, setSubjectExcludeEntries, 'archive_rules.subjects')}
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
{t('accounts.filters.addPattern')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Size Limit */}
|
||||
<div className="space-y-4 rounded-md border p-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="text-sm font-semibold">{t('accounts.filters.sizeLimit')}</h4>
|
||||
</div>
|
||||
<FormField
|
||||
control={control}
|
||||
name="archive_rules.skip_larger_than"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('accounts.filters.skipLargerThan')}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={t('accounts.filters.noLimit')}
|
||||
className="max-w-[160px]"
|
||||
value={field.value ? field.value / BYTES_PER_MB : ''}
|
||||
onChange={(e) => {
|
||||
const parsed = parseInt(e.target.value, 10);
|
||||
field.onChange(isNaN(parsed) ? undefined : parsed * BYTES_PER_MB);
|
||||
}}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">MB</span>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormDescription>{t('accounts.filters.sizeLimitDesc')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Spam Headers */}
|
||||
<div className="space-y-4 rounded-md border p-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="text-sm font-semibold">{t('accounts.filters.spamHeaders')}</h4>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t('accounts.filters.spamHeadersHelp')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{spamHeaders.length > 0 ? (
|
||||
spamHeaders.map((header) => (
|
||||
<div key={header} className="flex items-center gap-2">
|
||||
<div className="flex-1 rounded-md border bg-muted/50 px-3 py-1.5 text-sm font-mono">
|
||||
{header}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 shrink-0"
|
||||
onClick={() => removeSpamHeader(header)}
|
||||
>
|
||||
<span className="text-muted-foreground">✕</span>
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground italic">
|
||||
{t('accounts.filters.noSpamHeaders')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
className="h-8 text-sm max-w-[220px]"
|
||||
placeholder="X-Spam-Flag"
|
||||
value={newSpamHeader}
|
||||
onChange={(e) => setNewSpamHeader(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleAddCustomSpamHeader();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
type="button"
|
||||
size="sm"
|
||||
className="h-8 text-xs"
|
||||
onClick={handleAddCustomSpamHeader}
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
{t('accounts.filters.addHeader')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground mb-2">{t('accounts.filters.suggestions')}</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{SUGGESTED_SPAM_HEADERS.filter((h) => !spamHeaders.includes(h)).map((header) => (
|
||||
<button
|
||||
key={header}
|
||||
type="button"
|
||||
className="inline-flex items-center rounded-full border bg-background px-2.5 py-0.5 text-xs font-mono text-muted-foreground hover:text-foreground hover:border-primary/50 transition-colors"
|
||||
onClick={() => addSpamHeader(header)}
|
||||
>
|
||||
+ {header}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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<AccountFormValues>();
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<FormField
|
||||
control={control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('accounts.email')}<span className="text-red-500 align-super text-xs">*</span></FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={isEdit} placeholder={t('accounts.emailPlaceholder')} />
|
||||
</FormControl>
|
||||
{isEdit && (
|
||||
<FormDescription>{t('accounts.emailCannotBeModified')}</FormDescription>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={control}
|
||||
name="account_name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('accounts.name')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} value={field.value ?? ''} placeholder={t('accounts.nameDescription')} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={control}
|
||||
name="enabled"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
|
||||
<FormControl>
|
||||
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>{t('accounts.enabled')}</FormLabel>
|
||||
<FormDescription>{t('accounts.enabledDescription')}</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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<AccountFormValues>();
|
||||
const authType = watch('imap.auth.auth_type');
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<FormField
|
||||
control={control}
|
||||
name="imap.host"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('accounts.imapHost')}<span className="text-red-500 align-super text-xs">*</span></FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder={t('accounts.imapHostPlaceholder')} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={control}
|
||||
name="imap.port"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('accounts.imapPort')}<span className="text-red-500 align-super text-xs">*</span></FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(parseInt(e.target.value, 10))}
|
||||
placeholder={t('accounts.imapPortPlaceholder')}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={control}
|
||||
name="imap.encryption"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('accounts.imapEncryption')}<span className="text-red-500 align-super text-xs">*</span></FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="Ssl">SSL/TLS</SelectItem>
|
||||
<SelectItem value="StartTls">StartTLS</SelectItem>
|
||||
<SelectItem value="None">{t('accounts.none')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={control}
|
||||
name="login_name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('accounts.login_name')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} value={field.value ?? ''} placeholder={t('accounts.namePlaceholder')} disabled={isEdit} />
|
||||
</FormControl>
|
||||
<FormDescription>{t('accounts.nameDescription')}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={control}
|
||||
name="imap.auth.auth_type"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('accounts.imapAuthMethod')}<span className="text-red-500 align-super text-xs">*</span></FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="Password">{t('accounts.authPassword')}</SelectItem>
|
||||
<SelectItem value="OAuth2">OAuth2</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{authType === 'Password' && (
|
||||
<FormField
|
||||
control={control}
|
||||
name="imap.auth.password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('accounts.imapPassword')}{!isEdit && <span className="text-red-500 align-super text-xs">*</span>}</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput placeholder={isEdit ? t('accounts.leaveEmptyToKeepPassword') : t('accounts.enterPassword')} {...field} />
|
||||
</FormControl>
|
||||
{isEdit && <FormDescription>{t('accounts.leaveEmptyToKeepPassword')}</FormDescription>}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={control}
|
||||
name="use_dangerous"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
|
||||
<FormControl>
|
||||
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>{t('accounts.useDangerous')}</FormLabel>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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<AccountModel | null>(null)
|
||||
@@ -77,15 +77,14 @@ export default function Accounts() {
|
||||
</p>
|
||||
</div>
|
||||
{require_any_permission(['system:root', 'account:create']) && <div className="flex gap-2">
|
||||
<div className="flex rounded-md shadow-sm">
|
||||
<Button
|
||||
onClick={() => setOpen("add")}
|
||||
className="border-r-0"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
{t('accounts.add')}
|
||||
</Button>
|
||||
</div>
|
||||
<Button onClick={() => navigate({ to: '/accounts/new' })}>
|
||||
<Mail className="mr-1.5 h-4 w-4" />
|
||||
{t('accounts.imapAccount')}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setOpen("add-nosync")}>
|
||||
<Database className="mr-1.5 h-4 w-4" />
|
||||
{t('accounts.noSyncAccount')}
|
||||
</Button>
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
@@ -107,8 +106,13 @@ export default function Accounts() {
|
||||
{t('accounts.noAccountConfigurationsDesc')}
|
||||
</p>
|
||||
<div className="mt-4 flex flex-col items-center gap-3 sm:flex-row sm:flex-wrap sm:justify-center sm:gap-4">
|
||||
<Button variant="default" className="w-64" onClick={() => setOpen("add")}>
|
||||
{t('accounts.add')}
|
||||
<Button variant="default" className="w-64" onClick={() => navigate({ to: '/accounts/new' })}>
|
||||
<Mail className="mr-1.5 h-4 w-4" />
|
||||
{t('accounts.imapAccount')}
|
||||
</Button>
|
||||
<Button variant="outline" className="w-64" onClick={() => setOpen('add-nosync')}>
|
||||
<Database className="mr-1.5 h-4 w-4" />
|
||||
{t('accounts.noSyncAccount')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -117,16 +121,6 @@ export default function Accounts() {
|
||||
</div>
|
||||
</div>
|
||||
</Main>
|
||||
<AddAccountDialog
|
||||
key='account-add'
|
||||
open={open === 'add'}
|
||||
onOpenChange={() => setOpen('add')} />
|
||||
|
||||
<AccountActionDialog
|
||||
key='imap-account-add'
|
||||
open={open === 'add-imap'}
|
||||
onOpenChange={() => setOpen('add-imap')}
|
||||
/>
|
||||
|
||||
<NoSyncAccountDialog
|
||||
key='nosync-account-add'
|
||||
@@ -136,17 +130,6 @@ export default function Accounts() {
|
||||
|
||||
{currentRow && (
|
||||
<>
|
||||
<AccountActionDialog
|
||||
key={`imap-account-edit-${currentRow.id}`}
|
||||
open={open === 'edit-imap'}
|
||||
onOpenChange={() => {
|
||||
setOpen('edit-imap')
|
||||
setTimeout(() => {
|
||||
setCurrentRow(null)
|
||||
}, 500)
|
||||
}}
|
||||
currentRow={currentRow}
|
||||
/>
|
||||
<NoSyncAccountDialog
|
||||
key={`nosync-account-edit-${currentRow.id}`}
|
||||
open={open === 'edit-nosync'}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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()}`;
|
||||
}
|
||||
+53
-1
@@ -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": "الحالة",
|
||||
|
||||
+53
-1
@@ -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",
|
||||
|
||||
+53
-1
@@ -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",
|
||||
|
||||
+63
-1
@@ -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",
|
||||
|
||||
+53
-1
@@ -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",
|
||||
|
||||
+53
-1
@@ -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",
|
||||
|
||||
+53
-1
@@ -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",
|
||||
|
||||
+53
-1
@@ -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",
|
||||
|
||||
+53
-1
@@ -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": "状態",
|
||||
|
||||
+53
-1
@@ -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": "상태",
|
||||
|
||||
+53
-1
@@ -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",
|
||||
|
||||
+53
-1
@@ -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",
|
||||
|
||||
+53
-1
@@ -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",
|
||||
|
||||
+53
-1
@@ -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",
|
||||
|
||||
+53
-1
@@ -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": "Состояние",
|
||||
|
||||
+53
-1
@@ -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",
|
||||
|
||||
@@ -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": "狀態",
|
||||
|
||||
+64
-2
@@ -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": "状态",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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 <AccountSettingsPage accountId={Number(id)} />
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { createLazyFileRoute } from '@tanstack/react-router'
|
||||
import { AccountNewPage } from '@/features/accounts/account-new'
|
||||
|
||||
export const Route = createLazyFileRoute('/_authenticated/accounts/new')({
|
||||
component: AccountNewPage,
|
||||
})
|
||||
Reference in New Issue
Block a user