7 Commits
1.4.3 ... 1.5.0

Author SHA1 Message Date
rustmailer
769630f9d7 Merge branch 'main' of https://github.com/rustmailer/bichon 2026-06-04 23:46:28 +08:00
rustmailer
368b18c45f bump to v1.5.0 2026-06-04 23:46:25 +08:00
rustmailer
42861f6cc9 Merge pull request #288 from Korov/fix/tencent-mail-uidvalidity
fix: Add fallback UIDVALIDITY support for non-compliant IMAP servers
2026-06-04 23:45:40 +08:00
rustmailer
327a3f39d9 Merge pull request #287 from fama/dedup-cache-fix
fix: open NewIndexWriter once across all migration segments
2026-06-04 10:24:22 +08:00
fama
427f7248d2 fix: open NewIndexWriter once across all migration segments
Previously, do_migrate_segment created a fresh NewIndexWriter (and
therefore a new Fjall Database) on every call, meaning the Fjall
database at bichon-storage/ was opened and closed once per segment.

This caused the migration to fail mid-way through (observed at segment
9/16) with:

  Storage(InvalidTag(("ChecksumType", 171)))

Root cause: after segment N writes email blobs via Fjall's ingestion
API (start_ingestion / write / finish), those SSTables and KV-separated
blob files are flushed to disk and the Database is dropped. When segment
N+1 calls Database::builder(storage_dir).open(), Fjall must discover and
catalog all on-disk files produced by the previous segments. During that
discovery it reads SSTable or blob-file block headers and encounters a
ChecksumType discriminant byte (171 / 0xAB) that lsm-tree 3.1.4 does
not recognise, causing the fatal error.

The first N segments succeed because the cumulative set of ingested
SSTables stays small enough that Fjall does not need to read the
offending headers during reopen. Once enough data has accumulated the
reopen triggers a manifest or compaction read that exposes the mismatch.

Fix: open NewIndexWriter once, before the segment loop, and pass a
&mut reference into each do_migrate_segment call. finish_writers() is
called a single time after all segments complete. The Fjall Database
stays open for the entire migration and is never closed and reopened,
eliminating the incompatible-reopen path entirely.
2026-06-03 15:10:03 -06:00
rustmailer
a2a51a2037 feat(imap): add message size check before download 2026-06-03 21:17:35 +08:00
Lei Zhu
e8469da3bc fix: Add fallback UIDVALIDITY support for non-compliant IMAP servers
This commit adds support for IMAP servers that don't provide UIDVALIDITY,
such as Tencent Enterprise Mail (腾讯企业邮箱).

Changes:
- Added `generate_synthetic_uidvalidity()` function that creates a stable
  hash-based UIDVALIDITY from the mailbox name
- Modified `reconcile_mailboxes()` to use synthetic UIDVALIDITY when the
  server doesn't provide one
- Servers without UIDVALIDITY can now sync all mailboxes including system
  folders (Sent Messages, Drafts, Deleted Messages)
- Incremental sync is supported via the synthetic UIDVALIDITY
- Added warning logs to indicate when synthetic UIDVALIDITY is in use
- Updated mailbox metadata to store the resolved UIDVALIDITY

Fixes issues with:
- Tencent Enterprise Mail (腾讯企业邮箱)
- Other non-compliant IMAP servers
- Mailboxes that don't properly support UIDVALIDITY"
2026-05-30 16:32:06 +08:00
35 changed files with 438 additions and 68 deletions

10
Cargo.lock generated
View File

