feat(import): introduce /api/v1/import endpoint to support batch EML email import

This commit is contained in:
rustmailer
2025-11-27 22:11:05 +08:00
parent 9a72ce9154
commit 6ce1420714
25 changed files with 335 additions and 34 deletions
+7 -6
View File
@@ -16,7 +16,7 @@
// 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/>.
use crate::modules::account::migration::AccountType;
use crate::modules::context::Initialize;
use crate::modules::error::code::ErrorCode;
use crate::raise_error;
@@ -33,8 +33,7 @@ use dashmap::DashMap;
use std::sync::{Arc, LazyLock};
use tracing::info;
pub static MAIL_CONTEXT: LazyLock<EmailClientExecutors> =
LazyLock::new(EmailClientExecutors::new);
pub static MAIL_CONTEXT: LazyLock<EmailClientExecutors> = LazyLock::new(EmailClientExecutors::new);
pub struct EmailClientExecutors {
start_at: i64,
@@ -88,15 +87,17 @@ impl EmailClientExecutors {
pub async fn start_account_syncers(&self) -> BichonResult<()> {
let accounts = AccountModel::list_all().await?;
let active_accounts: Vec<AccountModel> =
accounts.into_iter().filter(|a| a.enabled).collect();
let active_accounts: Vec<AccountModel> = accounts
.into_iter()
.filter(|a| a.enabled && matches!(a.account_type, AccountType::IMAP))
.collect();
if active_accounts.is_empty() {
info!("No active accounts found for account initialization.");
return Ok(());
}
info!(
"System has {} active accounts to initialize.",
"System has {} active IMAP accounts to initialize.",
active_accounts.len()
);
for account in active_accounts {
+87 -2
View File
@@ -16,7 +16,6 @@
// 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/>.
use crate::modules::common::AddrVec;
use crate::modules::error::code::ErrorCode;
use crate::modules::error::BichonResult;
@@ -33,12 +32,12 @@ pub fn extract_envelope(fetch: &Fetch, account_id: u64, mailbox_id: u64) -> Bich
.map(|d| d.timestamp_millis())
.unwrap_or(0);
let uid = fetch.uid.unwrap_or(0);
let size = fetch.size.unwrap_or(0);
let body = fetch
.body()
.ok_or_else(|| raise_error!("No body available".into(), ErrorCode::InternalError))?;
let size = fetch.size.unwrap_or(body.len() as u32);
let message = MessageParser::new().parse(body).ok_or_else(|| {
raise_error!(
"Email header parse result is not available".into(),
@@ -118,6 +117,92 @@ pub fn extract_envelope(fetch: &Fetch, account_id: u64, mailbox_id: u64) -> Bich
Ok(envelope)
}
pub fn extract_envelope_from_eml(
body: &[u8],
account_id: u64,
mailbox_id: u64,
) -> BichonResult<Envelope> {
let uid = 0;
let size = body.len() as u32;
let message = MessageParser::new().parse(body).ok_or_else(|| {
raise_error!(
"Email header parse result is not available".into(),
ErrorCode::InternalError
)
})?;
let text = if let Some(text) = message.body_text(0).map(|cow| cow.into_owned()) {
text
} else if let Some(html) = message.body_html(0).map(|cow| cow.into_owned()) {
from_read(html.as_bytes(), 0)
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?
} else {
String::new()
};
let message_id = message
.message_id()
.map(String::from)
.unwrap_or(generate_message_id());
let in_reply_to = message.in_reply_to().as_text().map(String::from);
let references = extract_references(&message);
let thread_id = compute_thread_id(in_reply_to, references, &message_id);
let subject = message.subject().map(String::from).unwrap_or("".into());
let date = message.date().map(|d| d.to_timestamp() * 1000).unwrap_or(0);
let bcc: Option<Vec<String>> = message.bcc().map(|addr| {
AddrVec::from(addr)
.0
.into_iter()
.filter_map(|a| a.address)
.collect()
});
let cc: Option<Vec<String>> = message.cc().map(|addr| {
AddrVec::from(addr)
.0
.into_iter()
.filter_map(|a| a.address)
.collect()
});
let to: Option<Vec<String>> = message.to().map(|addr| {
AddrVec::from(addr)
.0
.into_iter()
.filter_map(|a| a.address)
.collect()
});
let from = message
.from()
.and_then(|addr| AddrVec::from(addr).0.into_iter().next())
.and_then(|add| add.address)
.unwrap_or_else(|| "unknown".to_string());
let attachments: Vec<String> = message
.attachments()
.filter_map(|att| att.attachment_name())
.map(|name| name.to_string())
.collect();
let envelope = Envelope {
id: create_hash(account_id, &message_id),
message_id,
account_id,
mailbox_id,
uid,
subject,
text,
from,
to: to.unwrap_or_default(),
cc: cc.unwrap_or_default(),
bcc: bcc.unwrap_or_default(),
date,
internal_date: date,
size,
thread_id,
attachments,
tags: None,
};
Ok(envelope)
}
pub fn compute_thread_id(
in_reply_to: Option<String>,
references: Option<Vec<String>>,
+162
View File
@@ -0,0 +1,162 @@
use poem_openapi::Object;
use serde::{Deserialize, Serialize};
use tantivy::doc;
use crate::{
base64_decode_url_safe,
modules::{
account::migration::{AccountModel, AccountType},
cache::imap::mailbox::{Attribute, AttributeEnum, MailBox},
envelope::extractor::extract_envelope_from_eml,
error::{code::ErrorCode, BichonResult},
indexer::{
manager::{EML_INDEX_MANAGER, ENVELOPE_INDEX_MANAGER},
schema::SchemaTools,
},
utils::create_hash,
},
raise_error,
};
#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, Object)]
pub struct BatchEmlRequest {
pub account_id: u64,
pub mail_folder: String,
/// A list of emails in base64-encoded format. Each element represents one .eml file.
pub emls: Vec<String>,
}
#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, Object)]
pub struct FailedEmlDetail {
/// The 0-based index of the failed EML in the request list
pub index: usize,
/// The error message that caused the import to fail
pub error_message: String,
}
#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, Object)]
pub struct BatchEmlResult {
/// Total number of emails processed
pub total: usize,
/// Number of emails successfully imported
pub success: usize,
/// Number of emails failed to import
pub failed: usize,
/// A list of details for failed imports
pub failed_details: Vec<FailedEmlDetail>,
}
pub struct ImportEmls;
impl ImportEmls {
pub async fn do_import(request: BatchEmlRequest) -> BichonResult<BatchEmlResult> {
let account = AccountModel::check_account_exists(request.account_id).await?;
if !account.enabled {
return Err(raise_error!("The account is disabled and cannot be used for this operation.".into(), ErrorCode::InvalidParameter));
}
let mailbox_id = match account.account_type {
AccountType::IMAP => {
let all_mailboxes = MailBox::list_all(account.id).await?;
let mailbox = all_mailboxes.into_iter().find(|m| m.name == request.mail_folder);
match mailbox {
Some(mailbox) => mailbox.id,
None => return Err(raise_error!(
format!("Mail folder '{}' not found for account ID {}. The target folder must exist before importing.",
request.mail_folder,
request.account_id).into(),
ErrorCode::ResourceNotFound
)),
}
},
AccountType::NoSync => {
let mailbox = MailBox {
id: create_hash(request.account_id, &request.mail_folder),
account_id: request.account_id,
name: request.mail_folder.clone(),
delimiter: Some("/".to_string()),
attributes: vec![Attribute {
attr: AttributeEnum::Extension,
extension: Some("CreatedByBichon".into()),
}],
exists: 0,
unseen: None,
uid_next: None,
uid_validity: None,
};
let mailbox_id = mailbox.id;
// Upsert the mailbox, creating it if it doesn't exist
MailBox::batch_upsert(&[mailbox]).await?;
mailbox_id
},
};
let fields = SchemaTools::eml_fields();
let account_id = account.id;
let mut success_count = 0;
let mut failed_details: Vec<FailedEmlDetail> = Vec::new(); // Store failure details
let total = request.emls.len();
for (index, eml_base64) in request.emls.into_iter().enumerate() {
// 1. Decode Base64
let decoded = match base64_decode_url_safe!(eml_base64.as_bytes()) {
Ok(bytes) => bytes,
Err(e) => {
let error_msg =
format!("Failed to decode base64 EML at index {}: {:?}", index, e);
tracing::error!("{}", error_msg);
failed_details.push(FailedEmlDetail {
index,
error_message: error_msg,
});
continue;
}
};
let envelope = match extract_envelope_from_eml(&decoded, account_id, mailbox_id) {
Ok(env) => env,
Err(e) => {
let error_msg = format!(
"Failed to extract envelope from EML at index {}: {:?}",
index, e
);
tracing::error!("{}", error_msg);
failed_details.push(FailedEmlDetail {
index,
error_message: error_msg,
});
continue;
}
};
ENVELOPE_INDEX_MANAGER
.add_document(envelope.id, envelope.to_document(mailbox_id).unwrap())
.await;
EML_INDEX_MANAGER
.add_document(
envelope.id,
doc!(
fields.f_id => envelope.id,
fields.f_account_id => account_id,
fields.f_mailbox_id => mailbox_id,
fields.f_eml => decoded
),
)
.await;
success_count += 1;
}
let failed_count = failed_details.len();
Ok(BatchEmlResult {
total,
success: success_count,
failed: failed_count,
failed_details, // Return the list of failure details
})
}
}
+1 -1
View File
@@ -16,7 +16,6 @@
// 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/>.
pub mod account;
pub mod autoconfig;
pub mod cache;
@@ -27,6 +26,7 @@ pub mod database;
pub mod envelope;
pub mod error;
pub mod imap;
pub mod import;
pub mod indexer;
pub mod logger;
pub mod mailbox;
+49
View File
@@ -0,0 +1,49 @@
//
// Copyright (c) 2025 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/>.
use crate::modules::common::auth::ClientContext;
use crate::modules::import::BatchEmlResult;
use crate::modules::import::{BatchEmlRequest, ImportEmls};
use crate::modules::rest::api::ApiTags;
use crate::modules::rest::ApiResult;
use poem_openapi::payload::Json;
use poem_openapi::OpenApi;
pub struct ImportApi;
#[OpenApi(prefix_path = "/api/v1", tag = "ApiTags::Import")]
impl ImportApi {
/// Batch import one or more EML files into a specified account and mail folder.
///
/// This endpoint accepts a JSON payload containing:
/// - `account_id`: the target account to import emails into
/// - `mail_folder`: the mailbox/folder name
/// - `emls`: a list of base64-encoded .eml files
///
/// Returns a summary of the import result, including total processed, successful, and failed emails.
#[oai(path = "/import", method = "post", operation_id = "do_batch_import")]
async fn do_batch_import(
&self,
/// JSON payload with account info and EML files to import
payload: Json<BatchEmlRequest>,
context: ClientContext,
) -> ApiResult<Json<BatchEmlResult>> {
context.require_root()?;
Ok(Json(ImportEmls::do_import(payload.0).await?))
}
}
+5 -2
View File
@@ -16,7 +16,6 @@
// 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/>.
use access_token::AccessTokenApi;
use account::AccountApi;
use auto_config::AutoConfigApi;
@@ -26,11 +25,12 @@ use oauth2::OAuth2Api;
use poem_openapi::{OpenApiService, Tags};
use system::SystemApi;
use crate::bichon_version;
use crate::{bichon_version, modules::rest::api::import::ImportApi};
pub mod access_token;
pub mod account;
pub mod auto_config;
pub mod import;
pub mod mailbox;
pub mod message;
pub mod oauth2;
@@ -45,6 +45,7 @@ pub enum ApiTags {
OAuth2,
Message,
System,
Import,
}
type RustMailOpenApi = (
@@ -55,6 +56,7 @@ type RustMailOpenApi = (
MailBoxApi,
OAuth2Api,
MessageApi,
ImportApi,
);
pub fn create_openapi_service() -> OpenApiService<RustMailOpenApi, ()> {
@@ -67,6 +69,7 @@ pub fn create_openapi_service() -> OpenApiService<RustMailOpenApi, ()> {
MailBoxApi,
OAuth2Api,
MessageApi,
ImportApi,
),
"BichonApi",
bichon_version!(),
@@ -37,10 +37,10 @@ import { AccountModel } from '../data/schema';
import { useTranslation } from 'react-i18next';
const accountSchema = () =>
const accountSchema = (t: (key: string) => string) =>
z.object({
name: z.string().optional(),
email: z.string({ required_error: 'Email is required' }).email({ message: 'Invalid email address' }),
email: z.string({ required_error: t('validation.emailRequired') }).email({ message: t('validation.invalidEmail') }),
enabled: z.boolean()
});
@@ -85,7 +85,7 @@ export function NoSyncAccountDialog({ currentRow, open, onOpenChange }: Props) {
const form = useForm<NoSyncAccount>({
mode: "all",
defaultValues: isEdit ? mapCurrentRowToFormValues(currentRow) : defaultValues,
resolver: zodResolver(accountSchema()),
resolver: zodResolver(accountSchema(t)),
});
const queryClient = useQueryClient();
@@ -118,7 +118,7 @@ export function NoSyncAccountDialog({ currentRow, open, onOpenChange }: Props) {
const errorMessage =
(error.response?.data as { message?: string })?.message ||
error.message ||
`${isEdit ? 'Update' : 'Creation'} failed, please try again later`;
(isEdit ? t('accounts.updateFailed') : t('accounts.creationFailed'));
toast({
variant: "destructive",
@@ -134,7 +134,8 @@ export function NoSyncAccountDialog({ currentRow, open, onOpenChange }: Props) {
const commonData = {
email: data.email,
name: data.name,
enabled: data.enabled
enabled: data.enabled,
use_dangerous: false
};
if (isEdit) {
updateMutation.mutate(commonData);
@@ -33,7 +33,7 @@ export function OAuth2Action({ row }: DataTableRowActionsProps) {
const account_type = mailer.account_type;
if (account_type === "NoSync") {
return <span className="text-xs text-muted-foreground">n/a</span>
return <Button variant={"ghost"} className="text-xs text-muted-foreground">n/a</Button>
}
const isOAuth2 = mailer.imap?.auth.auth_type === "OAuth2"
+1 -1
View File
@@ -251,7 +251,7 @@
"updateTheEmailAccountHere": "تحديث حساب البريد الإلكتروني هنا. ",
"addNewEmailAccountHere": "إضافة حساب بريد إلكتروني جديد هنا. ",
"thisAccountIsUsedForIdentificationPurposesOnly": "يستخدم هذا الحساب لأغراض التعريف فقط ولا يتطلب المزامنة مع خادم بريد إلكتروني. إنه يساعد في استيراد بيانات البريد الإلكتروني.",
"determinesWhetherThisAccountIsActiveNoSync": "يحدد ما إذا كان هذا الحساب نشطًا. إذا تم تعطيله، فلن يتمكن الحساب من استيراد البيانات أو إجراء استعلامات.",
"determinesWhetherThisAccountIsActiveNoSync": "يحدد ما إذا كان هذا الحساب نشطًا. إذا تم تعطيله، فلن يكون قادرًا على استيراد البيانات.",
"folderSync": {
"noData": "لا توجد بيانات",
"noFolders": "لا توجد مجلدات للمزامنة",
+1 -1
View File
@@ -251,7 +251,7 @@
"updateTheEmailAccountHere": "Opdater e-mailkontoen her. ",
"addNewEmailAccountHere": "Tilføj ny e-mailkonto her. ",
"thisAccountIsUsedForIdentificationPurposesOnly": "Denne konto bruges kun til identifikationsformål og kræver ikke synkronisering med en e-mailserver. Den hjælper med import af e-mail-data.",
"determinesWhetherThisAccountIsActiveNoSync": "Bestemmer, om denne konto er aktiv. Hvis deaktiveret, vil kontoen ikke kunne importere data eller udføre søgninger.",
"determinesWhetherThisAccountIsActiveNoSync": "Bestemmer, om denne konto er aktiv. Hvis den er deaktiveret, kan data ikke importeres.",
"folderSync": {
"noData": "Ingen data",
"noFolders": "Ingen mapper at synkronisere",
+1 -1
View File
@@ -251,7 +251,7 @@
"updateTheEmailAccountHere": "Aktualisieren Sie das E-Mail-Konto hier. ",
"addNewEmailAccountHere": "Fügen Sie hier ein neues E-Mail-Konto hinzu. ",
"thisAccountIsUsedForIdentificationPurposesOnly": "Dieses Konto dient nur zur Identifizierung und erfordert keine Synchronisierung mit dem Mailserver. Es hilft beim Import von E-Mail-Daten.",
"determinesWhetherThisAccountIsActiveNoSync": "Bestimmt, ob dieses Konto aktiv ist. Wenn es deaktiviert ist, kann das Konto keine Daten importieren oder Abfragen ausführen.",
"determinesWhetherThisAccountIsActiveNoSync": "Legt fest, ob dieses Konto aktiv ist. Wenn es deaktiviert ist, können keine Daten importiert werden.",
"folderSync": {
"noData": "Keine Daten",
"noFolders": "Keine Ordner zum Synchronisieren",
+1 -1
View File
@@ -251,7 +251,7 @@
"updateTheEmailAccountHere": "Update the email account here. ",
"addNewEmailAccountHere": "Add new email account here. ",
"thisAccountIsUsedForIdentificationPurposesOnly": "This account is used for identification purposes only and does not require syncing with an email server. It helps with importing email data.",
"determinesWhetherThisAccountIsActiveNoSync": "Determines whether this account is active. If disabled, the account will not be able to import data or perform queries.",
"determinesWhetherThisAccountIsActiveNoSync": "Determines whether this account is active. If disabled, the account will not be able to import data.",
"folderSync": {
"noData": "No Data",
"noFolders": "No folders to sync",
+1 -1
View File
@@ -251,7 +251,7 @@
"updateTheEmailAccountHere": "Actualiza la cuenta de correo electrónico aquí. ",
"addNewEmailAccountHere": "Añade una nueva cuenta de correo electrónico aquí. ",
"thisAccountIsUsedForIdentificationPurposesOnly": "Esta cuenta se utiliza solo para fines de identificación y no requiere sincronización con el servidor de correo. Ayuda a importar datos de correo electrónico.",
"determinesWhetherThisAccountIsActiveNoSync": "Determina si esta cuenta está activa. Si está deshabilitada, la cuenta no podrá importar datos ni ejecutar consultas.",
"determinesWhetherThisAccountIsActiveNoSync": "Determina si esta cuenta está activa. Si está desactivada, no podrá importar datos.",
"folderSync": {
"noData": "Sin datos",
"noFolders": "No hay carpetas para sincronizar",
+1 -1
View File
@@ -251,7 +251,7 @@
"updateTheEmailAccountHere": "Päivitä sähköpostitili täällä. ",
"addNewEmailAccountHere": "Lisää uusi sähköpostitili täällä. ",
"thisAccountIsUsedForIdentificationPurposesOnly": "Tätä tiliä käytetään vain tunnistamistarkoituksiin, eikä se vaadi synkronointia sähköpostipalvelimen kanssa. Auttaa sähköpostidatan tuomisessa.",
"determinesWhetherThisAccountIsActiveNoSync": "Määrittää, onko tämä tili aktiivinen. Jos poistettu käytöstä, tili ei voi tuoda tietoja tai suorittaa kyselyjä.",
"determinesWhetherThisAccountIsActiveNoSync": "Määrittää, onko tämä tili aktiivinen. Jos se on poistettu käytöstä, tietoja ei voi tuoda.",
"folderSync": {
"noData": "Ei tietoja",
"noFolders": "Ei synkronoitavia kansioita",
+1 -1
View File
@@ -251,7 +251,7 @@
"updateTheEmailAccountHere": "Mettez à jour le compte e-mail ici. ",
"addNewEmailAccountHere": "Ajoutez un nouveau compte e-mail ici. ",
"thisAccountIsUsedForIdentificationPurposesOnly": "Ce compte est utilisé uniquement à des fins d'identification et ne nécessite pas de synchronisation avec un serveur de messagerie. Il est utile pour l'importation de données d'e-mails.",
"determinesWhetherThisAccountIsActiveNoSync": "Détermine si ce compte est actif. S'il est désactivé, l'importation de données ou l'exécution de requêtes ne sera pas possible pour ce compte.",
"determinesWhetherThisAccountIsActiveNoSync": "Détermine si ce compte est actif. Sil est désactivé, il ne pourra pas importer de données.",
"folderSync": {
"noData": "Aucune Donnée",
"noFolders": "Aucun dossier à synchroniser",
+1 -1
View File
@@ -251,7 +251,7 @@
"updateTheEmailAccountHere": "Aggiorna l'account email qui. ",
"addNewEmailAccountHere": "Aggiungi un nuovo account email qui. ",
"thisAccountIsUsedForIdentificationPurposesOnly": "Questo account viene utilizzato solo a scopo di identificazione e non richiede la sincronizzazione con un server email. Aiuta nell'importazione dei dati email.",
"determinesWhetherThisAccountIsActiveNoSync": "Determina se questo account è attivo. Se disabilitato, l'account non potrà importare dati o eseguire query.",
"determinesWhetherThisAccountIsActiveNoSync": "Determina se questo account è attivo. Se disattivato, non sarà possibile importare dati.",
"folderSync": {
"noData": "Nessun Dato",
"noFolders": "Nessuna cartella da sincronizzare",
+1 -1
View File
@@ -251,7 +251,7 @@
"updateTheEmailAccountHere": "こちらでメールアカウントを更新してください。",
"addNewEmailAccountHere": "こちらで新しいメールアカウントを追加してください。",
"thisAccountIsUsedForIdentificationPurposesOnly": "このアカウントは識別目的でのみ使用され、メールサーバーとの同期は必要ありません。メールデータのインポートに役立ちます。",
"determinesWhetherThisAccountIsActiveNoSync": "このアカウントが有効かどうかを定します。無効の場合、データインポートやクエリの実行はできません。",
"determinesWhetherThisAccountIsActiveNoSync": "このアカウントが有効かどうかを定します。無効の場合、データインポートできません。",
"folderSync": {
"noData": "データなし",
"noFolders": "同期するフォルダーがありません",
+1 -1
View File
@@ -251,7 +251,7 @@
"updateTheEmailAccountHere": "여기에서 이메일 계정을 업데이트하십시오.",
"addNewEmailAccountHere": "여기에서 새 이메일 계정을 추가하십시오.",
"thisAccountIsUsedForIdentificationPurposesOnly": "이 계정은 식별 목적으로만 사용되며 이메일 서버와의 동기화가 필요하지 않습니다. 이메일 데이터 가져오기에 도움이 됩니다.",
"determinesWhetherThisAccountIsActiveNoSync": "이 계정이 활성화되었는지 여부를 결정합니다. 비활성화된 경우 데이터 가져오기 또는 쿼리 실행이 불가능합니다.",
"determinesWhetherThisAccountIsActiveNoSync": "이 계정이 활성화되어 있는지 결정합니다. 비활성화되면 데이터 가져올 수 없습니다.",
"folderSync": {
"noData": "데이터 없음",
"noFolders": "동기화할 폴더가 없습니다",
+1 -1
View File
@@ -251,7 +251,7 @@
"updateTheEmailAccountHere": "Werk het e-mailaccount hier bij. ",
"addNewEmailAccountHere": "Voeg hier een nieuw e-mailaccount toe. ",
"thisAccountIsUsedForIdentificationPurposesOnly": "Dit account wordt alleen gebruikt voor identificatiedoeleinden en vereist geen synchronisatie met een e-mailserver. Het helpt bij het importeren van e-mailgegevens.",
"determinesWhetherThisAccountIsActiveNoSync": "Bepaalt of dit account actief is. Indien uitgeschakeld, kan het account geen gegevens importeren of zoekopdrachten uitvoeren.",
"determinesWhetherThisAccountIsActiveNoSync": "Bepaalt of dit account actief is. Als het is uitgeschakeld, kan er geen data worden geïmporteerd.",
"folderSync": {
"noData": "Geen Gegevens",
"noFolders": "Geen mappen om te synchroniseren",
+1 -1
View File
@@ -251,7 +251,7 @@
"updateTheEmailAccountHere": "Oppdater e-postkontoen her. ",
"addNewEmailAccountHere": "Legg til ny e-postkonto her. ",
"thisAccountIsUsedForIdentificationPurposesOnly": "Denne kontoen brukes kun for identifikasjonsformål og krever ikke synkronisering med en e-postserver. Den hjelper med import av e-postdata.",
"determinesWhetherThisAccountIsActiveNoSync": "Bestemmer om denne kontoen er aktiv. Hvis deaktivert, vil ikke kontoen kunne importere data eller utføre spørringer.",
"determinesWhetherThisAccountIsActiveNoSync": "Bestemmer om denne kontoen er aktiv. Hvis den er deaktivert, kan data ikke importeres.",
"folderSync": {
"noData": "Ingen data",
"noFolders": "Ingen mapper å synkronisere",
+1 -1
View File
@@ -251,7 +251,7 @@
"updateTheEmailAccountHere": "Atualize a conta de email aqui.",
"addNewEmailAccountHere": "Adicione uma nova conta de email aqui.",
"thisAccountIsUsedForIdentificationPurposesOnly": "Esta conta é usada apenas para fins de identificação e não requer sincronização com um servidor de email. Ajuda na importação de dados de email.",
"determinesWhetherThisAccountIsActiveNoSync": "Determina se esta conta está ativa. Se desativada, nenhuma importação de dados ou execução de consulta será possível.",
"determinesWhetherThisAccountIsActiveNoSync": "Determina se esta conta está ativa. Se estiver desativada, não será possível importar dados.",
"folderSync": {
"noData": "Sem Dados",
"noFolders": "Nenhuma pasta para sincronizar",
+1 -1
View File
@@ -251,7 +251,7 @@
"updateTheEmailAccountHere": "Обновите почтовый аккаунт здесь. ",
"addNewEmailAccountHere": "Добавьте новый почтовый аккаунт здесь. ",
"thisAccountIsUsedForIdentificationPurposesOnly": "Этот аккаунт используется только для идентификации и не требует синхронизации с почтовым сервером. Он помогает при импорте почтовых данных.",
"determinesWhetherThisAccountIsActiveNoSync": "Определяет, активен ли этот аккаунт. Если отключено, аккаунт не сможет импортировать данные или выполнять запросы.",
"determinesWhetherThisAccountIsActiveNoSync": "Определяет, активна ли учетная запись. Если она отключена, импорт данных будет невозможен.",
"folderSync": {
"noData": "Нет данных",
"noFolders": "Нет папок для синхронизации",
+1 -1
View File
@@ -251,7 +251,7 @@
"updateTheEmailAccountHere": "Uppdatera e-postkontot här. ",
"addNewEmailAccountHere": "Lägg till nytt e-postkonto här. ",
"thisAccountIsUsedForIdentificationPurposesOnly": "Detta konto används endast för identifieringssyften och kräver inte synkronisering med en e-postserver. Det hjälper till med import av e-postdata.",
"determinesWhetherThisAccountIsActiveNoSync": "Avgör om detta konto är aktivt. Om inaktiverat kommer kontot inte kunna importera data eller utföra sökningar.",
"determinesWhetherThisAccountIsActiveNoSync": "Avgör om kontot är aktivt. Om det är inaktiverat kan ingen data importeras.",
"folderSync": {
"noData": "Inga data",
"noFolders": "Inga mappar att synkronisera",
+1 -1
View File
@@ -251,7 +251,7 @@
"updateTheEmailAccountHere": "在此更新電子郵件帳號。",
"addNewEmailAccountHere": "在此新增電子郵件帳號。",
"thisAccountIsUsedForIdentificationPurposesOnly": "此帳號僅用於識別目的,不需要與郵件伺服器同步。有助於匯入郵件資料。",
"determinesWhetherThisAccountIsActiveNoSync": "決定此帳號是否啟用狀態。如果停用,無法匯入資料或執行查詢。",
"determinesWhetherThisAccountIsActiveNoSync": "用來判斷帳戶是否啟用。若停用,無法匯入資料。",
"folderSync": {
"noData": "無資料",
"noFolders": "沒有可同步的資料夾",
+1 -1
View File
@@ -251,7 +251,7 @@
"updateTheEmailAccountHere": "在此更新邮件账户。",
"addNewEmailAccountHere": "在此添加新邮件账户。",
"thisAccountIsUsedForIdentificationPurposesOnly": "此账户仅用于识别目的,不需要与邮件服务器同步。它有助于导入邮件数据。",
"determinesWhetherThisAccountIsActiveNoSync": "确定此账户是否处于活动状态。如果禁用,账户将无法导入数据或执行查询。",
"determinesWhetherThisAccountIsActiveNoSync": "用于判断账户是否处于启用状态。如果禁用,将无法导入数据。",
"folderSync": {
"noData": "无数据",
"noFolders": "没有需要同步的文件夹",