feat: add manual download and cancel download for email accounts

This commit is contained in:
rustmailer
2026-05-08 01:00:46 +08:00
parent 7eacfbfb20
commit 174d56e7b4
30 changed files with 404 additions and 49 deletions
Generated
+1
View File
@@ -529,6 +529,7 @@ dependencies = [
"ring",
"rustls",
"rustls-pki-types",
"scopeguard",
"serde",
"serde_json",
"snafu",
+1
View File
@@ -70,3 +70,4 @@ tracing-log.workspace = true
tokio-util.workspace = true
whichlang = "0.1.1"
deunicode = "1.6.2"
scopeguard = "1.2.0"
+1 -1
View File
@@ -318,7 +318,7 @@ impl AccountV4 {
if matches!(cloned.account_type, AccountType::IMAP) {
DOWNLOAD_CONTROLLER
.trigger_start(cloned.id, cloned.email.clone())
.trigger_schedule(cloned.id, cloned.email.clone())
.await;
}
Ok(cloned)
+25 -19
View File
@@ -17,6 +17,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use crate::{
utc_now,
{
account::{
migration::AccountModel,
@@ -24,7 +25,6 @@ use crate::{
},
error::BichonResult,
},
utc_now,
};
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -34,27 +34,33 @@ pub enum DownloadTask {
Idle,
}
pub async fn decide_next_download_task(account: &AccountModel) -> BichonResult<DownloadTask> {
Ok(match DownloadState::get(account.id).await? {
Some(state) => {
let should_trigger = should_trigger_next_download(
state.last_trigger_at,
state.last_finished_at.unwrap_or(0),
account.download_interval_min.unwrap(),
);
if should_trigger {
DownloadState::start_new_session(account.id, TriggerType::Scheduled).await?;
DownloadTask::TraceFetch
} else {
DownloadTask::Idle
}
}
pub async fn decide_next_download_task(
account: &AccountModel,
trigger_type: TriggerType,
) -> BichonResult<DownloadTask> {
let state = match DownloadState::get(account.id).await? {
None => {
DownloadState::init(account.id).await?;
DownloadTask::FullFetch
return Ok(DownloadTask::FullFetch);
}
})
Some(s) => s,
};
let should_start = match trigger_type {
TriggerType::Manual => true,
TriggerType::Scheduled => should_trigger_next_download(
state.last_trigger_at,
state.last_finished_at.unwrap_or(0),
account.download_interval_min.unwrap_or(60),
),
};
if should_start {
DownloadState::start_new_session(account.id, trigger_type).await?;
Ok(DownloadTask::TraceFetch)
} else {
Ok(DownloadTask::Idle)
}
}
fn should_trigger_next_download(
+8 -7
View File
@@ -19,33 +19,34 @@
use crate::{
account::{
migration::{AccountModel, AccountType},
state::{DownloadState, DownloadStatus},
state::{DownloadState, DownloadStatus, TriggerType},
},
cache::imap::{mailbox::MailBox, download::flow::FetchDirection},
cache::imap::{download::flow::FetchDirection, mailbox::MailBox},
error::BichonResult,
imap::executor::ImapExecutor,
};
use download_folders::get_download_folders;
use download_type::{decide_next_download_task, DownloadTask};
use flow::reconcile_mailboxes;
use rebuild::{rebuild_cache, rebuild_cache_by_date};
use std::time::Instant;
use download_folders::get_download_folders;
use download_type::{decide_next_download_task, DownloadTask};
use tokio_util::sync::CancellationToken;
use tracing::{debug, warn};
pub mod flow;
pub mod rebuild;
pub mod download_folders;
pub mod download_type;
pub mod flow;
pub mod rebuild;
pub async fn process_imap_download(
account: &AccountModel,
token: CancellationToken,
trigger_type: TriggerType,
) -> BichonResult<()> {
assert_eq!(account.account_type, AccountType::IMAP);
let start_time = Instant::now();
let account_id = account.id;
let download_task = decide_next_download_task(account).await?;
let download_task = decide_next_download_task(account, trigger_type).await?;
if matches!(download_task, DownloadTask::Idle) {
return Ok(());
}
+121 -8
View File
@@ -17,37 +17,56 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use crate::account::entity::AuthType;
use crate::account::state::DownloadState;
use crate::account::state::{DownloadState, TriggerType};
use crate::cache::imap::download::process_imap_download;
use crate::common::periodic::{PeriodicTask, TaskHandle};
use crate::error::code::ErrorCode;
use crate::oauth2::token::OAuth2AccessToken;
use crate::utc_now;
use crate::{account::migration::AccountModel, error::BichonResult};
use std::collections::HashMap;
use crate::{raise_error, utc_now};
use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicI64, Ordering};
use std::{sync::LazyLock, time::Duration};
use tokio::sync::Mutex;
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use tracing::{error, info, warn};
static _DESCRIPTION: &str = "This task periodically synchronizes mailbox data for a specified account, ensuring that all local data is up-to-date.";
const TASK_INTERVAL: Duration = Duration::from_secs(10);
pub static SYNC_TASKS: LazyLock<AccountSyncTask> = LazyLock::new(AccountSyncTask::new);
pub static SYNC_TASKS: LazyLock<AccountDownTask> = LazyLock::new(AccountDownTask::new);
static LAST_WARN_TIME: AtomicI64 = AtomicI64::new(0);
const WARN_INTERVAL_MS: i64 = 600_000;
pub struct AccountSyncTask {
pub struct AccountDownTask {
tasks: Mutex<Option<HashMap<u64, (TaskHandle, CancellationToken)>>>,
manual_tasks: Mutex<HashMap<u64, (JoinHandle<()>, CancellationToken)>>,
busy_accounts: Mutex<HashSet<u64>>,
}
impl AccountSyncTask {
impl AccountDownTask {
pub fn new() -> Self {
Self {
tasks: Mutex::new(Some(HashMap::new())),
manual_tasks: Mutex::new(HashMap::new()),
busy_accounts: Mutex::new(HashSet::new()),
}
}
pub async fn start_account_download_task(&self, account_id: u64, email: String) {
async fn set_busy(&self, account_id: u64, is_busy: bool) {
let mut guard = self.busy_accounts.lock().await;
if is_busy {
guard.insert(account_id);
} else {
guard.remove(&account_id);
}
}
async fn is_busy(&self, account_id: u64) -> bool {
self.busy_accounts.lock().await.contains(&account_id)
}
pub async fn start_download_task(&self, account_id: u64, email: String) {
let task_name = format!("account-download-task-{}-{}", account_id, &email);
let periodic_task = PeriodicTask::new(&task_name);
@@ -58,6 +77,28 @@ impl AccountSyncTask {
let account_id = param.unwrap();
let internal_token = task_token.clone();
Box::pin(async move {
if SYNC_TASKS.is_manual_running(account_id).await {
info!(
"Account {}: Scheduled task skipped (Manual task is running).",
account_id
);
return Ok(());
}
if SYNC_TASKS.is_busy(account_id).await {
warn!(
"Account {}: Scheduled task skipped (Previous sync still active).",
account_id
);
return Ok(());
}
SYNC_TASKS.set_busy(account_id, true).await;
let _busy_guard = scopeguard::guard(account_id, |id| {
tokio::spawn(async move {
SYNC_TASKS.set_busy(id, false).await;
});
});
let account = AccountModel::async_get(account_id).await.ok();
match account {
Some(account) => {
@@ -82,7 +123,13 @@ impl AccountSyncTask {
}
}
}
if let Err(e) = process_imap_download(&account, internal_token).await {
if let Err(e) = process_imap_download(
&account,
internal_token,
TriggerType::Scheduled,
)
.await
{
DownloadState::append_session_error(
account.id,
format!("error in account download task: {:#?}", e),
@@ -150,4 +197,70 @@ impl AccountSyncTask {
info!("Shutdown: All download tasks processed.");
}
}
pub async fn start_manual_task(&self, account_id: u64) -> BichonResult<()> {
{
if self.is_manual_running(account_id).await {
return Err(raise_error!(
"Manual task already running.".into(),
ErrorCode::Forbidden
));
}
if self.is_busy(account_id).await {
return Err(raise_error!(
"The background synchronization is currently active. Please try again in a few seconds.".into(),
ErrorCode::Forbidden
));
}
}
let cancel_token = CancellationToken::new();
let token_clone = cancel_token.clone();
let handle = tokio::spawn(async move {
SYNC_TASKS.set_busy(account_id, true).await;
let _cleanup = scopeguard::guard(account_id, |id| {
tokio::spawn(async move {
SYNC_TASKS.set_busy(id, false).await;
let mut guard = SYNC_TASKS.manual_tasks.lock().await;
guard.remove(&id);
});
});
if token_clone.is_cancelled() {
return;
}
let account = match AccountModel::async_get(account_id).await {
Ok(acc) => acc,
Err(e) => {
error!("Failed to fetch account {}: {:?}", account_id, e);
return;
}
};
if let Err(e) = process_imap_download(&account, token_clone, TriggerType::Manual).await
{
error!("Manual download failed for {}: {:?}", account_id, e);
let error_msg = format!("error in account download task: {:#?}", e);
let _ = DownloadState::append_session_error(account.id, error_msg).await;
}
});
{
let mut guard = self.manual_tasks.lock().await;
guard.insert(account_id, (handle, cancel_token));
}
Ok(())
}
pub async fn cancel_manual_task(&self, account_id: u64) {
let mut guard = self.manual_tasks.lock().await;
if let Some((handle, token)) = guard.remove(&account_id) {
token.cancel();
let _ = handle.await;
}
}
pub async fn is_manual_running(&self, account_id: u64) -> bool {
let guard = self.manual_tasks.lock().await;
guard.contains_key(&account_id)
}
}
+6 -7
View File
@@ -35,11 +35,10 @@ impl DownloadController {
tokio::spawn(async move {
while let Some((account_id, email)) = rx.recv().await {
match Self::start_download(account_id, email.clone()).await {
Ok(Some(_)) => {}
Ok(None) => {}
Ok(_) => {}
Err(err) => {
error!(
"Failed to prepare and start download of account {{{}-{}}}, error: {:#?}",
"Failed to prepare and start scheduled download of account {{{}-{}}}, error: {:#?}",
&account_id, &email, err
);
}
@@ -51,7 +50,7 @@ impl DownloadController {
}
/// Trigger synchronization for a specific account
pub async fn trigger_start(&self, account_id: u64, email: String) {
pub async fn trigger_schedule(&self, account_id: u64, email: String) {
if let Err(e) = self.channel.send((account_id, email)).await {
error!(
"Failed to trigger download for account={{{}}}, error: {:?}",
@@ -60,13 +59,13 @@ impl DownloadController {
}
}
async fn start_download(account_id: u64, email: String) -> BichonResult<Option<()>> {
async fn start_download(account_id: u64, email: String) -> BichonResult<()> {
info!(
"Account download starting for account: {}-{}.",
account_id, email
);
SYNC_TASKS.start_account_download_task(account_id, email).await;
SYNC_TASKS.start_download_task(account_id, email).await;
tokio::time::sleep(Duration::from_millis(100)).await;
Ok(Some(()))
Ok(())
}
}
+3 -3
View File
@@ -35,7 +35,7 @@ pub struct BichonContext {
impl Initialize for BichonContext {
async fn initialize() -> BichonResult<()> {
BICHON_CONTEXT.start_account_syncers().await
BICHON_CONTEXT.start_account_downloader().await
}
}
@@ -49,7 +49,7 @@ impl BichonContext {
utc_now!() - self.start_at
}
pub async fn start_account_syncers(&self) -> BichonResult<()> {
pub async fn start_account_downloader(&self) -> BichonResult<()> {
let accounts = AccountModel::list_all().await?;
let active_accounts: Vec<AccountModel> = accounts
.into_iter()
@@ -66,7 +66,7 @@ impl BichonContext {
);
for account in active_accounts {
DOWNLOAD_CONTROLLER
.trigger_start(account.id, account.email)
.trigger_schedule(account.id, account.email)
.await
}
+66 -1
View File
@@ -20,14 +20,17 @@ use crate::common::auth::WrappedContext;
use crate::rest::api::ApiTags;
use crate::rest::ApiResult;
use bichon_core::account::grant::BatchAccountRoleRequest;
use bichon_core::account::migration::AccountModel;
use bichon_core::account::migration::{AccountModel, AccountType};
use bichon_core::account::payload::{
filter_accessible_accounts, AccountCreateRequest, AccountUpdateRequest, MinimalAccount,
};
use bichon_core::account::state::DownloadState;
use bichon_core::account::stats::AccountStats;
use bichon_core::account::view::AccountResp;
use bichon_core::cache::imap::task::SYNC_TASKS;
use bichon_core::common::paginated::{paginate_vec, DataPage};
use bichon_core::error::code::ErrorCode;
use bichon_core::raise_error;
use bichon_core::store::tantivy::envelope::ENVELOPE_MANAGER;
use bichon_core::users::permissions::Permission;
use bichon_core::users::UserModel;
@@ -204,6 +207,68 @@ impl AccountApi {
Ok(Json(state))
}
/// Start a manual download task for an account
#[oai(
path = "/accounts/:account_id/start-download",
method = "post",
operation_id = "accounts_start_download"
)]
async fn accounts_start_download(
&self,
/// The account ID to start download for
account_id: Path<u64>,
context: WrappedContext,
) -> ApiResult<()> {
let account_id = account_id.0;
let account = AccountModel::check_account_exists(account_id).await?;
if !matches!(account.account_type, AccountType::IMAP) {
return Err(raise_error!(
format!("Manual download is not supported for '{:#?}' accounts. Only IMAP accounts are supported.", account.account_type),
ErrorCode::InvalidParameter
))?;
}
context
.require_permission(Some(account_id), Permission::ACCOUNT_MANAGE)
.await?;
SYNC_TASKS.start_manual_task(account_id).await?;
Ok(())
}
/// Cancel a running manual download task
#[oai(
path = "/accounts/:account_id/cancel-download",
method = "post",
operation_id = "accounts_cancel_download"
)]
async fn accounts_cancel_download(
&self,
/// The account ID to cancel download for
account_id: Path<u64>,
context: WrappedContext,
) -> ApiResult<()> {
let account_id = account_id.0;
let account = AccountModel::check_account_exists(account_id).await?;
if !matches!(account.account_type, AccountType::IMAP) {
return Err(raise_error!(
"This operation is only supported for IMAP accounts.".into(),
ErrorCode::InvalidParameter
))?;
}
context
.require_permission(Some(account_id), Permission::ACCOUNT_MANAGE)
.await?;
if !SYNC_TASKS.is_manual_running(account_id).await {
return Err(raise_error!(
"No running manual task found for this account.".into(),
ErrorCode::ResourceNotFound
))?;
}
SYNC_TASKS.cancel_manual_task(account_id).await;
Ok(())
}
/// Get the stats of an account
#[oai(
path = "/accounts/:account_id/stats",
+11
View File
@@ -170,6 +170,17 @@ export const remove_account = async (account_id: number) => {
return response.data;
};
export const start_account_download = async (account_id: number) => {
const response = await axiosInstance.post(`api/v1/accounts/${account_id}/start-download`);
return response.data;
};
export const cancel_account_download = async (account_id: number) => {
const response = await axiosInstance.post(`api/v1/accounts/${account_id}/cancel-download`);
return response.data;
};
export interface AutoConfigResult {
imap: ServerConfig;
oauth2?: OAuth2Config;
@@ -19,7 +19,7 @@
import { DotsHorizontalIcon } from '@radix-ui/react-icons'
import { Row } from '@tanstack/react-table'
import { IconEdit, IconShieldLock, IconTrash } from '@tabler/icons-react'
import { IconEdit, IconPlayerPlay, IconPlayerStop, IconShieldLock, IconTrash } from '@tabler/icons-react'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
@@ -33,7 +33,8 @@ import { useAccountContext } from '../context'
import { Mailbox, MessageSquareMore } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { useCurrentUser } from '@/hooks/use-current-user'
import { AccountModel } from '@/api/account/api'
import { AccountModel, cancel_account_download, start_account_download } from '@/api/account/api'
import { toast } from '@/hooks/use-toast'
interface DataTableRowActionsProps {
row: Row<AccountModel>
@@ -55,6 +56,35 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
(account_type === 'IMAP' && hasPermission) ||
(account_type === 'IMAP' && hasReadPermission);
const showDownload = account_type === 'IMAP' && hasPermission;
const handleStartDownload = async () => {
try {
await start_account_download(row.original.id);
toast({ title: t('accounts.downloadStarted') });
} catch (error: any) {
toast({
variant: "destructive",
title: t('accounts.downloadFailed'),
description: error.response?.data?.message || error.message
});
}
}
const handleCancelDownload = async () => {
try {
await cancel_account_download(row.original.id);
toast({ title: t('accounts.downloadCancelled') });
} catch (error: any) {
toast({
variant: "destructive",
title: t('accounts.cancelFailed'),
description: error.response?.data?.message || error.message
});
}
}
return (
<>
<DropdownMenu modal={false}>
@@ -68,6 +98,26 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end' className='w-[220px]'>
{showDownload && (
<DropdownMenuItem onClick={handleStartDownload}>
{t('accounts.startDownload')}
<DropdownMenuShortcut>
<IconPlayerPlay size={16} />
</DropdownMenuShortcut>
</DropdownMenuItem>
)}
{showDownload && (
<DropdownMenuItem onClick={handleCancelDownload}>
{t('accounts.cancelDownload')}
<DropdownMenuShortcut>
<IconPlayerStop size={16} />
</DropdownMenuShortcut>
</DropdownMenuItem>
)}
{showDownload && <DropdownMenuSeparator />}
{hasPermission && <DropdownMenuItem
onClick={() => {
setCurrentRow(row.original)
@@ -298,7 +298,7 @@ export function RunningStateDialog({ currentRow, open, onOpenChange }: Props) {
{format(new Date(h.start_time), 'yyyy-MM-dd HH:mm:ss')}
</div>
<StatusBadge status={h.status} />
<div className="hidden xs:block"><TriggerBadge trigger={h.trigger} /></div>
<div className="xs:block"><TriggerBadge trigger={h.trigger} /></div>
</div>
<span className="text-[10px] font-bold text-muted-foreground bg-muted px-2 py-0.5 rounded-full self-start sm:self-auto">
{Object.keys(h.folder_details).length} {t('accounts.runningState.folders')}
+6
View File
@@ -104,6 +104,8 @@
"autoConfiguring": "جارٍ التكوين التلقائي...",
"beforeRelative": "تنزيل رسائل البريد الإلكتروني القديمة فقط",
"beforeRelativeValue": "تنزيل رسائل البريد الإلكتروني قبل {{value}} {{unit}} مضت",
"cancelDownload": "إلغاء التنزيل",
"cancelFailed": "فشل إلغاء مهمة التنزيل",
"capabilities": "الإمكانيات",
"chooseAuthMethod": "اختر طريقة المصادقة لـ IMAP.",
"chooseEncryptionMethod": "اختر طريقة التشفير لـ IMAP.",
@@ -127,10 +129,13 @@
"downloadAll": "تنزيل جميع رسائل البريد الإلكتروني",
"downloadBatchSize": "حجم دفعة التنزيل",
"downloadBatchSizeDescription": "عدد الرسائل التي يتم جلبها لكل طلب IMAP",
"downloadCancelled": "تم إلغاء المهمة",
"downloadFailed": "فشل بدء مهمة التنزيل",
"downloadInterval": "دورة التنزيل (بالدقائق)",
"downloadIntervalPlaceholder": "أدخل الدقائق",
"downloadScope": "استراتيجية التنزيل",
"downloadScopeDescription": "اختر رسائل البريد الإلكتروني التي يجب فهرستها وتنزيلها.",
"downloadStarted": "بدأت مهمة التنزيل",
"duration": "المدة",
"edit": "تعديل",
"email": "البريد الإلكتروني",
@@ -255,6 +260,7 @@
"sinceFixed": "منذ تاريخ محدد",
"sinceRelative": "تنزيل رسائل البريد الإلكتروني الأخيرة فقط",
"sinceRelativeValue": "تنزيل رسائل البريد الإلكتروني من آخر",
"startDownload": "بدء التنزيل",
"state": "الحالة",
"status": "الحالة",
"step": "الخطوة {{index}}",
+6
View File
@@ -104,6 +104,8 @@
"autoConfiguring": "Konfigurerer automatisk...",
"beforeRelative": "Download kun gamle e-mails",
"beforeRelativeValue": "Download e-mails før {{value}} {{unit}} siden",
"cancelDownload": "Annuller download",
"cancelFailed": "Kunne ikke annullere download-opgave",
"capabilities": "Funktioner",
"chooseAuthMethod": "Vælg godkendelsesmetode til IMAP.",
"chooseEncryptionMethod": "Vælg krypteringsmetode til IMAP.",
@@ -127,10 +129,13 @@
"downloadAll": "Download alle e-mails",
"downloadBatchSize": "Download batchstørrelse",
"downloadBatchSizeDescription": "Antal beskeder hentet pr. IMAP-anmodning",
"downloadCancelled": "Opgave annulleret",
"downloadFailed": "Kunne ikke starte download-opgave",
"downloadInterval": "Downloadinterval (minutter)",
"downloadIntervalPlaceholder": "Indtast minutter",
"downloadScope": "Downloadstrategi",
"downloadScopeDescription": "Vælg hvilke e-mails der skal indekseres og downloades.",
"downloadStarted": "Download-opgave startet",
"duration": "Varighed",
"edit": "Rediger",
"email": "E-mail",
@@ -255,6 +260,7 @@
"sinceFixed": "Siden specifik dato",
"sinceRelative": "Download kun seneste e-mails",
"sinceRelativeValue": "Download e-mails fra de sidste",
"startDownload": "Start download",
"state": "Tilstand",
"status": "Status",
"step": "Trin {{index}}",
+6
View File
@@ -104,6 +104,8 @@
"autoConfiguring": "Automatische Konfiguration läuft...",
"beforeRelative": "Nur alte E-Mails herunterladen",
"beforeRelativeValue": "E-Mails von vor {{value}} {{unit}} herunterladen",
"cancelDownload": "Download abbrechen",
"cancelFailed": "Download-Aufgabe konnte nicht abgebrochen werden",
"capabilities": "Funktionen",
"chooseAuthMethod": "Wählen Sie die Authentifizierungsmethode für IMAP.",
"chooseEncryptionMethod": "Wählen Sie die Verschlüsselungsmethode für IMAP.",
@@ -127,10 +129,13 @@
"downloadAll": "Alle E-Mails herunterladen",
"downloadBatchSize": "Download-Batch-Größe",
"downloadBatchSizeDescription": "Anzahl der pro IMAP-Anfrage abgerufenen Nachrichten",
"downloadCancelled": "Aufgabe abgebrochen",
"downloadFailed": "Download-Aufgabe konnte nicht gestartet werden",
"downloadInterval": "Download-Intervall (Minuten)",
"downloadIntervalPlaceholder": "Minuten eingeben",
"downloadScope": "Download-Strategie",
"downloadScopeDescription": "Wählen Sie aus, welche E-Mails indiziert und heruntergeladen werden sollen.",
"downloadStarted": "Download-Aufgabe gestartet",
"duration": "Dauer",
"edit": "Bearbeiten",
"email": "E-Mail",
@@ -255,6 +260,7 @@
"sinceFixed": "Seit einem bestimmten Datum",
"sinceRelative": "Nur aktuelle E-Mails herunterladen",
"sinceRelativeValue": "E-Mails der letzten Zeit herunterladen",
"startDownload": "Download starten",
"state": "Zustand",
"status": "Status",
"step": "Schritt {{index}}",
+6
View File
@@ -104,6 +104,8 @@
"autoConfiguring": "Auto-configuring...",
"beforeRelative": "Download Old Emails Only",
"beforeRelativeValue": "Download emails before {{value}} {{unit}} ago",
"cancelDownload": "Cancel download",
"cancelFailed": "Failed to cancel download task",
"capabilities": "Capabilities",
"chooseAuthMethod": "Choose the authentication method for IMAP.",
"chooseEncryptionMethod": "Choose the encryption method for IMAP.",
@@ -127,10 +129,13 @@
"downloadAll": "Download All Emails",
"downloadBatchSize": "Download batch size",
"downloadBatchSizeDescription": "Number of messages fetched per IMAP request",
"downloadCancelled": "Task cancelled",
"downloadFailed": "Failed to start download task",
"downloadInterval": "Download Interval (minutes)",
"downloadIntervalPlaceholder": "Enter minutes",
"downloadScope": "Download Strategy",
"downloadScopeDescription": "Choose which emails should be indexed and downloaded.",
"downloadStarted": "Download task started",
"duration": "Duration",
"edit": "Edit",
"email": "Email",
@@ -255,6 +260,7 @@
"sinceFixed": "Since Specific Date",
"sinceRelative": "Download Recent Emails Only",
"sinceRelativeValue": "Download emails from the last",
"startDownload": "Start download",
"state": "State",
"status": "Status",
"step": "Step {{index}}",
+6
View File
@@ -104,6 +104,8 @@
"autoConfiguring": "Autoconfigurando...",
"beforeRelative": "Descargar solo correos antiguos",
"beforeRelativeValue": "Descargar correos de hace {{value}} {{unit}}",
"cancelDownload": "Cancelar descarga",
"cancelFailed": "Error al cancelar la tarea de descarga",
"capabilities": "Capacidades",
"chooseAuthMethod": "Elige el método de autenticación a utilizar para IMAP.",
"chooseEncryptionMethod": "Elige el método de cifrado a utilizar para IMAP.",
@@ -127,10 +129,13 @@
"downloadAll": "Descargar todos los correos",
"downloadBatchSize": "Tamaño del lote de descarga",
"downloadBatchSizeDescription": "Número de mensajes obtenidos por solicitud IMAP",
"downloadCancelled": "Tarea cancelada",
"downloadFailed": "Error al iniciar la tarea de descarga",
"downloadInterval": "Intervalo de descarga (minutos)",
"downloadIntervalPlaceholder": "Ingresa los minutos",
"downloadScope": "Estrategia de descarga",
"downloadScopeDescription": "Elija qué correos electrónicos deben indexarse y descargarse.",
"downloadStarted": "Tarea de descarga iniciada",
"duration": "Duración",
"edit": "Editar",
"email": "Correo electrónico",
@@ -255,6 +260,7 @@
"sinceFixed": "Desde una fecha específica",
"sinceRelative": "Descargar solo correos recientes",
"sinceRelativeValue": "Descargar correos de los últimos",
"startDownload": "Iniciar descarga",
"state": "Estado",
"status": "Estado",
"step": "Paso {{index}}",
+6
View File
@@ -104,6 +104,8 @@
"autoConfiguring": "Automaattinen määritys...",
"beforeRelative": "Lataa vain vanhat sähköpostit",
"beforeRelativeValue": "Lataa sähköpostit {{value}} {{unit}} sitten",
"cancelDownload": "Peruuta lataus",
"cancelFailed": "Lataustehtävän peruuttaminen epäonnistui",
"capabilities": "Ominaisuudet",
"chooseAuthMethod": "Valitse IMAP:lle käytettävä todennusmenetelmä.",
"chooseEncryptionMethod": "Valitse IMAP:lle käytettävä salausmenetelmä.",
@@ -127,10 +129,13 @@
"downloadAll": "Lataa kaikki sähköpostit",
"downloadBatchSize": "Latauserän koko",
"downloadBatchSizeDescription": "IMAP-pyyntöä kohden noudettujen viestien määrä",
"downloadCancelled": "Tehtävä peruutettu",
"downloadFailed": "Lataustehtävän aloittaminen epäonnistui",
"downloadInterval": "Latausväli (minuuttia)",
"downloadIntervalPlaceholder": "Syötä minuutit",
"downloadScope": "Latausstrategia",
"downloadScopeDescription": "Valitse, mitkä sähköpostit indeksoidaan ja ladataan.",
"downloadStarted": "Lataustehtävä aloitettu",
"duration": "Kesto",
"edit": "Muokkaa",
"email": "Sähköposti",
@@ -255,6 +260,7 @@
"sinceFixed": "Tietystä päivämäärästä lähtien",
"sinceRelative": "Lataa vain viimeisimmät sähköpostit",
"sinceRelativeValue": "Lataa sähköpostit viimeisimmiltä",
"startDownload": "Aloita lataus",
"state": "Tila",
"status": "Tila",
"step": "Vaihe {{index}}",
+6
View File
@@ -104,6 +104,8 @@
"autoConfiguring": "Configuration automatique...",
"beforeRelative": "Télécharger uniquement 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",
"capabilities": "Capacités",
"chooseAuthMethod": "Choisissez la méthode d'authentification à utiliser pour IMAP.",
"chooseEncryptionMethod": "Choisissez la méthode de chiffrement à utiliser pour IMAP.",
@@ -127,10 +129,13 @@
"downloadAll": "Télécharger tous les e-mails",
"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",
"downloadFailed": "Échec du lancement de la tâche de téléchargement",
"downloadInterval": "Intervalle de téléchargement (minutes)",
"downloadIntervalPlaceholder": "Entrer les minutes",
"downloadScope": "Stratégie de téléchargement",
"downloadScopeDescription": "Choisissez les e-mails à indexer et à télécharger.",
"downloadStarted": "Tâche de téléchargement lancée",
"duration": "Durée",
"edit": "Modifier",
"email": "E-mail",
@@ -255,6 +260,7 @@
"sinceFixed": "Depuis une date spécifique",
"sinceRelative": "Télécharger uniquement les e-mails récents",
"sinceRelativeValue": "Télécharger les e-mails des derniers",
"startDownload": "Lancer le téléchargement",
"state": "État",
"status": "Statut",
"step": "Étape {{index}}",
+6
View File
@@ -104,6 +104,8 @@
"autoConfiguring": "Configurazione automatica...",
"beforeRelative": "Scarica solo le vecchie email",
"beforeRelativeValue": "Scarica email da {{value}} {{unit}} fa",
"cancelDownload": "Annulla download",
"cancelFailed": "Annullamento attività di download non riuscito",
"capabilities": "Capacità",
"chooseAuthMethod": "Scegli il metodo di autenticazione per IMAP.",
"chooseEncryptionMethod": "Scegli il metodo di crittografia per IMAP.",
@@ -127,10 +129,13 @@
"downloadAll": "Scarica tutte le email",
"downloadBatchSize": "Dimensione del lotto di download",
"downloadBatchSizeDescription": "Numero di messaggi recuperati per richiesta IMAP",
"downloadCancelled": "Attività annullata",
"downloadFailed": "Avvio attività di download non riuscito",
"downloadInterval": "Intervallo di download (minuti)",
"downloadIntervalPlaceholder": "Inserisci i minuti",
"downloadScope": "Strategia di download",
"downloadScopeDescription": "Scegli quali email indicizzare e scaricare.",
"downloadStarted": "Attività di download avviata",
"duration": "Durata",
"edit": "Modifica",
"email": "Email",
@@ -255,6 +260,7 @@
"sinceFixed": "Da una data specifica",
"sinceRelative": "Scarica solo le email recenti",
"sinceRelativeValue": "Scarica email degli ultimi",
"startDownload": "Avvia download",
"state": "Stato",
"status": "Stato",
"step": "Passo {{index}}",
+6
View File
@@ -104,6 +104,8 @@
"autoConfiguring": "自動設定中...",
"beforeRelative": "古いメールのみダウンロード",
"beforeRelativeValue": "{{value}} {{unit}}前より古いメールをダウンロード",
"cancelDownload": "ダウンロードをキャンセル",
"cancelFailed": "ダウンロードタスクのキャンセルに失敗しました",
"capabilities": "機能",
"chooseAuthMethod": "IMAPの認証方式を選択してください。",
"chooseEncryptionMethod": "IMAPの暗号化方式を選択してください。",
@@ -127,10 +129,13 @@
"downloadAll": "すべてのメールをダウンロード",
"downloadBatchSize": "ダウンロードバッチサイズ",
"downloadBatchSizeDescription": "IMAPリクエストごとに取得されるメッセージ数",
"downloadCancelled": "タスクをキャンセルしました",
"downloadFailed": "ダウンロードタスクの起動に失敗しました",
"downloadInterval": "ダウンロード間隔 (分)",
"downloadIntervalPlaceholder": "分を入力してください",
"downloadScope": "ダウンロード戦略",
"downloadScopeDescription": "インデックスとダウンロードの対象となるメールを選択してください。",
"downloadStarted": "ダウンロードタスクを開始しました",
"duration": "期間",
"edit": "編集",
"email": "メールアドレス",
@@ -255,6 +260,7 @@
"sinceFixed": "指定した日付以降",
"sinceRelative": "最近のメールのみダウンロード",
"sinceRelativeValue": "直近の期間のメールをダウンロード",
"startDownload": "ダウンロードを開始",
"state": "状態",
"status": "ステータス",
"step": "ステップ {{index}}",
+6
View File
@@ -104,6 +104,8 @@
"autoConfiguring": "자동 구성 중...",
"beforeRelative": "이전 이메일만 다운로드",
"beforeRelativeValue": "{{value}} {{unit}} 전의 이메일 다운로드",
"cancelDownload": "다운로드 취소",
"cancelFailed": "다운로드 작업 취소 실패",
"capabilities": "기능",
"chooseAuthMethod": "IMAP에 사용할 인증 방법을 선택하십시오.",
"chooseEncryptionMethod": "IMAP에 사용할 암호화 방법을 선택하십시오.",
@@ -127,10 +129,13 @@
"downloadAll": "모든 이메일 다운로드",
"downloadBatchSize": "다운로드 일괄 처리 크기",
"downloadBatchSizeDescription": "IMAP 요청당 가져온 메시지 수",
"downloadCancelled": "작업 취소됨",
"downloadFailed": "다운로드 작업 시작 실패",
"downloadInterval": "다운로드 주기 (분)",
"downloadIntervalPlaceholder": "분 단위 입력",
"downloadScope": "다운로드 전략",
"downloadScopeDescription": "색인화 및 다운로드할 이메일을 선택하십시오.",
"downloadStarted": "다운로드 작업 시작됨",
"duration": "기간",
"edit": "편집",
"email": "이메일",
@@ -255,6 +260,7 @@
"sinceFixed": "특정 날짜 이후",
"sinceRelative": "최근 이메일만 다운로드",
"sinceRelativeValue": "최근 기간의 이메일 다운로드",
"startDownload": "다운로드 시작",
"state": "상태",
"status": "상태",
"step": "단계 {{index}}",
+6
View File
@@ -104,6 +104,8 @@
"autoConfiguring": "Automatisch configureren...",
"beforeRelative": "Download alleen oude e-mails",
"beforeRelativeValue": "Download e-mails van {{value}} {{unit}} geleden",
"cancelDownload": "Download annuleren",
"cancelFailed": "Downloadtaak annuleren mislukt",
"capabilities": "Mogelijkheden",
"chooseAuthMethod": "Kies de authenticatiemethode voor IMAP.",
"chooseEncryptionMethod": "Kies de versleutelingsmethode voor IMAP.",
@@ -127,10 +129,13 @@
"downloadAll": "Download alle e-mails",
"downloadBatchSize": "Download batchgrootte",
"downloadBatchSizeDescription": "Aantal berichten opgehaald per IMAP-verzoek",
"downloadCancelled": "Taak geannuleerd",
"downloadFailed": "Downloadtaak starten mislukt",
"downloadInterval": "Download-interval (minuten)",
"downloadIntervalPlaceholder": "Voer minuten in",
"downloadScope": "Downloadstrategie",
"downloadScopeDescription": "Kies welke e-mails moeten worden geïndexeerd en gedownload.",
"downloadStarted": "Downloadtaak gestart",
"duration": "Duur",
"edit": "Bewerken",
"email": "E-mail",
@@ -255,6 +260,7 @@
"sinceFixed": "Sinds een specifieke datum",
"sinceRelative": "Download alleen recente e-mails",
"sinceRelativeValue": "Download e-mails van de laatste",
"startDownload": "Download starten",
"state": "Status",
"status": "Status",
"step": "Stap {{index}}",
+6
View File
@@ -104,6 +104,8 @@
"autoConfiguring": "Konfigurerer automatisk...",
"beforeRelative": "Last ned kun gamle e-poster",
"beforeRelativeValue": "Last ned e-poster fra {{value}} {{unit}} siden",
"cancelDownload": "Avbryt nedlasting",
"cancelFailed": "Kunne ikke avbryte nedlastingsoppgave",
"capabilities": "Funksjoner",
"chooseAuthMethod": "Velg autentiseringsmetoden for IMAP.",
"chooseEncryptionMethod": "Velg krypteringsmetoden for IMAP.",
@@ -127,10 +129,13 @@
"downloadAll": "Last ned alle e-poster",
"downloadBatchSize": "Nedlastingsbatchstørrelse",
"downloadBatchSizeDescription": "Antall meldinger hentet per IMAP-forespørsel",
"downloadCancelled": "Oppgave avbrutt",
"downloadFailed": "Kunne ikke starte nedlastingsoppgave",
"downloadInterval": "Nedlastingsintervall (minutter)",
"downloadIntervalPlaceholder": "Skriv inn minutter",
"downloadScope": "Nedlastingsstrategi",
"downloadScopeDescription": "Velg hvilke e-poster som skal indekseres og lastes ned.",
"downloadStarted": "Nedlastingsoppgave startet",
"duration": "Varighet",
"edit": "Rediger",
"email": "E-post",
@@ -255,6 +260,7 @@
"sinceFixed": "Siden spesifikk dato",
"sinceRelative": "Last ned kun nylige e-poster",
"sinceRelativeValue": "Last ned e-poster fra de siste",
"startDownload": "Start nedlasting",
"state": "Tilstand",
"status": "Status",
"step": "Trinn {{index}}",
+6
View File
@@ -104,6 +104,8 @@
"autoConfiguring": "Auto konfiguracja...",
"beforeRelative": "Pobierz tylko stare e-maile",
"beforeRelativeValue": "Pobierz e-maile sprzed {{value}} {{unit}}",
"cancelDownload": "Anuluj pobieranie",
"cancelFailed": "Nie udało się anulować zadania pobierania",
"capabilities": "Możliwości",
"chooseAuthMethod": "Wybierz metodę uwierzytelniania IMAP.",
"chooseEncryptionMethod": "Wybierz metodę szyfrowania dla IMAP.",
@@ -127,10 +129,13 @@
"downloadAll": "Pobierz wszystkie e-maile",
"downloadBatchSize": "Rozmiar partii pobierania",
"downloadBatchSizeDescription": "Liczba wiadomości pobieranych na żądanie IMAP",
"downloadCancelled": "Zadanie anulowane",
"downloadFailed": "Nie udało się uruchomić zadania pobierania",
"downloadInterval": "Cykl pobierania (minuty)",
"downloadIntervalPlaceholder": "Wprowadź minuty",
"downloadScope": "Strategia pobierania",
"downloadScopeDescription": "Wybierz, które wiadomości e-mail mają być indeksowane i pobierane.",
"downloadStarted": "Uruchomiono zadanie pobierania",
"duration": "Czas trwania",
"edit": "Edytuj",
"email": "Email",
@@ -255,6 +260,7 @@
"sinceFixed": "Od konkretnej daty",
"sinceRelative": "Pobierz tylko ostatnie e-maile",
"sinceRelativeValue": "Pobierz e-maile z ostatnich",
"startDownload": "Uruchom pobieranie",
"state": "Status",
"status": "Status",
"step": "Krok {{index}}",
+6
View File
@@ -104,6 +104,8 @@
"autoConfiguring": "Configurando Automaticamente...",
"beforeRelative": "Baixar apenas e-mails antigos",
"beforeRelativeValue": "Baixar e-mails de {{value}} {{unit}} atrás",
"cancelDownload": "Cancelar download",
"cancelFailed": "Falha ao cancelar tarefa de download",
"capabilities": "Capacidades",
"chooseAuthMethod": "Por favor, escolha o método de autenticação para IMAP.",
"chooseEncryptionMethod": "Por favor, escolha o método de criptografia para IMAP.",
@@ -127,10 +129,13 @@
"downloadAll": "Baixar todos os e-mails",
"downloadBatchSize": "Tamanho do lote de download",
"downloadBatchSizeDescription": "Número de mensagens recuperadas por solicitação IMAP",
"downloadCancelled": "Tarefa cancelada",
"downloadFailed": "Falha ao iniciar tarefa de download",
"downloadInterval": "Intervalo de download (minutos)",
"downloadIntervalPlaceholder": "Insira os minutos",
"downloadScope": "Estratégia de download",
"downloadScopeDescription": "Escolha quais e-mails devem ser indexados e baixados.",
"downloadStarted": "Tarefa de download iniciada",
"duration": "Duração",
"edit": "Editar",
"email": "Email",
@@ -255,6 +260,7 @@
"sinceFixed": "Desde uma data específica",
"sinceRelative": "Baixar apenas e-mails recentes",
"sinceRelativeValue": "Baixar e-mails dos últimos",
"startDownload": "Iniciar download",
"state": "Estado",
"status": "Status",
"step": "Passo {{index}}",
+6
View File
@@ -104,6 +104,8 @@
"autoConfiguring": "Автонастройка...",
"beforeRelative": "Скачать только старые письма",
"beforeRelativeValue": "Скачать письма за {{value}} {{unit}} назад",
"cancelDownload": "Отменить загрузку",
"cancelFailed": "Не удалось отменить задачу загрузки",
"capabilities": "Возможности",
"chooseAuthMethod": "Выберите метод авторизации для IMAP.",
"chooseEncryptionMethod": "Выберите метод шифрования для IMAP.",
@@ -127,10 +129,13 @@
"downloadAll": "Скачать все письма",
"downloadBatchSize": "Размер пакета загрузки",
"downloadBatchSizeDescription": "Количество сообщений, получаемых за один IMAP-запрос",
"downloadCancelled": "Задача отменена",
"downloadFailed": "Не удалось запустить задачу загрузки",
"downloadInterval": "Интервал загрузки (мин.)",
"downloadIntervalPlaceholder": "Введите минуты",
"downloadScope": "Стратегия загрузки",
"downloadScopeDescription": "Выберите, какие электронные письма должны быть проиндексированы и скачаны.",
"downloadStarted": "Задача загрузки запущена",
"duration": "Продолжительность",
"edit": "Ред.",
"email": "Email",
@@ -255,6 +260,7 @@
"sinceFixed": "С определенной даты",
"sinceRelative": "Скачать только недавние письма",
"sinceRelativeValue": "Скачать письма за последние",
"startDownload": "Запустить загрузку",
"state": "Состояние",
"status": "Статус",
"step": "Шаг {{index}}",
+6
View File
@@ -104,6 +104,8 @@
"autoConfiguring": "Konfigurerar automatiskt...",
"beforeRelative": "Ladda ner endast gamla e-postmeddelanden",
"beforeRelativeValue": "Ladda ner e-post från {{value}} {{unit}} sedan",
"cancelDownload": "Avbryt hämtning",
"cancelFailed": "Misslyckades med att avbryta hämtningsuppgift",
"capabilities": "Funktioner",
"chooseAuthMethod": "Välj autentiseringsmetod för IMAP.",
"chooseEncryptionMethod": "Välj krypteringsmetod för IMAP.",
@@ -127,10 +129,13 @@
"downloadAll": "Ladda ner alla e-postmeddelanden",
"downloadBatchSize": "Batchstorlek för nedladdning",
"downloadBatchSizeDescription": "Antal meddelanden som hämtas per IMAP-begäran",
"downloadCancelled": "Uppgiften avbruten",
"downloadFailed": "Misslyckades med att starta hämtningsuppgift",
"downloadInterval": "Nedladdningsintervall (minuter)",
"downloadIntervalPlaceholder": "Ange minuter",
"downloadScope": "Nedladdningsstrategi",
"downloadScopeDescription": "Välj vilka e-postmeddelanden som ska indexeras och laddas ner.",
"downloadStarted": "Hämtningsuppgift startad",
"duration": "Varaktighet",
"edit": "Redigera",
"email": "E-post",
@@ -255,6 +260,7 @@
"sinceFixed": "Sedan specifikt datum",
"sinceRelative": "Ladda ner endast senaste e-postmeddelanden",
"sinceRelativeValue": "Ladda ner e-post från de senaste",
"startDownload": "Starta hämtning",
"state": "Tillstånd",
"status": "Status",
"step": "Steg {{index}}",
+6
View File
@@ -104,6 +104,8 @@
"autoConfiguring": "正在自動設定...",
"beforeRelative": "僅下載舊郵件",
"beforeRelativeValue": "下載 {{value}} {{unit}} 之前的郵件",
"cancelDownload": "取消下載",
"cancelFailed": "取消下載任務失敗",
"capabilities": "功能",
"chooseAuthMethod": "請選擇 IMAP 的驗證方法。",
"chooseEncryptionMethod": "請選擇 IMAP 的加密方法。",
@@ -127,10 +129,13 @@
"downloadAll": "下載所有郵件",
"downloadBatchSize": "下載批量大小",
"downloadBatchSizeDescription": "每個 IMAP 請求獲取的郵件數量",
"downloadCancelled": "下載任務已取消",
"downloadFailed": "啟動下載任務失敗",
"downloadInterval": "下載週期 (分鐘)",
"downloadIntervalPlaceholder": "請輸入分鐘數",
"downloadScope": "下載策略",
"downloadScopeDescription": "選擇哪些郵件應被索引和下載。",
"downloadStarted": "下載任務已啟動",
"duration": "時長",
"edit": "編輯",
"email": "電子郵件",
@@ -255,6 +260,7 @@
"sinceFixed": "自特定日期起",
"sinceRelative": "僅下載最近郵件",
"sinceRelativeValue": "下載最近一段時期的郵件",
"startDownload": "啟動下載",
"state": "狀態",
"status": "狀態",
"step": "步驟 {{index}}",
+6
View File
@@ -104,6 +104,8 @@
"autoConfiguring": "自动配置中...",
"beforeRelative": "仅下载旧邮件",
"beforeRelativeValue": "下载 {{value}} {{unit}} 之前的邮件",
"cancelDownload": "取消下载",
"cancelFailed": "取消下载任务失败",
"capabilities": "功能",
"chooseAuthMethod": "选择 IMAP 的认证方法。",
"chooseEncryptionMethod": "选择 IMAP 的加密方法。",
@@ -127,10 +129,13 @@
"downloadAll": "下载所有邮件",
"downloadBatchSize": "下载批量大小",
"downloadBatchSizeDescription": "每个 IMAP 请求获取的邮件数量",
"downloadCancelled": "下载任务已取消",
"downloadFailed": "启动下载任务失败",
"downloadInterval": "下载周期 (分钟)",
"downloadIntervalPlaceholder": "请输入分钟数",
"downloadScope": "下载策略",
"downloadScopeDescription": "选择哪些邮件应被索引和下载。",
"downloadStarted": "下载任务已启动",
"duration": "时长",
"edit": "编辑",
"email": "邮箱",
@@ -255,6 +260,7 @@
"sinceFixed": "自特定日期起",
"sinceRelative": "仅下载最近邮件",
"sinceRelativeValue": "下载最近一段时期的邮件",
"startDownload": "启动下载",
"state": "状态",
"status": "状态",
"step": "步骤 {{index}}",