@@ -293,7 +293,7 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bichon-admin"
version = "1.4.3"
version = "1.5.0"
dependencies = [
"bichon-core",
"console",
@@ -311,7 +311,7 @@ dependencies = [
[[package]]
name = "bichon-cli"
version = "1.4.3"
version = "1.5.0"
dependencies = [
"base64 0.22.1",
"bichon-core",
@@ -337,7 +337,7 @@ dependencies = [
[[package]]
name = "bichon-core"
version = "1.4.3"
version = "1.5.0"
dependencies = [
"async-imap",
"base64 0.22.1",
@@ -396,7 +396,7 @@ dependencies = [
[[package]]
name = "bichon-server"
version = "1.4.3"
version = "1.5.0"
dependencies = [
"bichon-core",
"bichon-smtp",
@@ -420,7 +420,7 @@ dependencies = [
[[package]]
name = "bichon-smtp"
version = "1.4.3"
version = "1.5.0"
dependencies = [
"base64 0.22.1",
"bichon-core",

View File

@@ -12,7 +12,7 @@ members = [
resolver = "2"
[workspace.package]
version = "1.4.3"
version = "1.5.0"
edition = "2021"
[workspace.dependencies]

View File

@@ -250,6 +250,7 @@ impl From<AccountV3> for AccountModel {
account_type: value.account_type,
download_interval_min: value.sync_interval_min,
download_batch_size: value.sync_batch_size,
max_email_size_bytes: None,
known_folders: value.known_folders,
created_at: value.created_at,
updated_at: value.updated_at,

View File

@@ -2,7 +2,7 @@ use std::path::{Path, PathBuf};
use bichon_core::migrate::{
count_eml_segments, do_migrate_segment, is_tantivy_index_dir,
store::{LegacyDirs, NewDirs},
store::{LegacyDirs, NewDirs, NewIndexWriter},
};
use console::style;
use dialoguer::{theme::ColorfulTheme, Confirm, Input};
@@ -326,6 +326,18 @@ pub fn handle_migration(theme: &ColorfulTheme) {
.progress_chars("#>-"),
);
let mut writer = match NewIndexWriter::open(NewDirs::new(
new_index_path.clone(),
new_data_path.clone(),
)) {
Ok(w) => w,
Err(e) => {
pb.finish_with_message(format!("{}", style("Migration failed.").red()));
eprintln!("\n{} {:?}", style("").red().bold(), e);
return;
}
};
let mut grand_total_migrated: usize = 0;
let mut grand_total_skipped: usize = 0;
@@ -337,7 +349,7 @@ pub fn handle_migration(theme: &ColorfulTheme) {
match do_migrate_segment(
batch_size,
legacy,
NewDirs::new(new_index_path.clone(), new_data_path.clone()),
&mut writer,
seg_idx,
|msg| {
if let Some(data) = msg.strip_prefix("TOTAL:") {
@@ -407,6 +419,13 @@ pub fn handle_migration(theme: &ColorfulTheme) {
pb.set_position((seg_idx + 1) as u64);
}
pb.set_message(style("Finalizing indexes...").dim().to_string());
if let Err(e) = writer.finish_writers() {
pb.finish_with_message(format!("{}", style("Migration failed.").red()));
eprintln!("\n{} {:?}", style("").red().bold(), e);
return;
}
pb.finish_with_message(format!(
"Migration finished. Total: {}, Skipped: {}",
grand_total_migrated, grand_total_skipped

View File

@@ -84,6 +84,8 @@ pub struct Account {
pub account_type: AccountType,
pub download_interval_min: Option<i64>,
pub download_batch_size: Option<u32>,
#[serde(default)]
pub max_email_size_bytes: Option<u64>,
pub known_folders: Option<BTreeSet<String>>,
pub created_at: i64,
pub updated_at: i64,
@@ -128,6 +130,7 @@ impl Account {
pgp_key: request.pgp_key,
created_by: user_id,
download_batch_size: request.download_batch_size,
max_email_size_bytes: request.max_email_size_bytes,
date_before: request.date_before,
auto_download_new_mailboxes: request.auto_download_new_mailboxes,
imap_quota_bytes: request.imap_quota_bytes,
@@ -395,6 +398,10 @@ impl Account {
new.download_batch_size = Some(*download_batch_size);
}
if let Some(max_email_size_bytes) = request.max_email_size_bytes {
new.max_email_size_bytes = Some(max_email_size_bytes);
}
if let Some(use_proxy) = request.use_proxy {
new.use_proxy = Some(use_proxy);
}

View File

@@ -48,6 +48,7 @@ pub struct AccountCreateRequest {
oai(validator(minimum(value = "10"), maximum(value = "200")))
)]
pub download_batch_size: Option<u32>,
pub max_email_size_bytes: Option<u64>,
pub use_proxy: Option<u64>,
pub use_dangerous: bool,
pub pgp_key: Option<String>,
@@ -165,6 +166,7 @@ pub struct AccountUpdateRequest {
oai(validator(minimum(value = "10"), maximum(value = "200")))
)]
pub download_batch_size: Option<u32>,
pub max_email_size_bytes: Option<u64>,
/// Optional proxy ID for establishing the connection to external APIs (e.g., Gmail, Outlook).
/// - If `None` or not provided, the client will connect directly to the API server.
/// - If `Some(proxy_id)`, the client will use the pre-configured proxy with the given ID for API requests.

View File

@@ -44,6 +44,7 @@ pub struct AccountResp {
pub account_type: AccountType,
pub download_interval_min: Option<i64>,
pub download_batch_size: Option<u32>,
pub max_email_size_bytes: Option<u64>,
pub known_folders: Option<BTreeSet<String>>,
pub created_at: i64,
pub updated_at: i64,
@@ -76,6 +77,7 @@ impl AccountResp {
account_type: account.account_type,
download_interval_min: account.download_interval_min,
download_batch_size: account.download_batch_size,
max_email_size_bytes: account.max_email_size_bytes,
known_folders: account.known_folders,
created_at: account.created_at,
updated_at: account.updated_at,

View File

@@ -157,12 +157,13 @@ pub async fn fetch_and_save_by_date(
account_id,
mailbox.id,
&batch.0,
account.max_email_size_bytes,
token.clone(),
)
.await
{
Ok(_) => {
current_processed += batch.1;
Ok(processed) => {
current_processed += processed;
DownloadState::update_folder_progress(
account_id,
mailbox.name.clone(),
@@ -290,6 +291,7 @@ pub async fn fetch_and_save_full_mailbox(
page as u64,
page_size as u64,
&mailbox.encoded_name(),
account.max_email_size_bytes,
token.clone(),
&mut max_uid,
)
@@ -337,6 +339,17 @@ pub async fn fetch_and_save_full_mailbox(
Ok(max_uid)
}
/// Generates a synthetic UIDVALIDITY for IMAP servers that don't provide it.
/// Uses a stable hash of the mailbox name to ensure consistent IDs across sessions.
fn generate_synthetic_uidvalidity(mailbox_name: &str) -> u32 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
mailbox_name.hash(&mut hasher);
(hasher.finish() as u32).wrapping_add(1) // Avoid 0, which might be reserved
}
pub async fn reconcile_mailboxes(
account: &AccountModel,
remote_mailboxes: &[MailBox],
@@ -364,30 +377,30 @@ pub async fn reconcile_mailboxes(
break;
}
let new_highest_uid = if local_mailbox.uid_validity != remote_mailbox.uid_validity {
if remote_mailbox.uid_validity.is_none() {
let err_msg = format!(
"Mailbox '{}' logic error: Server did not provide UIDVALIDITY.",
local_mailbox.name
// Handle missing UIDVALIDITY from non-compliant IMAP servers
// (e.g., Tencent Enterprise Mail, etc.)
let remote_uid_validity = match remote_mailbox.uid_validity {
Some(uid) => uid,
None => {
// Generate a synthetic UIDVALIDITY based on mailbox name
let synthetic_uid = generate_synthetic_uidvalidity(&remote_mailbox.name);
warn!(
"Account {}: Mailbox '{}' - Server did not provide UIDVALIDITY. \
Using synthetic UIDVALIDITY {} based on mailbox name. \
This mailbox will be synced but may require periodic rebuilds if the server's mailbox structure changes.",
account_id, remote_mailbox.name, synthetic_uid
);
warn!("Account {}: {}", account_id, err_msg);
DownloadState::update_folder_progress(
account_id,
remote_mailbox.name.clone(),
0,
0,
FolderStatus::Failed,
Some(err_msg.clone()),
)?;
DownloadState::append_session_error(account_id, err_msg)?;
continue;
synthetic_uid
}
};
let new_highest_uid = if local_mailbox.uid_validity != Some(remote_uid_validity) {
info!(
"Account {}: Mailbox '{}' detected with changed uid_validity (local: {:#?}, remote: {:#?}). \
The mailbox data may be invalid, resetting its envelopes and rebuilding the cache.",
account_id, local_mailbox.name, &local_mailbox.uid_validity, &remote_mailbox.uid_validity
account_id, local_mailbox.name, &local_mailbox.uid_validity, &remote_uid_validity
);
DownloadState::update_folder_progress(
@@ -441,6 +454,10 @@ pub async fn reconcile_mailboxes(
let mut updated = remote_mailbox.clone();
updated.highest_uid = new_highest_uid;
// Update uid_validity with the resolved value (either from server or synthetic)
if updated.uid_validity.is_none() {
updated.uid_validity = Some(remote_uid_validity);
}
mailboxes_to_update.push(updated);
}
//The metadata of this mailbox must only be updated after a successful synchronization;

View File

@@ -32,6 +32,7 @@ use tokio_util::sync::CancellationToken;
use tracing::info;
const BODY_FETCH_COMMAND: &str = "(UID INTERNALDATE RFC822.SIZE BODY.PEEK[])";
const SIZE_ONLY_FETCH: &str = "(UID RFC822.SIZE)";
pub struct ImapExecutor;
@@ -183,15 +184,16 @@ impl ImapExecutor {
ErrorCode::InternalError
));
}
Self::uid_batch_retrieve_emails(
let processed = Self::uid_batch_retrieve_emails(
session,
account.id,
mailbox.id,
&batch.0,
account.max_email_size_bytes,
token.clone(),
)
.await?;
count += batch.1;
count += processed;
DownloadState::update_folder_progress(
account.id,
mailbox.name.clone(),
@@ -239,7 +241,9 @@ impl ImapExecutor {
})?;
let mut count = 0u64;
let mut skipped = 0u64;
let mut max_uid: Option<u32> = None;
let size_limit = account.max_email_size_bytes.unwrap_or(DEFAULT_MAX_EMAIL_SIZE);
while let Some(fetch) = stream
.try_next()
.await
@@ -258,6 +262,20 @@ impl ImapExecutor {
));
}
let msg_size = fetch.size.unwrap_or(0) as u64;
if msg_size > 0 && msg_size > size_limit {
tracing::warn!(
account_id = account.id,
mailbox_id = mailbox.id,
uid = fetch.uid,
size = msg_size,
limit = size_limit,
"Skipping oversized email (streaming mode)"
);
skipped += 1;
continue;
}
if let Some(uid) = fetch.uid {
max_uid = Some(max_uid.unwrap_or(0).max(uid));
}
@@ -265,7 +283,8 @@ impl ImapExecutor {
count += 1;
}
if count == 0 {
let total = count + skipped;
if total == 0 {
DownloadState::update_folder_progress(
account.id,
mailbox.name.clone(),
@@ -278,10 +297,14 @@ impl ImapExecutor {
DownloadState::update_folder_progress(
account.id,
mailbox.name.clone(),
count,
total,
count,
FolderStatus::Success,
None,
if skipped > 0 {
Some(format!("{skipped} email(s) skipped due to size limit"))
} else {
None
},
)?;
}
@@ -296,6 +319,7 @@ impl ImapExecutor {
page: u64,
page_size: u64,
encoded_mailbox_name: &str,
max_email_size_bytes: Option<u64>,
token: CancellationToken,
max_uid: &mut Option<u32>,
) -> BichonResult<usize> {
@@ -315,13 +339,52 @@ impl ImapExecutor {
encoded_mailbox_name, sequence_set, page, page_size
);
let mut stream = session
.fetch(sequence_set.as_str(), BODY_FETCH_COMMAND)
let limit = max_email_size_bytes.unwrap_or(DEFAULT_MAX_EMAIL_SIZE);
// PASS 1: fetch only SIZE to identify oversized messages
let acceptable_uids = {
let mut size_stream = session
.fetch(sequence_set.as_str(), SIZE_ONLY_FETCH)
.await
.map_err(|e| {
raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed)
})?;
let mut uids: Vec<u32> = Vec::new();
while let Some(fetch) = size_stream.try_next().await.map_err(|e| {
raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed)
})? {
let uid = fetch.uid.unwrap_or(0);
let msg_size = fetch.size.unwrap_or(0) as u64;
if msg_size == 0 || msg_size <= limit {
uids.push(uid);
} else {
tracing::warn!(
account_id,
mailbox_id,
uid,
size = msg_size,
limit,
"Skipping oversized email"
);
}
}
uids
};
if acceptable_uids.is_empty() {
return Ok(0);
}
// PASS 2: fetch bodies only for acceptable UIDs
let filtered = compress_uid_list(acceptable_uids);
let mut body_stream = session
.uid_fetch(&filtered, BODY_FETCH_COMMAND)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
let mut count = 0;
while let Some(fetch) = stream
while let Some(fetch) = body_stream
.try_next()
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?
@@ -347,13 +410,55 @@ impl ImapExecutor {
account_id: u64,
mailbox_id: u64,
uid_set: &str,
max_email_size_bytes: Option<u64>,
token: CancellationToken,
) -> BichonResult<()> {
let mut stream = session
.uid_fetch(uid_set, BODY_FETCH_COMMAND)
) -> BichonResult<u64> {
let limit = max_email_size_bytes.unwrap_or(DEFAULT_MAX_EMAIL_SIZE);
// PASS 1: fetch only SIZE to identify oversized messages
let acceptable_uids = {
let mut size_stream = session
.uid_fetch(uid_set, SIZE_ONLY_FETCH)
.await
.map_err(|e| {
raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed)
})?;
let mut uids: Vec<u32> = Vec::new();
while let Some(fetch) = size_stream.try_next().await.map_err(|e| {
raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed)
})? {
let uid = fetch.uid.unwrap_or(0);
let msg_size = fetch.size.unwrap_or(0) as u64;
if msg_size == 0 || msg_size <= limit {
uids.push(uid);
} else {
tracing::warn!(
account_id,
mailbox_id,
uid,
size = msg_size,
limit,
"Skipping oversized email"
);
}
}
uids
};
if acceptable_uids.is_empty() {
return Ok(0);
}
// PASS 2: fetch bodies only for acceptable UIDs
let filtered = compress_uid_list(acceptable_uids);
let mut body_stream = session
.uid_fetch(&filtered, BODY_FETCH_COMMAND)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
while let Some(fetch) = stream
let mut count = 0u64;
while let Some(fetch) = body_stream
.try_next()
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?
@@ -366,8 +471,9 @@ impl ImapExecutor {
));
}
extract_envelope_and_store_it(fetch, account_id, mailbox_id).await?;
count += 1;
}
Ok(())
Ok(count)
}
/// Fetches the raw RFC822 body of a single message by UID.
@@ -430,6 +536,7 @@ impl ImapExecutor {
}
pub const DEFAULT_BATCH_SIZE: u32 = 30;
pub const DEFAULT_MAX_EMAIL_SIZE: u64 = 100 * 1024 * 1024;
/// Compresses a sorted list of UIDs into an IMAP sequence-set string.
/// Consecutive UIDs become ranges (e.g. `1:5`), non-consecutive are

View File

@@ -121,7 +121,7 @@ fn is_dir_not_empty(path: &PathBuf) -> std::io::Result<bool> {
pub fn do_migrate_segment<F>(
batch_size: u32,
legacy: LegacyDirs,
new_dirs: NewDirs,
writer: &mut NewIndexWriter,
segment_index: usize,
mut on_progress: F,
) -> BichonResult<()>
@@ -226,8 +226,6 @@ where
drop(envelope_index);
// ── Phase 2: process EML docs, streaming one at a time ─────────────
let mut writer = NewIndexWriter::open(new_dirs)?;
let mut total_migrated = 0usize;
let mut total_skipped = 0usize;
@@ -308,7 +306,6 @@ where
chunk_start = chunk_end;
}
writer.finish_writers()?;
on_progress(&format!("DONE:{}:{}", total_migrated, total_skipped));
Ok(())
}

View File

@@ -131,6 +131,7 @@ export interface AccountModel {
download_folders: string[];
download_interval_min?: number;
download_batch_size?: number;
max_email_size_bytes?: number;
created_by: number;
created_user_name: string;
created_user_email: string;

View File

@@ -28,18 +28,54 @@ interface GithubLinkButtonProps {
title?: string;
}
const CACHE_KEY = "github_stars_cache";
const CACHE_TTL = 6 * 60 * 60 * 1000; // 6 hours
interface StarsCache {
stars: number;
fetchedAt: number;
}
function getCachedStars(repo: string): number | null {
try {
const raw = localStorage.getItem(`${CACHE_KEY}_${repo}`);
if (!raw) return null;
const cache: StarsCache = JSON.parse(raw);
if (Date.now() - cache.fetchedAt > CACHE_TTL) return null;
return cache.stars;
} catch {
return null;
}
}
function setCachedStars(repo: string, stars: number) {
try {
localStorage.setItem(
`${CACHE_KEY}_${repo}`,
JSON.stringify({ stars, fetchedAt: Date.now() })
);
} catch { }
}
export const GithubLinkButton: React.FC<GithubLinkButtonProps> = ({
href = "https://github.com/rustmailer/bichon",
repo = "rustmailer/bichon",
size = 18,
title = "View on GitHub",
}) => {
const [stars, setStars] = useState<number | null>(null);
const [stars, setStars] = useState<number | null>(() => getCachedStars(repo));
useEffect(() => {
if (stars !== null) return; // already have cached value, skip fetch
fetch(`https://api.github.com/repos/${repo}`)
.then(res => res.json())
.then(data => setStars(data.stargazers_count))
.then(data => {
const count = data.stargazers_count;
if (typeof count === "number") {
setStars(count);
setCachedStars(repo, count);
}
})
.catch(() => { });
}, [repo]);

View File

@@ -97,6 +97,10 @@ export function AccountDetailDrawer({ open, onOpenChange, currentRow }: Props) {
<span className="text-muted-foreground">{t('accounts.downloadBatchSize')}:</span>
<span>{currentRow.download_batch_size}</span>
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="text-muted-foreground">{t('accounts.maxEmailSizeBytes')}:</span>
<span>{currentRow.max_email_size_bytes ? `${(currentRow.max_email_size_bytes / 1024 / 1024).toFixed(0)} MB` : t('accounts.maxEmailSizeBytesUnlimited')}</span>
</div>
<div className="flex flex-col gap-2">
<span className="text-muted-foreground">{t('accounts.capabilities')}:</span>
<code className="rounded-md bg-muted/50 px-2 py-1 text-sm border overflow-x-auto inline-block">

View File

@@ -49,7 +49,7 @@ 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", "auto_download_new_mailboxes", "download_schedule"] },
{ 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: [] },
];
@@ -81,6 +81,7 @@ const defaultValues: Account = {
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,
};
@@ -111,6 +112,7 @@ const mapCurrentRowToFormValues = (currentRow: AccountModel): Account => {
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,
};
@@ -193,6 +195,7 @@ export function AccountActionDialog({ currentRow, open, onOpenChange }: Props) {
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,
};

View File

@@ -100,6 +100,13 @@ export const getAccountSchema = (isEdit: boolean, t: (key: string) => string) =>
.max(200, {
message: t('validation.singleRequestBatchSizeTooLarge'),
}),
max_email_size_bytes: z
.number({
invalid_type_error: t('validation.maxEmailSizeMustBeNumber'),
})
.int()
.min(1 * 1024 * 1024, { message: t('validation.maxEmailSizeTooSmall') })
.max(100 * 1024 * 1024, { message: t('validation.maxEmailSizeTooLarge') }),
auto_download_new_mailboxes: z.boolean(),
download_schedule: z
.string()

View File

@@ -392,6 +392,38 @@ export default function Step3() {
</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>

View File

@@ -50,7 +50,11 @@ export default function Step4() {
return (
<div className="rounded-xl">
<Accordion type="multiple" defaultValue={['email', 'account_name', 'login_name', 'imap', 'date_since', 'sync_interval', 'sync_scope', 'sync_batch_size', 'download_schedule']}>
<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>
@@ -164,6 +168,11 @@ export default function Step4() {
<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>

View File

@@ -208,6 +208,10 @@
"leaveEmptyToKeepExisting": "اترك فارغًا للاحتفاظ بكلمة المرور الحالية، أو أدخل كلمة مرور جديدة لتحديثها.",
"leaveEmptyToKeepPassword": "اترك فارغًا للاحتفاظ بكلمة المرور الحالية",
"login_name": "اسم الدخول",
"maxEmailSizeBytes": "الحد الأقصى لحجم البريد",
"maxEmailSizeBytesDescription": "سيتم تخطي الرسائل الأكبر من هذا الحجم. اتركه فارغاً لاستخدام الحد الافتراضي (100 ميجابايت).",
"maxEmailSizeBytesPlaceholder": "الافتراضي: 100 ميجابايت",
"maxEmailSizeBytesUnlimited": "الافتراضي: 100 ميجابايت",
"minutes": "دقائق",
"months": "أشهر",
"mustBeAtLeast1": "يجب أن يكون 1 على الأقل",
@@ -1681,6 +1685,9 @@
"invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')",
"invalidEmail": "عنوان بريد إلكتروني غير صالح",
"invalidUrl": "عنوان URL غير صالح",
"maxEmailSizeMustBeNumber": "يجب أن يكون الحد الأقصى لحجم البريد رقماً.",
"maxEmailSizeTooLarge": "يجب ألا يتجاوز الحد الأقصى لحجم البريد 100 ميجابايت.",
"maxEmailSizeTooSmall": "يجب أن يكون الحد الأقصى لحجم البريد 1 ميجابايت على الأقل.",
"passwordMinLength": "يجب أن تتكون كلمة المرور من {{min}} أحرف على الأقل",
"passwordRequired": "كلمة المرور مطلوبة عندما تكون طريقة المصادقة هي كلمة المرور",
"pleaseEnterPassword": "الرجاء إدخال كلمة المرور الخاصة بك",
@@ -1690,4 +1697,4 @@
"singleRequestBatchSizeTooLarge": "يجب أن يكون حجم الدُفعة على الأكثر 200",
"singleRequestBatchSizeTooSmall": "يجب أن يكون حجم الدُفعة على الأقل 10"
}
}
}

View File

@@ -208,6 +208,10 @@
"leaveEmptyToKeepExisting": "Lad stå tomt for at beholde den eksisterende adgangskode, eller indtast en ny for at opdatere den.",
"leaveEmptyToKeepPassword": "Lad stå tomt for at beholde nuværende adgangskode",
"login_name": "Logindnavn",
"maxEmailSizeBytes": "Maks. e-mailstørrelse",
"maxEmailSizeBytesDescription": "E-mails større end dette vil blive oversprunget. Lad være tom for at bruge standarden (100 MB).",
"maxEmailSizeBytesPlaceholder": "Standard: 100 MB",
"maxEmailSizeBytesUnlimited": "Standard: 100 MB",
"minutes": "minutter",
"months": "Måneder",
"mustBeAtLeast1": "Skal være mindst 1",
@@ -1681,6 +1685,9 @@
"invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')",
"invalidEmail": "Ugyldig e-mailadresse",
"invalidUrl": "Ugyldig URL",
"maxEmailSizeMustBeNumber": "Maks. e-mailstørrelse skal være et tal.",
"maxEmailSizeTooLarge": "Maks. e-mailstørrelse må ikke overstige 100 MB.",
"maxEmailSizeTooSmall": "Maks. e-mailstørrelse skal være mindst 1 MB.",
"passwordMinLength": "Adgangskoden skal være mindst {{min}} tegn lang",
"passwordRequired": "Adgangskode er påkrævet, når godkendelsesmetoden er Adgangskode",
"pleaseEnterPassword": "Indtast venligst din adgangskode",
@@ -1690,4 +1697,4 @@
"singleRequestBatchSizeTooLarge": "Batchstørrelse skal være højst 200",
"singleRequestBatchSizeTooSmall": "Batchstørrelse skal være mindst 10"
}
}
}

View File

@@ -208,6 +208,10 @@
"leaveEmptyToKeepExisting": "Leer lassen, um das bestehende Passwort beizubehalten, oder einen neuen Wert eingeben, um es zu aktualisieren.",
"leaveEmptyToKeepPassword": "Leer lassen, um das aktuelle Passwort beizubehalten",
"login_name": "Anmeldename",
"maxEmailSizeBytes": "Max. E-Mail-Größe",
"maxEmailSizeBytesDescription": "Größere E-Mails werden übersprungen. Leer lassen, um den Standardwert (100 MB) zu verwenden.",
"maxEmailSizeBytesPlaceholder": "Standard: 100 MB",
"maxEmailSizeBytesUnlimited": "Standard: 100 MB",
"minutes": "Minuten",
"months": "Monate",
"mustBeAtLeast1": "Muss mindestens 1 sein",
@@ -1681,6 +1685,9 @@
"invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')",
"invalidEmail": "Ungültige E-Mail-Adresse",
"invalidUrl": "Ungültige URL",
"maxEmailSizeMustBeNumber": "Die maximale E-Mail-Größe muss eine Zahl sein.",
"maxEmailSizeTooLarge": "Die maximale E-Mail-Größe darf 100 MB nicht überschreiten.",
"maxEmailSizeTooSmall": "Die maximale E-Mail-Größe muss mindestens 1 MB betragen.",
"passwordMinLength": "Das Passwort muss mindestens {{min}} Zeichen lang sein",
"passwordRequired": "Passwort ist erforderlich, wenn die Authentifizierungsmethode Passwort ist",
"pleaseEnterPassword": "Bitte geben Sie Ihr Passwort ein",
@@ -1690,4 +1697,4 @@
"singleRequestBatchSizeTooLarge": "Die Stapelgröße darf höchstens 200 sein",
"singleRequestBatchSizeTooSmall": "Die Stapelgröße muss mindestens 10 sein"
}
}
}

View File

@@ -208,6 +208,10 @@
"leaveEmptyToKeepExisting": "Leave empty to keep the existing password, or enter a new password to update it.",
"leaveEmptyToKeepPassword": "Leave empty to keep current password",
"login_name": "Login Name",
"maxEmailSizeBytes": "Max email size",
"maxEmailSizeBytesDescription": "Emails larger than this will be skipped. Leave empty to use the default (100 MB).",
"maxEmailSizeBytesPlaceholder": "Default: 100 MB",
"maxEmailSizeBytesUnlimited": "Default: 100 MB",
"minutes": "minutes",
"months": "Months",
"mustBeAtLeast1": "Must be at least 1",
@@ -1681,6 +1685,9 @@
"invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')",
"invalidEmail": "Invalid email address",
"invalidUrl": "Invalid URL",
"maxEmailSizeMustBeNumber": "Max email size must be a number.",
"maxEmailSizeTooLarge": "Max email size must not exceed 100 MB.",
"maxEmailSizeTooSmall": "Max email size must be at least 1 MB.",
"passwordMinLength": "Password must be at least {{min}} characters long",
"passwordRequired": "Password is required when auth method is Password",
"pleaseEnterPassword": "Please enter your password",
@@ -1690,4 +1697,4 @@
"singleRequestBatchSizeTooLarge": "Batch size must be at most 200",
"singleRequestBatchSizeTooSmall": "Batch size must be at least 10"
}
}
}

View File

@@ -208,6 +208,10 @@
"leaveEmptyToKeepExisting": "Deja vacío para mantener la contraseña existente, o introduce un nuevo valor para actualizarla.",
"leaveEmptyToKeepPassword": "Deja vacío para mantener la contraseña actual",
"login_name": "Nombre de usuario",
"maxEmailSizeBytes": "Tamaño máx. de correo",
"maxEmailSizeBytesDescription": "Se omitirán los correos más grandes. Déjelo vacío para usar el valor predeterminado (100 MB).",
"maxEmailSizeBytesPlaceholder": "Predeterminado: 100 MB",
"maxEmailSizeBytesUnlimited": "Predeterminado: 100 MB",
"minutes": "minutos",
"months": "Meses",
"mustBeAtLeast1": "Debe ser al menos 1",
@@ -1681,6 +1685,9 @@
"invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')",
"invalidEmail": "Dirección de correo electrónico inválida",
"invalidUrl": "URL inválida",
"maxEmailSizeMustBeNumber": "El tamaño máximo de correo debe ser un número.",
"maxEmailSizeTooLarge": "El tamaño máximo de correo no debe superar los 100 MB.",
"maxEmailSizeTooSmall": "El tamaño máximo de correo debe ser de al menos 1 MB.",
"passwordMinLength": "La contraseña debe tener al menos {{min}} caracteres",
"passwordRequired": "La contraseña es obligatoria cuando el método de autenticación es Contraseña",
"pleaseEnterPassword": "Por favor, introduce tu contraseña",
@@ -1690,4 +1697,4 @@
"singleRequestBatchSizeTooLarge": "El tamaño del lote debe ser como máximo 200",
"singleRequestBatchSizeTooSmall": "El tamaño del lote debe ser al menos 10"
}
}
}

View File

@@ -208,6 +208,10 @@
"leaveEmptyToKeepExisting": "Jätä tyhjäksi säilyttääksesi olemassa olevan salasanan, tai syötä uusi päivittääksesi sen.",
"leaveEmptyToKeepPassword": "Jätä tyhjäksi säilyttääksesi nykyisen salasanan",
"login_name": "Kirjautumisnimi",
"maxEmailSizeBytes": "Sähköpostin maksimikoko",
"maxEmailSizeBytesDescription": "Tätä suuremmat sähköpostit ohitetaan. Jätä tyhjäksi käyttääksesi oletusarvoa (100 MB).",
"maxEmailSizeBytesPlaceholder": "Oletus: 100 MB",
"maxEmailSizeBytesUnlimited": "Oletus: 100 MB",
"minutes": "minuuttia",
"months": "Kuukautta",
"mustBeAtLeast1": "Täytyy olla vähintään 1",
@@ -1681,6 +1685,9 @@
"invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')",
"invalidEmail": "Virheellinen sähköpostiosoite",
"invalidUrl": "Virheellinen URL-osoite",
"maxEmailSizeMustBeNumber": "Sähköpostin maksimikoon on oltava numero.",
"maxEmailSizeTooLarge": "Sähköpostin maksimikoko ei saa ylittää 100 megatavua.",
"maxEmailSizeTooSmall": "Sähköpostin maksimikoon on oltava vähintään 1 MB.",
"passwordMinLength": "Salasanan on oltava vähintään {{min}} merkkiä pitkä",
"passwordRequired": "Salasana on pakollinen, kun todennusmenetelmä on Salasana",
"pleaseEnterPassword": "Syötä salasanasi",
@@ -1690,4 +1697,4 @@
"singleRequestBatchSizeTooLarge": "Eräkoko tulee olla enintään 200",
"singleRequestBatchSizeTooSmall": "Eräkoko tulee olla vähintään 10"
}
}
}

View File

@@ -208,6 +208,10 @@
"leaveEmptyToKeepExisting": "Laissez vide pour conserver le mot de passe existant, ou entrez-en un nouveau pour le mettre à jour.",
"leaveEmptyToKeepPassword": "Laisser vide pour conserver le mot de passe actuel",
"login_name": "Nom de connexion",
"maxEmailSizeBytes": "Taille max. des e-mails",
"maxEmailSizeBytesDescription": "Les e-mails plus grands seront ignorés. Laisser vide pour utiliser la valeur par défaut (100 MB).",
"maxEmailSizeBytesPlaceholder": "Par défaut : 100 MB",
"maxEmailSizeBytesUnlimited": "Par défaut : 100 MB",
"minutes": "minutes",
"months": "Mois",
"mustBeAtLeast1": "Doit être au moins 1",
@@ -1681,6 +1685,9 @@
"invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')",
"invalidEmail": "Adresse e-mail non valide",
"invalidUrl": "URL non valide",
"maxEmailSizeMustBeNumber": "La taille maximale des e-mails doit être un nombre.",
"maxEmailSizeTooLarge": "La taille maximale des e-mails ne doit pas dépasser 100 MB.",
"maxEmailSizeTooSmall": "La taille maximale des e-mails doit être d'au moins 1 MB.",
"passwordMinLength": "Le mot de passe doit contenir au moins {{min}} caractères",
"passwordRequired": "Le mot de passe est obligatoire lorsque la méthode d'authentification est Mot de passe",
"pleaseEnterPassword": "Veuillez entrer votre mot de passe",
@@ -1690,4 +1697,4 @@
"singleRequestBatchSizeTooLarge": "La taille du lot doit être au plus 200",
"singleRequestBatchSizeTooSmall": "La taille du lot doit être au moins 10"
}
}
}

View File

@@ -208,6 +208,10 @@
"leaveEmptyToKeepExisting": "Lascia vuoto per mantenere la password esistente, o inseriscine una nuova per aggiornarla.",
"leaveEmptyToKeepPassword": "Lascia vuoto per mantenere la password attuale",
"login_name": "Nome di accesso",
"maxEmailSizeBytes": "Dimensione massima email",
"maxEmailSizeBytesDescription": "Le email più grandi saranno ignorate. Lascia vuoto per utilizzare il valore predefinito (100 MB).",
"maxEmailSizeBytesPlaceholder": "Predefinito: 100 MB",
"maxEmailSizeBytesUnlimited": "Predefinito: 100 MB",
"minutes": "minuti",
"months": "Mesi",
"mustBeAtLeast1": "Deve essere almeno 1",
@@ -1681,6 +1685,9 @@
"invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')",
"invalidEmail": "Indirizzo email non valido",
"invalidUrl": "URL non valido",
"maxEmailSizeMustBeNumber": "La dimensione massima dell'email deve essere un numero.",
"maxEmailSizeTooLarge": "La dimensione massima dell'email non deve superare i 100 MB.",
"maxEmailSizeTooSmall": "La dimensione massima dell'email deve essere di almeno 1 MB.",
"passwordMinLength": "La password deve contenere almeno {{min}} caratteri",
"passwordRequired": "La password è obbligatoria quando il metodo di autenticazione è Password",
"pleaseEnterPassword": "Inserisci la tua password",
@@ -1690,4 +1697,4 @@
"singleRequestBatchSizeTooLarge": "La dimensione del batch deve essere al massimo 200",
"singleRequestBatchSizeTooSmall": "La dimensione del batch deve essere almeno 10"
}
}
}

View File

@@ -208,6 +208,10 @@
"leaveEmptyToKeepExisting": "既存のパスワードを保持する場合は空欄にしてください。更新する場合は新しいパスワードを入力してください。",
"leaveEmptyToKeepPassword": "現在のパスワードを保持する場合は空欄にしてください",
"login_name": "ログイン名",
"maxEmailSizeBytes": "最大メールサイズ",
"maxEmailSizeBytesDescription": "これより大きいメールはスキップされます。空欄にするとデフォルト100 MBが使用されます。",
"maxEmailSizeBytesPlaceholder": "デフォルト100 MB",
"maxEmailSizeBytesUnlimited": "デフォルト100 MB",
"minutes": "分",
"months": "月",
"mustBeAtLeast1": "1以上である必要があります",
@@ -1681,6 +1685,9 @@
"invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')",
"invalidEmail": "無効なメールアドレスです",
"invalidUrl": "無効なURLです",
"maxEmailSizeMustBeNumber": "最大メールサイズは数値で入力してください。",
"maxEmailSizeTooLarge": "最大メールサイズは 100 MB 以下にしてください。",
"maxEmailSizeTooSmall": "最大メールサイズは 1 MB 以上にしてください。",
"passwordMinLength": "パスワードは{{min}}文字以上である必要があります",
"passwordRequired": "認証方式がパスワードの場合、パスワードは必須です",
"pleaseEnterPassword": "パスワードを入力してください",
@@ -1690,4 +1697,4 @@
"singleRequestBatchSizeTooLarge": "バッチサイズは最大でも200でなければなりません",
"singleRequestBatchSizeTooSmall": "バッチサイズは最低でも10でなければなりません"
}
}
}

