diff --git a/crates/flectar-mail-core/src/error.rs b/crates/flectar-mail-core/src/error.rs index b863e1d..84ce2ee 100644 --- a/crates/flectar-mail-core/src/error.rs +++ b/crates/flectar-mail-core/src/error.rs @@ -10,6 +10,10 @@ pub enum CoreError { Network(String), #[error("imap error: {0}")] Imap(String), + /// A selective FETCH response could not be parsed. Its session must be + /// discarded; explicit opens may retry using the complete message. + #[error("imap response parse error: {0}")] + ImapParse(String), #[error("jmap error: {0}")] Jmap(String), #[error("send status uncertain: {0}")] @@ -49,7 +53,7 @@ impl CoreError { CoreError::Db(_) => "db", CoreError::Io(_) => "io", CoreError::Network(_) => "network", - CoreError::Imap(_) => "imap", + CoreError::Imap(_) | CoreError::ImapParse(_) => "imap", CoreError::Jmap(_) => "jmap", CoreError::SendUncertain(_) => "send_uncertain", CoreError::Smtp(_) => "smtp", diff --git a/crates/flectar-mail-core/src/imap/mod.rs b/crates/flectar-mail-core/src/imap/mod.rs index 8cfbaa4..37e2b19 100644 --- a/crates/flectar-mail-core/src/imap/mod.rs +++ b/crates/flectar-mail-core/src/imap/mod.rs @@ -531,7 +531,10 @@ pub async fn fetch_headers(session: &mut Session, uid_set: &str) -> Result Result> { - let query = format!("(UID FLAGS INTERNALDATE RFC822.SIZE BODYSTRUCTURE {HEADER_FIELDS})"); + // Keep header sync independent from BODYSTRUCTURE: an incompatible MIME + // response must not prevent historical headers from being stored. MIME + // plans are fetched separately when preparing or opening message bodies. + let query = format!("(UID FLAGS INTERNALDATE RFC822.SIZE {HEADER_FIELDS})"); let mut out = Vec::new(); { let mut stream = session @@ -567,6 +570,83 @@ pub struct FetchedMimePlan { const MIME_PLAN_QUERY: &str = "(UID BODYSTRUCTURE)"; +fn selective_fetch_error(error: async_imap::error::Error) -> CoreError { + match error { + // Do not include response bytes: they can contain private headers. + async_imap::error::Error::Parse(_) => { + CoreError::ImapParse("could not parse selective FETCH response".into()) + } + // async-imap 0.11.3 wraps its wire decoder's nom errors in Io(Other) + // rather than Parse. Match that decoder format only, never NO/BAD or + // an arbitrary transport error. Keep the embedded response private. + async_imap::error::Error::Io(ref detail) + if detail.kind() == std::io::ErrorKind::Other + && (detail.to_string().starts_with("Error(") + || detail.to_string().starts_with("Failure(")) + && detail.to_string().contains(" during parsing of ") => + { + CoreError::ImapParse("could not parse selective FETCH response".into()) + } + error => CoreError::Imap(error.to_string()), + } +} + +/// async-imap's FETCH stream discards the tagged completion status. Read +/// selective responses directly so NO/BAD cannot masquerade as missing MIME. +/// The main actor independently reconciles unsolicited flags and expunges. +async fn selective_fetch( + session: &mut Session, + uid_set: &str, + query: &str, + mut consume: impl FnMut(u32, &[async_imap::imap_proto::AttributeValue<'_>]), +) -> Result<()> { + use async_imap::imap_proto::{AttributeValue, Response, Status}; + let tag = session + .run_command(format!("UID FETCH {uid_set} {query}")) + .await + .map_err(selective_fetch_error)?; + loop { + let response = session + .read_response() + .await + .map_err(|error| selective_fetch_error(error.into()))? + .ok_or_else(|| CoreError::Imap("connection closed before FETCH completed".into()))?; + match response.parsed() { + Response::Done { + tag: received, + status, + code, + information, + } if *received == tag => { + return match status { + Status::Ok => Ok(()), + _ => Err(CoreError::Imap(format!( + "FETCH rejected: {status:?}, {code:?}, {information:?}" + ))), + }; + } + Response::Done { .. } => { + return Err(CoreError::ImapParse( + "unexpected FETCH completion tag".into(), + )); + } + Response::Data { + status: Status::Bye, + .. + } => return Err(CoreError::Imap("server closed FETCH session".into())), + Response::Fetch(_, attributes) => { + if let Some(uid) = attributes.iter().find_map(|attr| match attr { + AttributeValue::Uid(uid) => Some(*uid), + _ => None, + }) { + consume(uid, attributes); + } + } + _ => {} + } + } +} + async fn fetch_mime_plans_batch_inner( session: &mut Session, uids: &[u32], @@ -577,19 +657,18 @@ async fn fetch_mime_plans_batch_inner( } let requested: std::collections::HashSet = uids.iter().copied().collect(); let mut result = std::collections::BTreeMap::::new(); - let mut stream = session - .uid_fetch(&uid_set, MIME_PLAN_QUERY) - .await - .map_err(|error| CoreError::Imap(error.to_string()))?; - while let Some(item) = stream.next().await { - let fetch = item.map_err(|error| CoreError::Imap(error.to_string()))?; - let Some(uid) = fetch.uid.filter(|uid| requested.contains(uid)) else { - continue; - }; - if let Some(bodystructure) = fetch.bodystructure() { - result.insert(uid, mime::plan_bodystructure(bodystructure)); + selective_fetch(session, &uid_set, MIME_PLAN_QUERY, |uid, attributes| { + if requested.contains(&uid) { + for attribute in attributes { + if let async_imap::imap_proto::AttributeValue::BodyStructure(bodystructure) = + attribute + { + result.insert(uid, mime::plan_bodystructure(bodystructure)); + } + } } - } + }) + .await?; Ok(result .into_iter() .map(|(uid, plan)| FetchedMimePlan { uid, plan }) @@ -694,36 +773,37 @@ async fn fetch_sections_batch_inner( let requested: std::collections::HashSet = uids.iter().copied().collect(); let mut result = std::collections::BTreeMap::>::new(); - let mut stream = session - .uid_fetch(&uid_set, &query) - .await - .map_err(|error| CoreError::Imap(error.to_string()))?; - while let Some(item) = stream.next().await { - let fetch = item.map_err(|error| CoreError::Imap(error.to_string()))?; - let Some(uid) = fetch.uid.filter(|uid| requested.contains(uid)) else { - continue; + selective_fetch(session, &uid_set, &query, |uid, attributes| { + if !requested.contains(&uid) { + return; + } + let section_bytes = |path: &SectionPath| { + attributes.iter().find_map(|attribute| match attribute { + async_imap::imap_proto::AttributeValue::BodySection { + section: Some(section), + data: Some(data), + .. + } if section == path => Some(data.to_vec()), + _ => None, + }) }; let fetched = result.entry(uid).or_default(); for (section, numbers) in &paths { let body_path = SectionPath::Part(numbers.clone(), None); let mime_path = SectionPath::Part(numbers.clone(), Some(MessageSection::Mime)); - if let Some(body) = fetch.section(&body_path) { - // A duplicate response for the same UID should not duplicate - // a section in the public result. + if let Some(body) = section_bytes(&body_path) { if fetched.iter().any(|item| item.section == *section) { continue; } fetched.push(FetchedSection { section: section.clone(), - mime_header: fetch - .section(&mime_path) - .map(ToOwned::to_owned) - .unwrap_or_default(), - body: body.to_vec(), + mime_header: section_bytes(&mime_path).unwrap_or_default(), + body, }); } } - } + }) + .await?; Ok(result .into_iter() .map(|(uid, sections)| FetchedMessageSections { uid, sections }) diff --git a/crates/flectar-mail-core/src/sync/engine.rs b/crates/flectar-mail-core/src/sync/engine.rs index 9488563..947bb90 100644 --- a/crates/flectar-mail-core/src/sync/engine.rs +++ b/crates/flectar-mail-core/src/sync/engine.rs @@ -342,8 +342,7 @@ async fn run_body_fetcher( } } } - let Some(s) = session.as_mut() else { break }; - match fetch_bodies_batch(&ctx, &config, s, &ids).await { + match fetch_bodies_batch(&ctx, &config, &mut session, &ids).await { Ok(()) => { fetched = true; break; @@ -355,9 +354,7 @@ async fn run_body_fetcher( ); // The session may be broken; drop it so the retry (and any // later request) reconnects. - if let Some(s) = session.take() { - imap::logout(s).await; - } + drop(session.take()); } } } @@ -423,7 +420,7 @@ async fn fetch_attachment_bytes( async fn fetch_bodies_batch( ctx: &SyncCtx, config: &AccountConfig, - session: &mut Session, + session: &mut Option, ids: &[i64], ) -> Result<()> { use std::collections::HashMap; @@ -462,7 +459,8 @@ async fn fetch_bodies_batch( reset_bodies_none(ctx, items.iter().map(|(_, m)| *m)).await; continue; }; - select_folder_for_remote_read(ctx, session, &folder).await?; + select_folder_for_remote_read(ctx, session.as_mut().ok_or(CoreError::Offline)?, &folder) + .await?; items.sort_unstable_by_key(|(uid, _)| *uid); for (uid, message_id) in items { if !remote_location_is_current(ctx, message_id, folder_id, i64::from(uid)).await? { @@ -868,10 +866,13 @@ async fn run_actor( imap::IdleOutcome::Command(SyncCmd::FetchBody { message_id, }) => { - if let Some(ref mut s) = session - && let Err(e) = - fetch_one_body(&ctx, &config, s, message_id) - .await + if let Err(e) = fetch_one_body( + &ctx, + &config, + &mut session, + message_id, + ) + .await { tracing::warn!("priority body fetch failed: {e}"); } @@ -947,8 +948,10 @@ async fn run_actor( break; } Ok(Some(SyncCmd::FetchBody { message_id })) => { - if let Some(ref mut s) = session { - if let Err(e) = fetch_one_body(&ctx, &config, s, message_id).await { + if session.is_some() { + if let Err(e) = + fetch_one_body(&ctx, &config, &mut session, message_id).await + { tracing::warn!("priority body fetch failed: {e}"); deadline = tokio::time::Instant::now(); } @@ -2188,9 +2191,10 @@ enum BodyChunkError { fn classify_body_chunk_error(error: CoreError) -> BodyChunkError { match error { - error @ (CoreError::Imap(_) | CoreError::Network(_) | CoreError::Offline) => { - BodyChunkError::Reconnect(error) - } + error @ (CoreError::Imap(_) + | CoreError::ImapParse(_) + | CoreError::Network(_) + | CoreError::Offline) => BodyChunkError::Reconnect(error), error => BodyChunkError::Fatal(error), } } @@ -2625,6 +2629,23 @@ async fn body_worker( "body worker: reconnect after batch split failed" ), } + } else if matches!(error, CoreError::ImapParse(_)) { + let failures = retry + .items + .iter() + .map(|(message_id, _)| (*message_id, error.to_string())) + .collect(); + record_content_failure_details(&ctx, failures).await?; + skip.lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .extend(retry.items.iter().map(|(message_id, _)| *message_id)); + drop(session); + if queue.lock().await.is_empty() { + return Ok(()); + } + session = connect(&ctx, &config).await?; + selected = None; + continue; } else { queue.lock().await.push_front(retry); } @@ -2965,7 +2986,7 @@ async fn revalidate_background_fetches( async fn fetch_one_body( ctx: &SyncCtx, config: &AccountConfig, - session: &mut Session, + session: &mut Option, message_id: i64, ) -> Result<()> { let row = ctx @@ -2984,7 +3005,8 @@ async fn fetch_one_body( .read(move |conn| repo::folders::get(conn, folder_id)) .await? .ok_or_else(|| CoreError::NotFound("folder".into()))?; - select_folder_for_remote_read(ctx, session, &folder).await?; + select_folder_for_remote_read(ctx, session.as_mut().ok_or(CoreError::Offline)?, &folder) + .await?; if !remote_location_is_current(ctx, message_id, folder_id, uid).await? { reset_bodies_none(ctx, std::iter::once(message_id)).await; return Ok(()); @@ -2995,7 +3017,7 @@ async fn fetch_one_body( async fn store_one_body( ctx: &SyncCtx, config: &AccountConfig, - session: &mut Session, + session: &mut Option, message_id: i64, folder_id: i64, uid: u32, @@ -3045,7 +3067,7 @@ fn decode_selective_content( let mut calendar_parts = Vec::new(); for planned in &item.plan.text_sections { let section = fetched_by_section.get(&planned.section).ok_or_else(|| { - CoreError::Imap(format!( + CoreError::Mime(format!( "server omitted planned MIME section {}", planned.section )) @@ -3079,7 +3101,7 @@ fn decode_selective_content( async fn fetch_selective_content( ctx: &SyncCtx, config: &AccountConfig, - session: &mut Session, + session: &mut Option, message_id: i64, folder_id: i64, uid: u32, @@ -3105,7 +3127,28 @@ async fn fetch_selective_content( "message remote location changed before MIME-plan fetch".into(), )); } - let plans = imap::fetch_mime_plans_batch(session, &[uid]).await?; + let plans = match imap::fetch_mime_plans_batch( + session.as_mut().ok_or(CoreError::Offline)?, + &[uid], + ) + .await + { + Ok(plans) => plans, + Err(error @ CoreError::ImapParse(_)) if allow_open_fallback => { + tracing::warn!(message_id, error = %error, "MIME plan unavailable; retrying full message on a fresh session"); + reconnect_body_session(ctx, config, session, folder_id).await?; + return fetch_full_open_fallback( + ctx, + config, + session.as_mut().ok_or(CoreError::Offline)?, + message_id, + folder_id, + uid, + ) + .await; + } + Err(error) => return Err(error), + }; plan = plans .into_iter() .find(|fetched| fetched.uid == uid) @@ -3134,8 +3177,15 @@ async fn fetch_selective_content( let Some(plan) = plan else { if allow_open_fallback { - return fetch_full_open_fallback(ctx, config, session, message_id, folder_id, uid) - .await; + return fetch_full_open_fallback( + ctx, + config, + session.as_mut().ok_or(CoreError::Offline)?, + message_id, + folder_id, + uid, + ) + .await; } return Err(CoreError::Mime( "server did not provide a usable BODYSTRUCTURE".into(), @@ -3151,7 +3201,12 @@ async fn fetch_selective_content( )); } let section_ids = plan.text_section_ids(); - let fetched = imap::fetch_content_sections(session, uid, §ion_ids).await?; + let fetched = imap::fetch_content_sections( + session.as_mut().ok_or(CoreError::Offline)?, + uid, + §ion_ids, + ) + .await?; decode_selective_content( PlannedBodyFetch { message_id, @@ -3164,14 +3219,24 @@ async fn fetch_selective_content( .await; let content = match selective { Ok(content) => content, - Err(error) if allow_open_fallback => { + Err(error @ (CoreError::ImapParse(_) | CoreError::Mime(_))) if allow_open_fallback => { tracing::warn!( message_id, error = %error, "selective content unavailable; using explicit-open full MIME fallback" ); - return fetch_full_open_fallback(ctx, config, session, message_id, folder_id, uid) - .await; + if matches!(error, CoreError::ImapParse(_)) { + reconnect_body_session(ctx, config, session, folder_id).await?; + } + return fetch_full_open_fallback( + ctx, + config, + session.as_mut().ok_or(CoreError::Offline)?, + message_id, + folder_id, + uid, + ) + .await; } Err(error) => return Err(error), }; @@ -3185,6 +3250,26 @@ async fn fetch_selective_content( .map(|_| ()) } +/// A parser failure can leave unread response bytes. Close the old socket +/// without LOGOUT, and verify the UID namespace before using its replacement. +async fn reconnect_body_session( + ctx: &SyncCtx, + config: &AccountConfig, + session: &mut Option, + folder_id: i64, +) -> Result<()> { + drop(session.take()); + let folder = ctx + .db + .read(move |conn| repo::folders::get(conn, folder_id)) + .await? + .ok_or_else(|| CoreError::NotFound("folder".into()))?; + let mut fresh = connect(ctx, config).await?; + select_folder_for_remote_read(ctx, &mut fresh, &folder).await?; + *session = Some(fresh); + Ok(()) +} + async fn fetch_full_open_fallback( ctx: &SyncCtx, config: &AccountConfig, @@ -3401,3 +3486,7 @@ async fn persist_body( } Ok(()) } + +#[cfg(all(test, not(any(target_os = "android", target_os = "ios"))))] +#[path = "mime_recovery_tests.rs"] +mod mime_recovery_tests; diff --git a/crates/flectar-mail-core/src/sync/mime_recovery_tests.rs b/crates/flectar-mail-core/src/sync/mime_recovery_tests.rs new file mode 100644 index 0000000..f46a662 --- /dev/null +++ b/crates/flectar-mail-core/src/sync/mime_recovery_tests.rs @@ -0,0 +1,474 @@ +//! Local TLS transcripts exercise the real async-imap decoder and sync paths. +use super::*; +use rustls::pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject}; +use std::sync::Mutex; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; + +const CERT: &str = include_str!("../../tests/fixtures/tls/server.pem"); +const KEY: &str = include_str!("../../tests/fixtures/tls/server-key.pem"); +const TEXT_PLAN: &str = "(\"TEXT\" \"PLAIN\" (\"CHARSET\" \"UTF-8\") NIL NIL \"7BIT\" 6 1)"; + +#[derive(Clone, Copy)] +enum Behavior { + Broken, + Denied, + Reauth, + Reset, + GlobalLegacy, + GlobalExtended, +} + +struct Fixture { + ctx: SyncCtx, + config: AccountConfig, + folder: Folder, + ids: Vec, + commands: Arc>>, + server: tokio::task::JoinHandle<()>, + _dir: tempfile::TempDir, +} +impl Drop for Fixture { + fn drop(&mut self) { + self.server.abort(); + } +} +fn raw(uid: u32) -> String { + format!( + "Message-ID: \r\nFrom: sender@example.test\r\nTo: reader@example.test\r\nSubject: Message {uid}\r\nDate: Tue, 01 Sep 2026 12:00:00 +0000\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nBody {uid}" + ) +} +fn uids(set: &str) -> Vec { + set.split(',') + .flat_map(|part| { + let mut ends = part.split(':').map(|n| n.parse::().unwrap()); + let first = ends.next().unwrap(); + first..=ends.next().unwrap_or(first) + }) + .collect() +} +async fn fixture(behavior: Behavior) -> (Fixture, Option) { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let port = listener.local_addr().unwrap().port(); + let tls = rustls::ServerConfig::builder_with_provider(Arc::new( + rustls::crypto::ring::default_provider(), + )) + .with_safe_default_protocol_versions() + .unwrap() + .with_no_client_auth() + .with_single_cert( + vec![CertificateDer::from_pem_slice(CERT.as_bytes()).unwrap()], + PrivateKeyDer::from_pem_slice(KEY.as_bytes()).unwrap(), + ) + .unwrap(); + let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(tls)); + let commands = Arc::new(Mutex::new(Vec::new())); + let log = commands.clone(); + let server = tokio::spawn(async move { + for connection in 0.. { + let (tcp, _) = listener.accept().await.unwrap(); + let mut stream = BufReader::new(acceptor.accept(tcp).await.unwrap()); + stream.write_all(b"* OK fixture ready\r\n").await.unwrap(); + loop { + let mut line = String::new(); + if stream.read_line(&mut line).await.unwrap_or(0) == 0 { + break; + } + log.lock().unwrap().push((connection, line.clone())); + let parts: Vec<_> = line.split_whitespace().collect(); + let tag = parts[0]; + let mut response = String::new(); + match parts[1] { + "LOGIN" if connection > 0 && matches!(behavior, Behavior::Reauth) => { + stream + .write_all( + format!("{tag} NO [AUTHENTICATIONFAILED] denied\r\n").as_bytes(), + ) + .await + .unwrap(); + continue; + } + "LOGIN" => {} + "SELECT" => { + let validity = if connection > 0 && matches!(behavior, Behavior::Reset) { + 8 + } else { + 7 + }; + response = + format!("* 3 EXISTS\r\n* OK [UIDVALIDITY {validity}] namespace\r\n"); + } + "UID" => { + assert_eq!(parts[2], "FETCH"); + let requested = uids(parts[3]); + if line.contains("BODYSTRUCTURE") && matches!(behavior, Behavior::Denied) { + stream + .write_all(format!("{tag} NO [NOPERM] denied\r\n").as_bytes()) + .await + .unwrap(); + continue; + } + for uid in requested { + if line.contains("BODYSTRUCTURE") { + if uid == 2 + && matches!( + behavior, + Behavior::GlobalLegacy | Behavior::GlobalExtended + ) + { + let plan = if matches!(behavior, Behavior::GlobalLegacy) { + "(\"MESSAGE\" \"GLOBAL\" NIL NIL NIL \"7BIT\" 123)" + .to_owned() + } else { + format!( + "(\"MESSAGE\" \"GLOBAL\" NIL NIL NIL \"7BIT\" 123 (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL) {TEXT_PLAN} 1)" + ) + }; + response.push_str(&format!( + "* {uid} FETCH (UID {uid} BODYSTRUCTURE {plan})\r\n" + )); + continue; + } + if uid == 2 { + response + .push_str("* 2 FETCH (UID 2 BODYSTRUCTURE (BOGUS))\r\n"); + break; + } + response.push_str(&format!( + "* {uid} FETCH (UID {uid} BODYSTRUCTURE {TEXT_PLAN})\r\n" + )); + } else if line.contains("HEADER.FIELDS") { + let header = raw(uid); + response.push_str(&format!("* {uid} FETCH (UID {uid} FLAGS () BODY[HEADER.FIELDS (FROM TO SUBJECT DATE MESSAGE-ID CONTENT-TYPE)] {{{}}}\r\n{header})\r\n", header.len())); + } else if line.contains("BODY.PEEK[]") { + let message = raw(uid); + response.push_str(&format!( + "* {uid} FETCH (UID {uid} BODY[] {{{}}}\r\n{message})\r\n", + message.len() + )); + } else { + assert!(line.contains("BODY.PEEK[1]"), "unexpected fetch: {line}"); + let mime = "Content-Type: text/plain; charset=utf-8\r\n\r\n"; + let body = format!("Body {uid}"); + response.push_str(&format!("* {uid} FETCH (UID {uid} BODY[1.MIME] {{{}}}\r\n{mime} BODY[1] {{{}}}\r\n{body})\r\n", mime.len(), body.len())); + } + } + } + "LOGOUT" => { + response.push_str("* BYE closing\r\n"); + } + other => panic!("unexpected command: {other}"), + } + response.push_str(&format!("{tag} OK complete\r\n")); + stream.write_all(response.as_bytes()).await.unwrap(); + if parts[1] == "LOGOUT" { + break; + } + } + } + }); + let dir = tempfile::tempdir().unwrap(); + let paths = Arc::new(Paths::for_tests(dir.path())); + let db = Db::open(&dir.path().join("mail.db")).unwrap(); + let calendar_db = Db::open_calendar(&dir.path().join("calendar.db")).unwrap(); + let credentials: CredentialStoreHandle = Arc::new( + credentials::DevelopmentFileCredentialStore::new(dir.path().join("credentials.json")), + ); + let tokens = TokenProvider::new( + credentials.clone(), + Arc::new(crate::oauth::redirect::LoopbackRedirectBroker::default()), + ); + let ctx = SyncCtx { + db, + calendar_db, + bus: EventBus::new(), + paths, + tokens, + credentials, + }; + let (mut config, folder) = ctx + .db + .write(move |conn| { + let id = repo::accounts::insert( + conn, + &repo::accounts::NewAccount { + email: "reader@example.test", + display_name: None, + avatar_url: None, + provider: Provider::Imap, + auth_kind: AuthKind::Password, + mail_protocol: MailProtocol::Imap, + username: "reader@example.test", + jmap_url: "", + jmap_account_id: None, + imap_host: "127.0.0.1", + imap_port: port, + smtp_host: "127.0.0.1", + smtp_port: 465, + }, + )?; + let folder_id = + repo::folders::upsert(conn, id, "INBOX", Some("/"), Some(roles::INBOX))?; + repo::folders::set_uid_state(conn, folder_id, Some(7), Some(4), None)?; + Ok(( + repo::accounts::get_config(conn, id)?.unwrap(), + repo::folders::get(conn, folder_id)?.unwrap(), + )) + }) + .await + .unwrap(); + config.settings.connection = MailConnectionSettings { + imap_security: ConnectionSecurity::Tls, + smtp_security: ConnectionSecurity::Tls, + trusted_certificate_pem: CERT.into(), + }; + credentials::store_async( + ctx.credentials.clone(), + config.id, + Slot::Password, + "fixture-secret".into(), + ) + .await + .unwrap(); + let mut session = connect(&ctx, &config).await.unwrap(); + select_folder_for_remote_read(&ctx, &mut session, &folder) + .await + .unwrap(); + let headers = imap::fetch_headers(&mut session, "1:3").await.unwrap(); + store_headers(&ctx, &config, &folder, headers, None) + .await + .unwrap(); + let folder_id = folder.id; + let ids = ctx + .db + .read(move |conn| { + (1..=3) + .map(|uid| { + Ok(repo::messages::by_folder_uid(conn, folder_id, uid)? + .unwrap() + .id) + }) + .collect::>>() + }) + .await + .unwrap(); + ( + Fixture { + ctx, + config, + folder, + ids, + commands, + server, + _dir: dir, + }, + Some(session), + ) +} +async fn open_second(f: &Fixture, session: &mut Option) -> Result<()> { + let id = f.ids[1]; + f.ctx + .db + .write(move |conn| { + conn.execute( + "UPDATE messages SET body_state='fetching' WHERE id=?1", + [id], + )?; + Ok(()) + }) + .await + .unwrap(); + tokio::time::timeout( + std::time::Duration::from_secs(10), + fetch_one_body(&f.ctx, &f.config, session, id), + ) + .await + .unwrap() +} + +#[tokio::test] +async fn headers_survive_parse_failure_and_open_reconnects_without_marking_seen() { + let (f, mut session) = fixture(Behavior::Broken).await; + open_second(&f, &mut session).await.unwrap(); + let ids = f.ids.clone(); + f.ctx + .db + .read(move |conn| { + for id in &ids { + assert!(repo::messages::get_row(conn, *id)?.is_some()); + } + let row = repo::messages::get_row(conn, ids[1])?.unwrap(); + assert_eq!(row.body_state, "cached"); + assert!(!row.is_read); + assert!(row.raw_path.is_some()); + Ok(()) + }) + .await + .unwrap(); + let commands = f.commands.lock().unwrap(); + assert!( + commands + .iter() + .any(|(c, line)| *c == 0 && line.contains("BODYSTRUCTURE")) + ); + assert!( + commands + .iter() + .any(|(c, line)| *c == 1 && line.contains("SELECT")) + ); + assert!( + commands + .iter() + .any(|(c, line)| *c == 1 && line.contains("BODY.PEEK[]")) + ); + assert!( + !commands + .iter() + .any(|(_, line)| line.contains("STORE") || line.contains("LOGOUT")) + ); +} + +#[tokio::test] +async fn background_isolates_bad_uid_and_caches_both_siblings() { + let (f, session) = fixture(Behavior::Broken).await; + drop(session); + let items = f + .ids + .iter() + .enumerate() + .map(|(i, id)| (*id, i as i64 + 1)) + .collect(); + let queue = Arc::new(tokio::sync::Mutex::new(std::collections::VecDeque::from([ + BodyChunk { + folder_id: f.folder.id, + folder_name: "INBOX".into(), + uid_validity: Some(7), + items, + }, + ]))); + let skip = Arc::new(Mutex::new(Default::default())); + let persisted = Arc::new(std::sync::atomic::AtomicU64::new(0)); + let (_settings, rx) = watch::channel(f.config.settings.clone()); + tokio::time::timeout( + std::time::Duration::from_secs(10), + body_worker( + f.ctx.clone(), + f.config.clone(), + queue, + skip, + persisted.clone(), + rx, + ), + ) + .await + .unwrap() + .unwrap(); + assert_eq!(persisted.load(std::sync::atomic::Ordering::Relaxed), 2); + let ids = f.ids.clone(); + f.ctx + .db + .read(move |conn| { + for i in [0, 2] { + assert_eq!( + repo::messages::get_row(conn, ids[i])?.unwrap().body_state, + "cached" + ); + } + assert_eq!( + repo::messages::get_row(conn, ids[1])?.unwrap().body_state, + "none" + ); + assert!( + conn.query_row( + "SELECT count(*) FROM sync_failures WHERE message_id=?1", + [ids[1]], + |r| r.get::<_, i64>(0) + )? > 0 + ); + Ok(()) + }) + .await + .unwrap(); + assert!( + !f.commands + .lock() + .unwrap() + .iter() + .any(|(_, line)| line.contains("BODY.PEEK[]")) + ); +} + +#[tokio::test] +async fn rejected_fetch_does_not_trigger_compatibility_fallback() { + let (f, mut session) = fixture(Behavior::Denied).await; + let result = open_second(&f, &mut session).await; + assert!( + matches!(result, Err(CoreError::Imap(_))), + "{result:?}: {:?}", + f.commands.lock().unwrap() + ); + assert!( + f.commands + .lock() + .unwrap() + .iter() + .all(|(c, line)| *c == 0 && !line.contains("BODY.PEEK[]")) + ); +} + +#[tokio::test] +async fn reconnect_authentication_failure_is_returned_without_fetching_body() { + let (f, mut session) = fixture(Behavior::Reauth).await; + assert!(matches!( + open_second(&f, &mut session).await, + Err(CoreError::Auth(_)) + )); + assert!(session.is_none()); + assert!( + !f.commands + .lock() + .unwrap() + .iter() + .any(|(_, line)| line.contains("BODY.PEEK[]")) + ); +} + +#[tokio::test] +async fn changed_uidvalidity_prevents_full_message_fetch() { + let (f, mut session) = fixture(Behavior::Reset).await; + let result = open_second(&f, &mut session).await; + assert!( + matches!(result, Err(CoreError::Imap(_))), + "{result:?}: {:?}", + f.commands.lock().unwrap() + ); + assert!(session.is_none()); + assert!( + !f.commands + .lock() + .unwrap() + .iter() + .any(|(_, line)| line.contains("BODY.PEEK[]")) + ); +} + +// Both legal message/global representations must remain readable even when +// the pinned IMAP parser cannot build a selective plan for one of them. +#[tokio::test] +async fn legal_message_global_variants_remain_readable() { + for behavior in [Behavior::GlobalLegacy, Behavior::GlobalExtended] { + let (f, mut session) = fixture(behavior).await; + open_second(&f, &mut session).await.unwrap(); + let id = f.ids[1]; + f.ctx + .db + .read(move |conn| { + assert_eq!( + repo::messages::get_row(conn, id)?.unwrap().body_state, + "cached" + ); + Ok(()) + }) + .await + .unwrap(); + } +} diff --git a/lang/es/LC_MESSAGES/flectar-mail.po b/lang/es/LC_MESSAGES/flectar-mail.po index a3ab58e..2147e5a 100644 --- a/lang/es/LC_MESSAGES/flectar-mail.po +++ b/lang/es/LC_MESSAGES/flectar-mail.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: flectar-mail 0.1.0\n" -"POT-Creation-Date: 2026-09-11 13:50+0000\n" +"POT-Creation-Date: 2026-09-12 22:12+0000\n" "PO-Revision-Date: 2026-08-31 02:35+0000\n" "Last-Translator: Flectar contributors\n" "Language-Team: Spanish\n" @@ -152,7 +152,7 @@ msgstr "Historial de conversaciones disponible" msgid "New address-book entry" msgstr "Nueva entrada de contactos" -#: ui/app.slint:95 ui/app.slint:686 ui/components/composer-fields.slint:63 +#: ui/app.slint:95 ui/app.slint:694 ui/components/composer-fields.slint:63 #: ui/dialogs/compose-dialog.slint:34 msgid "No connected account" msgstr "No hay ninguna cuenta conectada" @@ -167,11 +167,11 @@ msgstr[1] "{n} interacciones" msgid "Last contacted {} {}, {}" msgstr "Último contacto: {1} de {0} de {2}" -#: ui/app.slint:102 ui/app.slint:299 +#: ui/app.slint:102 ui/app.slint:305 msgid "Loading local data…" msgstr "Cargando datos locales…" -#: ui/app.slint:103 ui/app.slint:300 +#: ui/app.slint:103 ui/app.slint:306 msgid "Local data ready." msgstr "Datos locales listos." @@ -395,340 +395,364 @@ msgstr "" "ajustando." #: ui/app.slint:198 +msgid "Account removed from this device." +msgstr "Cuenta eliminada de este dispositivo." + +#: ui/app.slint:199 +msgid "Account removed. Mail refresh will retry." +msgstr "Cuenta eliminada. Se volverá a intentar actualizar el correo." + +#: ui/app.slint:200 +msgid "Account removed; local cleanup needs a retry: {}" +msgstr "Cuenta eliminada; hay que volver a intentar la limpieza local: {}" + +#: ui/app.slint:201 +msgid "Could not remove account: {}" +msgstr "No se pudo eliminar la cuenta: {}" + +#: ui/app.slint:202 +msgid "Removing account…" +msgstr "Eliminando cuenta…" + +#: ui/app.slint:203 +msgid "Synchronizing account…" +msgstr "Sincronizando cuenta…" + +#: ui/app.slint:204 msgid "Could not update mail history: {}" msgstr "No se pudo actualizar el historial de correo: {}" -#: ui/app.slint:199 +#: ui/app.slint:205 msgid "Connected {}. Initial sync is running in the background." msgstr "Se conectó {}. La sincronización inicial se ejecuta en segundo plano." -#: ui/app.slint:200 +#: ui/app.slint:206 msgid "Account setup failed: {}" msgstr "Falló la configuración de la cuenta: {}" -#: ui/app.slint:201 +#: ui/app.slint:207 msgid "OAuth failed: {}" msgstr "Falló OAuth: {}" -#: ui/app.slint:202 +#: ui/app.slint:208 msgid "Reconnected {}. Synchronization is resuming." msgstr "Se volvió a conectar {}. La sincronización se está reanudando." -#: ui/app.slint:203 +#: ui/app.slint:209 msgid "Reconnection failed: {}" msgstr "Falló la reconexión: {}" -#: ui/app.slint:204 +#: ui/app.slint:210 msgid "Calendar connected and initial sync started." msgstr "Calendario conectado y sincronización inicial iniciada." -#: ui/app.slint:205 +#: ui/app.slint:211 msgid "Could not connect calendar: {}" msgstr "No se pudo conectar el calendario: {}" -#: ui/app.slint:206 +#: ui/app.slint:212 msgid "Calendar sync enabled." msgstr "Sincronización del calendario activada." -#: ui/app.slint:207 +#: ui/app.slint:213 msgid "Calendar sync paused." msgstr "Sincronización del calendario en pausa." -#: ui/app.slint:208 +#: ui/app.slint:214 msgid "Could not update calendar sync: {}" msgstr "No se pudo actualizar la sincronización del calendario: {}" -#: ui/app.slint:209 +#: ui/app.slint:215 msgid "CalDAV connected and initial sync started." msgstr "CalDAV conectado y sincronización inicial iniciada." -#: ui/app.slint:210 +#: ui/app.slint:216 msgid "Could not connect CalDAV: {}" msgstr "No se pudo conectar CalDAV: {}" -#: ui/app.slint:215 +#: ui/app.slint:221 msgid "Connect an account before creating a backup." msgstr "Conecta una cuenta antes de crear una copia de seguridad." -#: ui/app.slint:216 +#: ui/app.slint:222 msgid "Export Flectar Mail account backup" msgstr "Exportar copia de seguridad de cuentas de Flectar Mail" -#: ui/app.slint:217 +#: ui/app.slint:223 msgid "Import Flectar Mail account backup" msgstr "Importar copia de seguridad de cuentas de Flectar Mail" -#: ui/app.slint:218 +#: ui/app.slint:224 msgid "Flectar Mail backup" msgstr "Copia de seguridad de Flectar Mail" -#: ui/app.slint:219 +#: ui/app.slint:225 msgid "Choose where to export the database snapshot" msgstr "Elige dónde exportar la instantánea de la base de datos" -#: ui/app.slint:220 +#: ui/app.slint:226 msgid "The platform host does not provide document export." msgstr "La plataforma no permite exportar documentos." -#: ui/app.slint:221 +#: ui/app.slint:227 msgid "The platform host does not provide document import." msgstr "La plataforma no permite importar documentos." -#: ui/app.slint:222 +#: ui/app.slint:228 msgid "Backup exported. Passwords and OAuth tokens were not included." msgstr "" "Copia de seguridad exportada. No se incluyeron contraseñas ni tokens de " "OAuth." -#: ui/app.slint:223 +#: ui/app.slint:229 msgid "Could not export backup: {}" msgstr "No se pudo exportar la copia de seguridad: {}" -#: ui/app.slint:224 +#: ui/app.slint:230 msgid "Local data storage is unavailable. Retry startup." msgstr "" "El almacenamiento de datos locales no está disponible. Vuelve a intentar el " "inicio." -#: ui/app.slint:225 +#: ui/app.slint:231 msgid "Creating verified database snapshot…" msgstr "Creando una instantánea verificada de la base de datos…" -#: ui/app.slint:226 +#: ui/app.slint:232 msgid "Backup imported. All {} account configurations already existed." msgstr "" "Copia de seguridad importada. Las {} configuraciones de cuenta ya existían." -#: ui/app.slint:227 +#: ui/app.slint:233 msgid "" "Imported {} account configurations. Open Accounts and sign in to each one." msgstr "" "Se importaron {} configuraciones de cuenta. Abre Cuentas e inicia sesión en " "cada una." -#: ui/app.slint:228 +#: ui/app.slint:234 msgid "Could not import backup: {}" msgstr "No se pudo importar la copia de seguridad: {}" -#: ui/app.slint:229 +#: ui/app.slint:235 msgid "All local Flectar Mail data was deleted." msgstr "Se eliminaron todos los datos locales de Flectar Mail." -#: ui/app.slint:230 +#: ui/app.slint:236 msgid "Could not delete all local data: {}" msgstr "No se pudieron eliminar todos los datos locales: {}" -#: ui/app.slint:231 +#: ui/app.slint:237 msgid "Deleting local accounts and cached data…" msgstr "Eliminando las cuentas locales y los datos en caché…" -#: ui/app.slint:236 +#: ui/app.slint:242 msgid "Background mail refresh failed: {}" msgstr "Falló la actualización del correo en segundo plano: {}" -#: ui/app.slint:237 +#: ui/app.slint:243 msgid "No messages in this folder" msgstr "No hay mensajes en esta carpeta" -#: ui/app.slint:238 +#: ui/app.slint:244 msgid "No messages match this search" msgstr "Ningún mensaje coincide con esta búsqueda" -#: ui/app.slint:239 +#: ui/app.slint:245 msgid "Mail core is ready for account synchronization and message actions." msgstr "" "El núcleo de correo está listo para sincronizar cuentas y realizar acciones " "con mensajes." -#: ui/app.slint:240 +#: ui/app.slint:246 msgid "Showing {} of {} message" msgstr "Mostrando {} de {} mensaje" -#: ui/app.slint:241 +#: ui/app.slint:247 msgid "Showing {} of {} message · more available" msgstr "Mostrando {} de {} mensaje · hay más disponibles" -#: ui/app.slint:242 +#: ui/app.slint:248 msgid "Showing {} of {} message · current results complete" msgstr "Mostrando {} de {} mensaje · resultados actuales completos" -#: ui/app.slint:243 +#: ui/app.slint:249 msgid "Showing {} of {} messages" msgstr "Mostrando {} de {} mensajes" -#: ui/app.slint:244 +#: ui/app.slint:250 msgid "Showing {} of {} messages · more available" msgstr "Mostrando {} de {} mensajes · hay más disponibles" -#: ui/app.slint:245 +#: ui/app.slint:251 msgid "Showing {} of {} messages · current results complete" msgstr "Mostrando {} de {} mensajes · resultados actuales completos" -#: ui/app.slint:246 +#: ui/app.slint:252 msgid "Showing {} of {} message matching \"{}\"" msgstr "Mostrando {} de {} mensaje que coincide con «{}»" -#: ui/app.slint:247 +#: ui/app.slint:253 msgid "Showing {} of {} message matching \"{}\" · more available" msgstr "Mostrando {} de {} mensaje que coincide con «{}» · hay más disponibles" -#: ui/app.slint:248 +#: ui/app.slint:254 msgid "Showing {} of {} message matching \"{}\" · current results complete" msgstr "" "Mostrando {} de {} mensaje que coincide con «{}» · resultados actuales " "completos" -#: ui/app.slint:249 +#: ui/app.slint:255 msgid "Showing {} of {} messages matching \"{}\"" msgstr "Mostrando {} de {} mensajes que coinciden con «{}»" -#: ui/app.slint:250 +#: ui/app.slint:256 msgid "Showing {} of {} messages matching \"{}\" · more available" msgstr "" "Mostrando {} de {} mensajes que coinciden con «{}» · hay más disponibles" -#: ui/app.slint:251 +#: ui/app.slint:257 msgid "Showing {} of {} messages matching \"{}\" · current results complete" msgstr "" "Mostrando {} de {} mensajes que coinciden con «{}» · resultados actuales " "completos" -#: ui/app.slint:252 +#: ui/app.slint:258 msgid "Rendered with software fallback after GPU error: {}" msgstr "Renderizado por software tras un error de la GPU: {}" -#: ui/app.slint:253 +#: ui/app.slint:259 msgid "Blitz render failed (GPU: {}; software: {})" msgstr "Falló la renderización de Blitz (GPU: {}; software: {})" -#: ui/app.slint:258 +#: ui/app.slint:264 msgid "Add an email address, then save the contact." msgstr "Añade una dirección de correo y guarda el contacto." -#: ui/app.slint:259 +#: ui/app.slint:265 msgid "Enter a valid email address before saving." msgstr "Introduce una dirección de correo válida antes de guardar." -#: ui/app.slint:260 +#: ui/app.slint:266 msgid "Could not save: contact storage is unavailable." msgstr "No se pudo guardar: el almacenamiento de contactos no está disponible." -#: ui/app.slint:261 +#: ui/app.slint:267 msgid "Could not save contact: {}" msgstr "No se pudo guardar el contacto: {}" -#: ui/app.slint:262 +#: ui/app.slint:268 msgid "Contact saved." msgstr "Contacto guardado." -#: ui/app.slint:263 +#: ui/app.slint:269 msgid "Could not delete: contact storage is unavailable." msgstr "" "No se pudo eliminar: el almacenamiento de contactos no está disponible." -#: ui/app.slint:264 +#: ui/app.slint:270 msgid "Could not delete contact: {}" msgstr "No se pudo eliminar el contacto: {}" -#: ui/app.slint:265 +#: ui/app.slint:271 msgid "Contact deleted." msgstr "Contacto eliminado." -#: ui/app.slint:266 +#: ui/app.slint:272 msgid "Could not load contacts: {}" msgstr "No se pudieron cargar los contactos: {}" -#: ui/app.slint:267 +#: ui/app.slint:273 msgid "Local contact storage is still starting…" msgstr "El almacenamiento local de contactos aún se está iniciando…" -#: ui/app.slint:268 +#: ui/app.slint:274 msgid "Loading contacts…" msgstr "Cargando contactos…" -#: ui/app.slint:273 +#: ui/app.slint:279 msgid "Could not load calendar: {}" msgstr "No se pudo cargar el calendario: {}" -#: ui/app.slint:274 +#: ui/app.slint:280 msgid "Calendar shown and syncing." msgstr "Calendario visible y sincronizándose." -#: ui/app.slint:275 +#: ui/app.slint:281 msgid "Calendar hidden and sync paused." msgstr "Calendario oculto y sincronización en pausa." -#: ui/app.slint:276 +#: ui/app.slint:282 msgid "Calendar update failed: {}" msgstr "Falló la actualización del calendario: {}" -#: ui/app.slint:277 +#: ui/app.slint:283 msgid "Only events created in Flectar Mail can be edited." msgstr "Solo se pueden editar los eventos creados en Flectar Mail." -#: ui/app.slint:278 +#: ui/app.slint:284 msgid "Only events created in Flectar Mail can be deleted." msgstr "Solo se pueden eliminar los eventos creados en Flectar Mail." -#: ui/app.slint:279 +#: ui/app.slint:285 msgid "Calendar storage is unavailable." msgstr "El almacenamiento del calendario no está disponible." -#: ui/app.slint:280 +#: ui/app.slint:286 msgid "Could not delete event: {}" msgstr "No se pudo eliminar el evento: {}" -#: ui/app.slint:281 +#: ui/app.slint:287 msgid "Event deleted, but refresh failed: {}" msgstr "El evento se eliminó, pero falló la actualización: {}" -#: ui/app.slint:282 +#: ui/app.slint:288 msgid "Event deleted." msgstr "Evento eliminado." -#: ui/app.slint:283 +#: ui/app.slint:289 msgid "Use YYYY-MM-DD and a 24-hour time such as 09:30." msgstr "Usa AAAA-MM-DD y una hora en formato de 24 horas, como 09:30." -#: ui/app.slint:284 +#: ui/app.slint:290 msgid "Event duration must be a number of minutes." msgstr "La duración del evento debe ser un número de minutos." -#: ui/app.slint:285 +#: ui/app.slint:291 msgid "Add a title and choose a duration from 15 to 1440 minutes." msgstr "Añade un título y elige una duración de entre 15 y 1440 minutos." -#: ui/app.slint:286 +#: ui/app.slint:292 msgid "That local time does not exist in the current time zone." msgstr "Esa hora local no existe en la zona horaria actual." -#: ui/app.slint:287 +#: ui/app.slint:293 msgid "Could not save event: {}" msgstr "No se pudo guardar el evento: {}" -#: ui/app.slint:288 +#: ui/app.slint:294 msgid "Event saved, but refresh failed: {}" msgstr "El evento se guardó, pero falló la actualización: {}" -#: ui/app.slint:289 +#: ui/app.slint:295 msgid "Event saved. Provider sync will run when enabled." msgstr "" "Evento guardado. La sincronización del proveedor se ejecutará cuando esté " "activada." -#: ui/app.slint:294 +#: ui/app.slint:300 msgid "Email resource render failed: {}" msgstr "Falló la renderización de un recurso del correo: {}" -#: ui/app.slint:295 +#: ui/app.slint:301 msgid "Message refresh failed: {}" msgstr "Falló la actualización del mensaje: {}" -#: ui/app.slint:296 +#: ui/app.slint:302 msgid "Message load failed: {}" msgstr "Falló la carga del mensaje: {}" -#: ui/app.slint:297 +#: ui/app.slint:303 msgid "" "Your local mailbox could not be opened. No account operation was started. " "Retry, or copy the diagnostics for support." @@ -736,230 +760,230 @@ msgstr "" "No se pudo abrir tu buzón local. No se inició ninguna operación de cuenta. " "Vuelve a intentarlo o copia el diagnóstico para solicitar asistencia." -#: ui/app.slint:298 +#: ui/app.slint:304 msgid "Local storage startup failed." msgstr "Falló el inicio del almacenamiento local." -#: ui/app.slint:305 +#: ui/app.slint:311 msgid "Could not identify the dragged account." msgstr "No se pudo identificar la cuenta arrastrada." -#: ui/app.slint:306 +#: ui/app.slint:312 msgid "Account ordering requires local mail data." msgstr "Para ordenar las cuentas se necesitan los datos de correo locales." -#: ui/app.slint:307 +#: ui/app.slint:313 msgid "Could not find the account to reorder." msgstr "No se encontró la cuenta que se iba a reordenar." -#: ui/app.slint:308 +#: ui/app.slint:314 msgid "Saving account order…" msgstr "Guardando el orden de las cuentas…" -#: ui/app.slint:309 +#: ui/app.slint:315 msgid "Mail history requires local mail data." msgstr "El historial de correo necesita los datos de correo locales." -#: ui/app.slint:310 +#: ui/app.slint:316 msgid "Updating mail history…" msgstr "Actualizando el historial de correo…" -#: ui/app.slint:311 +#: ui/app.slint:317 msgid "Remote images are unavailable in this minimal build." msgstr "Las imágenes remotas no están disponibles en esta compilación mínima." -#: ui/app.slint:312 +#: ui/app.slint:318 msgid "Could not save remote-image setting: {}" msgstr "No se pudo guardar el ajuste de imágenes remotas: {}" -#: ui/app.slint:313 +#: ui/app.slint:319 msgid "Remote sender icons unavailable: {}" msgstr "Los iconos remotos de los remitentes no están disponibles: {}" -#: ui/app.slint:314 +#: ui/app.slint:320 msgid "Remote images unavailable: {}" msgstr "Las imágenes remotas no están disponibles: {}" -#: ui/app.slint:315 +#: ui/app.slint:321 msgid "Remote body images and sender favicons enabled." msgstr "Imágenes remotas del cuerpo e iconos de remitentes activados." -#: ui/app.slint:316 +#: ui/app.slint:322 msgid "Remote images blocked." msgstr "Imágenes remotas bloqueadas." -#: ui/app.slint:317 +#: ui/app.slint:323 msgid "Could not refresh message images: {}" msgstr "No se pudieron actualizar las imágenes del mensaje: {}" -#: ui/app.slint:318 +#: ui/app.slint:324 msgid "Remote-image loading is disabled." msgstr "La carga de imágenes remotas está desactivada." -#: ui/app.slint:319 +#: ui/app.slint:325 msgid "Could not load message images: {}" msgstr "No se pudieron cargar las imágenes del mensaje: {}" -#: ui/app.slint:320 +#: ui/app.slint:326 msgid "Sync finished, refresh failed: {}" msgstr "La sincronización terminó, pero falló la actualización: {}" -#: ui/app.slint:321 +#: ui/app.slint:327 msgid "Sync complete." msgstr "Sincronización completada." -#: ui/app.slint:322 +#: ui/app.slint:328 msgid "Sync failed: {}" msgstr "Falló la sincronización: {}" -#: ui/app.slint:323 +#: ui/app.slint:329 msgid "Local mail data is unavailable; preview data is read-only." msgstr "" "Los datos de correo locales no están disponibles; los datos de vista previa " "son de solo lectura." -#: ui/app.slint:324 +#: ui/app.slint:330 msgid "Synchronizing accounts…" msgstr "Sincronizando cuentas…" -#: ui/app.slint:329 +#: ui/app.slint:335 msgid "Message action completed." msgstr "Acción del mensaje completada." -#: ui/app.slint:330 +#: ui/app.slint:336 msgid "Message action failed: {}" msgstr "Falló la acción del mensaje: {}" -#: ui/app.slint:331 +#: ui/app.slint:337 msgid "Message action failed: message is no longer available" msgstr "Falló la acción del mensaje: el mensaje ya no está disponible" -#: ui/app.slint:332 +#: ui/app.slint:338 msgid "Label added." msgstr "Etiqueta agregada." -#: ui/app.slint:333 +#: ui/app.slint:339 msgid "Label removed." msgstr "Etiqueta eliminada." -#: ui/app.slint:334 +#: ui/app.slint:340 msgid "Label created and added." msgstr "Etiqueta creada y agregada." -#: ui/app.slint:335 +#: ui/app.slint:341 msgid "Label updated." msgstr "Etiqueta actualizada." -#: ui/app.slint:336 +#: ui/app.slint:342 msgid "Label deleted." msgstr "Etiqueta eliminada." -#: ui/app.slint:337 +#: ui/app.slint:343 msgid "Could not update label: {}" msgstr "No se pudo actualizar la etiqueta: {}" -#: ui/app.slint:338 +#: ui/app.slint:344 msgid "Could not save label: {}" msgstr "No se pudo guardar la etiqueta: {}" -#: ui/app.slint:339 +#: ui/app.slint:345 msgid "Could not delete label: {}" msgstr "No se pudo eliminar la etiqueta: {}" -#: ui/app.slint:340 +#: ui/app.slint:346 msgid "Folder created." msgstr "Carpeta creada." -#: ui/app.slint:341 +#: ui/app.slint:347 msgid "Folder renamed." msgstr "Carpeta renombrada." -#: ui/app.slint:342 +#: ui/app.slint:348 msgid "Folder deleted." msgstr "Carpeta eliminada." -#: ui/app.slint:343 +#: ui/app.slint:349 msgid "Could not create folder: {}" msgstr "No se pudo crear la carpeta: {}" -#: ui/app.slint:344 +#: ui/app.slint:350 msgid "Could not rename folder: {}" msgstr "No se pudo renombrar la carpeta: {}" -#: ui/app.slint:345 +#: ui/app.slint:351 msgid "Could not delete folder: {}" msgstr "No se pudo eliminar la carpeta: {}" -#: ui/app.slint:346 +#: ui/app.slint:352 msgid "Could not close preview: {}" msgstr "No se pudo cerrar la vista previa: {}" -#: ui/app.slint:347 +#: ui/app.slint:353 msgid "Opened the system print view." msgstr "Se abrió la vista de impresión del sistema." -#: ui/app.slint:348 +#: ui/app.slint:354 msgid "Could not print message: {}" msgstr "No se pudo imprimir el mensaje: {}" -#: ui/app.slint:349 +#: ui/app.slint:355 msgid "Opened the message in your browser." msgstr "Se abrió el mensaje en el navegador." -#: ui/app.slint:350 +#: ui/app.slint:356 msgid "Could not open message in browser: {}" msgstr "No se pudo abrir el mensaje en el navegador: {}" -#: ui/app.slint:351 +#: ui/app.slint:357 msgid "Connect an account before replying or forwarding." msgstr "Conecta una cuenta antes de responder o reenviar." -#: ui/app.slint:352 +#: ui/app.slint:358 msgid "Choose a message first." msgstr "Elige primero un mensaje." -#: ui/app.slint:353 +#: ui/app.slint:359 msgid "Only drafts can be reopened for editing." msgstr "Solo se pueden volver a abrir borradores para editarlos." -#: ui/app.slint:354 +#: ui/app.slint:360 msgid "Could not open draft: {}" msgstr "No se pudo abrir el borrador: {}" -#: ui/app.slint:355 +#: ui/app.slint:361 msgid "Could not open draft: invalid account identifier" msgstr "" "No se pudo abrir el borrador: el identificador de la cuenta no es válido" -#: ui/app.slint:356 +#: ui/app.slint:362 msgid "Connected account" msgstr "Cuenta conectada" -#: ui/app.slint:357 +#: ui/app.slint:363 msgid "Could not open composer: {}" msgstr "No se pudo abrir el editor: {}" -#: ui/app.slint:358 +#: ui/app.slint:364 msgid "Could not open composer: invalid account identifier" msgstr "No se pudo abrir el editor: el identificador de la cuenta no es válido" -#: ui/app.slint:363 +#: ui/app.slint:369 msgid "Attach files to this email" msgstr "Adjuntar archivos a este correo" -#: ui/app.slint:364 +#: ui/app.slint:370 msgid "The platform host does not provide a document picker." msgstr "La plataforma no proporciona un selector de documentos." -#: ui/app.slint:365 +#: ui/app.slint:371 msgid "Added 1 attachment." msgstr "Se añadió 1 archivo adjunto." -#: ui/app.slint:366 +#: ui/app.slint:372 msgid "Added {} attachments." msgstr "Se añadieron {} archivos adjuntos." -#: ui/app.slint:367 +#: ui/app.slint:373 msgid "" "Attachments added: {}. Skipped: {} (duplicate, unreadable, or over the 25 MB " "total limit)." @@ -967,57 +991,57 @@ msgstr "" "Archivos adjuntos añadidos: {}. Omitidos: {} (duplicados, ilegibles o por " "superar el límite total de 25 MB)." -#: ui/app.slint:368 +#: ui/app.slint:374 msgid "Attachment removed." msgstr "Archivo adjunto eliminado." -#: ui/app.slint:369 +#: ui/app.slint:375 msgid "Choose the account this message should be sent from." msgstr "Elige la cuenta desde la que se debe enviar este mensaje." -#: ui/app.slint:370 +#: ui/app.slint:376 msgid "Connect an account before saving or sending mail." msgstr "Conecta una cuenta antes de guardar o enviar correo." -#: ui/app.slint:371 +#: ui/app.slint:377 msgid "Message queued for delivery (action {})." msgstr "Mensaje en cola para su entrega (acción {})." -#: ui/app.slint:372 +#: ui/app.slint:378 msgid "Draft saved locally (#{})." msgstr "Borrador guardado localmente (#{})." -#: ui/app.slint:373 +#: ui/app.slint:379 msgid "Compose failed: {}" msgstr "Falló la redacción: {}" -#: ui/app.slint:374 +#: ui/app.slint:380 msgid "IMAP port must be between 1 and 65535." msgstr "El puerto IMAP debe estar entre 1 y 65535." -#: ui/app.slint:375 +#: ui/app.slint:381 msgid "SMTP port must be between 1 and 65535." msgstr "El puerto SMTP debe estar entre 1 y 65535." -#: ui/app.slint:376 +#: ui/app.slint:382 msgid "Local mail data is unavailable. Retry startup." msgstr "" "Los datos de correo locales no están disponibles. Vuelve a intentar el " "inicio." -#: ui/app.slint:377 +#: ui/app.slint:383 msgid "Discovering JMAP and checking capabilities…" msgstr "Detectando JMAP y comprobando sus capacidades…" -#: ui/app.slint:378 +#: ui/app.slint:384 msgid "Certificate imported. Connect to verify the server." msgstr "Certificado importado. Conecta para verificar el servidor." -#: ui/app.slint:379 +#: ui/app.slint:385 msgid "Could not load account preferences: {}" msgstr "No se pudieron cargar las preferencias de la cuenta: {}" -#: ui/app.slint:380 +#: ui/app.slint:386 msgid "" "Your edited signature was preserved. Choose No signature to keep it as " "message text before inserting another." @@ -1025,19 +1049,19 @@ msgstr "" "Se conservó la firma que editaste. Elige Sin firma para mantenerla como " "texto del mensaje antes de insertar otra." -#: ui/app.slint:381 +#: ui/app.slint:387 msgid "Certificate import failed: {}" msgstr "Error al importar el certificado: {}" -#: ui/app.slint:382 +#: ui/app.slint:388 msgid "Paste the PEM certificate into the certificate field." msgstr "Pega el certificado PEM en el campo del certificado." -#: ui/app.slint:383 +#: ui/app.slint:389 msgid "Testing IMAP and SMTP connections…" msgstr "Comprobando las conexiones IMAP y SMTP…" -#: ui/app.slint:388 +#: ui/app.slint:394 msgid "" "Another browser authorization is already in progress; finish it or choose " "Cancel." @@ -1045,107 +1069,107 @@ msgstr "" "Ya hay otra autorización del navegador en curso; termínala o selecciona " "Cancelar." -#: ui/app.slint:389 +#: ui/app.slint:395 msgid "Add app keys in Sign-in settings to enable this provider." msgstr "" "Añade claves de aplicación en los ajustes de inicio de sesión para habilitar " "este proveedor." -#: ui/app.slint:390 +#: ui/app.slint:396 msgid "Choose Gmail or Outlook OAuth." msgstr "Elige OAuth de Gmail o Outlook." -#: ui/app.slint:391 +#: ui/app.slint:397 msgid "Complete sign-in in your browser, or choose Cancel here." msgstr "" "Completa el inicio de sesión en el navegador o selecciona Cancelar aquí." -#: ui/app.slint:392 +#: ui/app.slint:398 msgid "Cancelling browser sign-in…" msgstr "Cancelando el inicio de sesión en el navegador…" -#: ui/app.slint:393 +#: ui/app.slint:399 msgid "Finishing browser sign-in…" msgstr "Finalizando el inicio de sesión en el navegador…" -#: ui/app.slint:394 +#: ui/app.slint:400 msgid "Could not use that browser address: {}" msgstr "No se pudo usar esa dirección del navegador: {}" -#: ui/app.slint:395 +#: ui/app.slint:401 msgid "Calendar core is unavailable." msgstr "El núcleo del calendario no está disponible." -#: ui/app.slint:396 +#: ui/app.slint:402 msgid "Account no longer exists." msgstr "La cuenta ya no existe." -#: ui/app.slint:397 +#: ui/app.slint:403 msgid "Authorize calendar access in your browser." msgstr "Autoriza el acceso al calendario en el navegador." -#: ui/app.slint:398 +#: ui/app.slint:404 msgid "OAuth app keys saved." msgstr "Claves de la aplicación OAuth guardadas." -#: ui/app.slint:399 +#: ui/app.slint:405 msgid "{} OAuth settings saved." msgstr "Ajustes de OAuth de {} guardados." -#: ui/app.slint:400 +#: ui/app.slint:406 msgid "OAuth settings failed: {}" msgstr "Fallaron los ajustes de OAuth: {}" -#: ui/app.slint:401 +#: ui/app.slint:407 msgid "Mail refresh failed: {}" msgstr "Falló la actualización del correo: {}" -#: ui/app.slint:402 +#: ui/app.slint:408 msgid "Mail filter failed: {}" msgstr "Falló el filtro de correo: {}" -#: ui/app.slint:403 +#: ui/app.slint:409 msgid "Mail pagination failed: {}" msgstr "Falló la paginación del correo: {}" -#: ui/app.slint:404 +#: ui/app.slint:410 msgid "Could not mark message as read: {}" msgstr "No se pudo marcar el mensaje como leído: {}" -#: ui/app.slint:405 ui/views/mail-reading-pane.slint:755 +#: ui/app.slint:411 ui/views/mail-reading-pane.slint:755 msgid "Loading message…" msgstr "Cargando mensaje…" -#: ui/app.slint:411 ui/dialogs/settings-dialog.slint:971 ui/tray.slint:9 +#: ui/app.slint:417 ui/dialogs/settings-dialog.slint:1039 ui/tray.slint:9 #: ui/views/onboarding-view.slint:97 msgid "Flectar Mail" msgstr "Flectar Mail" -#: ui/app.slint:1138 +#: ui/app.slint:1146 msgid "Showing {} of {} contacts" msgstr "Mostrando {} de {} contactos" -#: ui/app.slint:1629 +#: ui/app.slint:1639 msgid "Mailbox startup failed" msgstr "Error al iniciar el buzón" -#: ui/app.slint:1635 +#: ui/app.slint:1645 msgid "Flectar Mail could not open local storage" msgstr "Flectar Mail no pudo abrir el almacenamiento local" -#: ui/app.slint:1648 +#: ui/app.slint:1658 msgid "Diagnostic ID: {}" msgstr "ID de diagnóstico: {}" -#: ui/app.slint:1651 +#: ui/app.slint:1661 msgid "Diagnostic ID {}" msgstr "ID de diagnóstico {}" -#: ui/app.slint:1656 +#: ui/app.slint:1666 msgid "Retry" msgstr "Reintentar" -#: ui/app.slint:1664 +#: ui/app.slint:1674 msgid "Copy diagnostics" msgstr "Copiar datos de diagnóstico" @@ -1441,7 +1465,7 @@ msgstr "MIS CALENDARIOS" #: ui/components/calendar.slint:268 ui/components/calendar.slint:278 #: ui/components/calendar.slint:317 ui/components/product-rail.slint:111 -#: ui/dialogs/settings-dialog.slint:733 +#: ui/dialogs/settings-dialog.slint:787 msgid "Calendar" msgstr "Calendario" @@ -1548,7 +1572,7 @@ msgstr "¿Eliminar este evento?" msgid "This event will be removed from your calendar." msgstr "Se eliminará este evento de tu calendario." -#: ui/components/calendar.slint:833 ui/components/confirmation-dialog.slint:11 +#: ui/components/calendar.slint:833 ui/components/confirmation-dialog.slint:12 msgid "This action cannot be undone." msgstr "Esta acción no se puede deshacer." @@ -1649,7 +1673,7 @@ msgid "Remove link" msgstr "Eliminar enlace" #: ui/components/composer-actions.slint:393 ui/dialogs/folder-dialog.slint:118 -#: ui/dialogs/settings-dialog.slint:799 ui/views/contacts-view.slint:538 +#: ui/dialogs/settings-dialog.slint:867 ui/views/contacts-view.slint:538 #: ui/views/files-view.slint:534 msgid "Cancel" msgstr "Cancelar" @@ -1733,7 +1757,7 @@ msgstr "Asunto" msgid "What is this email about?" msgstr "¿De qué trata este correo?" -#: ui/components/confirmation-dialog.slint:12 +#: ui/components/confirmation-dialog.slint:13 msgid "Confirm" msgstr "Confirmar" @@ -1741,7 +1765,7 @@ msgstr "Confirmar" msgid "On" msgstr "Activado" -#: ui/components/controls.slint:160 ui/dialogs/settings-dialog.slint:127 +#: ui/components/controls.slint:160 ui/dialogs/settings-dialog.slint:165 msgid "Off" msgstr "Desactivado" @@ -2026,7 +2050,7 @@ msgid "Palette" msgstr "Paleta" #: ui/components/theme-editor.slint:567 ui/components/theme-editor.slint:579 -#: ui/dialogs/settings-dialog.slint:359 +#: ui/dialogs/settings-dialog.slint:425 msgid "Default" msgstr "Predeterminado" @@ -2218,6 +2242,7 @@ msgid "Open meeting link" msgstr "Abrir enlace de la reunión" #: ui/dialogs/calendar-details-dialog.slint:219 +#: ui/dialogs/settings-dialog.slint:809 msgid "Edit" msgstr "Editar" @@ -2381,7 +2406,7 @@ msgstr "ID de cliente de escritorio de Google" msgid "Microsoft client ID" msgstr "ID de cliente de Microsoft" -#: ui/dialogs/oauth-setup-dialog.slint:95 ui/dialogs/settings-dialog.slint:787 +#: ui/dialogs/oauth-setup-dialog.slint:95 ui/dialogs/settings-dialog.slint:855 msgid "Microsoft Entra desktop client ID" msgstr "ID de cliente de escritorio de Microsoft Entra" @@ -2458,220 +2483,228 @@ msgstr "Copiar código fuente" msgid "Report issue" msgstr "Informar de un problema" -#: ui/dialogs/settings-dialog.slint:122 +#: ui/dialogs/settings-dialog.slint:37 +msgid "Remove" +msgstr "Eliminar" + +#: ui/dialogs/settings-dialog.slint:44 +msgid "Sync" +msgstr "Sincronizar" + +#: ui/dialogs/settings-dialog.slint:160 msgid "Every minute" msgstr "Cada minuto" -#: ui/dialogs/settings-dialog.slint:123 +#: ui/dialogs/settings-dialog.slint:161 msgid "Every 5 minutes" msgstr "Cada 5 minutos" -#: ui/dialogs/settings-dialog.slint:124 +#: ui/dialogs/settings-dialog.slint:162 msgid "Every 15 minutes" msgstr "Cada 15 minutos" -#: ui/dialogs/settings-dialog.slint:128 +#: ui/dialogs/settings-dialog.slint:166 msgid "Important mail" msgstr "Correo importante" -#: ui/dialogs/settings-dialog.slint:129 +#: ui/dialogs/settings-dialog.slint:167 msgid "All new mail" msgstr "Todo el correo nuevo" -#: ui/dialogs/settings-dialog.slint:132 +#: ui/dialogs/settings-dialog.slint:170 msgid "1 month" msgstr "1 mes" -#: ui/dialogs/settings-dialog.slint:133 +#: ui/dialogs/settings-dialog.slint:171 msgid "3 months" msgstr "3 meses" -#: ui/dialogs/settings-dialog.slint:134 +#: ui/dialogs/settings-dialog.slint:172 msgid "6 months · Recommended" msgstr "6 meses · Recomendado" -#: ui/dialogs/settings-dialog.slint:135 +#: ui/dialogs/settings-dialog.slint:173 msgid "1 year" msgstr "1 año" -#: ui/dialogs/settings-dialog.slint:136 ui/views/mail-list-pane.slint:208 +#: ui/dialogs/settings-dialog.slint:174 ui/views/mail-list-pane.slint:208 msgid "All mail" msgstr "Todo el correo" -#: ui/dialogs/settings-dialog.slint:233 ui/dialogs/settings-dialog.slint:1112 +#: ui/dialogs/settings-dialog.slint:299 ui/dialogs/settings-dialog.slint:1180 msgid "SETTINGS" msgstr "AJUSTES" -#: ui/dialogs/settings-dialog.slint:235 ui/dialogs/settings-dialog.slint:262 +#: ui/dialogs/settings-dialog.slint:301 ui/dialogs/settings-dialog.slint:328 msgid "General" msgstr "General" -#: ui/dialogs/settings-dialog.slint:236 ui/dialogs/settings-dialog.slint:558 +#: ui/dialogs/settings-dialog.slint:302 ui/dialogs/settings-dialog.slint:624 msgid "Accounts" msgstr "Cuentas" -#: ui/dialogs/settings-dialog.slint:237 ui/dialogs/settings-dialog.slint:873 +#: ui/dialogs/settings-dialog.slint:303 ui/dialogs/settings-dialog.slint:941 msgid "Notifications" msgstr "Notificaciones" -#: ui/dialogs/settings-dialog.slint:238 ui/dialogs/settings-dialog.slint:1116 +#: ui/dialogs/settings-dialog.slint:304 ui/dialogs/settings-dialog.slint:1184 msgid "Shortcuts" msgstr "Atajos" -#: ui/dialogs/settings-dialog.slint:239 ui/dialogs/settings-dialog.slint:817 -#: ui/dialogs/settings-dialog.slint:1117 +#: ui/dialogs/settings-dialog.slint:305 ui/dialogs/settings-dialog.slint:885 +#: ui/dialogs/settings-dialog.slint:1185 msgid "Data management" msgstr "Gestión de datos" -#: ui/dialogs/settings-dialog.slint:240 ui/dialogs/settings-dialog.slint:947 +#: ui/dialogs/settings-dialog.slint:306 ui/dialogs/settings-dialog.slint:1015 msgid "About" msgstr "Acerca de" -#: ui/dialogs/settings-dialog.slint:263 +#: ui/dialogs/settings-dialog.slint:329 msgid "Manage appearance, privacy, and synchronization." msgstr "Gestiona la apariencia, la privacidad y la sincronización." -#: ui/dialogs/settings-dialog.slint:309 +#: ui/dialogs/settings-dialog.slint:375 msgid "Interface" msgstr "Interfaz" -#: ui/dialogs/settings-dialog.slint:312 +#: ui/dialogs/settings-dialog.slint:378 msgid "Monochrome sidebar icons" msgstr "Iconos monocromos en la barra lateral" -#: ui/dialogs/settings-dialog.slint:313 +#: ui/dialogs/settings-dialog.slint:379 msgid "Use one neutral color for mailbox and folder icons." msgstr "Usa un único color neutro para los iconos de buzones y carpetas." -#: ui/dialogs/settings-dialog.slint:325 +#: ui/dialogs/settings-dialog.slint:391 msgid "No avatars" msgstr "Sin avatares" -#: ui/dialogs/settings-dialog.slint:326 +#: ui/dialogs/settings-dialog.slint:392 msgid "Hide sender images in the message list and reader." msgstr "" "Oculta las imágenes de los remitentes en la lista de mensajes y el lector." -#: ui/dialogs/settings-dialog.slint:339 ui/dialogs/settings-dialog.slint:355 +#: ui/dialogs/settings-dialog.slint:405 ui/dialogs/settings-dialog.slint:421 msgid "Workspace layout" msgstr "Diseño del espacio de trabajo" -#: ui/dialogs/settings-dialog.slint:346 ui/dialogs/settings-dialog.slint:360 +#: ui/dialogs/settings-dialog.slint:412 ui/dialogs/settings-dialog.slint:426 msgid "Minimal" msgstr "Mínimo" -#: ui/dialogs/settings-dialog.slint:378 +#: ui/dialogs/settings-dialog.slint:444 msgid "Language" msgstr "Idioma" -#: ui/dialogs/settings-dialog.slint:382 +#: ui/dialogs/settings-dialog.slint:448 msgid "System language" msgstr "Idioma del sistema" -#: ui/dialogs/settings-dialog.slint:383 +#: ui/dialogs/settings-dialog.slint:449 msgid "Spanish" msgstr "Español" -#: ui/dialogs/settings-dialog.slint:384 +#: ui/dialogs/settings-dialog.slint:450 msgid "English" msgstr "Inglés" -#: ui/dialogs/settings-dialog.slint:401 +#: ui/dialogs/settings-dialog.slint:467 msgid "Inbox behavior" msgstr "Comportamiento de la bandeja de entrada" -#: ui/dialogs/settings-dialog.slint:404 +#: ui/dialogs/settings-dialog.slint:470 msgid "Unified Inbox" msgstr "Bandeja unificada" -#: ui/dialogs/settings-dialog.slint:406 +#: ui/dialogs/settings-dialog.slint:472 msgid "DEFAULT" msgstr "PREDETERMINADO" -#: ui/dialogs/settings-dialog.slint:408 +#: ui/dialogs/settings-dialog.slint:474 msgid "Combines incoming messages from every connected account." msgstr "Combina los mensajes entrantes de todas las cuentas conectadas." -#: ui/dialogs/settings-dialog.slint:417 +#: ui/dialogs/settings-dialog.slint:483 msgid "Mark messages as read when opened" msgstr "Marcar los mensajes como leídos al abrirlos" -#: ui/dialogs/settings-dialog.slint:418 +#: ui/dialogs/settings-dialog.slint:484 msgid "Turn this off to keep unread status until you change it manually." msgstr "" "Desactiva esta opción para mantenerlos sin leer hasta que los cambies " "manualmente." -#: ui/dialogs/settings-dialog.slint:445 +#: ui/dialogs/settings-dialog.slint:511 msgid "Remote images" msgstr "Imágenes remotas" -#: ui/dialogs/settings-dialog.slint:446 +#: ui/dialogs/settings-dialog.slint:512 msgid "Load sender favicons and images embedded in message bodies." msgstr "" "Carga los iconos de los remitentes y las imágenes incluidas en los mensajes." -#: ui/dialogs/settings-dialog.slint:446 +#: ui/dialogs/settings-dialog.slint:512 msgid "Unavailable in this minimal build." msgstr "No disponible en esta compilación mínima." -#: ui/dialogs/settings-dialog.slint:447 +#: ui/dialogs/settings-dialog.slint:513 msgid "Off by default to block tracking pixels and minimize memory use." msgstr "" "Desactivadas de forma predeterminada para bloquear píxeles de seguimiento y " "reducir el uso de memoria." -#: ui/dialogs/settings-dialog.slint:473 +#: ui/dialogs/settings-dialog.slint:539 msgid "Keep running in system tray" msgstr "Seguir ejecutándose en la bandeja del sistema" -#: ui/dialogs/settings-dialog.slint:474 +#: ui/dialogs/settings-dialog.slint:540 msgid "Closing the window hides Flectar Mail while mail continues syncing." msgstr "" "Al cerrar la ventana, Flectar Mail se oculta mientras el correo sigue " "sincronizándose." -#: ui/dialogs/settings-dialog.slint:475 +#: ui/dialogs/settings-dialog.slint:541 msgid "Disabled by default; Quit from the tray menu stops the app." msgstr "" "Desactivado de forma predeterminada; Salir en el menú de la bandeja detiene " "la aplicación." -#: ui/dialogs/settings-dialog.slint:500 +#: ui/dialogs/settings-dialog.slint:566 msgid "Sync and notifications" msgstr "Sincronización y notificaciones" -#: ui/dialogs/settings-dialog.slint:503 +#: ui/dialogs/settings-dialog.slint:569 msgid "Check for new mail" msgstr "Buscar correo nuevo" -#: ui/dialogs/settings-dialog.slint:504 +#: ui/dialogs/settings-dialog.slint:570 msgid "How often to refresh when an instant provider update is unavailable." msgstr "" "Con qué frecuencia actualizar cuando no haya una actualización instantánea " "del proveedor." -#: ui/dialogs/settings-dialog.slint:514 ui/dialogs/settings-dialog.slint:890 +#: ui/dialogs/settings-dialog.slint:580 ui/dialogs/settings-dialog.slint:958 msgid "Desktop notifications" msgstr "Notificaciones de escritorio" -#: ui/dialogs/settings-dialog.slint:515 ui/dialogs/settings-dialog.slint:891 +#: ui/dialogs/settings-dialog.slint:581 ui/dialogs/settings-dialog.slint:959 msgid "Choose which incoming messages can alert you." msgstr "Elige qué mensajes entrantes pueden enviarte alertas." -#: ui/dialogs/settings-dialog.slint:528 +#: ui/dialogs/settings-dialog.slint:594 msgid "Provider synchronization is ready." msgstr "La sincronización del proveedor está lista." -#: ui/dialogs/settings-dialog.slint:536 ui/dialogs/settings-dialog.slint:694 +#: ui/dialogs/settings-dialog.slint:602 ui/dialogs/settings-dialog.slint:764 msgid "Syncing…" msgstr "Sincronizando…" -#: ui/dialogs/settings-dialog.slint:536 +#: ui/dialogs/settings-dialog.slint:602 msgid "Sync now" msgstr "Sincronizar ahora" -#: ui/dialogs/settings-dialog.slint:559 +#: ui/dialogs/settings-dialog.slint:625 msgid "" "Manage connected mailboxes and add providers. Drag an account to change its " "order everywhere." @@ -2679,67 +2712,67 @@ msgstr "" "Gestiona los buzones conectados y añade proveedores. Arrastra una cuenta " "para cambiar su orden en todas partes." -#: ui/dialogs/settings-dialog.slint:578 ui/dialogs/settings-dialog.slint:622 +#: ui/dialogs/settings-dialog.slint:644 ui/dialogs/settings-dialog.slint:688 msgid "Add Gmail" msgstr "Añadir Gmail" -#: ui/dialogs/settings-dialog.slint:590 ui/dialogs/settings-dialog.slint:623 +#: ui/dialogs/settings-dialog.slint:656 ui/dialogs/settings-dialog.slint:689 msgid "Add Outlook" msgstr "Añadir Outlook" -#: ui/dialogs/settings-dialog.slint:602 ui/dialogs/settings-dialog.slint:624 +#: ui/dialogs/settings-dialog.slint:668 ui/dialogs/settings-dialog.slint:690 msgid "Add mail account" msgstr "Añadir cuenta de correo" -#: ui/dialogs/settings-dialog.slint:615 ui/dialogs/settings-dialog.slint:625 +#: ui/dialogs/settings-dialog.slint:681 ui/dialogs/settings-dialog.slint:691 msgid "Cancel sign-in" msgstr "Cancelar inicio de sesión" -#: ui/dialogs/settings-dialog.slint:694 +#: ui/dialogs/settings-dialog.slint:764 msgid "Offline · retrying automatically" msgstr "Sin conexión · reintentando automáticamente" -#: ui/dialogs/settings-dialog.slint:694 +#: ui/dialogs/settings-dialog.slint:764 msgid "Sign-in required" msgstr "Se requiere iniciar sesión" -#: ui/dialogs/settings-dialog.slint:694 +#: ui/dialogs/settings-dialog.slint:764 msgid "Connected · JMAP" msgstr "Conectado · JMAP" -#: ui/dialogs/settings-dialog.slint:694 +#: ui/dialogs/settings-dialog.slint:764 msgid "Connected · IMAP + SMTP" msgstr "Conectado · IMAP + SMTP" -#: ui/dialogs/settings-dialog.slint:694 +#: ui/dialogs/settings-dialog.slint:764 msgid "Account status: {}" msgstr "Estado de la cuenta: {}" -#: ui/dialogs/settings-dialog.slint:698 +#: ui/dialogs/settings-dialog.slint:768 msgid "Mail synchronization error: {}" msgstr "Error de sincronización del correo: {}" -#: ui/dialogs/settings-dialog.slint:707 +#: ui/dialogs/settings-dialog.slint:775 ui/dialogs/settings-dialog.slint:809 msgid "Reconnect" msgstr "Volver a conectar" -#: ui/dialogs/settings-dialog.slint:740 +#: ui/dialogs/settings-dialog.slint:794 msgid "Add calendar" msgstr "Añadir calendario" -#: ui/dialogs/settings-dialog.slint:740 +#: ui/dialogs/settings-dialog.slint:794 msgid "Retry calendar" msgstr "Reintentar calendario" -#: ui/dialogs/settings-dialog.slint:747 +#: ui/dialogs/settings-dialog.slint:801 msgid "Set up CalDAV" msgstr "Configurar CalDAV" -#: ui/dialogs/settings-dialog.slint:766 +#: ui/dialogs/settings-dialog.slint:833 msgid "Mail history" msgstr "Historial de correo" -#: ui/dialogs/settings-dialog.slint:768 +#: ui/dialogs/settings-dialog.slint:836 msgid "" "Download the full mailbox. Large accounts can take considerable time and " "storage." @@ -2747,66 +2780,66 @@ msgstr "" "Descarga todo el buzón. Las cuentas grandes pueden requerir bastante tiempo " "y almacenamiento." -#: ui/dialogs/settings-dialog.slint:769 +#: ui/dialogs/settings-dialog.slint:837 msgid "How far back to download. Mail already stored on this device is kept." msgstr "" "Hasta qué fecha descargar. Se conserva el correo ya almacenado en este " "dispositivo." -#: ui/dialogs/settings-dialog.slint:781 +#: ui/dialogs/settings-dialog.slint:849 msgid "OAuth app credentials" msgstr "Credenciales de la aplicación OAuth" -#: ui/dialogs/settings-dialog.slint:781 +#: ui/dialogs/settings-dialog.slint:849 msgid "Custom desktop app credentials are configured." msgstr "Las credenciales personalizadas de escritorio están configuradas." -#: ui/dialogs/settings-dialog.slint:781 +#: ui/dialogs/settings-dialog.slint:849 msgid "Advanced: use your own Gmail or Microsoft desktop app registration." msgstr "" "Avanzado: usa tu propio registro de aplicación de escritorio de Gmail o " "Microsoft." -#: ui/dialogs/settings-dialog.slint:782 +#: ui/dialogs/settings-dialog.slint:850 msgid "Hide" msgstr "Ocultar" -#: ui/dialogs/settings-dialog.slint:782 +#: ui/dialogs/settings-dialog.slint:850 msgid "Configure" msgstr "Configurar" -#: ui/dialogs/settings-dialog.slint:785 ui/dialogs/settings-dialog.slint:786 +#: ui/dialogs/settings-dialog.slint:853 ui/dialogs/settings-dialog.slint:854 msgid "Gmail desktop client ID" msgstr "ID de cliente de escritorio de Gmail" -#: ui/dialogs/settings-dialog.slint:785 ui/dialogs/settings-dialog.slint:786 +#: ui/dialogs/settings-dialog.slint:853 ui/dialogs/settings-dialog.slint:854 msgid "Gmail client secret (optional)" msgstr "Secreto de cliente de Gmail (opcional)" -#: ui/dialogs/settings-dialog.slint:785 +#: ui/dialogs/settings-dialog.slint:853 msgid "Save Gmail" msgstr "Guardar Gmail" -#: ui/dialogs/settings-dialog.slint:786 +#: ui/dialogs/settings-dialog.slint:854 msgid "Save Gmail settings" msgstr "Guardar ajustes de Gmail" -#: ui/dialogs/settings-dialog.slint:787 +#: ui/dialogs/settings-dialog.slint:855 msgid "Save Outlook" msgstr "Guardar Outlook" -#: ui/dialogs/settings-dialog.slint:788 +#: ui/dialogs/settings-dialog.slint:856 msgid "" "Outlook public desktop clients use PKCE and do not need a client secret." msgstr "" "Los clientes públicos de escritorio de Outlook usan PKCE y no necesitan un " "secreto de cliente." -#: ui/dialogs/settings-dialog.slint:794 +#: ui/dialogs/settings-dialog.slint:862 msgid "Connect CalDAV" msgstr "Conectar CalDAV" -#: ui/dialogs/settings-dialog.slint:794 +#: ui/dialogs/settings-dialog.slint:862 msgid "" "Works with iCloud, Fastmail, Nextcloud, and standards-compliant CalDAV " "servers." @@ -2814,23 +2847,23 @@ msgstr "" "Funciona con iCloud, Fastmail, Nextcloud y servidores CalDAV compatibles con " "los estándares." -#: ui/dialogs/settings-dialog.slint:797 +#: ui/dialogs/settings-dialog.slint:865 msgid "https://calendar.example.com/" msgstr "https://calendario.ejemplo.com/" -#: ui/dialogs/settings-dialog.slint:798 ui/dialogs/settings-dialog.slint:1201 +#: ui/dialogs/settings-dialog.slint:866 ui/dialogs/settings-dialog.slint:1269 msgid "Username" msgstr "Nombre de usuario" -#: ui/dialogs/settings-dialog.slint:798 +#: ui/dialogs/settings-dialog.slint:866 msgid "App password" msgstr "Contraseña de aplicación" -#: ui/dialogs/settings-dialog.slint:799 ui/views/files-view.slint:586 +#: ui/dialogs/settings-dialog.slint:867 ui/views/files-view.slint:586 msgid "Connect" msgstr "Conectar" -#: ui/dialogs/settings-dialog.slint:820 +#: ui/dialogs/settings-dialog.slint:888 msgid "" "Transfer your setup, export a full snapshot, or remove local data from this " "device." @@ -2838,15 +2871,15 @@ msgstr "" "Transfiere tu configuración, exporta una instantánea completa o elimina los " "datos locales de este dispositivo." -#: ui/dialogs/settings-dialog.slint:820 +#: ui/dialogs/settings-dialog.slint:888 msgid "Remove local data from this device." msgstr "Elimina los datos locales de este dispositivo." -#: ui/dialogs/settings-dialog.slint:831 +#: ui/dialogs/settings-dialog.slint:899 msgid "Database snapshot" msgstr "Instantánea de la base de datos" -#: ui/dialogs/settings-dialog.slint:831 +#: ui/dialogs/settings-dialog.slint:899 msgid "" "Export verified copies of the mail and calendar databases with their schema " "versions." @@ -2854,7 +2887,7 @@ msgstr "" "Exporta copias verificadas de las bases de datos de correo y calendario con " "sus versiones de esquema." -#: ui/dialogs/settings-dialog.slint:833 +#: ui/dialogs/settings-dialog.slint:901 msgid "" "The snapshot contains private local mail and settings, but not passwords or " "OAuth tokens. Store it securely." @@ -2862,15 +2895,15 @@ msgstr "" "La instantánea contiene correo local y ajustes privados, pero no contraseñas " "ni tokens OAuth. Guárdala de forma segura." -#: ui/dialogs/settings-dialog.slint:834 +#: ui/dialogs/settings-dialog.slint:902 msgid "Export snapshot" msgstr "Exportar instantánea" -#: ui/dialogs/settings-dialog.slint:841 +#: ui/dialogs/settings-dialog.slint:909 msgid "Transfer account setup" msgstr "Transferir configuración de cuentas" -#: ui/dialogs/settings-dialog.slint:841 +#: ui/dialogs/settings-dialog.slint:909 msgid "Import a backup or export one connected account and your preferences." msgid_plural "" "Import a backup or export {n} connected accounts and your preferences." @@ -2881,7 +2914,7 @@ msgstr[1] "" "Importa una copia de seguridad o exporta {n} cuentas conectadas y tus " "preferencias." -#: ui/dialogs/settings-dialog.slint:843 +#: ui/dialogs/settings-dialog.slint:911 msgid "" "Passwords and OAuth tokens stay in the system keyring. A restored device " "asks you to sign in securely." @@ -2889,19 +2922,19 @@ msgstr "" "Las contraseñas y los tokens OAuth permanecen en el llavero del sistema. Un " "dispositivo restaurado te pedirá que inicies sesión de forma segura." -#: ui/dialogs/settings-dialog.slint:850 +#: ui/dialogs/settings-dialog.slint:918 msgid "Import" msgstr "Importar" -#: ui/dialogs/settings-dialog.slint:851 +#: ui/dialogs/settings-dialog.slint:919 msgid "Export" msgstr "Exportar" -#: ui/dialogs/settings-dialog.slint:858 +#: ui/dialogs/settings-dialog.slint:926 msgid "Delete all local data" msgstr "Eliminar todos los datos locales" -#: ui/dialogs/settings-dialog.slint:858 +#: ui/dialogs/settings-dialog.slint:926 msgid "" "Remove accounts, downloaded mail, calendars, contacts, preferences, and " "credentials from this device." @@ -2909,125 +2942,125 @@ msgstr "" "Elimina de este dispositivo las cuentas, el correo descargado, los " "calendarios, los contactos, las preferencias y las credenciales." -#: ui/dialogs/settings-dialog.slint:860 +#: ui/dialogs/settings-dialog.slint:928 msgid "This does not delete anything from Gmail, Outlook, or your IMAP server." msgstr "Esto no elimina nada de Gmail, Outlook ni de tu servidor IMAP." -#: ui/dialogs/settings-dialog.slint:861 ui/dialogs/settings-dialog.slint:1285 +#: ui/dialogs/settings-dialog.slint:929 ui/dialogs/settings-dialog.slint:1353 msgid "Delete everything" msgstr "Eliminar todo" -#: ui/dialogs/settings-dialog.slint:874 +#: ui/dialogs/settings-dialog.slint:942 msgid "Choose how Flectar Mail gets your attention." msgstr "Elige cómo Flectar Mail llama tu atención." -#: ui/dialogs/settings-dialog.slint:901 +#: ui/dialogs/settings-dialog.slint:969 msgid "Notification sounds" msgstr "Sonidos de notificación" -#: ui/dialogs/settings-dialog.slint:902 +#: ui/dialogs/settings-dialog.slint:970 msgid "Play a sound for new mail and sent messages." msgstr "Reproduce un sonido al recibir correo y al enviar mensajes." -#: ui/dialogs/settings-dialog.slint:921 +#: ui/dialogs/settings-dialog.slint:989 msgid "Keyboard shortcuts" msgstr "Atajos de teclado" -#: ui/dialogs/settings-dialog.slint:922 +#: ui/dialogs/settings-dialog.slint:990 msgid "Move through mail without leaving the keyboard." msgstr "Muévete por el correo sin apartar las manos del teclado." -#: ui/dialogs/settings-dialog.slint:925 +#: ui/dialogs/settings-dialog.slint:993 msgid "Compose a new message" msgstr "Redactar un mensaje nuevo" -#: ui/dialogs/settings-dialog.slint:925 +#: ui/dialogs/settings-dialog.slint:993 msgid "C" msgstr "C" -#: ui/dialogs/settings-dialog.slint:926 ui/views/mail-list-pane.slint:206 +#: ui/dialogs/settings-dialog.slint:994 ui/views/mail-list-pane.slint:206 msgid "Search mail" msgstr "Buscar correo" -#: ui/dialogs/settings-dialog.slint:926 +#: ui/dialogs/settings-dialog.slint:994 msgid "/" msgstr "/" -#: ui/dialogs/settings-dialog.slint:927 +#: ui/dialogs/settings-dialog.slint:995 msgid "Archive selected message" msgstr "Archivar el mensaje seleccionado" -#: ui/dialogs/settings-dialog.slint:927 +#: ui/dialogs/settings-dialog.slint:995 msgid "E" msgstr "E" -#: ui/dialogs/settings-dialog.slint:928 +#: ui/dialogs/settings-dialog.slint:996 msgid "Mark as spam" msgstr "Marcar como spam" -#: ui/dialogs/settings-dialog.slint:928 +#: ui/dialogs/settings-dialog.slint:996 msgid "!" msgstr "!" -#: ui/dialogs/settings-dialog.slint:929 ui/views/mail-list-pane.slint:538 +#: ui/dialogs/settings-dialog.slint:997 ui/views/mail-list-pane.slint:538 #: ui/views/mail-reading-pane.slint:576 msgid "Move to trash" msgstr "Mover a la papelera" -#: ui/dialogs/settings-dialog.slint:929 +#: ui/dialogs/settings-dialog.slint:997 msgid "#" msgstr "#" -#: ui/dialogs/settings-dialog.slint:948 +#: ui/dialogs/settings-dialog.slint:1016 msgid "Flectar Mail, its makers, and its community." msgstr "Flectar Mail, sus creadores y su comunidad." -#: ui/dialogs/settings-dialog.slint:964 ui/views/onboarding-view.slint:91 +#: ui/dialogs/settings-dialog.slint:1032 ui/views/onboarding-view.slint:91 msgid "Flectar Mail logo" msgstr "Logotipo de Flectar Mail" -#: ui/dialogs/settings-dialog.slint:984 +#: ui/dialogs/settings-dialog.slint:1052 msgid "A native mail client for a calm, private inbox." msgstr "" "Un cliente de correo nativo para una bandeja de entrada tranquila y privada." -#: ui/dialogs/settings-dialog.slint:996 +#: ui/dialogs/settings-dialog.slint:1064 msgid "Version 0.1.0" msgstr "Versión 0.1.0" -#: ui/dialogs/settings-dialog.slint:1007 +#: ui/dialogs/settings-dialog.slint:1075 msgid "Credits" msgstr "Créditos" -#: ui/dialogs/settings-dialog.slint:1007 +#: ui/dialogs/settings-dialog.slint:1075 msgid "Built by Flectar with help from the open-source community." msgstr "Creado por Flectar con la ayuda de la comunidad de código abierto." -#: ui/dialogs/settings-dialog.slint:1015 +#: ui/dialogs/settings-dialog.slint:1083 msgid "Flectar" msgstr "Flectar" -#: ui/dialogs/settings-dialog.slint:1016 +#: ui/dialogs/settings-dialog.slint:1084 msgid "Company website" msgstr "Sitio web de la empresa" -#: ui/dialogs/settings-dialog.slint:1026 +#: ui/dialogs/settings-dialog.slint:1094 msgid "Open-source contributors" msgstr "Colaboradores de código abierto" -#: ui/dialogs/settings-dialog.slint:1027 +#: ui/dialogs/settings-dialog.slint:1095 msgid "Meet the people who have contributed on GitHub" msgstr "Conoce a las personas que han contribuido en GitHub" -#: ui/dialogs/settings-dialog.slint:1040 +#: ui/dialogs/settings-dialog.slint:1108 msgid "License" msgstr "Licencia" -#: ui/dialogs/settings-dialog.slint:1040 +#: ui/dialogs/settings-dialog.slint:1108 msgid "AGPL-3.0-only." msgstr "Solo AGPL-3.0." -#: ui/dialogs/settings-dialog.slint:1040 +#: ui/dialogs/settings-dialog.slint:1108 msgid "" "Flectar Mail is released under the GNU Affero General Public License v3.0 " "only." @@ -3035,109 +3068,109 @@ msgstr "" "Flectar Mail se publica únicamente bajo la GNU Affero General Public License " "v3.0." -#: ui/dialogs/settings-dialog.slint:1046 +#: ui/dialogs/settings-dialog.slint:1114 msgid "Third-party components retain their own licenses and notices." msgstr "Los componentes de terceros conservan sus propias licencias y avisos." -#: ui/dialogs/settings-dialog.slint:1067 +#: ui/dialogs/settings-dialog.slint:1135 msgid "Settings sections" msgstr "Secciones de ajustes" -#: ui/dialogs/settings-dialog.slint:1079 +#: ui/dialogs/settings-dialog.slint:1147 msgid "Close settings" msgstr "Cerrar ajustes" -#: ui/dialogs/settings-dialog.slint:1096 +#: ui/dialogs/settings-dialog.slint:1164 msgid "Close settings sections" msgstr "Cerrar secciones de ajustes" -#: ui/dialogs/settings-dialog.slint:1131 +#: ui/dialogs/settings-dialog.slint:1199 msgid "Edit mail account" msgstr "Editar cuenta de correo" -#: ui/dialogs/settings-dialog.slint:1131 ui/dialogs/settings-dialog.slint:1179 +#: ui/dialogs/settings-dialog.slint:1199 ui/dialogs/settings-dialog.slint:1247 msgid "Connect a mail account" msgstr "Conectar una cuenta de correo" -#: ui/dialogs/settings-dialog.slint:1179 +#: ui/dialogs/settings-dialog.slint:1247 msgid "Edit {}" msgstr "Editar {}" -#: ui/dialogs/settings-dialog.slint:1180 +#: ui/dialogs/settings-dialog.slint:1248 msgid "Uses JMAP for mail sync and sending, with automatic discovery." msgstr "Usa JMAP para sincronizar y enviar correo, con detección automática." -#: ui/dialogs/settings-dialog.slint:1180 +#: ui/dialogs/settings-dialog.slint:1248 msgid "Uses separate IMAP and SMTP servers for mail sync and sending." msgstr "Usa servidores IMAP y SMTP separados para sincronizar y enviar correo." -#: ui/dialogs/settings-dialog.slint:1200 ui/dialogs/settings-dialog.slint:1207 +#: ui/dialogs/settings-dialog.slint:1268 ui/dialogs/settings-dialog.slint:1275 #: ui/views/onboarding-view.slint:234 msgid "Email address" msgstr "Dirección de correo" -#: ui/dialogs/settings-dialog.slint:1200 ui/dialogs/settings-dialog.slint:1207 +#: ui/dialogs/settings-dialog.slint:1268 ui/dialogs/settings-dialog.slint:1275 msgid "name@example.com" msgstr "nombre@ejemplo.com" -#: ui/dialogs/settings-dialog.slint:1201 ui/dialogs/settings-dialog.slint:1208 +#: ui/dialogs/settings-dialog.slint:1269 ui/dialogs/settings-dialog.slint:1276 msgid "Usually your email address" msgstr "Normalmente, tu dirección de correo" -#: ui/dialogs/settings-dialog.slint:1210 ui/views/onboarding-view.slint:249 +#: ui/dialogs/settings-dialog.slint:1278 ui/views/onboarding-view.slint:249 msgid "Password or app password" msgstr "Contraseña o contraseña de aplicación" -#: ui/dialogs/settings-dialog.slint:1210 +#: ui/dialogs/settings-dialog.slint:1278 msgid "Stored securely in your system keyring" msgstr "Guardada de forma segura en el llavero del sistema" -#: ui/dialogs/settings-dialog.slint:1215 ui/views/onboarding-view.slint:263 +#: ui/dialogs/settings-dialog.slint:1283 ui/views/onboarding-view.slint:263 msgid "IMAP + SMTP" msgstr "IMAP + SMTP" -#: ui/dialogs/settings-dialog.slint:1216 ui/views/onboarding-view.slint:271 +#: ui/dialogs/settings-dialog.slint:1284 ui/views/onboarding-view.slint:271 msgid "JMAP" msgstr "JMAP" -#: ui/dialogs/settings-dialog.slint:1220 +#: ui/dialogs/settings-dialog.slint:1288 msgid "Server address · Optional" msgstr "Dirección del servidor · Opcional" -#: ui/dialogs/settings-dialog.slint:1222 +#: ui/dialogs/settings-dialog.slint:1290 msgid "Discovered from your email domain" msgstr "Se detecta a partir de tu dominio de correo" -#: ui/dialogs/settings-dialog.slint:1230 ui/views/onboarding-view.slint:296 +#: ui/dialogs/settings-dialog.slint:1298 ui/views/onboarding-view.slint:296 msgid "IMAP server" msgstr "Servidor IMAP" -#: ui/dialogs/settings-dialog.slint:1230 +#: ui/dialogs/settings-dialog.slint:1298 msgid "imap.example.com" msgstr "imap.ejemplo.com" -#: ui/dialogs/settings-dialog.slint:1231 ui/dialogs/settings-dialog.slint:1238 +#: ui/dialogs/settings-dialog.slint:1299 ui/dialogs/settings-dialog.slint:1306 #: ui/views/onboarding-view.slint:329 msgid "Port" msgstr "Puerto" -#: ui/dialogs/settings-dialog.slint:1231 ui/views/onboarding-view.slint:309 +#: ui/dialogs/settings-dialog.slint:1299 ui/views/onboarding-view.slint:309 msgid "993" msgstr "993" -#: ui/dialogs/settings-dialog.slint:1237 ui/views/onboarding-view.slint:320 +#: ui/dialogs/settings-dialog.slint:1305 ui/views/onboarding-view.slint:320 msgid "SMTP server" msgstr "Servidor SMTP" -#: ui/dialogs/settings-dialog.slint:1237 +#: ui/dialogs/settings-dialog.slint:1305 msgid "smtp.example.com" msgstr "smtp.ejemplo.com" -#: ui/dialogs/settings-dialog.slint:1238 ui/views/onboarding-view.slint:333 +#: ui/dialogs/settings-dialog.slint:1306 ui/views/onboarding-view.slint:333 msgid "465" msgstr "465" -#: ui/dialogs/settings-dialog.slint:1241 ui/views/onboarding-view.slint:338 +#: ui/dialogs/settings-dialog.slint:1309 ui/views/onboarding-view.slint:338 msgid "" "Proton Bridge: use the local servers, ports and password shown in Bridge, " "and match its encryption settings." @@ -3145,23 +3178,23 @@ msgstr "" "Proton Bridge: usa los servidores locales, puertos y contraseña que aparecen " "en Bridge, con los mismos ajustes de cifrado." -#: ui/dialogs/settings-dialog.slint:1265 ui/views/onboarding-view.slint:351 +#: ui/dialogs/settings-dialog.slint:1333 ui/views/onboarding-view.slint:351 msgid "Connecting…" msgstr "Conectando…" -#: ui/dialogs/settings-dialog.slint:1265 +#: ui/dialogs/settings-dialog.slint:1333 msgid "Save account" msgstr "Guardar cuenta" -#: ui/dialogs/settings-dialog.slint:1265 ui/views/onboarding-view.slint:351 +#: ui/dialogs/settings-dialog.slint:1333 ui/views/onboarding-view.slint:351 msgid "Connect account" msgstr "Conectar cuenta" -#: ui/dialogs/settings-dialog.slint:1282 +#: ui/dialogs/settings-dialog.slint:1350 msgid "Delete all local data?" msgstr "¿Eliminar todos los datos locales?" -#: ui/dialogs/settings-dialog.slint:1283 +#: ui/dialogs/settings-dialog.slint:1351 msgid "" "This removes every account, downloaded message, calendar, contact, " "preference, and credential stored by Flectar Mail on this device." @@ -3170,17 +3203,37 @@ msgstr "" "contactos, preferencias y credenciales que Flectar Mail almacena en este " "dispositivo." -#: ui/dialogs/settings-dialog.slint:1284 +#: ui/dialogs/settings-dialog.slint:1352 msgid "" "Mail and events on Gmail, Outlook, and IMAP servers will not be deleted." msgstr "" "No se eliminarán el correo ni los eventos de Gmail, Outlook o los servidores " "IMAP." -#: ui/dialogs/settings-dialog.slint:1286 +#: ui/dialogs/settings-dialog.slint:1354 msgid "Keep my data" msgstr "Conservar mis datos" +#: ui/dialogs/settings-dialog.slint:1368 +msgid "Remove account?" +msgstr "¿Eliminar cuenta?" + +#: ui/dialogs/settings-dialog.slint:1370 +msgid "Remove this account and its downloaded mail from this device." +msgstr "Elimina esta cuenta y su correo descargado de este dispositivo." + +#: ui/dialogs/settings-dialog.slint:1371 +msgid "Messages on the mail server will not be deleted." +msgstr "Los mensajes del servidor de correo no se eliminarán." + +#: ui/dialogs/settings-dialog.slint:1372 +msgid "Remove account" +msgstr "Eliminar cuenta" + +#: ui/dialogs/settings-dialog.slint:1373 +msgid "Keep account" +msgstr "Conservar cuenta" + #: ui/tray.slint:19 msgid "Show Flectar Mail" msgstr "Mostrar Flectar Mail" diff --git a/lang/flectar-mail.pot b/lang/flectar-mail.pot index adda0cf..bcb30fe 100644 --- a/lang/flectar-mail.pot +++ b/lang/flectar-mail.pot @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: flectar-mail 0.1.0\n" -"POT-Creation-Date: 2026-09-11 13:50+0000\n" +"POT-Creation-Date: 2026-09-12 22:12+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -152,8 +152,8 @@ msgstr "" msgid "New address-book entry" msgstr "" -#: ui/app.slint:95 ui/app.slint:686 ui/components/composer-fields.slint:63 -#: ui/app.slint:95 ui/app.slint:686 ui/components/composer-fields.slint:63 ui/dialogs/compose-dialog.slint:34 +#: ui/app.slint:95 ui/app.slint:694 ui/components/composer-fields.slint:63 +#: ui/app.slint:95 ui/app.slint:694 ui/components/composer-fields.slint:63 ui/dialogs/compose-dialog.slint:34 msgid "No connected account" msgstr "" @@ -167,11 +167,11 @@ msgstr[1] "" msgid "Last contacted {} {}, {}" msgstr "" -#: ui/app.slint:102 ui/app.slint:299 +#: ui/app.slint:102 ui/app.slint:305 msgid "Loading local data…" msgstr "" -#: ui/app.slint:103 ui/app.slint:300 +#: ui/app.slint:103 ui/app.slint:306 msgid "Local data ready." msgstr "" @@ -388,724 +388,748 @@ msgid "Mail history updated. Background sync is adjusting now." msgstr "" #: ui/app.slint:198 -msgid "Could not update mail history: {}" +msgid "Account removed from this device." msgstr "" #: ui/app.slint:199 -msgid "Connected {}. Initial sync is running in the background." +msgid "Account removed. Mail refresh will retry." msgstr "" #: ui/app.slint:200 -msgid "Account setup failed: {}" +msgid "Account removed; local cleanup needs a retry: {}" msgstr "" #: ui/app.slint:201 -msgid "OAuth failed: {}" +msgid "Could not remove account: {}" msgstr "" #: ui/app.slint:202 -msgid "Reconnected {}. Synchronization is resuming." +msgid "Removing account…" msgstr "" #: ui/app.slint:203 -msgid "Reconnection failed: {}" +msgid "Synchronizing account…" msgstr "" #: ui/app.slint:204 -msgid "Calendar connected and initial sync started." +msgid "Could not update mail history: {}" msgstr "" #: ui/app.slint:205 -msgid "Could not connect calendar: {}" +msgid "Connected {}. Initial sync is running in the background." msgstr "" #: ui/app.slint:206 -msgid "Calendar sync enabled." +msgid "Account setup failed: {}" msgstr "" #: ui/app.slint:207 -msgid "Calendar sync paused." +msgid "OAuth failed: {}" msgstr "" #: ui/app.slint:208 -msgid "Could not update calendar sync: {}" +msgid "Reconnected {}. Synchronization is resuming." msgstr "" #: ui/app.slint:209 -msgid "CalDAV connected and initial sync started." +msgid "Reconnection failed: {}" msgstr "" #: ui/app.slint:210 -msgid "Could not connect CalDAV: {}" +msgid "Calendar connected and initial sync started." +msgstr "" + +#: ui/app.slint:211 +msgid "Could not connect calendar: {}" +msgstr "" + +#: ui/app.slint:212 +msgid "Calendar sync enabled." +msgstr "" + +#: ui/app.slint:213 +msgid "Calendar sync paused." +msgstr "" + +#: ui/app.slint:214 +msgid "Could not update calendar sync: {}" msgstr "" #: ui/app.slint:215 -msgid "Connect an account before creating a backup." +msgid "CalDAV connected and initial sync started." msgstr "" #: ui/app.slint:216 +msgid "Could not connect CalDAV: {}" +msgstr "" + +#: ui/app.slint:221 +msgid "Connect an account before creating a backup." +msgstr "" + +#: ui/app.slint:222 msgid "Export Flectar Mail account backup" msgstr "" -#: ui/app.slint:217 +#: ui/app.slint:223 msgid "Import Flectar Mail account backup" msgstr "" -#: ui/app.slint:218 +#: ui/app.slint:224 msgid "Flectar Mail backup" msgstr "" -#: ui/app.slint:219 +#: ui/app.slint:225 msgid "Choose where to export the database snapshot" msgstr "" -#: ui/app.slint:220 +#: ui/app.slint:226 msgid "The platform host does not provide document export." msgstr "" -#: ui/app.slint:221 +#: ui/app.slint:227 msgid "The platform host does not provide document import." msgstr "" -#: ui/app.slint:222 +#: ui/app.slint:228 msgid "Backup exported. Passwords and OAuth tokens were not included." msgstr "" -#: ui/app.slint:223 +#: ui/app.slint:229 msgid "Could not export backup: {}" msgstr "" -#: ui/app.slint:224 +#: ui/app.slint:230 msgid "Local data storage is unavailable. Retry startup." msgstr "" -#: ui/app.slint:225 +#: ui/app.slint:231 msgid "Creating verified database snapshot…" msgstr "" -#: ui/app.slint:226 +#: ui/app.slint:232 msgid "Backup imported. All {} account configurations already existed." msgstr "" -#: ui/app.slint:227 +#: ui/app.slint:233 msgid "" "Imported {} account configurations. Open Accounts and sign in to each one." msgstr "" -#: ui/app.slint:228 +#: ui/app.slint:234 msgid "Could not import backup: {}" msgstr "" -#: ui/app.slint:229 +#: ui/app.slint:235 msgid "All local Flectar Mail data was deleted." msgstr "" -#: ui/app.slint:230 +#: ui/app.slint:236 msgid "Could not delete all local data: {}" msgstr "" -#: ui/app.slint:231 +#: ui/app.slint:237 msgid "Deleting local accounts and cached data…" msgstr "" -#: ui/app.slint:236 +#: ui/app.slint:242 msgid "Background mail refresh failed: {}" msgstr "" -#: ui/app.slint:237 +#: ui/app.slint:243 msgid "No messages in this folder" msgstr "" -#: ui/app.slint:238 +#: ui/app.slint:244 msgid "No messages match this search" msgstr "" -#: ui/app.slint:239 +#: ui/app.slint:245 msgid "Mail core is ready for account synchronization and message actions." msgstr "" -#: ui/app.slint:240 +#: ui/app.slint:246 msgid "Showing {} of {} message" msgstr "" -#: ui/app.slint:241 +#: ui/app.slint:247 msgid "Showing {} of {} message · more available" msgstr "" -#: ui/app.slint:242 +#: ui/app.slint:248 msgid "Showing {} of {} message · current results complete" msgstr "" -#: ui/app.slint:243 +#: ui/app.slint:249 msgid "Showing {} of {} messages" msgstr "" -#: ui/app.slint:244 +#: ui/app.slint:250 msgid "Showing {} of {} messages · more available" msgstr "" -#: ui/app.slint:245 +#: ui/app.slint:251 msgid "Showing {} of {} messages · current results complete" msgstr "" -#: ui/app.slint:246 +#: ui/app.slint:252 msgid "Showing {} of {} message matching \"{}\"" msgstr "" -#: ui/app.slint:247 +#: ui/app.slint:253 msgid "Showing {} of {} message matching \"{}\" · more available" msgstr "" -#: ui/app.slint:248 +#: ui/app.slint:254 msgid "Showing {} of {} message matching \"{}\" · current results complete" msgstr "" -#: ui/app.slint:249 +#: ui/app.slint:255 msgid "Showing {} of {} messages matching \"{}\"" msgstr "" -#: ui/app.slint:250 +#: ui/app.slint:256 msgid "Showing {} of {} messages matching \"{}\" · more available" msgstr "" -#: ui/app.slint:251 +#: ui/app.slint:257 msgid "Showing {} of {} messages matching \"{}\" · current results complete" msgstr "" -#: ui/app.slint:252 +#: ui/app.slint:258 msgid "Rendered with software fallback after GPU error: {}" msgstr "" -#: ui/app.slint:253 +#: ui/app.slint:259 msgid "Blitz render failed (GPU: {}; software: {})" msgstr "" -#: ui/app.slint:258 +#: ui/app.slint:264 msgid "Add an email address, then save the contact." msgstr "" -#: ui/app.slint:259 +#: ui/app.slint:265 msgid "Enter a valid email address before saving." msgstr "" -#: ui/app.slint:260 +#: ui/app.slint:266 msgid "Could not save: contact storage is unavailable." msgstr "" -#: ui/app.slint:261 +#: ui/app.slint:267 msgid "Could not save contact: {}" msgstr "" -#: ui/app.slint:262 +#: ui/app.slint:268 msgid "Contact saved." msgstr "" -#: ui/app.slint:263 +#: ui/app.slint:269 msgid "Could not delete: contact storage is unavailable." msgstr "" -#: ui/app.slint:264 +#: ui/app.slint:270 msgid "Could not delete contact: {}" msgstr "" -#: ui/app.slint:265 +#: ui/app.slint:271 msgid "Contact deleted." msgstr "" -#: ui/app.slint:266 +#: ui/app.slint:272 msgid "Could not load contacts: {}" msgstr "" -#: ui/app.slint:267 +#: ui/app.slint:273 msgid "Local contact storage is still starting…" msgstr "" -#: ui/app.slint:268 +#: ui/app.slint:274 msgid "Loading contacts…" msgstr "" -#: ui/app.slint:273 +#: ui/app.slint:279 msgid "Could not load calendar: {}" msgstr "" -#: ui/app.slint:274 +#: ui/app.slint:280 msgid "Calendar shown and syncing." msgstr "" -#: ui/app.slint:275 +#: ui/app.slint:281 msgid "Calendar hidden and sync paused." msgstr "" -#: ui/app.slint:276 +#: ui/app.slint:282 msgid "Calendar update failed: {}" msgstr "" -#: ui/app.slint:277 +#: ui/app.slint:283 msgid "Only events created in Flectar Mail can be edited." msgstr "" -#: ui/app.slint:278 +#: ui/app.slint:284 msgid "Only events created in Flectar Mail can be deleted." msgstr "" -#: ui/app.slint:279 +#: ui/app.slint:285 msgid "Calendar storage is unavailable." msgstr "" -#: ui/app.slint:280 +#: ui/app.slint:286 msgid "Could not delete event: {}" msgstr "" -#: ui/app.slint:281 +#: ui/app.slint:287 msgid "Event deleted, but refresh failed: {}" msgstr "" -#: ui/app.slint:282 +#: ui/app.slint:288 msgid "Event deleted." msgstr "" -#: ui/app.slint:283 +#: ui/app.slint:289 msgid "Use YYYY-MM-DD and a 24-hour time such as 09:30." msgstr "" -#: ui/app.slint:284 +#: ui/app.slint:290 msgid "Event duration must be a number of minutes." msgstr "" -#: ui/app.slint:285 +#: ui/app.slint:291 msgid "Add a title and choose a duration from 15 to 1440 minutes." msgstr "" -#: ui/app.slint:286 +#: ui/app.slint:292 msgid "That local time does not exist in the current time zone." msgstr "" -#: ui/app.slint:287 +#: ui/app.slint:293 msgid "Could not save event: {}" msgstr "" -#: ui/app.slint:288 +#: ui/app.slint:294 msgid "Event saved, but refresh failed: {}" msgstr "" -#: ui/app.slint:289 +#: ui/app.slint:295 msgid "Event saved. Provider sync will run when enabled." msgstr "" -#: ui/app.slint:294 +#: ui/app.slint:300 msgid "Email resource render failed: {}" msgstr "" -#: ui/app.slint:295 +#: ui/app.slint:301 msgid "Message refresh failed: {}" msgstr "" -#: ui/app.slint:296 +#: ui/app.slint:302 msgid "Message load failed: {}" msgstr "" -#: ui/app.slint:297 +#: ui/app.slint:303 msgid "" "Your local mailbox could not be opened. No account operation was started. " "Retry, or copy the diagnostics for support." msgstr "" -#: ui/app.slint:298 +#: ui/app.slint:304 msgid "Local storage startup failed." msgstr "" -#: ui/app.slint:305 +#: ui/app.slint:311 msgid "Could not identify the dragged account." msgstr "" -#: ui/app.slint:306 +#: ui/app.slint:312 msgid "Account ordering requires local mail data." msgstr "" -#: ui/app.slint:307 +#: ui/app.slint:313 msgid "Could not find the account to reorder." msgstr "" -#: ui/app.slint:308 +#: ui/app.slint:314 msgid "Saving account order…" msgstr "" -#: ui/app.slint:309 +#: ui/app.slint:315 msgid "Mail history requires local mail data." msgstr "" -#: ui/app.slint:310 +#: ui/app.slint:316 msgid "Updating mail history…" msgstr "" -#: ui/app.slint:311 +#: ui/app.slint:317 msgid "Remote images are unavailable in this minimal build." msgstr "" -#: ui/app.slint:312 +#: ui/app.slint:318 msgid "Could not save remote-image setting: {}" msgstr "" -#: ui/app.slint:313 +#: ui/app.slint:319 msgid "Remote sender icons unavailable: {}" msgstr "" -#: ui/app.slint:314 +#: ui/app.slint:320 msgid "Remote images unavailable: {}" msgstr "" -#: ui/app.slint:315 +#: ui/app.slint:321 msgid "Remote body images and sender favicons enabled." msgstr "" -#: ui/app.slint:316 +#: ui/app.slint:322 msgid "Remote images blocked." msgstr "" -#: ui/app.slint:317 +#: ui/app.slint:323 msgid "Could not refresh message images: {}" msgstr "" -#: ui/app.slint:318 +#: ui/app.slint:324 msgid "Remote-image loading is disabled." msgstr "" -#: ui/app.slint:319 +#: ui/app.slint:325 msgid "Could not load message images: {}" msgstr "" -#: ui/app.slint:320 +#: ui/app.slint:326 msgid "Sync finished, refresh failed: {}" msgstr "" -#: ui/app.slint:321 +#: ui/app.slint:327 msgid "Sync complete." msgstr "" -#: ui/app.slint:322 +#: ui/app.slint:328 msgid "Sync failed: {}" msgstr "" -#: ui/app.slint:323 +#: ui/app.slint:329 msgid "Local mail data is unavailable; preview data is read-only." msgstr "" -#: ui/app.slint:324 +#: ui/app.slint:330 msgid "Synchronizing accounts…" msgstr "" -#: ui/app.slint:329 +#: ui/app.slint:335 msgid "Message action completed." msgstr "" -#: ui/app.slint:330 +#: ui/app.slint:336 msgid "Message action failed: {}" msgstr "" -#: ui/app.slint:331 +#: ui/app.slint:337 msgid "Message action failed: message is no longer available" msgstr "" -#: ui/app.slint:332 +#: ui/app.slint:338 msgid "Label added." msgstr "" -#: ui/app.slint:333 +#: ui/app.slint:339 msgid "Label removed." msgstr "" -#: ui/app.slint:334 +#: ui/app.slint:340 msgid "Label created and added." msgstr "" -#: ui/app.slint:335 +#: ui/app.slint:341 msgid "Label updated." msgstr "" -#: ui/app.slint:336 +#: ui/app.slint:342 msgid "Label deleted." msgstr "" -#: ui/app.slint:337 +#: ui/app.slint:343 msgid "Could not update label: {}" msgstr "" -#: ui/app.slint:338 +#: ui/app.slint:344 msgid "Could not save label: {}" msgstr "" -#: ui/app.slint:339 +#: ui/app.slint:345 msgid "Could not delete label: {}" msgstr "" -#: ui/app.slint:340 +#: ui/app.slint:346 msgid "Folder created." msgstr "" -#: ui/app.slint:341 +#: ui/app.slint:347 msgid "Folder renamed." msgstr "" -#: ui/app.slint:342 +#: ui/app.slint:348 msgid "Folder deleted." msgstr "" -#: ui/app.slint:343 +#: ui/app.slint:349 msgid "Could not create folder: {}" msgstr "" -#: ui/app.slint:344 +#: ui/app.slint:350 msgid "Could not rename folder: {}" msgstr "" -#: ui/app.slint:345 +#: ui/app.slint:351 msgid "Could not delete folder: {}" msgstr "" -#: ui/app.slint:346 +#: ui/app.slint:352 msgid "Could not close preview: {}" msgstr "" -#: ui/app.slint:347 +#: ui/app.slint:353 msgid "Opened the system print view." msgstr "" -#: ui/app.slint:348 +#: ui/app.slint:354 msgid "Could not print message: {}" msgstr "" -#: ui/app.slint:349 +#: ui/app.slint:355 msgid "Opened the message in your browser." msgstr "" -#: ui/app.slint:350 +#: ui/app.slint:356 msgid "Could not open message in browser: {}" msgstr "" -#: ui/app.slint:351 +#: ui/app.slint:357 msgid "Connect an account before replying or forwarding." msgstr "" -#: ui/app.slint:352 +#: ui/app.slint:358 msgid "Choose a message first." msgstr "" -#: ui/app.slint:353 +#: ui/app.slint:359 msgid "Only drafts can be reopened for editing." msgstr "" -#: ui/app.slint:354 +#: ui/app.slint:360 msgid "Could not open draft: {}" msgstr "" -#: ui/app.slint:355 +#: ui/app.slint:361 msgid "Could not open draft: invalid account identifier" msgstr "" -#: ui/app.slint:356 +#: ui/app.slint:362 msgid "Connected account" msgstr "" -#: ui/app.slint:357 +#: ui/app.slint:363 msgid "Could not open composer: {}" msgstr "" -#: ui/app.slint:358 +#: ui/app.slint:364 msgid "Could not open composer: invalid account identifier" msgstr "" -#: ui/app.slint:363 +#: ui/app.slint:369 msgid "Attach files to this email" msgstr "" -#: ui/app.slint:364 +#: ui/app.slint:370 msgid "The platform host does not provide a document picker." msgstr "" -#: ui/app.slint:365 +#: ui/app.slint:371 msgid "Added 1 attachment." msgstr "" -#: ui/app.slint:366 +#: ui/app.slint:372 msgid "Added {} attachments." msgstr "" -#: ui/app.slint:367 +#: ui/app.slint:373 msgid "" "Attachments added: {}. Skipped: {} (duplicate, unreadable, or over the 25 MB " "total limit)." msgstr "" -#: ui/app.slint:368 +#: ui/app.slint:374 msgid "Attachment removed." msgstr "" -#: ui/app.slint:369 +#: ui/app.slint:375 msgid "Choose the account this message should be sent from." msgstr "" -#: ui/app.slint:370 +#: ui/app.slint:376 msgid "Connect an account before saving or sending mail." msgstr "" -#: ui/app.slint:371 +#: ui/app.slint:377 msgid "Message queued for delivery (action {})." msgstr "" -#: ui/app.slint:372 +#: ui/app.slint:378 msgid "Draft saved locally (#{})." msgstr "" -#: ui/app.slint:373 +#: ui/app.slint:379 msgid "Compose failed: {}" msgstr "" -#: ui/app.slint:374 +#: ui/app.slint:380 msgid "IMAP port must be between 1 and 65535." msgstr "" -#: ui/app.slint:375 +#: ui/app.slint:381 msgid "SMTP port must be between 1 and 65535." msgstr "" -#: ui/app.slint:376 +#: ui/app.slint:382 msgid "Local mail data is unavailable. Retry startup." msgstr "" -#: ui/app.slint:377 +#: ui/app.slint:383 msgid "Discovering JMAP and checking capabilities…" msgstr "" -#: ui/app.slint:378 +#: ui/app.slint:384 msgid "Certificate imported. Connect to verify the server." msgstr "" -#: ui/app.slint:379 +#: ui/app.slint:385 msgid "Could not load account preferences: {}" msgstr "" -#: ui/app.slint:380 +#: ui/app.slint:386 msgid "" "Your edited signature was preserved. Choose No signature to keep it as " "message text before inserting another." msgstr "" -#: ui/app.slint:381 +#: ui/app.slint:387 msgid "Certificate import failed: {}" msgstr "" -#: ui/app.slint:382 +#: ui/app.slint:388 msgid "Paste the PEM certificate into the certificate field." msgstr "" -#: ui/app.slint:383 +#: ui/app.slint:389 msgid "Testing IMAP and SMTP connections…" msgstr "" -#: ui/app.slint:388 +#: ui/app.slint:394 msgid "" "Another browser authorization is already in progress; finish it or choose " "Cancel." msgstr "" -#: ui/app.slint:389 +#: ui/app.slint:395 msgid "Add app keys in Sign-in settings to enable this provider." msgstr "" -#: ui/app.slint:390 +#: ui/app.slint:396 msgid "Choose Gmail or Outlook OAuth." msgstr "" -#: ui/app.slint:391 +#: ui/app.slint:397 msgid "Complete sign-in in your browser, or choose Cancel here." msgstr "" -#: ui/app.slint:392 +#: ui/app.slint:398 msgid "Cancelling browser sign-in…" msgstr "" -#: ui/app.slint:393 +#: ui/app.slint:399 msgid "Finishing browser sign-in…" msgstr "" -#: ui/app.slint:394 +#: ui/app.slint:400 msgid "Could not use that browser address: {}" msgstr "" -#: ui/app.slint:395 +#: ui/app.slint:401 msgid "Calendar core is unavailable." msgstr "" -#: ui/app.slint:396 +#: ui/app.slint:402 msgid "Account no longer exists." msgstr "" -#: ui/app.slint:397 +#: ui/app.slint:403 msgid "Authorize calendar access in your browser." msgstr "" -#: ui/app.slint:398 +#: ui/app.slint:404 msgid "OAuth app keys saved." msgstr "" -#: ui/app.slint:399 +#: ui/app.slint:405 msgid "{} OAuth settings saved." msgstr "" -#: ui/app.slint:400 +#: ui/app.slint:406 msgid "OAuth settings failed: {}" msgstr "" -#: ui/app.slint:401 +#: ui/app.slint:407 msgid "Mail refresh failed: {}" msgstr "" -#: ui/app.slint:402 +#: ui/app.slint:408 msgid "Mail filter failed: {}" msgstr "" -#: ui/app.slint:403 +#: ui/app.slint:409 msgid "Mail pagination failed: {}" msgstr "" -#: ui/app.slint:404 +#: ui/app.slint:410 msgid "Could not mark message as read: {}" msgstr "" -#: ui/app.slint:405 ui/views/mail-reading-pane.slint:755 +#: ui/app.slint:411 ui/views/mail-reading-pane.slint:755 msgid "Loading message…" msgstr "" -#: ui/app.slint:411 ui/dialogs/settings-dialog.slint:971 ui/tray.slint:9 -#: ui/app.slint:411 ui/dialogs/settings-dialog.slint:971 ui/tray.slint:9 ui/views/onboarding-view.slint:97 +#: ui/app.slint:417 ui/dialogs/settings-dialog.slint:1039 ui/tray.slint:9 +#: ui/app.slint:417 ui/dialogs/settings-dialog.slint:1039 ui/tray.slint:9 ui/views/onboarding-view.slint:97 msgid "Flectar Mail" msgstr "" -#: ui/app.slint:1138 +#: ui/app.slint:1146 msgid "Showing {} of {} contacts" msgstr "" -#: ui/app.slint:1629 +#: ui/app.slint:1639 msgid "Mailbox startup failed" msgstr "" -#: ui/app.slint:1635 +#: ui/app.slint:1645 msgid "Flectar Mail could not open local storage" msgstr "" -#: ui/app.slint:1648 +#: ui/app.slint:1658 msgid "Diagnostic ID: {}" msgstr "" -#: ui/app.slint:1651 +#: ui/app.slint:1661 msgid "Diagnostic ID {}" msgstr "" -#: ui/app.slint:1656 +#: ui/app.slint:1666 msgid "Retry" msgstr "" -#: ui/app.slint:1664 +#: ui/app.slint:1674 msgid "Copy diagnostics" msgstr "" @@ -1386,7 +1410,7 @@ msgstr "" #: ui/components/calendar.slint:268 ui/components/calendar.slint:278 #: ui/components/calendar.slint:268 ui/components/calendar.slint:278 ui/components/calendar.slint:317 #: ui/components/calendar.slint:268 ui/components/calendar.slint:278 ui/components/calendar.slint:317 ui/components/product-rail.slint:111 -#: ui/components/calendar.slint:268 ui/components/calendar.slint:278 ui/components/calendar.slint:317 ui/components/product-rail.slint:111 ui/dialogs/settings-dialog.slint:733 +#: ui/components/calendar.slint:268 ui/components/calendar.slint:278 ui/components/calendar.slint:317 ui/components/product-rail.slint:111 ui/dialogs/settings-dialog.slint:787 msgid "Calendar" msgstr "" @@ -1491,7 +1515,7 @@ msgstr "" msgid "This event will be removed from your calendar." msgstr "" -#: ui/components/calendar.slint:833 ui/components/confirmation-dialog.slint:11 +#: ui/components/calendar.slint:833 ui/components/confirmation-dialog.slint:12 msgid "This action cannot be undone." msgstr "" @@ -1596,10 +1620,10 @@ msgstr "" #: ui/components/composer-actions.slint:393 #: ui/components/composer-actions.slint:393 #: ui/components/composer-actions.slint:393 ui/dialogs/folder-dialog.slint:118 -#: ui/components/composer-actions.slint:393 ui/dialogs/folder-dialog.slint:118 ui/dialogs/settings-dialog.slint:799 -#: ui/components/composer-actions.slint:393 ui/dialogs/folder-dialog.slint:118 ui/dialogs/settings-dialog.slint:799 ui/views/contacts-view.slint:538 -#: ui/components/composer-actions.slint:393 ui/dialogs/folder-dialog.slint:118 ui/dialogs/settings-dialog.slint:799 ui/views/contacts-view.slint:538 ui/views/files-view.slint:534 -#: ui/components/composer-actions.slint:393 ui/dialogs/folder-dialog.slint:118 ui/dialogs/settings-dialog.slint:799 ui/views/contacts-view.slint:538 ui/views/files-view.slint:534 +#: ui/components/composer-actions.slint:393 ui/dialogs/folder-dialog.slint:118 ui/dialogs/settings-dialog.slint:867 +#: ui/components/composer-actions.slint:393 ui/dialogs/folder-dialog.slint:118 ui/dialogs/settings-dialog.slint:867 ui/views/contacts-view.slint:538 +#: ui/components/composer-actions.slint:393 ui/dialogs/folder-dialog.slint:118 ui/dialogs/settings-dialog.slint:867 ui/views/contacts-view.slint:538 ui/views/files-view.slint:534 +#: ui/components/composer-actions.slint:393 ui/dialogs/folder-dialog.slint:118 ui/dialogs/settings-dialog.slint:867 ui/views/contacts-view.slint:538 ui/views/files-view.slint:534 msgid "Cancel" msgstr "" @@ -1684,7 +1708,7 @@ msgstr "" msgid "What is this email about?" msgstr "" -#: ui/components/confirmation-dialog.slint:12 +#: ui/components/confirmation-dialog.slint:13 msgid "Confirm" msgstr "" @@ -1692,7 +1716,7 @@ msgstr "" msgid "On" msgstr "" -#: ui/components/controls.slint:160 ui/dialogs/settings-dialog.slint:127 +#: ui/components/controls.slint:160 ui/dialogs/settings-dialog.slint:165 msgid "Off" msgstr "" @@ -1979,7 +2003,7 @@ msgid "Palette" msgstr "" #: ui/components/theme-editor.slint:567 ui/components/theme-editor.slint:579 -#: ui/components/theme-editor.slint:567 ui/components/theme-editor.slint:579 ui/dialogs/settings-dialog.slint:359 +#: ui/components/theme-editor.slint:567 ui/components/theme-editor.slint:579 ui/dialogs/settings-dialog.slint:425 msgid "Default" msgstr "" @@ -2175,7 +2199,7 @@ msgid "Open meeting link" msgstr "" #: ui/dialogs/calendar-details-dialog.slint:219 -#: ui/dialogs/calendar-details-dialog.slint:219 +#: ui/dialogs/calendar-details-dialog.slint:219 ui/dialogs/settings-dialog.slint:809 msgid "Edit" msgstr "" @@ -2341,7 +2365,7 @@ msgstr "" msgid "Microsoft client ID" msgstr "" -#: ui/dialogs/oauth-setup-dialog.slint:95 ui/dialogs/settings-dialog.slint:787 +#: ui/dialogs/oauth-setup-dialog.slint:95 ui/dialogs/settings-dialog.slint:855 msgid "Microsoft Entra desktop client ID" msgstr "" @@ -2408,690 +2432,718 @@ msgstr "" msgid "Report issue" msgstr "" -#: ui/dialogs/settings-dialog.slint:122 +#: ui/dialogs/settings-dialog.slint:37 +msgid "Remove" +msgstr "" + +#: ui/dialogs/settings-dialog.slint:44 +msgid "Sync" +msgstr "" + +#: ui/dialogs/settings-dialog.slint:160 msgid "Every minute" msgstr "" -#: ui/dialogs/settings-dialog.slint:123 +#: ui/dialogs/settings-dialog.slint:161 msgid "Every 5 minutes" msgstr "" -#: ui/dialogs/settings-dialog.slint:124 +#: ui/dialogs/settings-dialog.slint:162 msgid "Every 15 minutes" msgstr "" -#: ui/dialogs/settings-dialog.slint:128 +#: ui/dialogs/settings-dialog.slint:166 msgid "Important mail" msgstr "" -#: ui/dialogs/settings-dialog.slint:129 +#: ui/dialogs/settings-dialog.slint:167 msgid "All new mail" msgstr "" -#: ui/dialogs/settings-dialog.slint:132 +#: ui/dialogs/settings-dialog.slint:170 msgid "1 month" msgstr "" -#: ui/dialogs/settings-dialog.slint:133 +#: ui/dialogs/settings-dialog.slint:171 msgid "3 months" msgstr "" -#: ui/dialogs/settings-dialog.slint:134 +#: ui/dialogs/settings-dialog.slint:172 msgid "6 months · Recommended" msgstr "" -#: ui/dialogs/settings-dialog.slint:135 +#: ui/dialogs/settings-dialog.slint:173 msgid "1 year" msgstr "" -#: ui/dialogs/settings-dialog.slint:136 ui/views/mail-list-pane.slint:208 +#: ui/dialogs/settings-dialog.slint:174 ui/views/mail-list-pane.slint:208 msgid "All mail" msgstr "" -#: ui/dialogs/settings-dialog.slint:233 ui/dialogs/settings-dialog.slint:1112 +#: ui/dialogs/settings-dialog.slint:299 ui/dialogs/settings-dialog.slint:1180 msgid "SETTINGS" msgstr "" -#: ui/dialogs/settings-dialog.slint:235 ui/dialogs/settings-dialog.slint:262 -#: ui/dialogs/settings-dialog.slint:235 ui/dialogs/settings-dialog.slint:262 +#: ui/dialogs/settings-dialog.slint:301 ui/dialogs/settings-dialog.slint:328 +#: ui/dialogs/settings-dialog.slint:301 ui/dialogs/settings-dialog.slint:328 msgid "General" msgstr "" -#: ui/dialogs/settings-dialog.slint:236 ui/dialogs/settings-dialog.slint:558 -#: ui/dialogs/settings-dialog.slint:236 ui/dialogs/settings-dialog.slint:558 +#: ui/dialogs/settings-dialog.slint:302 ui/dialogs/settings-dialog.slint:624 +#: ui/dialogs/settings-dialog.slint:302 ui/dialogs/settings-dialog.slint:624 msgid "Accounts" msgstr "" -#: ui/dialogs/settings-dialog.slint:237 ui/dialogs/settings-dialog.slint:873 -#: ui/dialogs/settings-dialog.slint:237 ui/dialogs/settings-dialog.slint:873 +#: ui/dialogs/settings-dialog.slint:303 ui/dialogs/settings-dialog.slint:941 +#: ui/dialogs/settings-dialog.slint:303 ui/dialogs/settings-dialog.slint:941 msgid "Notifications" msgstr "" -#: ui/dialogs/settings-dialog.slint:238 ui/dialogs/settings-dialog.slint:1116 +#: ui/dialogs/settings-dialog.slint:304 ui/dialogs/settings-dialog.slint:1184 msgid "Shortcuts" msgstr "" -#: ui/dialogs/settings-dialog.slint:239 ui/dialogs/settings-dialog.slint:817 -#: ui/dialogs/settings-dialog.slint:239 ui/dialogs/settings-dialog.slint:817 ui/dialogs/settings-dialog.slint:1117 +#: ui/dialogs/settings-dialog.slint:305 ui/dialogs/settings-dialog.slint:885 +#: ui/dialogs/settings-dialog.slint:305 ui/dialogs/settings-dialog.slint:885 ui/dialogs/settings-dialog.slint:1185 msgid "Data management" msgstr "" -#: ui/dialogs/settings-dialog.slint:240 ui/dialogs/settings-dialog.slint:947 -#: ui/dialogs/settings-dialog.slint:240 ui/dialogs/settings-dialog.slint:947 +#: ui/dialogs/settings-dialog.slint:306 ui/dialogs/settings-dialog.slint:1015 +#: ui/dialogs/settings-dialog.slint:306 ui/dialogs/settings-dialog.slint:1015 msgid "About" msgstr "" -#: ui/dialogs/settings-dialog.slint:263 +#: ui/dialogs/settings-dialog.slint:329 msgid "Manage appearance, privacy, and synchronization." msgstr "" -#: ui/dialogs/settings-dialog.slint:309 +#: ui/dialogs/settings-dialog.slint:375 msgid "Interface" msgstr "" -#: ui/dialogs/settings-dialog.slint:312 +#: ui/dialogs/settings-dialog.slint:378 msgid "Monochrome sidebar icons" msgstr "" -#: ui/dialogs/settings-dialog.slint:313 +#: ui/dialogs/settings-dialog.slint:379 msgid "Use one neutral color for mailbox and folder icons." msgstr "" -#: ui/dialogs/settings-dialog.slint:325 +#: ui/dialogs/settings-dialog.slint:391 msgid "No avatars" msgstr "" -#: ui/dialogs/settings-dialog.slint:326 +#: ui/dialogs/settings-dialog.slint:392 msgid "Hide sender images in the message list and reader." msgstr "" -#: ui/dialogs/settings-dialog.slint:339 ui/dialogs/settings-dialog.slint:355 +#: ui/dialogs/settings-dialog.slint:405 ui/dialogs/settings-dialog.slint:421 msgid "Workspace layout" msgstr "" -#: ui/dialogs/settings-dialog.slint:346 ui/dialogs/settings-dialog.slint:360 +#: ui/dialogs/settings-dialog.slint:412 ui/dialogs/settings-dialog.slint:426 msgid "Minimal" msgstr "" -#: ui/dialogs/settings-dialog.slint:378 +#: ui/dialogs/settings-dialog.slint:444 msgid "Language" msgstr "" -#: ui/dialogs/settings-dialog.slint:382 +#: ui/dialogs/settings-dialog.slint:448 msgid "System language" msgstr "" -#: ui/dialogs/settings-dialog.slint:383 +#: ui/dialogs/settings-dialog.slint:449 msgid "Spanish" msgstr "" -#: ui/dialogs/settings-dialog.slint:384 +#: ui/dialogs/settings-dialog.slint:450 msgid "English" msgstr "" -#: ui/dialogs/settings-dialog.slint:401 +#: ui/dialogs/settings-dialog.slint:467 msgid "Inbox behavior" msgstr "" -#: ui/dialogs/settings-dialog.slint:404 +#: ui/dialogs/settings-dialog.slint:470 msgid "Unified Inbox" msgstr "" -#: ui/dialogs/settings-dialog.slint:406 +#: ui/dialogs/settings-dialog.slint:472 msgid "DEFAULT" msgstr "" -#: ui/dialogs/settings-dialog.slint:408 +#: ui/dialogs/settings-dialog.slint:474 msgid "Combines incoming messages from every connected account." msgstr "" -#: ui/dialogs/settings-dialog.slint:417 +#: ui/dialogs/settings-dialog.slint:483 msgid "Mark messages as read when opened" msgstr "" -#: ui/dialogs/settings-dialog.slint:418 +#: ui/dialogs/settings-dialog.slint:484 msgid "Turn this off to keep unread status until you change it manually." msgstr "" -#: ui/dialogs/settings-dialog.slint:445 +#: ui/dialogs/settings-dialog.slint:511 msgid "Remote images" msgstr "" -#: ui/dialogs/settings-dialog.slint:446 +#: ui/dialogs/settings-dialog.slint:512 msgid "Load sender favicons and images embedded in message bodies." msgstr "" -#: ui/dialogs/settings-dialog.slint:446 +#: ui/dialogs/settings-dialog.slint:512 msgid "Unavailable in this minimal build." msgstr "" -#: ui/dialogs/settings-dialog.slint:447 +#: ui/dialogs/settings-dialog.slint:513 msgid "Off by default to block tracking pixels and minimize memory use." msgstr "" -#: ui/dialogs/settings-dialog.slint:473 +#: ui/dialogs/settings-dialog.slint:539 msgid "Keep running in system tray" msgstr "" -#: ui/dialogs/settings-dialog.slint:474 +#: ui/dialogs/settings-dialog.slint:540 msgid "Closing the window hides Flectar Mail while mail continues syncing." msgstr "" -#: ui/dialogs/settings-dialog.slint:475 +#: ui/dialogs/settings-dialog.slint:541 msgid "Disabled by default; Quit from the tray menu stops the app." msgstr "" -#: ui/dialogs/settings-dialog.slint:500 +#: ui/dialogs/settings-dialog.slint:566 msgid "Sync and notifications" msgstr "" -#: ui/dialogs/settings-dialog.slint:503 +#: ui/dialogs/settings-dialog.slint:569 msgid "Check for new mail" msgstr "" -#: ui/dialogs/settings-dialog.slint:504 +#: ui/dialogs/settings-dialog.slint:570 msgid "How often to refresh when an instant provider update is unavailable." msgstr "" -#: ui/dialogs/settings-dialog.slint:514 ui/dialogs/settings-dialog.slint:890 +#: ui/dialogs/settings-dialog.slint:580 ui/dialogs/settings-dialog.slint:958 msgid "Desktop notifications" msgstr "" -#: ui/dialogs/settings-dialog.slint:515 ui/dialogs/settings-dialog.slint:891 +#: ui/dialogs/settings-dialog.slint:581 ui/dialogs/settings-dialog.slint:959 msgid "Choose which incoming messages can alert you." msgstr "" -#: ui/dialogs/settings-dialog.slint:528 +#: ui/dialogs/settings-dialog.slint:594 msgid "Provider synchronization is ready." msgstr "" -#: ui/dialogs/settings-dialog.slint:536 ui/dialogs/settings-dialog.slint:694 +#: ui/dialogs/settings-dialog.slint:602 ui/dialogs/settings-dialog.slint:764 msgid "Syncing…" msgstr "" -#: ui/dialogs/settings-dialog.slint:536 +#: ui/dialogs/settings-dialog.slint:602 msgid "Sync now" msgstr "" -#: ui/dialogs/settings-dialog.slint:559 +#: ui/dialogs/settings-dialog.slint:625 msgid "" "Manage connected mailboxes and add providers. Drag an account to change its " "order everywhere." msgstr "" -#: ui/dialogs/settings-dialog.slint:578 ui/dialogs/settings-dialog.slint:622 +#: ui/dialogs/settings-dialog.slint:644 ui/dialogs/settings-dialog.slint:688 msgid "Add Gmail" msgstr "" -#: ui/dialogs/settings-dialog.slint:590 ui/dialogs/settings-dialog.slint:623 +#: ui/dialogs/settings-dialog.slint:656 ui/dialogs/settings-dialog.slint:689 msgid "Add Outlook" msgstr "" -#: ui/dialogs/settings-dialog.slint:602 ui/dialogs/settings-dialog.slint:624 +#: ui/dialogs/settings-dialog.slint:668 ui/dialogs/settings-dialog.slint:690 msgid "Add mail account" msgstr "" -#: ui/dialogs/settings-dialog.slint:615 ui/dialogs/settings-dialog.slint:625 -#: ui/dialogs/settings-dialog.slint:615 ui/dialogs/settings-dialog.slint:625 +#: ui/dialogs/settings-dialog.slint:681 ui/dialogs/settings-dialog.slint:691 +#: ui/dialogs/settings-dialog.slint:681 ui/dialogs/settings-dialog.slint:691 msgid "Cancel sign-in" msgstr "" -#: ui/dialogs/settings-dialog.slint:694 +#: ui/dialogs/settings-dialog.slint:764 msgid "Offline · retrying automatically" msgstr "" -#: ui/dialogs/settings-dialog.slint:694 +#: ui/dialogs/settings-dialog.slint:764 msgid "Sign-in required" msgstr "" -#: ui/dialogs/settings-dialog.slint:694 +#: ui/dialogs/settings-dialog.slint:764 msgid "Connected · JMAP" msgstr "" -#: ui/dialogs/settings-dialog.slint:694 +#: ui/dialogs/settings-dialog.slint:764 msgid "Connected · IMAP + SMTP" msgstr "" -#: ui/dialogs/settings-dialog.slint:694 +#: ui/dialogs/settings-dialog.slint:764 msgid "Account status: {}" msgstr "" -#: ui/dialogs/settings-dialog.slint:698 +#: ui/dialogs/settings-dialog.slint:768 msgid "Mail synchronization error: {}" msgstr "" -#: ui/dialogs/settings-dialog.slint:707 +#: ui/dialogs/settings-dialog.slint:775 ui/dialogs/settings-dialog.slint:809 msgid "Reconnect" msgstr "" -#: ui/dialogs/settings-dialog.slint:740 +#: ui/dialogs/settings-dialog.slint:794 msgid "Add calendar" msgstr "" -#: ui/dialogs/settings-dialog.slint:740 +#: ui/dialogs/settings-dialog.slint:794 msgid "Retry calendar" msgstr "" -#: ui/dialogs/settings-dialog.slint:747 +#: ui/dialogs/settings-dialog.slint:801 msgid "Set up CalDAV" msgstr "" -#: ui/dialogs/settings-dialog.slint:766 +#: ui/dialogs/settings-dialog.slint:833 msgid "Mail history" msgstr "" -#: ui/dialogs/settings-dialog.slint:768 +#: ui/dialogs/settings-dialog.slint:836 msgid "" "Download the full mailbox. Large accounts can take considerable time and " "storage." msgstr "" -#: ui/dialogs/settings-dialog.slint:769 +#: ui/dialogs/settings-dialog.slint:837 msgid "How far back to download. Mail already stored on this device is kept." msgstr "" -#: ui/dialogs/settings-dialog.slint:781 +#: ui/dialogs/settings-dialog.slint:849 msgid "OAuth app credentials" msgstr "" -#: ui/dialogs/settings-dialog.slint:781 +#: ui/dialogs/settings-dialog.slint:849 msgid "Custom desktop app credentials are configured." msgstr "" -#: ui/dialogs/settings-dialog.slint:781 +#: ui/dialogs/settings-dialog.slint:849 msgid "Advanced: use your own Gmail or Microsoft desktop app registration." msgstr "" -#: ui/dialogs/settings-dialog.slint:782 +#: ui/dialogs/settings-dialog.slint:850 msgid "Hide" msgstr "" -#: ui/dialogs/settings-dialog.slint:782 +#: ui/dialogs/settings-dialog.slint:850 msgid "Configure" msgstr "" -#: ui/dialogs/settings-dialog.slint:785 ui/dialogs/settings-dialog.slint:786 +#: ui/dialogs/settings-dialog.slint:853 ui/dialogs/settings-dialog.slint:854 msgid "Gmail desktop client ID" msgstr "" -#: ui/dialogs/settings-dialog.slint:785 ui/dialogs/settings-dialog.slint:786 +#: ui/dialogs/settings-dialog.slint:853 ui/dialogs/settings-dialog.slint:854 msgid "Gmail client secret (optional)" msgstr "" -#: ui/dialogs/settings-dialog.slint:785 +#: ui/dialogs/settings-dialog.slint:853 msgid "Save Gmail" msgstr "" -#: ui/dialogs/settings-dialog.slint:786 +#: ui/dialogs/settings-dialog.slint:854 msgid "Save Gmail settings" msgstr "" -#: ui/dialogs/settings-dialog.slint:787 +#: ui/dialogs/settings-dialog.slint:855 msgid "Save Outlook" msgstr "" -#: ui/dialogs/settings-dialog.slint:788 +#: ui/dialogs/settings-dialog.slint:856 msgid "Outlook public desktop clients use PKCE and do not need a client secret." msgstr "" -#: ui/dialogs/settings-dialog.slint:794 +#: ui/dialogs/settings-dialog.slint:862 msgid "Connect CalDAV" msgstr "" -#: ui/dialogs/settings-dialog.slint:794 +#: ui/dialogs/settings-dialog.slint:862 msgid "" "Works with iCloud, Fastmail, Nextcloud, and standards-compliant CalDAV " "servers." msgstr "" -#: ui/dialogs/settings-dialog.slint:797 +#: ui/dialogs/settings-dialog.slint:865 msgid "https://calendar.example.com/" msgstr "" -#: ui/dialogs/settings-dialog.slint:798 ui/dialogs/settings-dialog.slint:1201 -#: ui/dialogs/settings-dialog.slint:798 ui/dialogs/settings-dialog.slint:1201 +#: ui/dialogs/settings-dialog.slint:866 ui/dialogs/settings-dialog.slint:1269 +#: ui/dialogs/settings-dialog.slint:866 ui/dialogs/settings-dialog.slint:1269 msgid "Username" msgstr "" -#: ui/dialogs/settings-dialog.slint:798 +#: ui/dialogs/settings-dialog.slint:866 msgid "App password" msgstr "" -#: ui/dialogs/settings-dialog.slint:799 ui/views/files-view.slint:586 +#: ui/dialogs/settings-dialog.slint:867 ui/views/files-view.slint:586 msgid "Connect" msgstr "" -#: ui/dialogs/settings-dialog.slint:820 +#: ui/dialogs/settings-dialog.slint:888 msgid "" "Transfer your setup, export a full snapshot, or remove local data from this " "device." msgstr "" -#: ui/dialogs/settings-dialog.slint:820 +#: ui/dialogs/settings-dialog.slint:888 msgid "Remove local data from this device." msgstr "" -#: ui/dialogs/settings-dialog.slint:831 +#: ui/dialogs/settings-dialog.slint:899 msgid "Database snapshot" msgstr "" -#: ui/dialogs/settings-dialog.slint:831 +#: ui/dialogs/settings-dialog.slint:899 msgid "" "Export verified copies of the mail and calendar databases with their schema " "versions." msgstr "" -#: ui/dialogs/settings-dialog.slint:833 +#: ui/dialogs/settings-dialog.slint:901 msgid "" "The snapshot contains private local mail and settings, but not passwords or " "OAuth tokens. Store it securely." msgstr "" -#: ui/dialogs/settings-dialog.slint:834 +#: ui/dialogs/settings-dialog.slint:902 msgid "Export snapshot" msgstr "" -#: ui/dialogs/settings-dialog.slint:841 +#: ui/dialogs/settings-dialog.slint:909 msgid "Transfer account setup" msgstr "" -#: ui/dialogs/settings-dialog.slint:841 +#: ui/dialogs/settings-dialog.slint:909 msgid "Import a backup or export one connected account and your preferences." msgid_plural "" "Import a backup or export {n} connected accounts and your preferences." msgstr[0] "" msgstr[1] "" -#: ui/dialogs/settings-dialog.slint:843 +#: ui/dialogs/settings-dialog.slint:911 msgid "" "Passwords and OAuth tokens stay in the system keyring. A restored device " "asks you to sign in securely." msgstr "" -#: ui/dialogs/settings-dialog.slint:850 +#: ui/dialogs/settings-dialog.slint:918 msgid "Import" msgstr "" -#: ui/dialogs/settings-dialog.slint:851 +#: ui/dialogs/settings-dialog.slint:919 msgid "Export" msgstr "" -#: ui/dialogs/settings-dialog.slint:858 +#: ui/dialogs/settings-dialog.slint:926 msgid "Delete all local data" msgstr "" -#: ui/dialogs/settings-dialog.slint:858 +#: ui/dialogs/settings-dialog.slint:926 msgid "" "Remove accounts, downloaded mail, calendars, contacts, preferences, and " "credentials from this device." msgstr "" -#: ui/dialogs/settings-dialog.slint:860 +#: ui/dialogs/settings-dialog.slint:928 msgid "This does not delete anything from Gmail, Outlook, or your IMAP server." msgstr "" -#: ui/dialogs/settings-dialog.slint:861 ui/dialogs/settings-dialog.slint:1285 +#: ui/dialogs/settings-dialog.slint:929 ui/dialogs/settings-dialog.slint:1353 msgid "Delete everything" msgstr "" -#: ui/dialogs/settings-dialog.slint:874 +#: ui/dialogs/settings-dialog.slint:942 msgid "Choose how Flectar Mail gets your attention." msgstr "" -#: ui/dialogs/settings-dialog.slint:901 +#: ui/dialogs/settings-dialog.slint:969 msgid "Notification sounds" msgstr "" -#: ui/dialogs/settings-dialog.slint:902 +#: ui/dialogs/settings-dialog.slint:970 msgid "Play a sound for new mail and sent messages." msgstr "" -#: ui/dialogs/settings-dialog.slint:921 +#: ui/dialogs/settings-dialog.slint:989 msgid "Keyboard shortcuts" msgstr "" -#: ui/dialogs/settings-dialog.slint:922 +#: ui/dialogs/settings-dialog.slint:990 msgid "Move through mail without leaving the keyboard." msgstr "" -#: ui/dialogs/settings-dialog.slint:925 +#: ui/dialogs/settings-dialog.slint:993 msgid "Compose a new message" msgstr "" -#: ui/dialogs/settings-dialog.slint:925 +#: ui/dialogs/settings-dialog.slint:993 msgid "C" msgstr "" -#: ui/dialogs/settings-dialog.slint:926 ui/views/mail-list-pane.slint:206 +#: ui/dialogs/settings-dialog.slint:994 ui/views/mail-list-pane.slint:206 msgid "Search mail" msgstr "" -#: ui/dialogs/settings-dialog.slint:926 +#: ui/dialogs/settings-dialog.slint:994 msgid "/" msgstr "" -#: ui/dialogs/settings-dialog.slint:927 +#: ui/dialogs/settings-dialog.slint:995 msgid "Archive selected message" msgstr "" -#: ui/dialogs/settings-dialog.slint:927 +#: ui/dialogs/settings-dialog.slint:995 msgid "E" msgstr "" -#: ui/dialogs/settings-dialog.slint:928 +#: ui/dialogs/settings-dialog.slint:996 msgid "Mark as spam" msgstr "" -#: ui/dialogs/settings-dialog.slint:928 +#: ui/dialogs/settings-dialog.slint:996 msgid "!" msgstr "" -#: ui/dialogs/settings-dialog.slint:929 ui/views/mail-list-pane.slint:538 -#: ui/dialogs/settings-dialog.slint:929 ui/views/mail-list-pane.slint:538 ui/views/mail-reading-pane.slint:576 +#: ui/dialogs/settings-dialog.slint:997 ui/views/mail-list-pane.slint:538 +#: ui/dialogs/settings-dialog.slint:997 ui/views/mail-list-pane.slint:538 ui/views/mail-reading-pane.slint:576 msgid "Move to trash" msgstr "" -#: ui/dialogs/settings-dialog.slint:929 +#: ui/dialogs/settings-dialog.slint:997 msgid "#" msgstr "" -#: ui/dialogs/settings-dialog.slint:948 +#: ui/dialogs/settings-dialog.slint:1016 msgid "Flectar Mail, its makers, and its community." msgstr "" -#: ui/dialogs/settings-dialog.slint:964 ui/views/onboarding-view.slint:91 +#: ui/dialogs/settings-dialog.slint:1032 ui/views/onboarding-view.slint:91 msgid "Flectar Mail logo" msgstr "" -#: ui/dialogs/settings-dialog.slint:984 +#: ui/dialogs/settings-dialog.slint:1052 msgid "A native mail client for a calm, private inbox." msgstr "" -#: ui/dialogs/settings-dialog.slint:996 +#: ui/dialogs/settings-dialog.slint:1064 msgid "Version 0.1.0" msgstr "" -#: ui/dialogs/settings-dialog.slint:1007 +#: ui/dialogs/settings-dialog.slint:1075 msgid "Credits" msgstr "" -#: ui/dialogs/settings-dialog.slint:1007 +#: ui/dialogs/settings-dialog.slint:1075 msgid "Built by Flectar with help from the open-source community." msgstr "" -#: ui/dialogs/settings-dialog.slint:1015 +#: ui/dialogs/settings-dialog.slint:1083 msgid "Flectar" msgstr "" -#: ui/dialogs/settings-dialog.slint:1016 +#: ui/dialogs/settings-dialog.slint:1084 msgid "Company website" msgstr "" -#: ui/dialogs/settings-dialog.slint:1026 +#: ui/dialogs/settings-dialog.slint:1094 msgid "Open-source contributors" msgstr "" -#: ui/dialogs/settings-dialog.slint:1027 +#: ui/dialogs/settings-dialog.slint:1095 msgid "Meet the people who have contributed on GitHub" msgstr "" -#: ui/dialogs/settings-dialog.slint:1040 +#: ui/dialogs/settings-dialog.slint:1108 msgid "License" msgstr "" -#: ui/dialogs/settings-dialog.slint:1040 +#: ui/dialogs/settings-dialog.slint:1108 msgid "AGPL-3.0-only." msgstr "" -#: ui/dialogs/settings-dialog.slint:1040 +#: ui/dialogs/settings-dialog.slint:1108 msgid "" "Flectar Mail is released under the GNU Affero General Public License v3.0 " "only." msgstr "" -#: ui/dialogs/settings-dialog.slint:1046 +#: ui/dialogs/settings-dialog.slint:1114 msgid "Third-party components retain their own licenses and notices." msgstr "" -#: ui/dialogs/settings-dialog.slint:1067 +#: ui/dialogs/settings-dialog.slint:1135 msgid "Settings sections" msgstr "" -#: ui/dialogs/settings-dialog.slint:1079 +#: ui/dialogs/settings-dialog.slint:1147 msgid "Close settings" msgstr "" -#: ui/dialogs/settings-dialog.slint:1096 +#: ui/dialogs/settings-dialog.slint:1164 msgid "Close settings sections" msgstr "" -#: ui/dialogs/settings-dialog.slint:1131 +#: ui/dialogs/settings-dialog.slint:1199 msgid "Edit mail account" msgstr "" -#: ui/dialogs/settings-dialog.slint:1131 ui/dialogs/settings-dialog.slint:1179 -#: ui/dialogs/settings-dialog.slint:1131 ui/dialogs/settings-dialog.slint:1179 +#: ui/dialogs/settings-dialog.slint:1199 ui/dialogs/settings-dialog.slint:1247 +#: ui/dialogs/settings-dialog.slint:1199 ui/dialogs/settings-dialog.slint:1247 msgid "Connect a mail account" msgstr "" -#: ui/dialogs/settings-dialog.slint:1179 +#: ui/dialogs/settings-dialog.slint:1247 msgid "Edit {}" msgstr "" -#: ui/dialogs/settings-dialog.slint:1180 +#: ui/dialogs/settings-dialog.slint:1248 msgid "Uses JMAP for mail sync and sending, with automatic discovery." msgstr "" -#: ui/dialogs/settings-dialog.slint:1180 +#: ui/dialogs/settings-dialog.slint:1248 msgid "Uses separate IMAP and SMTP servers for mail sync and sending." msgstr "" -#: ui/dialogs/settings-dialog.slint:1200 ui/dialogs/settings-dialog.slint:1207 -#: ui/dialogs/settings-dialog.slint:1200 ui/dialogs/settings-dialog.slint:1207 ui/views/onboarding-view.slint:234 +#: ui/dialogs/settings-dialog.slint:1268 ui/dialogs/settings-dialog.slint:1275 +#: ui/dialogs/settings-dialog.slint:1268 ui/dialogs/settings-dialog.slint:1275 ui/views/onboarding-view.slint:234 msgid "Email address" msgstr "" -#: ui/dialogs/settings-dialog.slint:1200 ui/dialogs/settings-dialog.slint:1207 -#: ui/dialogs/settings-dialog.slint:1200 ui/dialogs/settings-dialog.slint:1207 +#: ui/dialogs/settings-dialog.slint:1268 ui/dialogs/settings-dialog.slint:1275 +#: ui/dialogs/settings-dialog.slint:1268 ui/dialogs/settings-dialog.slint:1275 msgid "name@example.com" msgstr "" -#: ui/dialogs/settings-dialog.slint:1201 ui/dialogs/settings-dialog.slint:1208 +#: ui/dialogs/settings-dialog.slint:1269 ui/dialogs/settings-dialog.slint:1276 msgid "Usually your email address" msgstr "" -#: ui/dialogs/settings-dialog.slint:1210 ui/views/onboarding-view.slint:249 -#: ui/dialogs/settings-dialog.slint:1210 ui/views/onboarding-view.slint:249 +#: ui/dialogs/settings-dialog.slint:1278 ui/views/onboarding-view.slint:249 +#: ui/dialogs/settings-dialog.slint:1278 ui/views/onboarding-view.slint:249 msgid "Password or app password" msgstr "" -#: ui/dialogs/settings-dialog.slint:1210 +#: ui/dialogs/settings-dialog.slint:1278 msgid "Stored securely in your system keyring" msgstr "" -#: ui/dialogs/settings-dialog.slint:1215 ui/views/onboarding-view.slint:263 +#: ui/dialogs/settings-dialog.slint:1283 ui/views/onboarding-view.slint:263 msgid "IMAP + SMTP" msgstr "" -#: ui/dialogs/settings-dialog.slint:1216 ui/views/onboarding-view.slint:271 +#: ui/dialogs/settings-dialog.slint:1284 ui/views/onboarding-view.slint:271 msgid "JMAP" msgstr "" -#: ui/dialogs/settings-dialog.slint:1220 +#: ui/dialogs/settings-dialog.slint:1288 msgid "Server address · Optional" msgstr "" -#: ui/dialogs/settings-dialog.slint:1222 +#: ui/dialogs/settings-dialog.slint:1290 msgid "Discovered from your email domain" msgstr "" -#: ui/dialogs/settings-dialog.slint:1230 ui/views/onboarding-view.slint:296 -#: ui/dialogs/settings-dialog.slint:1230 ui/views/onboarding-view.slint:296 +#: ui/dialogs/settings-dialog.slint:1298 ui/views/onboarding-view.slint:296 +#: ui/dialogs/settings-dialog.slint:1298 ui/views/onboarding-view.slint:296 msgid "IMAP server" msgstr "" -#: ui/dialogs/settings-dialog.slint:1230 +#: ui/dialogs/settings-dialog.slint:1298 msgid "imap.example.com" msgstr "" -#: ui/dialogs/settings-dialog.slint:1231 ui/dialogs/settings-dialog.slint:1238 -#: ui/dialogs/settings-dialog.slint:1231 ui/dialogs/settings-dialog.slint:1238 ui/views/onboarding-view.slint:329 +#: ui/dialogs/settings-dialog.slint:1299 ui/dialogs/settings-dialog.slint:1306 +#: ui/dialogs/settings-dialog.slint:1299 ui/dialogs/settings-dialog.slint:1306 ui/views/onboarding-view.slint:329 msgid "Port" msgstr "" -#: ui/dialogs/settings-dialog.slint:1231 ui/views/onboarding-view.slint:309 +#: ui/dialogs/settings-dialog.slint:1299 ui/views/onboarding-view.slint:309 msgid "993" msgstr "" -#: ui/dialogs/settings-dialog.slint:1237 ui/views/onboarding-view.slint:320 -#: ui/dialogs/settings-dialog.slint:1237 ui/views/onboarding-view.slint:320 +#: ui/dialogs/settings-dialog.slint:1305 ui/views/onboarding-view.slint:320 +#: ui/dialogs/settings-dialog.slint:1305 ui/views/onboarding-view.slint:320 msgid "SMTP server" msgstr "" -#: ui/dialogs/settings-dialog.slint:1237 +#: ui/dialogs/settings-dialog.slint:1305 msgid "smtp.example.com" msgstr "" -#: ui/dialogs/settings-dialog.slint:1238 ui/views/onboarding-view.slint:333 +#: ui/dialogs/settings-dialog.slint:1306 ui/views/onboarding-view.slint:333 msgid "465" msgstr "" -#: ui/dialogs/settings-dialog.slint:1241 ui/views/onboarding-view.slint:338 +#: ui/dialogs/settings-dialog.slint:1309 ui/views/onboarding-view.slint:338 msgid "" "Proton Bridge: use the local servers, ports and password shown in Bridge, " "and match its encryption settings." msgstr "" -#: ui/dialogs/settings-dialog.slint:1265 ui/views/onboarding-view.slint:351 +#: ui/dialogs/settings-dialog.slint:1333 ui/views/onboarding-view.slint:351 msgid "Connecting…" msgstr "" -#: ui/dialogs/settings-dialog.slint:1265 +#: ui/dialogs/settings-dialog.slint:1333 msgid "Save account" msgstr "" -#: ui/dialogs/settings-dialog.slint:1265 ui/views/onboarding-view.slint:351 +#: ui/dialogs/settings-dialog.slint:1333 ui/views/onboarding-view.slint:351 msgid "Connect account" msgstr "" -#: ui/dialogs/settings-dialog.slint:1282 +#: ui/dialogs/settings-dialog.slint:1350 msgid "Delete all local data?" msgstr "" -#: ui/dialogs/settings-dialog.slint:1283 +#: ui/dialogs/settings-dialog.slint:1351 msgid "" "This removes every account, downloaded message, calendar, contact, " "preference, and credential stored by Flectar Mail on this device." msgstr "" -#: ui/dialogs/settings-dialog.slint:1284 +#: ui/dialogs/settings-dialog.slint:1352 msgid "Mail and events on Gmail, Outlook, and IMAP servers will not be deleted." msgstr "" -#: ui/dialogs/settings-dialog.slint:1286 +#: ui/dialogs/settings-dialog.slint:1354 msgid "Keep my data" msgstr "" +#: ui/dialogs/settings-dialog.slint:1368 +msgid "Remove account?" +msgstr "" + +#: ui/dialogs/settings-dialog.slint:1370 +msgid "Remove this account and its downloaded mail from this device." +msgstr "" + +#: ui/dialogs/settings-dialog.slint:1371 +msgid "Messages on the mail server will not be deleted." +msgstr "" + +#: ui/dialogs/settings-dialog.slint:1372 +msgid "Remove account" +msgstr "" + +#: ui/dialogs/settings-dialog.slint:1373 +msgid "Keep account" +msgstr "" + #: ui/tray.slint:19 msgid "Show Flectar Mail" msgstr "" diff --git a/src/account_controller.rs b/src/account_controller.rs index 5023085..ddb14af 100644 --- a/src/account_controller.rs +++ b/src/account_controller.rs @@ -2,6 +2,107 @@ use super::*; +pub(super) struct AccountRemovalUpdate { + pub account_id: i64, + pub removed: bool, + pub scope: String, + pub query: String, + pub metadata: Option, + pub page: Option, + pub calendar_month: NaiveDate, + pub calendar_events: Option>, + pub calendar_sources: Option>, +} + +pub(super) fn account_was_removed( + account_id: i64, + result: &Result<(), String>, + snapshot: Option<&mail::AccountSnapshot>, +) -> bool { + result.is_ok() + || snapshot.is_some_and(|snapshot| { + !snapshot + .accounts + .iter() + .any(|account| account.id == account_id) + }) +} + +pub(super) fn snapshot_is_current(state: &Rc>, revision: u64) -> bool { + state + .borrow() + .core + .as_ref() + .is_some_and(|core| core.account_revision() == revision) +} + +pub(super) fn account_owns_scope(mailboxes: &[MailboxEntry], scope: &str, account_id: i64) -> bool { + mailboxes + .iter() + .any(|mailbox| mailbox.account_id == account_id && mailbox.scope == scope) +} + +/// Apply the local half of deletion before projecting any account or mail +/// models. Even if reloading fails after deletion, no deleted rows survive. +pub(super) fn reconcile_removed_account(state: &mut InboxState, account_id: i64) { + if account_owns_scope(&state.mailboxes, &state.scope, account_id) { + state.scope = "Unified Inbox".into(); + } + if state + .messages + .iter() + .any(|message| message.account_id == account_id && Some(message.id) == state.selected_id) + { + state.selected_id = None; + state.preview_closed = true; + state.remote_images_override_id = None; + } + state + .messages + .retain(|message| message.account_id != account_id); + state + .mailboxes + .retain(|mailbox| mailbox.account_id != account_id); + state + .connected_accounts + .retain(|account| account.id != account_id); + state + .account_configs + .retain(|config| config.id != account_id); + state + .calendar_connections + .retain(|connection| connection.account_id != account_id); + state.calendar_errors.remove(&account_id); + state.profile_avatar_images.remove(&account_id); + state.profile_avatar_missing.remove(&account_id); + state.profile_avatar_pending.remove(&account_id); + state.collapsed_folder_ids.retain(|id| { + state + .mailboxes + .iter() + .any(|mailbox| mailbox.folder_id == *id) + }); + state.checked_ids.clear(); + state.next_cursor = None; + state.page = 1; + state.total_count = state.messages.len(); + state.inbox_count = 0; + for mailbox in &mut state.unified_mailboxes { + mailbox.count.clear(); + } + if state.connected_accounts.is_empty() { + state.messages.clear(); + state.mailboxes.clear(); + state.unified_mailboxes.clear(); + state.labels.clear(); + state.scope = "Unified Inbox".into(); + state.query.clear(); + state.selected_id = None; + state.total_count = 0; + } + mail_work::invalidate(state); +} + pub(super) fn apply_connected_accounts( app: &AppWindow, accounts: &[Account], @@ -84,7 +185,8 @@ pub(super) fn refresh_connected_accounts(app: &AppWindow, state: &Rc().invoke_context_changed(); + app.global::() + .invoke_context_changed(); app.global::().invoke_context_changed(); } diff --git a/src/account_removal_tests.rs b/src/account_removal_tests.rs new file mode 100644 index 0000000..aab3fce --- /dev/null +++ b/src/account_removal_tests.rs @@ -0,0 +1,153 @@ +//! Account-removal transitions against the real Slint models. +use super::*; +use slint::platform::{ + Platform, WindowAdapter, + software_renderer::{MinimalSoftwareWindow, RepaintBufferType}, +}; + +struct Headless(Rc); +impl Platform for Headless { + fn create_window_adapter(&self) -> Result, slint::PlatformError> { + Ok(self.0.clone()) + } +} + +fn account(id: i64) -> Account { + Account { + id, + email: format!("account-{id}@example.test"), + display_name: Some(format!("Account {id}")), + avatar_url: None, + provider: Provider::Imap, + auth_kind: flectar_mail_core::models::AuthKind::Password, + mail_protocol: MailProtocol::Imap, + sync_state: "idle".into(), + sync_error: None, + } +} +fn config(account: &Account) -> AccountConfig { + AccountConfig { + id: account.id, + email: account.email.clone(), + display_name: account.display_name.clone(), + avatar_url: None, + provider: account.provider, + auth_kind: account.auth_kind, + mail_protocol: account.mail_protocol, + username: account.email.clone(), + jmap_url: String::new(), + jmap_account_id: None, + imap_host: "imap.example.test".into(), + imap_port: 993, + smtp_host: "smtp.example.test".into(), + smtp_port: 465, + settings: Default::default(), + } +} + +#[test] +fn account_removal_clears_deleted_models_and_preserves_surviving_selection() { + let window = MinimalSoftwareWindow::new(RepaintBufferType::NewBuffer); + slint::platform::set_platform(Box::new(Headless(window))).unwrap(); + let app = AppWindow::new().unwrap(); + app.set_startup_ready(true); + let runtime = Rc::new( + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(), + ); + let dir = tempfile::tempdir().unwrap(); + for removed in [2, 1] { + let (fav, _) = bounded_ui_channel(); + let (avatars, _) = bounded_ui_channel(); + let mut state = InboxState::empty( + None, + UiSender::new(fav, UiWake::new(app.as_weak(), |_| {})), + None, + UiSender::new(avatars, UiWake::new(app.as_weak(), |_| {})), + false, + false, + WarmStartCacheWriter::spawn(&runtime, dir.path().join(format!("warm-{removed}.json"))), + ); + state.using_core = true; + state.connected_accounts = vec![account(1), account(2)]; + state.account_configs = state.connected_accounts.iter().map(config).collect(); + state.messages = fixture_messages().into_iter().take(2).collect(); + for (i, message) in state.messages.iter_mut().enumerate() { + message.account_id = i as i64 + 1; + message.account = format!("Account {}", i + 1); + message.folder = "Inbox".into(); + } + state.mailboxes = fixture_mailboxes(&state.messages); + for mailbox in &mut state.mailboxes { + mailbox.account_id = if mailbox.context == "Account 1" { 1 } else { 2 }; + } + state.scope = "Account 1 / Inbox".into(); + state.selected_id = Some(state.messages[0].id); + state.rendered_id = state.selected_id; + state.preview_closed = false; + state.checked_ids = state.messages.iter().map(|message| message.id).collect(); + state.next_cursor = Some(ThreadCursor { + last_message_at: 1, + thread_id: 1, + }); + state.page = 3; + let first_id = state.messages[0].id; + let state = Rc::new(RefCell::new(state)); + mail_work::register(&app, &state, &runtime); + refresh_connected_accounts(&app, &state); + assert_eq!(app.get_connected_accounts().row_count(), 2); + let old_generation = mail_work::generation(&state.borrow()); + reconcile_removed_account(&mut state.borrow_mut(), removed); + { + let state = state.borrow(); + assert!( + state + .messages + .iter() + .all(|message| message.account_id != removed) + ); + assert!( + state + .mailboxes + .iter() + .all(|mailbox| mailbox.account_id != removed) + ); + assert!( + state + .account_configs + .iter() + .all(|account| account.id != removed) + ); + assert!(state.checked_ids.is_empty()); + assert!(state.next_cursor.is_none()); + assert_eq!(state.page, 1); + assert!(!mail_work::accepts_background(&state, old_generation)); + if removed == 2 { + assert_eq!(state.scope, "Account 1 / Inbox"); + assert_eq!(state.selected_id, Some(first_id)); + } else { + assert_eq!(state.scope, "Unified Inbox"); + assert_eq!(state.selected_id, None); + assert!(state.preview_closed); + } + } + refresh_connected_accounts(&app, &state); + assert_eq!(app.get_connected_accounts().row_count(), 1); + assert_eq!(app.get_compose_account_id(), (3 - removed) as i32); + // Removing the final account must empty both the Rust state and the + // models that drive onboarding, compose and the mail workspace. + reconcile_removed_account(&mut state.borrow_mut(), 3 - removed); + refresh_connected_accounts(&app, &state); + render_current(&app, &state, &runtime).unwrap(); + assert_eq!(app.get_connected_accounts().row_count(), 0); + assert_eq!(app.get_compose_account_id(), -1); + assert_eq!(state.borrow().email_rows.row_count(), 0); + assert!(state.borrow().mailboxes.is_empty()); + assert!(state.borrow().unified_mailboxes.is_empty()); + assert!(state.borrow().selected_id.is_none()); + assert!(state.borrow().rendered_id.is_none()); + assert!(!app.global::().get_available()); + } +} diff --git a/src/data_controller.rs b/src/data_controller.rs index c09997e..308468c 100644 --- a/src/data_controller.rs +++ b/src/data_controller.rs @@ -220,6 +220,7 @@ pub(super) fn register_data_management_callbacks( accounts: None, calendar_connections: None, calendar_error: None, + account_removal: None, clear_account_form: false, finishes_account_setup: false, finishes_oauth: false, diff --git a/src/mail.rs b/src/mail.rs index cafc505..a85970c 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -153,6 +153,7 @@ pub struct ComposeSource { #[derive(Clone, Debug)] pub struct MailPage { + pub account_revision: u64, pub messages: Vec, pub labels: Vec