| | |
| |
|
| | use std::collections::{BTreeMap, HashMap}; |
| | use std::ffi::OsString; |
| | use std::ops::Deref; |
| | use std::path::{Path, PathBuf}; |
| | use std::sync::atomic::AtomicBool; |
| | use std::sync::{Arc, OnceLock, Weak}; |
| | use std::time::Duration; |
| |
|
| | use anyhow::{Context as _, Result, bail, ensure}; |
| | use async_channel::{self as channel, Receiver, Sender}; |
| | use ratelimit::Ratelimit; |
| | use tokio::sync::{Mutex, Notify, RwLock}; |
| |
|
| | use crate::chat::{ChatId, get_chat_cnt}; |
| | use crate::config::Config; |
| | use crate::constants::{self, DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_VERSION_STR}; |
| | use crate::contact::{Contact, ContactId}; |
| | use crate::debug_logging::DebugLogging; |
| | use crate::events::{Event, EventEmitter, EventType, Events}; |
| | use crate::imap::{FolderMeaning, Imap, ServerMetadata}; |
| | use crate::key::self_fingerprint; |
| | use crate::log::warn; |
| | use crate::logged_debug_assert; |
| | use crate::message::{self, MessageState, MsgId}; |
| | use crate::net::tls::TlsSessionStore; |
| | use crate::peer_channels::Iroh; |
| | use crate::push::PushSubscriber; |
| | use crate::quota::QuotaInfo; |
| | use crate::scheduler::{ConnectivityStore, SchedulerState, convert_folder_meaning}; |
| | use crate::sql::Sql; |
| | use crate::stock_str::StockStrings; |
| | use crate::timesmearing::SmearedTimestamp; |
| | use crate::tools::{self, duration_to_str, time, time_elapsed}; |
| | use crate::transport::ConfiguredLoginParam; |
| | use crate::{chatlist_events, stats}; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | #[derive(Clone, Debug)] |
| | pub struct ContextBuilder { |
| | dbfile: PathBuf, |
| | id: u32, |
| | events: Events, |
| | stock_strings: StockStrings, |
| | password: Option<String>, |
| |
|
| | push_subscriber: Option<PushSubscriber>, |
| | } |
| |
|
| | impl ContextBuilder { |
| | |
| | |
| | |
| | |
| | |
| | pub fn new(dbfile: PathBuf) -> Self { |
| | ContextBuilder { |
| | dbfile, |
| | id: rand::random(), |
| | events: Events::new(), |
| | stock_strings: StockStrings::new(), |
| | password: None, |
| | push_subscriber: None, |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | pub fn with_id(mut self, id: u32) -> Self { |
| | self.id = id; |
| | self |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | pub fn with_events(mut self, events: Events) -> Self { |
| | self.events = events; |
| | self |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | pub fn with_stock_strings(mut self, stock_strings: StockStrings) -> Self { |
| | self.stock_strings = stock_strings; |
| | self |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | #[deprecated(since = "TBD")] |
| | pub fn with_password(mut self, password: String) -> Self { |
| | self.password = Some(password); |
| | self |
| | } |
| |
|
| | |
| | pub(crate) fn with_push_subscriber(mut self, push_subscriber: PushSubscriber) -> Self { |
| | self.push_subscriber = Some(push_subscriber); |
| | self |
| | } |
| |
|
| | |
| | pub async fn build(self) -> Result<Context> { |
| | let push_subscriber = self.push_subscriber.unwrap_or_default(); |
| | let context = Context::new_closed( |
| | &self.dbfile, |
| | self.id, |
| | self.events, |
| | self.stock_strings, |
| | push_subscriber, |
| | ) |
| | .await?; |
| | Ok(context) |
| | } |
| |
|
| | |
| | |
| | |
| | pub async fn open(self) -> Result<Context> { |
| | let password = self.password.clone().unwrap_or_default(); |
| | let context = self.build().await?; |
| | match context.open(password).await? { |
| | true => Ok(context), |
| | false => bail!("database could not be decrypted, incorrect or missing password"), |
| | } |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | #[derive(Clone, Debug)] |
| | pub struct Context { |
| | pub(crate) inner: Arc<InnerContext>, |
| | } |
| |
|
| | impl Deref for Context { |
| | type Target = InnerContext; |
| |
|
| | fn deref(&self) -> &Self::Target { |
| | &self.inner |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | #[derive(Clone, Debug)] |
| | pub(crate) struct WeakContext { |
| | inner: Weak<InnerContext>, |
| | } |
| |
|
| | impl WeakContext { |
| | |
| | pub(crate) fn upgrade(&self) -> Result<Context> { |
| | let inner = self |
| | .inner |
| | .upgrade() |
| | .ok_or_else(|| anyhow::anyhow!("Inner struct has been dropped"))?; |
| | Ok(Context { inner }) |
| | } |
| | } |
| |
|
| | |
| | #[derive(Debug)] |
| | pub struct InnerContext { |
| | |
| | pub(crate) blobdir: PathBuf, |
| | pub(crate) sql: Sql, |
| | pub(crate) smeared_timestamp: SmearedTimestamp, |
| | |
| | |
| | |
| | |
| | running_state: RwLock<RunningState>, |
| | |
| | pub(crate) generating_key_mutex: Mutex<()>, |
| | |
| | pub(crate) oauth2_mutex: Mutex<()>, |
| | |
| | pub(crate) wrong_pw_warning_mutex: Mutex<()>, |
| | pub(crate) translated_stockstrings: StockStrings, |
| | pub(crate) events: Events, |
| |
|
| | pub(crate) scheduler: SchedulerState, |
| | pub(crate) ratelimit: RwLock<Ratelimit>, |
| |
|
| | |
| | |
| | pub(crate) quota: RwLock<BTreeMap<u32, QuotaInfo>>, |
| |
|
| | |
| | |
| | |
| | pub(crate) new_msgs_notify: Notify, |
| |
|
| | |
| | |
| | |
| | pub(crate) server_id: RwLock<Option<HashMap<String, String>>>, |
| |
|
| | |
| | pub(crate) metadata: RwLock<Option<ServerMetadata>>, |
| |
|
| | |
| | |
| | |
| | |
| | pub(crate) id: u32, |
| |
|
| | creation_time: tools::Time, |
| |
|
| | |
| | |
| | |
| | pub(crate) last_error: parking_lot::RwLock<String>, |
| |
|
| | |
| | |
| | |
| | |
| | |
| | pub(crate) migration_error: parking_lot::RwLock<Option<String>>, |
| |
|
| | |
| | |
| | |
| | |
| | pub(crate) debug_logging: std::sync::RwLock<Option<DebugLogging>>, |
| |
|
| | |
| | |
| | pub(crate) push_subscriber: PushSubscriber, |
| |
|
| | |
| | pub(crate) push_subscribed: AtomicBool, |
| |
|
| | |
| | pub(crate) tls_session_store: TlsSessionStore, |
| |
|
| | |
| | pub(crate) iroh: Arc<RwLock<Option<Iroh>>>, |
| |
|
| | |
| | |
| | |
| | pub(crate) self_fingerprint: OnceLock<String>, |
| |
|
| | |
| | |
| | pub(crate) connectivities: parking_lot::Mutex<Vec<ConnectivityStore>>, |
| |
|
| | #[expect(clippy::type_complexity)] |
| | |
| | pub(crate) pre_encrypt_mime_hook: parking_lot::Mutex< |
| | Option< |
| | for<'a> fn( |
| | &Context, |
| | mail_builder::mime::MimePart<'a>, |
| | ) -> mail_builder::mime::MimePart<'a>, |
| | >, |
| | >, |
| | } |
| |
|
| | |
| | #[derive(Debug, Default)] |
| | enum RunningState { |
| | |
| | Running { cancel_sender: Sender<()> }, |
| |
|
| | |
| | ShallStop { request: tools::Time }, |
| |
|
| | |
| | #[default] |
| | Stopped, |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | pub fn get_info() -> BTreeMap<&'static str, String> { |
| | let mut res = BTreeMap::new(); |
| |
|
| | #[cfg(debug_assertions)] |
| | res.insert( |
| | "debug_assertions", |
| | "On - DO NOT RELEASE THIS BUILD".to_string(), |
| | ); |
| | #[cfg(not(debug_assertions))] |
| | res.insert("debug_assertions", "Off".to_string()); |
| |
|
| | res.insert("deltachat_core_version", format!("v{DC_VERSION_STR}")); |
| | res.insert("sqlite_version", rusqlite::version().to_string()); |
| | res.insert("arch", (std::mem::size_of::<usize>() * 8).to_string()); |
| | res.insert("num_cpus", num_cpus::get().to_string()); |
| | res.insert("level", "awesome".into()); |
| | res |
| | } |
| |
|
| | impl Context { |
| | |
| | pub async fn new( |
| | dbfile: &Path, |
| | id: u32, |
| | events: Events, |
| | stock_strings: StockStrings, |
| | ) -> Result<Context> { |
| | let context = |
| | Self::new_closed(dbfile, id, events, stock_strings, Default::default()).await?; |
| |
|
| | |
| | if context.check_passphrase("".to_string()).await? { |
| | context.sql.open(&context, "".to_string()).await?; |
| | } |
| | Ok(context) |
| | } |
| |
|
| | |
| | pub async fn new_closed( |
| | dbfile: &Path, |
| | id: u32, |
| | events: Events, |
| | stockstrings: StockStrings, |
| | push_subscriber: PushSubscriber, |
| | ) -> Result<Context> { |
| | let mut blob_fname = OsString::new(); |
| | blob_fname.push(dbfile.file_name().unwrap_or_default()); |
| | blob_fname.push("-blobs"); |
| | let blobdir = dbfile.with_file_name(blob_fname); |
| | if !blobdir.exists() { |
| | tokio::fs::create_dir_all(&blobdir).await?; |
| | } |
| | let context = Context::with_blobdir( |
| | dbfile.into(), |
| | blobdir, |
| | id, |
| | events, |
| | stockstrings, |
| | push_subscriber, |
| | )?; |
| | Ok(context) |
| | } |
| |
|
| | |
| | pub(crate) fn get_weak_context(&self) -> WeakContext { |
| | WeakContext { |
| | inner: Arc::downgrade(&self.inner), |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | #[deprecated(since = "TBD")] |
| | pub async fn open(&self, passphrase: String) -> Result<bool> { |
| | if self.sql.check_passphrase(passphrase.clone()).await? { |
| | self.sql.open(self, passphrase).await?; |
| | Ok(true) |
| | } else { |
| | Ok(false) |
| | } |
| | } |
| |
|
| | |
| | |
| | pub async fn change_passphrase(&self, passphrase: String) -> Result<()> { |
| | self.sql.change_passphrase(passphrase).await?; |
| | Ok(()) |
| | } |
| |
|
| | |
| | pub async fn is_open(&self) -> bool { |
| | self.sql.is_open().await |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | pub(crate) async fn check_passphrase(&self, passphrase: String) -> Result<bool> { |
| | self.sql.check_passphrase(passphrase).await |
| | } |
| |
|
| | pub(crate) fn with_blobdir( |
| | dbfile: PathBuf, |
| | blobdir: PathBuf, |
| | id: u32, |
| | events: Events, |
| | stockstrings: StockStrings, |
| | push_subscriber: PushSubscriber, |
| | ) -> Result<Context> { |
| | ensure!( |
| | blobdir.is_dir(), |
| | "Blobdir does not exist: {}", |
| | blobdir.display() |
| | ); |
| |
|
| | let new_msgs_notify = Notify::new(); |
| | |
| | |
| | new_msgs_notify.notify_one(); |
| |
|
| | let inner = InnerContext { |
| | id, |
| | blobdir, |
| | running_state: RwLock::new(Default::default()), |
| | sql: Sql::new(dbfile), |
| | smeared_timestamp: SmearedTimestamp::new(), |
| | generating_key_mutex: Mutex::new(()), |
| | oauth2_mutex: Mutex::new(()), |
| | wrong_pw_warning_mutex: Mutex::new(()), |
| | translated_stockstrings: stockstrings, |
| | events, |
| | scheduler: SchedulerState::new(), |
| | ratelimit: RwLock::new(Ratelimit::new(Duration::new(3, 0), 3.0)), |
| | quota: RwLock::new(BTreeMap::new()), |
| | new_msgs_notify, |
| | server_id: RwLock::new(None), |
| | metadata: RwLock::new(None), |
| | creation_time: tools::Time::now(), |
| | last_error: parking_lot::RwLock::new("".to_string()), |
| | migration_error: parking_lot::RwLock::new(None), |
| | debug_logging: std::sync::RwLock::new(None), |
| | push_subscriber, |
| | push_subscribed: AtomicBool::new(false), |
| | tls_session_store: TlsSessionStore::new(), |
| | iroh: Arc::new(RwLock::new(None)), |
| | self_fingerprint: OnceLock::new(), |
| | connectivities: parking_lot::Mutex::new(Vec::new()), |
| | pre_encrypt_mime_hook: None.into(), |
| | }; |
| |
|
| | let ctx = Context { |
| | inner: Arc::new(inner), |
| | }; |
| |
|
| | Ok(ctx) |
| | } |
| |
|
| | |
| | pub async fn start_io(&self) { |
| | if !self.is_configured().await.unwrap_or_default() { |
| | warn!(self, "can not start io on a context that is not configured"); |
| | return; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | self.sql.config_cache.write().await.clear(); |
| |
|
| | self.scheduler.start(self).await; |
| | } |
| |
|
| | |
| | pub async fn stop_io(&self) { |
| | self.scheduler.stop(self).await; |
| | if let Some(iroh) = self.iroh.write().await.take() { |
| | |
| |
|
| | |
| | |
| | |
| | |
| | tokio::spawn(async move { |
| | |
| | |
| | let _ = tokio::time::timeout(Duration::from_secs(60), iroh.close()).await; |
| | }); |
| | } |
| | } |
| |
|
| | |
| | |
| | pub async fn restart_io_if_running(&self) { |
| | self.scheduler.restart(self).await; |
| | } |
| |
|
| | |
| | pub async fn maybe_network(&self) { |
| | if let Some(ref iroh) = *self.iroh.read().await { |
| | iroh.network_change().await; |
| | } |
| | self.scheduler.maybe_network().await; |
| | } |
| |
|
| | |
| | pub async fn is_chatmail(&self) -> Result<bool> { |
| | self.get_config_bool(Config::IsChatmail).await |
| | } |
| |
|
| | |
| | pub(crate) async fn get_max_smtp_rcpt_to(&self) -> Result<usize> { |
| | let is_chatmail = self.is_chatmail().await?; |
| | let val = self |
| | .get_configured_provider() |
| | .await? |
| | .and_then(|provider| provider.opt.max_smtp_rcpt_to) |
| | .map_or_else( |
| | || match is_chatmail { |
| | true => constants::DEFAULT_CHATMAIL_MAX_SMTP_RCPT_TO, |
| | false => constants::DEFAULT_MAX_SMTP_RCPT_TO, |
| | }, |
| | usize::from, |
| | ); |
| | Ok(val) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | pub async fn background_fetch(&self) -> Result<()> { |
| | if !(self.is_configured().await?) { |
| | return Ok(()); |
| | } |
| |
|
| | let address = self.get_primary_self_addr().await?; |
| | let time_start = tools::Time::now(); |
| | info!(self, "background_fetch started fetching {address}."); |
| |
|
| | if self.scheduler.is_running().await { |
| | self.scheduler.maybe_network().await; |
| | self.wait_for_all_work_done().await; |
| | } else { |
| | |
| | |
| | let _pause_guard = self.scheduler.pause(self).await?; |
| |
|
| | |
| | let mut connection = Imap::new_configured(self, channel::bounded(1).1).await?; |
| | let mut session = connection.prepare(self).await?; |
| |
|
| | |
| | |
| | |
| | for folder_meaning in [FolderMeaning::Inbox, FolderMeaning::Mvbox] { |
| | if let Some((_folder_config, watch_folder)) = |
| | convert_folder_meaning(self, folder_meaning).await? |
| | { |
| | connection |
| | .fetch_move_delete(self, &mut session, &watch_folder, folder_meaning) |
| | .await?; |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | if self |
| | .quota_needs_update( |
| | session.transport_id(), |
| | DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, |
| | ) |
| | .await |
| | && let Err(err) = self.update_recent_quota(&mut session).await |
| | { |
| | warn!(self, "Failed to update quota: {err:#}."); |
| | } |
| | } |
| |
|
| | info!( |
| | self, |
| | "background_fetch done for {address} took {:?}.", |
| | time_elapsed(&time_start), |
| | ); |
| |
|
| | Ok(()) |
| | } |
| |
|
| | |
| | |
| | |
| | #[cfg(feature = "internals")] |
| | pub fn sql(&self) -> &Sql { |
| | &self.inner.sql |
| | } |
| |
|
| | |
| | pub fn get_dbfile(&self) -> &Path { |
| | self.sql.dbfile.as_path() |
| | } |
| |
|
| | |
| | pub fn get_blobdir(&self) -> &Path { |
| | self.blobdir.as_path() |
| | } |
| |
|
| | |
| | pub fn emit_event(&self, event: EventType) { |
| | { |
| | let lock = self.debug_logging.read().expect("RwLock is poisoned"); |
| | if let Some(debug_logging) = &*lock { |
| | debug_logging.log_event(event.clone()); |
| | } |
| | } |
| | self.events.emit(Event { |
| | id: self.id, |
| | typ: event, |
| | }); |
| | } |
| |
|
| | |
| | pub fn emit_msgs_changed_without_ids(&self) { |
| | self.emit_event(EventType::MsgsChanged { |
| | chat_id: ChatId::new(0), |
| | msg_id: MsgId::new(0), |
| | }); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | pub fn emit_msgs_changed(&self, chat_id: ChatId, msg_id: MsgId) { |
| | logged_debug_assert!( |
| | self, |
| | !chat_id.is_unset(), |
| | "emit_msgs_changed: chat_id is unset." |
| | ); |
| | logged_debug_assert!( |
| | self, |
| | !msg_id.is_unset(), |
| | "emit_msgs_changed: msg_id is unset." |
| | ); |
| |
|
| | self.emit_event(EventType::MsgsChanged { chat_id, msg_id }); |
| | chatlist_events::emit_chatlist_changed(self); |
| | chatlist_events::emit_chatlist_item_changed(self, chat_id); |
| | } |
| |
|
| | |
| | pub fn emit_msgs_changed_without_msg_id(&self, chat_id: ChatId) { |
| | logged_debug_assert!( |
| | self, |
| | !chat_id.is_unset(), |
| | "emit_msgs_changed_without_msg_id: chat_id is unset." |
| | ); |
| |
|
| | self.emit_event(EventType::MsgsChanged { |
| | chat_id, |
| | msg_id: MsgId::new(0), |
| | }); |
| | chatlist_events::emit_chatlist_changed(self); |
| | chatlist_events::emit_chatlist_item_changed(self, chat_id); |
| | } |
| |
|
| | |
| | pub fn emit_incoming_msg(&self, chat_id: ChatId, msg_id: MsgId) { |
| | debug_assert!(!chat_id.is_unset()); |
| | debug_assert!(!msg_id.is_unset()); |
| |
|
| | self.emit_event(EventType::IncomingMsg { chat_id, msg_id }); |
| | chatlist_events::emit_chatlist_changed(self); |
| | chatlist_events::emit_chatlist_item_changed(self, chat_id); |
| | } |
| |
|
| | |
| | pub async fn emit_location_changed(&self, contact_id: Option<ContactId>) -> Result<()> { |
| | self.emit_event(EventType::LocationChanged(contact_id)); |
| |
|
| | if let Some(msg_id) = self |
| | .get_config_parsed::<u32>(Config::WebxdcIntegration) |
| | .await? |
| | { |
| | self.emit_event(EventType::WebxdcStatusUpdate { |
| | msg_id: MsgId::new(msg_id), |
| | status_update_serial: Default::default(), |
| | }) |
| | } |
| |
|
| | Ok(()) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | pub fn get_event_emitter(&self) -> EventEmitter { |
| | self.events.get_emitter() |
| | } |
| |
|
| | |
| | pub fn get_id(&self) -> u32 { |
| | self.id |
| | } |
| |
|
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | pub(crate) async fn alloc_ongoing(&self) -> Result<Receiver<()>> { |
| | let mut s = self.running_state.write().await; |
| | ensure!( |
| | matches!(*s, RunningState::Stopped), |
| | "There is already another ongoing process running." |
| | ); |
| |
|
| | let (sender, receiver) = channel::bounded(1); |
| | *s = RunningState::Running { |
| | cancel_sender: sender, |
| | }; |
| |
|
| | Ok(receiver) |
| | } |
| |
|
| | pub(crate) async fn free_ongoing(&self) { |
| | let mut s = self.running_state.write().await; |
| | if let RunningState::ShallStop { request } = *s { |
| | info!(self, "Ongoing stopped in {:?}", time_elapsed(&request)); |
| | } |
| | *s = RunningState::Stopped; |
| | } |
| |
|
| | |
| | pub async fn stop_ongoing(&self) { |
| | let mut s = self.running_state.write().await; |
| | match &*s { |
| | RunningState::Running { cancel_sender } => { |
| | if let Err(err) = cancel_sender.send(()).await { |
| | warn!(self, "could not cancel ongoing: {:#}", err); |
| | } |
| | info!(self, "Signaling the ongoing process to stop ASAP.",); |
| | *s = RunningState::ShallStop { |
| | request: tools::Time::now(), |
| | }; |
| | } |
| | RunningState::ShallStop { .. } | RunningState::Stopped => { |
| | info!(self, "No ongoing process to stop.",); |
| | } |
| | } |
| | } |
| |
|
| | #[allow(unused)] |
| | pub(crate) async fn shall_stop_ongoing(&self) -> bool { |
| | match &*self.running_state.read().await { |
| | RunningState::Running { .. } => false, |
| | RunningState::ShallStop { .. } | RunningState::Stopped => true, |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| |
|
| | |
| | pub async fn get_info(&self) -> Result<BTreeMap<&'static str, String>> { |
| | let secondary_addrs = self.get_secondary_self_addrs().await?.join(", "); |
| | let all_transports: Vec<String> = ConfiguredLoginParam::load_all(self) |
| | .await? |
| | .into_iter() |
| | .map(|(transport_id, param)| format!("{transport_id}: {param}")) |
| | .collect(); |
| | let all_transports = if all_transports.is_empty() { |
| | "Not configured".to_string() |
| | } else { |
| | all_transports.join(",") |
| | }; |
| | let chats = get_chat_cnt(self).await?; |
| | let unblocked_msgs = message::get_unblocked_msg_cnt(self).await; |
| | let request_msgs = message::get_request_msg_cnt(self).await; |
| | let contacts = Contact::get_real_cnt(self).await?; |
| | let proxy_enabled = self.get_config_int(Config::ProxyEnabled).await?; |
| | let dbversion = self |
| | .sql |
| | .get_raw_config_int("dbversion") |
| | .await? |
| | .unwrap_or_default(); |
| | let journal_mode = self |
| | .sql |
| | .query_get_value("PRAGMA journal_mode;", ()) |
| | .await? |
| | .unwrap_or_else(|| "unknown".to_string()); |
| | let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await?; |
| | let bcc_self = self.get_config_int(Config::BccSelf).await?; |
| | let sync_msgs = self.get_config_int(Config::SyncMsgs).await?; |
| | let disable_idle = self.get_config_bool(Config::DisableIdle).await?; |
| |
|
| | let prv_key_cnt = self.sql.count("SELECT COUNT(*) FROM keypairs;", ()).await?; |
| |
|
| | let pub_key_cnt = self |
| | .sql |
| | .count("SELECT COUNT(*) FROM public_keys;", ()) |
| | .await?; |
| | let fingerprint_str = match self_fingerprint(self).await { |
| | Ok(fp) => fp.to_string(), |
| | Err(err) => format!("<key failure: {err}>"), |
| | }; |
| |
|
| | let mvbox_move = self.get_config_int(Config::MvboxMove).await?; |
| | let only_fetch_mvbox = self.get_config_int(Config::OnlyFetchMvbox).await?; |
| | let folders_configured = self |
| | .sql |
| | .get_raw_config_int(constants::DC_FOLDERS_CONFIGURED_KEY) |
| | .await? |
| | .unwrap_or_default(); |
| |
|
| | let configured_inbox_folder = self |
| | .get_config(Config::ConfiguredInboxFolder) |
| | .await? |
| | .unwrap_or_else(|| "<unset>".to_string()); |
| | let configured_mvbox_folder = self |
| | .get_config(Config::ConfiguredMvboxFolder) |
| | .await? |
| | .unwrap_or_else(|| "<unset>".to_string()); |
| | let configured_trash_folder = self |
| | .get_config(Config::ConfiguredTrashFolder) |
| | .await? |
| | .unwrap_or_else(|| "<unset>".to_string()); |
| |
|
| | let mut res = get_info(); |
| |
|
| | |
| | res.insert("bot", self.get_config_int(Config::Bot).await?.to_string()); |
| | res.insert("number_of_chats", chats.to_string()); |
| | res.insert("number_of_chat_messages", unblocked_msgs.to_string()); |
| | res.insert("messages_in_contact_requests", request_msgs.to_string()); |
| | res.insert("number_of_contacts", contacts.to_string()); |
| | res.insert("database_dir", self.get_dbfile().display().to_string()); |
| | res.insert("database_version", dbversion.to_string()); |
| | res.insert( |
| | "database_encrypted", |
| | self.sql |
| | .is_encrypted() |
| | .await |
| | .map_or_else(|| "closed".to_string(), |b| b.to_string()), |
| | ); |
| | res.insert("journal_mode", journal_mode); |
| | res.insert("blobdir", self.get_blobdir().display().to_string()); |
| | res.insert( |
| | "selfavatar", |
| | self.get_config(Config::Selfavatar) |
| | .await? |
| | .unwrap_or_else(|| "<unset>".to_string()), |
| | ); |
| | res.insert("proxy_enabled", proxy_enabled.to_string()); |
| | res.insert("used_transport_settings", all_transports); |
| |
|
| | if let Some(server_id) = &*self.server_id.read().await { |
| | res.insert("imap_server_id", format!("{server_id:?}")); |
| | } |
| |
|
| | res.insert("is_chatmail", self.is_chatmail().await?.to_string()); |
| | res.insert( |
| | "fix_is_chatmail", |
| | self.get_config_bool(Config::FixIsChatmail) |
| | .await? |
| | .to_string(), |
| | ); |
| | res.insert( |
| | "is_muted", |
| | self.get_config_bool(Config::IsMuted).await?.to_string(), |
| | ); |
| | res.insert( |
| | "private_tag", |
| | self.get_config(Config::PrivateTag) |
| | .await? |
| | .unwrap_or_else(|| "<unset>".to_string()), |
| | ); |
| |
|
| | if let Some(metadata) = &*self.metadata.read().await { |
| | if let Some(comment) = &metadata.comment { |
| | res.insert("imap_server_comment", format!("{comment:?}")); |
| | } |
| |
|
| | if let Some(admin) = &metadata.admin { |
| | res.insert("imap_server_admin", format!("{admin:?}")); |
| | } |
| | } |
| |
|
| | res.insert("secondary_addrs", secondary_addrs); |
| | res.insert( |
| | "fetched_existing_msgs", |
| | self.get_config_bool(Config::FetchedExistingMsgs) |
| | .await? |
| | .to_string(), |
| | ); |
| | res.insert( |
| | "show_emails", |
| | self.get_config_int(Config::ShowEmails).await?.to_string(), |
| | ); |
| | res.insert( |
| | "who_can_call_me", |
| | self.get_config_int(Config::WhoCanCallMe).await?.to_string(), |
| | ); |
| | res.insert( |
| | "download_limit", |
| | self.get_config_int(Config::DownloadLimit) |
| | .await? |
| | .to_string(), |
| | ); |
| | res.insert("mvbox_move", mvbox_move.to_string()); |
| | res.insert("only_fetch_mvbox", only_fetch_mvbox.to_string()); |
| | res.insert( |
| | constants::DC_FOLDERS_CONFIGURED_KEY, |
| | folders_configured.to_string(), |
| | ); |
| | res.insert("configured_inbox_folder", configured_inbox_folder); |
| | res.insert("configured_mvbox_folder", configured_mvbox_folder); |
| | res.insert("configured_trash_folder", configured_trash_folder); |
| | res.insert("mdns_enabled", mdns_enabled.to_string()); |
| | res.insert("bcc_self", bcc_self.to_string()); |
| | res.insert("sync_msgs", sync_msgs.to_string()); |
| | res.insert("disable_idle", disable_idle.to_string()); |
| | res.insert("private_key_count", prv_key_cnt.to_string()); |
| | res.insert("public_key_count", pub_key_cnt.to_string()); |
| | res.insert("fingerprint", fingerprint_str); |
| | res.insert( |
| | "media_quality", |
| | self.get_config_int(Config::MediaQuality).await?.to_string(), |
| | ); |
| | res.insert( |
| | "delete_device_after", |
| | self.get_config_int(Config::DeleteDeviceAfter) |
| | .await? |
| | .to_string(), |
| | ); |
| | res.insert( |
| | "delete_server_after", |
| | self.get_config_int(Config::DeleteServerAfter) |
| | .await? |
| | .to_string(), |
| | ); |
| | res.insert( |
| | "delete_to_trash", |
| | self.get_config(Config::DeleteToTrash) |
| | .await? |
| | .unwrap_or_else(|| "<unset>".to_string()), |
| | ); |
| | res.insert( |
| | "last_housekeeping", |
| | self.get_config_int(Config::LastHousekeeping) |
| | .await? |
| | .to_string(), |
| | ); |
| | res.insert( |
| | "last_cant_decrypt_outgoing_msgs", |
| | self.get_config_int(Config::LastCantDecryptOutgoingMsgs) |
| | .await? |
| | .to_string(), |
| | ); |
| | res.insert( |
| | "scan_all_folders_debounce_secs", |
| | self.get_config_int(Config::ScanAllFoldersDebounceSecs) |
| | .await? |
| | .to_string(), |
| | ); |
| | res.insert( |
| | "quota_exceeding", |
| | self.get_config_int(Config::QuotaExceeding) |
| | .await? |
| | .to_string(), |
| | ); |
| | res.insert( |
| | "authserv_id_candidates", |
| | self.get_config(Config::AuthservIdCandidates) |
| | .await? |
| | .unwrap_or_default(), |
| | ); |
| | res.insert( |
| | "sign_unencrypted", |
| | self.get_config_int(Config::SignUnencrypted) |
| | .await? |
| | .to_string(), |
| | ); |
| | res.insert( |
| | "debug_logging", |
| | self.get_config_int(Config::DebugLogging).await?.to_string(), |
| | ); |
| | res.insert( |
| | "last_msg_id", |
| | self.get_config_int(Config::LastMsgId).await?.to_string(), |
| | ); |
| | res.insert( |
| | "gossip_period", |
| | self.get_config_int(Config::GossipPeriod).await?.to_string(), |
| | ); |
| | res.insert( |
| | "webxdc_realtime_enabled", |
| | self.get_config_bool(Config::WebxdcRealtimeEnabled) |
| | .await? |
| | .to_string(), |
| | ); |
| | res.insert( |
| | "donation_request_next_check", |
| | self.get_config_i64(Config::DonationRequestNextCheck) |
| | .await? |
| | .to_string(), |
| | ); |
| | res.insert( |
| | "first_key_contacts_msg_id", |
| | self.sql |
| | .get_raw_config("first_key_contacts_msg_id") |
| | .await? |
| | .unwrap_or_default(), |
| | ); |
| | res.insert( |
| | "stats_id", |
| | self.get_config(Config::StatsId) |
| | .await? |
| | .unwrap_or_else(|| "<unset>".to_string()), |
| | ); |
| | res.insert( |
| | "stats_sending", |
| | stats::should_send_stats(self).await?.to_string(), |
| | ); |
| | res.insert( |
| | "stats_last_sent", |
| | self.get_config_i64(Config::StatsLastSent) |
| | .await? |
| | .to_string(), |
| | ); |
| | res.insert( |
| | "test_hooks", |
| | self.sql |
| | .get_raw_config("test_hooks") |
| | .await? |
| | .unwrap_or_default(), |
| | ); |
| | res.insert( |
| | "fail_on_receiving_full_msg", |
| | self.sql |
| | .get_raw_config("fail_on_receiving_full_msg") |
| | .await? |
| | .unwrap_or_default(), |
| | ); |
| | res.insert( |
| | "std_header_protection_composing", |
| | self.sql |
| | .get_raw_config("std_header_protection_composing") |
| | .await? |
| | .unwrap_or_default(), |
| | ); |
| | res.insert( |
| | "team_profile", |
| | self.get_config_bool(Config::TeamProfile).await?.to_string(), |
| | ); |
| |
|
| | let elapsed = time_elapsed(&self.creation_time); |
| | res.insert("uptime", duration_to_str(elapsed)); |
| |
|
| | Ok(res) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | pub async fn get_fresh_msgs(&self) -> Result<Vec<MsgId>> { |
| | let list = self |
| | .sql |
| | .query_map_vec( |
| | concat!( |
| | "SELECT m.id", |
| | " FROM msgs m", |
| | " LEFT JOIN contacts ct", |
| | " ON m.from_id=ct.id", |
| | " LEFT JOIN chats c", |
| | " ON m.chat_id=c.id", |
| | " WHERE m.state=?", |
| | " AND m.hidden=0", |
| | " AND m.chat_id>9", |
| | " AND ct.blocked=0", |
| | " AND c.blocked=0", |
| | " AND NOT(c.muted_until=-1 OR c.muted_until>?)", |
| | " ORDER BY m.timestamp DESC,m.id DESC;" |
| | ), |
| | (MessageState::InFresh, time()), |
| | |row| { |
| | let msg_id: MsgId = row.get(0)?; |
| | Ok(msg_id) |
| | }, |
| | ) |
| | .await?; |
| | Ok(list) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | pub async fn get_next_msgs(&self) -> Result<Vec<MsgId>> { |
| | let last_msg_id = match self.get_config(Config::LastMsgId).await? { |
| | Some(s) => MsgId::new(s.parse()?), |
| | None => { |
| | |
| | |
| | |
| | |
| | self.sql |
| | .query_row( |
| | "SELECT IFNULL((SELECT MAX(id) - 1 FROM msgs), 0)", |
| | (), |
| | |row| { |
| | let msg_id: MsgId = row.get(0)?; |
| | Ok(msg_id) |
| | }, |
| | ) |
| | .await? |
| | } |
| | }; |
| |
|
| | let list = self |
| | .sql |
| | .query_map_vec( |
| | "SELECT m.id |
| | FROM msgs m |
| | LEFT JOIN contacts ct |
| | ON m.from_id=ct.id |
| | LEFT JOIN chats c |
| | ON m.chat_id=c.id |
| | WHERE m.id>? |
| | AND m.hidden=0 |
| | AND m.chat_id>9 |
| | AND ct.blocked=0 |
| | AND c.blocked!=1 |
| | ORDER BY m.id ASC", |
| | ( |
| | last_msg_id.to_u32(), |
| | ), |
| | |row| { |
| | let msg_id: MsgId = row.get(0)?; |
| | Ok(msg_id) |
| | }, |
| | ) |
| | .await?; |
| | Ok(list) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | pub async fn wait_next_msgs(&self) -> Result<Vec<MsgId>> { |
| | self.new_msgs_notify.notified().await; |
| | let list = self.get_next_msgs().await?; |
| | Ok(list) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | pub async fn search_msgs(&self, chat_id: Option<ChatId>, query: &str) -> Result<Vec<MsgId>> { |
| | let real_query = query.trim().to_lowercase(); |
| | if real_query.is_empty() { |
| | return Ok(Vec::new()); |
| | } |
| | let str_like_in_text = format!("%{real_query}%"); |
| |
|
| | let list = if let Some(chat_id) = chat_id { |
| | self.sql |
| | .query_map_vec( |
| | "SELECT m.id AS id |
| | FROM msgs m |
| | LEFT JOIN contacts ct |
| | ON m.from_id=ct.id |
| | WHERE m.chat_id=? |
| | AND m.hidden=0 |
| | AND ct.blocked=0 |
| | AND IFNULL(txt_normalized, txt) LIKE ? |
| | ORDER BY m.timestamp,m.id;", |
| | (chat_id, str_like_in_text), |
| | |row| { |
| | let msg_id: MsgId = row.get("id")?; |
| | Ok(msg_id) |
| | }, |
| | ) |
| | .await? |
| | } else { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | self.sql |
| | .query_map_vec( |
| | "SELECT m.id AS id |
| | FROM msgs m |
| | LEFT JOIN contacts ct |
| | ON m.from_id=ct.id |
| | LEFT JOIN chats c |
| | ON m.chat_id=c.id |
| | WHERE m.chat_id>9 |
| | AND m.hidden=0 |
| | AND c.blocked!=1 |
| | AND ct.blocked=0 |
| | AND IFNULL(txt_normalized, txt) LIKE ? |
| | ORDER BY m.id DESC LIMIT 1000", |
| | (str_like_in_text,), |
| | |row| { |
| | let msg_id: MsgId = row.get("id")?; |
| | Ok(msg_id) |
| | }, |
| | ) |
| | .await? |
| | }; |
| |
|
| | Ok(list) |
| | } |
| |
|
| | |
| | pub async fn is_inbox(&self, folder_name: &str) -> Result<bool> { |
| | let inbox = self.get_config(Config::ConfiguredInboxFolder).await?; |
| | Ok(inbox.as_deref() == Some(folder_name)) |
| | } |
| |
|
| | |
| | pub async fn is_mvbox(&self, folder_name: &str) -> Result<bool> { |
| | let mvbox = self.get_config(Config::ConfiguredMvboxFolder).await?; |
| | Ok(mvbox.as_deref() == Some(folder_name)) |
| | } |
| |
|
| | |
| | pub async fn is_trash(&self, folder_name: &str) -> Result<bool> { |
| | let trash = self.get_config(Config::ConfiguredTrashFolder).await?; |
| | Ok(trash.as_deref() == Some(folder_name)) |
| | } |
| |
|
| | pub(crate) async fn should_delete_to_trash(&self) -> Result<bool> { |
| | if let Some(v) = self.get_config_bool_opt(Config::DeleteToTrash).await? { |
| | return Ok(v); |
| | } |
| | if let Some(provider) = self.get_configured_provider().await? { |
| | return Ok(provider.opt.delete_to_trash); |
| | } |
| | Ok(false) |
| | } |
| |
|
| | |
| | |
| | pub(crate) async fn get_delete_msgs_target(&self) -> Result<String> { |
| | if !self.should_delete_to_trash().await? { |
| | return Ok("".into()); |
| | } |
| | self.get_config(Config::ConfiguredTrashFolder) |
| | .await? |
| | .context("No configured trash folder") |
| | } |
| |
|
| | pub(crate) fn derive_blobdir(dbfile: &Path) -> PathBuf { |
| | let mut blob_fname = OsString::new(); |
| | blob_fname.push(dbfile.file_name().unwrap_or_default()); |
| | blob_fname.push("-blobs"); |
| | dbfile.with_file_name(blob_fname) |
| | } |
| |
|
| | pub(crate) fn derive_walfile(dbfile: &Path) -> PathBuf { |
| | let mut wal_fname = OsString::new(); |
| | wal_fname.push(dbfile.file_name().unwrap_or_default()); |
| | wal_fname.push("-wal"); |
| | dbfile.with_file_name(wal_fname) |
| | } |
| | } |
| |
|
| | #[cfg(test)] |
| | mod context_tests; |
| |
|