View File

@@ -208,6 +208,10 @@
"leaveEmptyToKeepExisting": "기존 비밀번호를 유지하려면 비워 두십시오. 업데이트할 경우에만 새 비밀번호를 입력하십시오.",
"leaveEmptyToKeepPassword": "현재 비밀번호를 유지하려면 비워 두십시오",
"login_name": "로그인 이름",
"maxEmailSizeBytes": "최대 이메일 크기",
"maxEmailSizeBytesDescription": "이보다 큰 이메일은 건너뜁니다. 기본값(100 MB)을 사용하려면 비워두세요.",
"maxEmailSizeBytesPlaceholder": "기본값: 100 MB",
"maxEmailSizeBytesUnlimited": "기본값: 100 MB",
"minutes": "분",
"months": "개월",
"mustBeAtLeast1": "최소 1 이상이어야 합니다",
@@ -1681,6 +1685,9 @@
"invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')",
"invalidEmail": "유효하지 않은 이메일 주소",
"invalidUrl": "유효하지 않은 URL",
"maxEmailSizeMustBeNumber": "최대 이메일 크기는 숫자여야 합니다.",
"maxEmailSizeTooLarge": "최대 이메일 크기는 100 MB를 초과할 수 없습니다.",
"maxEmailSizeTooSmall": "최대 이메일 크기는 최소 1 MB여야 합니다.",
"passwordMinLength": "비밀번호는 {{min}}자 이상이어야 합니다",
"passwordRequired": "인증 방법이 비밀번호인 경우 비밀번호는 필수입니다",
"pleaseEnterPassword": "비밀번호를 입력하십시오",
@@ -1690,4 +1697,4 @@
"singleRequestBatchSizeTooLarge": "배치 크기는 최대 200이어야 합니다",
"singleRequestBatchSizeTooSmall": "배치 크기는 최소 10이어야 합니다"
}
}
}

View File

@@ -208,6 +208,10 @@
"leaveEmptyToKeepExisting": "Laat leeg om het bestaande wachtwoord te behouden, of voer een nieuw wachtwoord in om het bij te werken.",
"leaveEmptyToKeepPassword": "Laat leeg om huidig wachtwoord te behouden",
"login_name": "Inlognaam",
"maxEmailSizeBytes": "Max. e-mailgrootte",
"maxEmailSizeBytesDescription": "E-mails groter dan dit worden overgeslagen. Laat leeg om de standaard (100 MB) te gebruiken.",
"maxEmailSizeBytesPlaceholder": "Standaard: 100 MB",
"maxEmailSizeBytesUnlimited": "Standaard: 100 MB",
"minutes": "minuten",
"months": "Maanden",
"mustBeAtLeast1": "Moet ten minste 1 zijn",
@@ -1681,6 +1685,9 @@
"invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')",
"invalidEmail": "Ongeldig e-mailadres",
"invalidUrl": "Ongeldige URL",
"maxEmailSizeMustBeNumber": "Maximale e-mailgrootte moet un nummer zijn.",
"maxEmailSizeTooLarge": "Maximale e-mailgrootte mag niet groter zijn dan 100 MB.",
"maxEmailSizeTooSmall": "Maximale e-mailgrootte moet minstens 1 MB zijn.",
"passwordMinLength": "Wachtwoord moet ten minste {{min}} tekens lang zijn",
"passwordRequired": "Wachtwoord is vereist wanneer de authenticatiemethode Wachtwoord is",
"pleaseEnterPassword": "Voer uw wachtwoord in",
@@ -1690,4 +1697,4 @@
"singleRequestBatchSizeTooLarge": "Batchgrootte moet hoogstens 200 zijn",
"singleRequestBatchSizeTooSmall": "Batchgrootte moet ten minste 10 zijn"
}
}
}

View File

@@ -208,6 +208,10 @@
"leaveEmptyToKeepExisting": "La stå tomt for å beholde det eksisterende passordet, eller skriv inn et nytt passord for å oppdatere det.",
"leaveEmptyToKeepPassword": "La stå tomt for å beholde nåværende passord",
"login_name": "Påloggingsnavn",
"maxEmailSizeBytes": "Maks. e-poststørrelse",
"maxEmailSizeBytesDescription": "E-poster større enn dette vil bli hoppet over. La stå tom for å bruke standarden (100 MB).",
"maxEmailSizeBytesPlaceholder": "Standard: 100 MB",
"maxEmailSizeBytesUnlimited": "Standard: 100 MB",
"minutes": "minutter",
"months": "Måneder",
"mustBeAtLeast1": "Må være minst 1",
@@ -1681,6 +1685,9 @@
"invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')",
"invalidEmail": "Ugyldig e-postadresse",
"invalidUrl": "Ugyldig URL",
"maxEmailSizeMustBeNumber": "Maks. e-poststørrelse må være et tall.",
"maxEmailSizeTooLarge": "Maks. e-poststørrelse må ikke overstige 100 MB.",
"maxEmailSizeTooSmall": "Maks. e-poststørrelse må være minst 1 MB.",
"passwordMinLength": "Passordet må være minst {{min}} tegn langt",
"passwordRequired": "Passord er påkrevd når autentiseringsmetoden er Passord",
"pleaseEnterPassword": "Vennligst skriv inn passordet ditt",
@@ -1690,4 +1697,4 @@
"singleRequestBatchSizeTooLarge": "Batchstørrelse må være maksimalt 200",
"singleRequestBatchSizeTooSmall": "Batchstørrelse må være minst 10"
}
}
}

View File

@@ -208,6 +208,10 @@
"leaveEmptyToKeepExisting": "Pozostaw to pole puste, jeśli chcesz zachować dotychczasowe hasło lub wpisz nowe, aby zaktualizować.",
"leaveEmptyToKeepPassword": "Pozostaw to pole puste, jeśli chcesz zachować dotychczasowe hasło",
"login_name": "Login",
"maxEmailSizeBytes": "Maks. rozmiar e-maila",
"maxEmailSizeBytesDescription": "Większe wiadomości zostaną pominięte. Pozostaw puste, aby użyć domyślnego limitu (100 MB).",
"maxEmailSizeBytesPlaceholder": "Domyślnie: 100 MB",
"maxEmailSizeBytesUnlimited": "Domyślnie: 100 MB",
"minutes": "minut",
"months": "Miesiące",
"mustBeAtLeast1": "Nie mniej jak 1",
@@ -1681,6 +1685,9 @@
"invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')",
"invalidEmail": "Niewłaściwy adres email",
"invalidUrl": "Niewłaściwy URL",
"maxEmailSizeMustBeNumber": "Maksymalny rozmiar e-maila musi być liczbą.",
"maxEmailSizeTooLarge": "Maksymalny rozmiar e-maila nie może przekraczać 100 MB.",
"maxEmailSizeTooSmall": "Maksymalny rozmiar e-maila musi wynosić co najmniej 1 MB.",
"passwordMinLength": "Hasło musi posiadać conajmniej {{min}} znaków",
"passwordRequired": "Hasło jest wymagane, gdy metodą uwierzytelniania jest hasło",
"pleaseEnterPassword": "Proszę podać hasło",
@@ -1690,4 +1697,4 @@
"singleRequestBatchSizeTooLarge": "Rozmiar partii może wynosić maksymalnie 200",
"singleRequestBatchSizeTooSmall": "Rozmiar partii musi wynosić co najmniej 10"
}
}
}

View File

@@ -208,6 +208,10 @@
"leaveEmptyToKeepExisting": "Deixe vazio para manter a senha existente. Insira a nova senha apenas se estiver atualizando.",
"leaveEmptyToKeepPassword": "Deixe vazio para manter a senha atual",
"login_name": "Nome de login",
"maxEmailSizeBytes": "Tamanho máx. do email",
"maxEmailSizeBytesDescription": "Emails maiores do que isso serão ignorados. Deixe vazio para usar o padrão (100 MB).",
"maxEmailSizeBytesPlaceholder": "Padrão: 100 MB",
"maxEmailSizeBytesUnlimited": "Padrão: 100 MB",
"minutes": "minutos",
"months": "Meses",
"mustBeAtLeast1": "Deve ser pelo menos 1",
@@ -1681,6 +1685,9 @@
"invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')",
"invalidEmail": "Endereço de email inválido",
"invalidUrl": "URL inválido",
"maxEmailSizeMustBeNumber": "O tamanho máximo do email deve ser um número.",
"maxEmailSizeTooLarge": "O tamanho máximo do email não deve exceder 100 MB.",
"maxEmailSizeTooSmall": "O tamanho máximo do email deve ser de pelo menos 1 MB.",
"passwordMinLength": "A senha deve ter pelo menos {{min}} caracteres",
"passwordRequired": "A senha é obrigatória se o método de autenticação for Senha",
"pleaseEnterPassword": "Por favor, insira a senha",
@@ -1690,4 +1697,4 @@
"singleRequestBatchSizeTooLarge": "O tamanho do lote deve ser no máximo 200",
"singleRequestBatchSizeTooSmall": "O tamanho do lote deve ser pelo menos 10"
}
}
}

View File

@@ -208,6 +208,10 @@
"leaveEmptyToKeepExisting": "Оставьте пустым, чтобы сохранить существующий пароль, или введите новый для обновления.",
"leaveEmptyToKeepPassword": "Оставьте пустым, чтобы сохранить текущий пароль",
"login_name": "Имя для входа",
"maxEmailSizeBytes": "Макс. размер письма",
"maxEmailSizeBytesDescription": "Письма больше этого размера будут пропущены. Оставьте пустым для использования значения по умолчанию (100 МБ).",
"maxEmailSizeBytesPlaceholder": "По умолчанию: 100 МБ",
"maxEmailSizeBytesUnlimited": "По умолчанию: 100 МБ",
"minutes": "минут",
"months": "Месяцы",
"mustBeAtLeast1": "Должно быть не менее 1",
@@ -1681,6 +1685,9 @@
"invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')",
"invalidEmail": "Неверный адрес электронной почты",
"invalidUrl": "Неверный URL",
"maxEmailSizeMustBeNumber": "Максимальный размер письма должен быть числом.",
"maxEmailSizeTooLarge": "Максимальный размер письма не должен превышать 100 МБ.",
"maxEmailSizeTooSmall": "Максимальный размер письма должен быть не менее 1 МБ.",
"passwordMinLength": "Пароль должен быть не менее {{min}} символов",
"passwordRequired": "Пароль обязателен, когда метод авторизации - Пароль",
"pleaseEnterPassword": "Пожалуйста, введите ваш пароль",
@@ -1690,4 +1697,4 @@
"singleRequestBatchSizeTooLarge": "Размер пакета должен быть не более 200",
"singleRequestBatchSizeTooSmall": "Размер пакета должен быть не менее 10"
}
}
}

View File

@@ -208,6 +208,10 @@
"leaveEmptyToKeepExisting": "Lämna tomt för att behålla det befintliga lösenordet, eller ange ett nytt för att uppdatera det.",
"leaveEmptyToKeepPassword": "Lämna tomt för att behålla nuvarande lösenord",
"login_name": "Inloggningsnamn",
"maxEmailSizeBytes": "Max e-poststorlek",
"maxEmailSizeBytesDescription": "E-post större än detta kommer att hoppas över. Lämna tomt för att använda standard (100 MB).",
"maxEmailSizeBytesPlaceholder": "Standard: 100 MB",
"maxEmailSizeBytesUnlimited": "Standard: 100 MB",
"minutes": "minuter",
"months": "Månader",
"mustBeAtLeast1": "Måste vara minst 1",
@@ -1681,6 +1685,9 @@
"invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')",
"invalidEmail": "Ogiltig e-postadress",
"invalidUrl": "Ogiltig URL",
"maxEmailSizeMustBeNumber": "Max e-poststorlek måste vara ett nummer.",
"maxEmailSizeTooLarge": "Max e-poststorlek får inte överstiga 100 MB.",
"maxEmailSizeTooSmall": "Max e-poststorlek måste vara minst 1 MB.",
"passwordMinLength": "Lösenordet måste vara minst {{min}} tecken långt",
"passwordRequired": "Lösenord krävs när autentiseringsmetoden är Lösenord",
"pleaseEnterPassword": "Vänligen ange ditt lösenord",
@@ -1690,4 +1697,4 @@
"singleRequestBatchSizeTooLarge": "Batchstorlek måste vara högst 200",
"singleRequestBatchSizeTooSmall": "Batchstorlek måste vara minst 10"
}
}
}

View File

@@ -208,6 +208,10 @@
"leaveEmptyToKeepExisting": "保留現有密碼請留空。若要更新,請輸入新密碼。",
"leaveEmptyToKeepPassword": "保留現有密碼請留空",
"login_name": "登入名稱",
"maxEmailSizeBytes": "最大郵件大小",
"maxEmailSizeBytesDescription": "超出此大小的郵件將被跳過。留空則使用預設值100 MB。",
"maxEmailSizeBytesPlaceholder": "預設100 MB",
"maxEmailSizeBytesUnlimited": "預設100 MB",
"minutes": "分鐘",
"months": "月",
"mustBeAtLeast1": "必須大於或等於 1",
@@ -1681,6 +1685,9 @@
"invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')",
"invalidEmail": "無效的電子郵件地址",
"invalidUrl": "無效的網址",
"maxEmailSizeMustBeNumber": "最大郵件大小必須是數字。",
"maxEmailSizeTooLarge": "最大郵件大小不能超過 100 MB。",
"maxEmailSizeTooSmall": "最大郵件大小不能小於 1 MB。",
"passwordMinLength": "密碼長度必須至少 {{min}} 個字元",
"passwordRequired": "如果驗證方法是密碼,則密碼為必填項",
"pleaseEnterPassword": "請輸入密碼",
@@ -1690,4 +1697,4 @@
"singleRequestBatchSizeTooLarge": "批次大小必須最多為200",
"singleRequestBatchSizeTooSmall": "批次大小必須至少為10"
}
}
}

View File

@@ -208,6 +208,10 @@
"leaveEmptyToKeepExisting": "留空以保持现有密码,或输入新密码进行更新。",
"leaveEmptyToKeepPassword": "留空以保持当前密码",
"login_name": "登录名",
"maxEmailSizeBytes": "最大邮件大小",
"maxEmailSizeBytesDescription": "超出此大小的邮件将被跳过。留空则使用默认值100 MB。",
"maxEmailSizeBytesPlaceholder": "默认100 MB",
"maxEmailSizeBytesUnlimited": "默认100 MB",
"minutes": "分钟",
"months": "月",
"mustBeAtLeast1": "必须至少为 1",
@@ -1681,6 +1685,9 @@
"invalidCronExpression": "Invalid cron expression. Must be 6 fields: second minute hour day-of-month month day-of-week (e.g. '0 0 0 * * *')",
"invalidEmail": "无效的电子邮件地址",
"invalidUrl": "无效的 URL",
"maxEmailSizeMustBeNumber": "最大邮件大小必须是数字。",
"maxEmailSizeTooLarge": "最大邮件大小不能超过 100 MB。",
"maxEmailSizeTooSmall": "最大邮件大小不能小于 1 MB。",
"passwordMinLength": "密码长度至少为 {{min}} 个字符",
"passwordRequired": "当认证方法为密码时,密码为必填项",
"pleaseEnterPassword": "请输入您的密码",
@@ -1690,4 +1697,4 @@
"singleRequestBatchSizeTooLarge": "批大小必须最多为200",
"singleRequestBatchSizeTooSmall": "批大小必须至少为10"
}
}
}