feat: improve internal sql interface

Switches from rusqlite to sqlx to have a fully async based interface
to sqlite.

Co-authored-by: B. Petersen <r10s@b44t.com>
Co-authored-by: Hocuri <hocuri@gmx.de>
Co-authored-by: link2xt <link2xt@testrun.org>
This commit is contained in:
Friedel Ziegelmayer
2021-04-06 16:03:10 +02:00
committed by dignifiedquire
parent 4dedc2d8ce
commit 6bb5721f29
52 changed files with 5505 additions and 4983 deletions

View File

@@ -135,15 +135,25 @@ impl Accounts {
let old_id = self.config.get_selected_account().await;
// create new account
let account_config = self.config.new_account(&self.dir).await?;
let account_config = self
.config
.new_account(&self.dir)
.await
.context("failed to create new account")?;
let new_dbfile = account_config.dbfile().into();
let new_blobdir = Context::derive_blobdir(&new_dbfile);
let res = {
fs::create_dir_all(&account_config.dir).await?;
fs::rename(&dbfile, &new_dbfile).await?;
fs::rename(&blobdir, &new_blobdir).await?;
fs::create_dir_all(&account_config.dir)
.await
.context("failed to create dir")?;
fs::rename(&dbfile, &new_dbfile)
.await
.context("failed to rename dbfile")?;
fs::rename(&blobdir, &new_blobdir)
.await
.context("failed to rename blobdir")?;
Ok(())
};
@@ -502,7 +512,10 @@ mod tests {
let ctx = accounts.get_selected_account().await;
assert_eq!(
"me@mail.com",
ctx.get_config(crate::config::Config::Addr).await.unwrap()
ctx.get_config(crate::config::Config::Addr)
.await
.unwrap()
.unwrap()
);
}

View File

@@ -384,7 +384,7 @@ impl<'a> BlobObject<'a> {
let blob_abs = self.to_abs_path();
let img_wh =
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await)
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
.unwrap_or_default()
{
MediaQuality::Balanced => BALANCED_AVATAR_SIZE,
@@ -403,7 +403,7 @@ impl<'a> BlobObject<'a> {
}
let img_wh =
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await)
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
.unwrap_or_default()
{
MediaQuality::Balanced => BALANCED_IMAGE_SIZE,
@@ -514,6 +514,10 @@ pub enum BlobError {
WrongBlobdir { blobdir: PathBuf, src: PathBuf },
#[error("Blob has a badname {}", .blobname.display())]
WrongName { blobname: PathBuf },
#[error("Sql: {0}")]
Sql(#[from] crate::sql::Error),
#[error("{0}")]
Other(#[from] anyhow::Error),
}
#[cfg(test)]

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,8 @@
//! # Chat list module
use anyhow::{bail, ensure, Result};
use async_std::prelude::*;
use sqlx::Row;
use crate::chat;
use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
@@ -110,17 +112,6 @@ impl Chatlist {
let mut add_archived_link_item = false;
let process_row = |row: &rusqlite::Row| {
let chat_id: ChatId = row.get(0)?;
let msg_id: MsgId = row.get(1).unwrap_or_default();
Ok((chat_id, msg_id))
};
let process_rows = |rows: rusqlite::MappedRows<_>| {
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
};
let skip_id = if flag_for_forwarding {
chat::lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
.await
@@ -130,6 +121,13 @@ impl Chatlist {
ChatId::new(0)
};
let process_row = |row: sqlx::Result<sqlx::sqlite::SqliteRow>| {
let row = row?;
let chat_id: ChatId = row.try_get(0)?;
let msg_id: MsgId = row.try_get(1).unwrap_or_default();
Ok((chat_id, msg_id))
};
// select with left join and minimum:
//
// - the inner select must use `hidden` and _not_ `m.hidden`
@@ -145,10 +143,10 @@ impl Chatlist {
// tg do the same) for the deaddrop, however, they should
// really be hidden, however, _currently_ the deaddrop is not
// shown at all permanent in the chatlist.
let mut ids = if let Some(query_contact_id) = query_contact_id {
let mut ids: Vec<_> = if let Some(query_contact_id) = query_contact_id {
// show chats shared with a given contact
context.sql.query_map(
"SELECT c.id, m.id
context.sql.fetch(
sqlx::query("SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
ON c.id=m.chat_id
@@ -162,11 +160,9 @@ impl Chatlist {
AND c.blocked=0
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
GROUP BY c.id
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
paramsv![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned],
process_row,
process_rows,
).await?
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;"
).bind(MessageState::OutDraft).bind(query_contact_id).bind(ChatVisibility::Pinned)
).await?.map(process_row).collect::<sqlx::Result<_>>().await?
} else if flag_archived_only {
// show archived chats
// (this includes the archived device-chat; we could skip it,
@@ -174,8 +170,9 @@ impl Chatlist {
// and adapting the number requires larger refactorings and seems not to be worth the effort)
context
.sql
.query_map(
"SELECT c.id, m.id
.fetch(
sqlx::query(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
ON c.id=m.chat_id
@@ -190,11 +187,13 @@ impl Chatlist {
AND c.archived=1
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
paramsv![MessageState::OutDraft],
process_row,
process_rows,
)
.bind(MessageState::OutDraft),
)
.await?
.map(process_row)
.collect::<sqlx::Result<_>>()
.await?
} else if let Some(query) = query {
let query = query.trim().to_string();
ensure!(!query.is_empty(), "missing query");
@@ -208,8 +207,9 @@ impl Chatlist {
let str_like_cmd = format!("%{}%", query);
context
.sql
.query_map(
"SELECT c.id, m.id
.fetch(
sqlx::query(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
ON c.id=m.chat_id
@@ -224,11 +224,15 @@ impl Chatlist {
AND c.name LIKE ?3
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
paramsv![MessageState::OutDraft, skip_id, str_like_cmd],
process_row,
process_rows,
)
.bind(MessageState::OutDraft)
.bind(skip_id)
.bind(str_like_cmd),
)
.await?
.map(process_row)
.collect::<sqlx::Result<_>>()
.await?
} else {
// show normal chatlist
let sort_id_up = if flag_for_forwarding {
@@ -239,7 +243,8 @@ impl Chatlist {
} else {
ChatId::new(0)
};
let mut ids = context.sql.query_map(
let mut ids: Vec<_> = context.sql.fetch(sqlx::query(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
@@ -254,19 +259,21 @@ impl Chatlist {
AND c.blocked=0
AND NOT c.archived=?3
GROUP BY c.id
ORDER BY c.id=?4 DESC, c.archived=?5 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
paramsv![MessageState::OutDraft, skip_id, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned],
process_row,
process_rows,
).await?;
ORDER BY c.id=?4 DESC, c.archived=?5 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;"
)
.bind(MessageState::OutDraft)
.bind(skip_id)
.bind(ChatVisibility::Archived)
.bind(sort_id_up)
.bind(ChatVisibility::Pinned)
).await?.map(process_row).collect::<sqlx::Result<_>>().await?;
if !flag_no_specials {
if let Some(last_deaddrop_fresh_msg_id) = get_last_deaddrop_fresh_msg(context).await
if let Some(last_deaddrop_fresh_msg_id) =
get_last_deaddrop_fresh_msg(context).await?
{
if !flag_for_forwarding {
ids.insert(
0,
(ChatId::new(DC_CHAT_ID_DEADDROP), last_deaddrop_fresh_msg_id),
);
ids.insert(0, (DC_CHAT_ID_DEADDROP, last_deaddrop_fresh_msg_id));
}
}
add_archived_link_item = true;
@@ -274,11 +281,11 @@ impl Chatlist {
ids
};
if add_archived_link_item && dc_get_archived_cnt(context).await > 0 {
if add_archived_link_item && dc_get_archived_cnt(context).await? > 0 {
if ids.is_empty() && flag_add_alldone_hint {
ids.push((ChatId::new(DC_CHAT_ID_ALLDONE_HINT), MsgId::new(0)));
ids.push((DC_CHAT_ID_ALLDONE_HINT, MsgId::new(0)));
}
ids.push((ChatId::new(DC_CHAT_ID_ARCHIVED_LINK), MsgId::new(0)));
ids.push((DC_CHAT_ID_ARCHIVED_LINK, MsgId::new(0)));
}
Ok(Chatlist { ids })
@@ -400,38 +407,31 @@ impl Chatlist {
}
/// Returns the number of archived chats
pub async fn dc_get_archived_cnt(context: &Context) -> u32 {
context
pub async fn dc_get_archived_cnt(context: &Context) -> Result<usize> {
let count = context
.sql
.query_get_value(
context,
"SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;",
paramsv![],
)
.await
.unwrap_or_default()
.count("SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;")
.await?;
Ok(count)
}
async fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> {
async fn get_last_deaddrop_fresh_msg(context: &Context) -> Result<Option<MsgId>> {
// We have an index over the state-column, this should be
// sufficient as there are typically only few fresh messages.
context
let id = context
.sql
.query_get_value(
context,
concat!(
"SELECT m.id",
" FROM msgs m",
" LEFT JOIN chats c",
" ON c.id=m.chat_id",
" WHERE m.state=10",
" AND m.hidden=0",
" AND c.blocked=2",
" ORDER BY m.timestamp DESC, m.id DESC;"
),
paramsv![],
)
.await
.query_get_value(concat!(
"SELECT m.id",
" FROM msgs m",
" LEFT JOIN chats c",
" ON c.id=m.chat_id",
" WHERE m.state=10",
" AND m.hidden=0",
" AND c.blocked=2",
" ORDER BY m.timestamp DESC, m.id DESC;"
))
.await?;
Ok(id)
}
#[cfg(test)]
@@ -466,7 +466,7 @@ mod tests {
// drafts are sorted to the top
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("hello".to_string()));
chat_id2.set_draft(&t, Some(&mut msg)).await;
chat_id2.set_draft(&t, Some(&mut msg)).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.get_chat_id(0), chat_id2);
@@ -554,7 +554,7 @@ mod tests {
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("foo:\nbar \r\n test".to_string()));
chat_id1.set_draft(&t, Some(&mut msg)).await;
chat_id1.set_draft(&t, Some(&mut msg)).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
let summary = chats.get_summary(&t, 0, None).await;

View File

@@ -1,5 +1,6 @@
//! # Key-value configuration management
use anyhow::Result;
use strum::{EnumProperty, IntoEnumIterator};
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
@@ -150,69 +151,66 @@ pub enum Config {
}
impl Context {
pub async fn config_exists(&self, key: Config) -> bool {
self.sql.get_raw_config(self, key).await.is_some()
pub async fn config_exists(&self, key: Config) -> Result<bool> {
Ok(self.sql.get_raw_config(key).await?.is_some())
}
/// Get a configuration key. Returns `None` if no value is set, and no default value found.
pub async fn get_config(&self, key: Config) -> Option<String> {
pub async fn get_config(&self, key: Config) -> Result<Option<String>> {
let value = match key {
Config::Selfavatar => {
let rel_path = self.sql.get_raw_config(self, key).await;
let rel_path = self.sql.get_raw_config(key).await?;
rel_path.map(|p| dc_get_abs_path(self, &p).to_string_lossy().into_owned())
}
Config::SysVersion => Some((&*DC_VERSION_STR).clone()),
Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)),
Config::SysConfigKeys => Some(get_config_keys_string()),
_ => self.sql.get_raw_config(self, key).await,
_ => self.sql.get_raw_config(key).await?,
};
if value.is_some() {
return value;
return Ok(value);
}
// Default values
match key {
Config::Selfstatus => Some(stock_str::status_line(self).await),
Config::ConfiguredInboxFolder => Some("INBOX".to_owned()),
_ => key.get_str("default").map(|s| s.to_string()),
Config::Selfstatus => Ok(Some(stock_str::status_line(self).await)),
Config::ConfiguredInboxFolder => Ok(Some("INBOX".to_owned())),
_ => Ok(key.get_str("default").map(|s| s.to_string())),
}
}
pub async fn get_config_int(&self, key: Config) -> i32 {
pub async fn get_config_int(&self, key: Config) -> Result<i32> {
self.get_config(key)
.await
.and_then(|s| s.parse().ok())
.unwrap_or_default()
.map(|s: Option<String>| s.and_then(|s| s.parse().ok()).unwrap_or_default())
}
pub async fn get_config_i64(&self, key: Config) -> i64 {
pub async fn get_config_i64(&self, key: Config) -> Result<i64> {
self.get_config(key)
.await
.and_then(|s| s.parse().ok())
.unwrap_or_default()
.map(|s: Option<String>| s.and_then(|s| s.parse().ok()).unwrap_or_default())
}
pub async fn get_config_u64(&self, key: Config) -> u64 {
pub async fn get_config_u64(&self, key: Config) -> Result<u64> {
self.get_config(key)
.await
.and_then(|s| s.parse().ok())
.unwrap_or_default()
.map(|s: Option<String>| s.and_then(|s| s.parse().ok()).unwrap_or_default())
}
pub async fn get_config_bool(&self, key: Config) -> bool {
self.get_config_int(key).await != 0
pub async fn get_config_bool(&self, key: Config) -> Result<bool> {
Ok(self.get_config_int(key).await? != 0)
}
/// Gets configured "delete_server_after" value.
///
/// `None` means never delete the message, `Some(0)` means delete
/// at once, `Some(x)` means delete after `x` seconds.
pub async fn get_config_delete_server_after(&self) -> Option<i64> {
match self.get_config_int(Config::DeleteServerAfter).await {
0 => None,
1 => Some(0),
x => Some(x as i64),
pub async fn get_config_delete_server_after(&self) -> Result<Option<i64>> {
match self.get_config_int(Config::DeleteServerAfter).await? {
0 => Ok(None),
1 => Ok(Some(0)),
x => Ok(Some(x as i64)),
}
}
@@ -220,41 +218,46 @@ impl Context {
///
/// The provider is determined by `get_provider_info()` during configuration and then saved
/// to the db in `param.save_to_database()`, together with all the other `configured_*` values.
pub async fn get_configured_provider(&self) -> Option<&'static Provider> {
get_provider_by_id(&self.get_config(Config::ConfiguredProvider).await?)
pub async fn get_configured_provider(&self) -> Result<Option<&'static Provider>> {
if let Some(cfg) = self.get_config(Config::ConfiguredProvider).await? {
return Ok(get_provider_by_id(&cfg));
}
Ok(None)
}
/// Gets configured "delete_device_after" value.
///
/// `None` means never delete the message, `Some(x)` means delete
/// after `x` seconds.
pub async fn get_config_delete_device_after(&self) -> Option<i64> {
match self.get_config_int(Config::DeleteDeviceAfter).await {
0 => None,
x => Some(x as i64),
pub async fn get_config_delete_device_after(&self) -> Result<Option<i64>> {
match self.get_config_int(Config::DeleteDeviceAfter).await? {
0 => Ok(None),
x => Ok(Some(x as i64)),
}
}
/// Set the given config key.
/// If `None` is passed as a value the value is cleared and set to the default if there is one.
pub async fn set_config(&self, key: Config, value: Option<&str>) -> crate::sql::Result<()> {
pub async fn set_config(&self, key: Config, value: Option<&str>) -> Result<()> {
match key {
Config::Selfavatar => {
self.sql
.execute("UPDATE contacts SET selfavatar_sent=0;", paramsv![])
.execute("UPDATE contacts SET selfavatar_sent=0;")
.await?;
self.sql
.set_raw_config_bool(self, "attach_selfavatar", true)
.set_raw_config_bool("attach_selfavatar", true)
.await?;
match value {
Some(value) => {
let blob = BlobObject::new_from_path(self, value).await?;
blob.recode_to_avatar_size(self).await?;
self.sql
.set_raw_config(self, key, Some(blob.as_name()))
.await
self.sql.set_raw_config(key, Some(blob.as_name())).await?;
Ok(())
}
None => {
self.sql.set_raw_config(key, None).await?;
Ok(())
}
None => self.sql.set_raw_config(self, key, None).await,
}
}
Config::Selfstatus => {
@@ -265,10 +268,15 @@ impl Context {
value
};
self.sql.set_raw_config(self, key, val).await
self.sql.set_raw_config(key, val).await?;
Ok(())
}
Config::DeleteDeviceAfter => {
let ret = self.sql.set_raw_config(self, key, value).await;
let ret = self
.sql
.set_raw_config(key, value)
.await
.map_err(Into::into);
// Force chatlist reload to delete old messages immediately.
self.emit_event(EventType::MsgsChanged {
msg_id: MsgId::new(0),
@@ -278,20 +286,29 @@ impl Context {
}
Config::Displayname => {
let value = value.map(improve_single_line_input);
self.sql.set_raw_config(self, key, value.as_deref()).await
self.sql.set_raw_config(key, value.as_deref()).await?;
Ok(())
}
Config::DeleteServerAfter => {
let ret = self.sql.set_raw_config(self, key, value).await;
let ret = self
.sql
.set_raw_config(key, value)
.await
.map_err(Into::into);
job::schedule_resync(self).await;
ret
}
_ => self.sql.set_raw_config(self, key, value).await,
_ => {
self.sql.set_raw_config(key, value).await?;
Ok(())
}
}
}
pub async fn set_config_bool(&self, key: Config, value: bool) -> crate::sql::Result<()> {
self.set_config(key, if value { Some("1") } else { None })
.await
.await?;
Ok(())
}
}
@@ -349,7 +366,7 @@ mod tests {
.unwrap();
assert!(avatar_blob.exists().await);
assert!(std::fs::metadata(&avatar_blob).unwrap().len() < avatar_bytes.len() as u64);
let avatar_cfg = t.get_config(Config::Selfavatar).await;
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
let img = image::open(avatar_src).unwrap();
@@ -378,7 +395,7 @@ mod tests {
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
.await
.unwrap();
let avatar_cfg = t.get_config(Config::Selfavatar).await;
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
assert_eq!(avatar_cfg, avatar_src.to_str().map(|s| s.to_string()));
let img = image::open(avatar_src).unwrap();
@@ -405,21 +422,21 @@ mod tests {
std::fs::metadata(&avatar_blob).unwrap().len(),
avatar_bytes.len() as u64
);
let avatar_cfg = t.get_config(Config::Selfavatar).await;
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
}
#[async_std::test]
async fn test_media_quality_config_option() {
let t = TestContext::new().await;
let media_quality = t.get_config_int(Config::MediaQuality).await;
let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap();
assert_eq!(media_quality, 0);
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
assert_eq!(media_quality, constants::MediaQuality::Balanced);
t.set_config(Config::MediaQuality, Some("1")).await.unwrap();
let media_quality = t.get_config_int(Config::MediaQuality).await;
let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap();
assert_eq!(media_quality, 1);
assert_eq!(constants::MediaQuality::Worse as i32, 1);
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();

View File

@@ -50,8 +50,11 @@ macro_rules! progress {
impl Context {
/// Checks if the context is already configured.
pub async fn is_configured(&self) -> bool {
self.sql.get_raw_config_bool(self, "configured").await
pub async fn is_configured(&self) -> Result<bool> {
self.sql
.get_raw_config_bool("configured")
.await
.map_err(Into::into)
}
/// Configures this account with the currently set parameters.
@@ -84,14 +87,14 @@ impl Context {
async fn inner_configure(&self) -> Result<()> {
info!(self, "Configure ...");
let mut param = LoginParam::from_database(self, "").await;
let mut param = LoginParam::from_database(self, "").await?;
let success = configure(self, &mut param).await;
self.set_config(Config::NotifyAboutWrongPw, None).await?;
if let Some(provider) = param.provider {
if let Some(config_defaults) = &provider.config_defaults {
for def in config_defaults.iter() {
if !self.config_exists(def.key).await {
if !self.config_exists(def.key).await? {
info!(self, "apply config_defaults {}={}", def.key, def.value);
self.set_config(def.key, Some(def.value)).await?;
} else {
@@ -177,13 +180,13 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
// if dc_get_oauth2_addr() is not available in the oauth2 implementation, just use the given one.
progress!(ctx, 10);
if let Some(oauth2_addr) = dc_get_oauth2_addr(ctx, &param.addr, &param.imap.password)
.await
.await?
.and_then(|e| e.parse().ok())
{
info!(ctx, "Authorized address is {}", oauth2_addr);
param.addr = oauth2_addr;
ctx.sql
.set_raw_config(ctx, "addr", Some(param.addr.as_str()))
.set_raw_config("addr", Some(param.addr.as_str()))
.await?;
}
progress!(ctx, 20);
@@ -397,8 +400,8 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
progress!(ctx, 900);
let create_mvbox = ctx.get_config_bool(Config::MvboxWatch).await
|| ctx.get_config_bool(Config::MvboxMove).await;
let create_mvbox = ctx.get_config_bool(Config::MvboxWatch).await?
|| ctx.get_config_bool(Config::MvboxMove).await?;
imap.configure_folders(ctx, create_mvbox).await?;
@@ -413,7 +416,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
// "configured_" prefix; also write the "configured"-flag */
// the trailing underscore is correct
param.save_to_database(ctx, "configured_").await?;
ctx.sql.set_raw_config_bool(ctx, "configured", true).await?;
ctx.sql.set_raw_config_bool("configured", true).await?;
ctx.set_config(Config::ConfiguredTimestamp, Some(&time().to_string()))
.await?;

View File

@@ -1,8 +1,9 @@
//! # Constants
use deltachat_derive::{FromSql, ToSql};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use crate::chat::ChatId;
pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION").to_string());
#[derive(
@@ -14,12 +15,11 @@ pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION")
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Serialize,
Deserialize,
sqlx::Type,
)]
#[repr(u8)]
#[repr(i8)]
pub enum Blocked {
Not = 0,
Manually = 1,
@@ -32,9 +32,7 @@ impl Default for Blocked {
}
}
#[derive(
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum ShowEmails {
Off = 0,
@@ -48,9 +46,7 @@ impl Default for ShowEmails {
}
}
#[derive(
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum MediaQuality {
Balanced = 0,
@@ -63,9 +59,7 @@ impl Default for MediaQuality {
}
}
#[derive(
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum KeyGenType {
Default = 0,
@@ -79,9 +73,7 @@ impl Default for KeyGenType {
}
}
#[derive(
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(i8)]
pub enum VideochatType {
Unknown = 0,
@@ -122,15 +114,15 @@ pub const DC_RESEND_USER_AVATAR_DAYS: i64 = 14;
pub const DC_OUTDATED_WARNING_DAYS: i64 = 365;
/// virtual chat showing all messages belonging to chats flagged with chats.blocked=2
pub const DC_CHAT_ID_DEADDROP: u32 = 1;
pub const DC_CHAT_ID_DEADDROP: ChatId = ChatId::new(1);
/// messages that should be deleted get this chat_id; the messages are deleted from the working thread later then. This is also needed as rfc724_mid should be preset as long as the message is not deleted on the server (otherwise it is downloaded again)
pub const DC_CHAT_ID_TRASH: u32 = 3;
pub const DC_CHAT_ID_TRASH: ChatId = ChatId::new(3);
/// only an indicator in a chatlist
pub const DC_CHAT_ID_ARCHIVED_LINK: u32 = 6;
pub const DC_CHAT_ID_ARCHIVED_LINK: ChatId = ChatId::new(6);
/// only an indicator in a chatlist
pub const DC_CHAT_ID_ALLDONE_HINT: u32 = 7;
pub const DC_CHAT_ID_ALLDONE_HINT: ChatId = ChatId::new(7);
/// larger chat IDs are "real" chats, their messages are "real" messages.
pub const DC_CHAT_ID_LAST_SPECIAL: u32 = 9;
pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
#[derive(
Debug,
@@ -141,11 +133,10 @@ pub const DC_CHAT_ID_LAST_SPECIAL: u32 = 9;
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
IntoStaticStr,
Serialize,
Deserialize,
sqlx::Type,
)]
#[repr(u32)]
pub enum Chattype {
@@ -256,12 +247,11 @@ pub const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Serialize,
Deserialize,
sqlx::Type,
)]
#[repr(i32)]
#[repr(u32)]
pub enum Viewtype {
Unknown = 0,

View File

@@ -1,11 +1,13 @@
//! Contacts module
use std::convert::TryFrom;
use anyhow::{bail, ensure, format_err, Context as _, Result};
use anyhow::{bail, ensure, format_err, Result};
use async_std::path::PathBuf;
use deltachat_derive::{FromSql, ToSql};
use async_std::prelude::*;
use itertools::Itertools;
use once_cell::sync::Lazy;
use regex::Regex;
use sqlx::Row;
use crate::aheader::EncryptPreference;
use crate::chat::ChatId;
@@ -77,9 +79,9 @@ pub struct Contact {
/// Possible origins of a contact.
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromPrimitive, ToPrimitive, FromSql, ToSql,
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromPrimitive, ToPrimitive, sqlx::Type,
)]
#[repr(i32)]
#[repr(u32)]
pub enum Origin {
Unknown = 0,
@@ -174,43 +176,45 @@ pub enum VerifiedStatus {
impl Contact {
pub async fn load_from_db(context: &Context, contact_id: u32) -> crate::sql::Result<Self> {
let mut res = context
let row = context
.sql
.query_row(
"SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param, c.status
.fetch_one(
sqlx::query(
"SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param, c.status
FROM contacts c
WHERE c.id=?;",
paramsv![contact_id as i32],
|row| {
let contact = Self {
id: contact_id,
name: row.get::<_, String>(0)?,
authname: row.get::<_, String>(4)?,
addr: row.get::<_, String>(1)?,
blocked: row.get::<_, Option<i32>>(3)?.unwrap_or_default() != 0,
origin: row.get(2)?,
param: row.get::<_, String>(5)?.parse().unwrap_or_default(),
status: row.get(6).unwrap_or_default(),
};
Ok(contact)
},
)
.bind(contact_id),
)
.await?;
let mut contact = Contact {
id: contact_id,
name: row.try_get(0)?,
authname: row.try_get(4)?,
addr: row.try_get(1)?,
blocked: row.try_get::<Option<i32>, _>(3)?.unwrap_or_default() != 0,
origin: row.try_get(2)?,
param: row.try_get::<String, _>(5)?.parse().unwrap_or_default(),
status: row.try_get::<Option<String>, _>(6)?.unwrap_or_default(),
};
if contact_id == DC_CONTACT_ID_SELF {
res.name = stock_str::self_msg(context).await;
res.addr = context
contact.name = stock_str::self_msg(context).await;
contact.addr = context
.get_config(Config::ConfiguredAddr)
.await
.await?
.unwrap_or_default();
res.status = context
contact.status = context
.get_config(Config::Selfstatus)
.await
.await?
.unwrap_or_default();
} else if contact_id == DC_CONTACT_ID_DEVICE {
res.name = stock_str::device_messages(context).await;
res.addr = DC_CONTACT_ID_DEVICE_ADDR.to_string();
contact.name = stock_str::device_messages(context).await;
contact.addr = DC_CONTACT_ID_DEVICE_ADDR.to_string();
}
Ok(res)
Ok(contact)
}
/// Returns `true` if this contact is blocked.
@@ -281,13 +285,15 @@ impl Contact {
if context
.sql
.execute(
"UPDATE msgs SET state=? WHERE from_id=? AND state=?;",
paramsv![MessageState::InNoticed, id as i32, MessageState::InFresh],
sqlx::query("UPDATE msgs SET state=? WHERE from_id=? AND state=?;")
.bind(MessageState::InNoticed)
.bind(id as i32)
.bind(MessageState::InFresh),
)
.await
.is_ok()
{
context.emit_event(EventType::MsgsNoticed(ChatId::new(DC_CHAT_ID_DEADDROP)));
context.emit_event(EventType::MsgsNoticed(DC_CHAT_ID_DEADDROP));
}
}
@@ -308,21 +314,27 @@ impl Contact {
let addr_normalized = addr_normalize(addr.as_ref());
if let Some(addr_self) = context.get_config(Config::ConfiguredAddr).await {
if let Some(addr_self) = context.get_config(Config::ConfiguredAddr).await? {
if addr_cmp(addr_normalized, addr_self) {
return Ok(Some(DC_CONTACT_ID_SELF));
}
}
context.sql.query_get_value_result(
"SELECT id FROM contacts WHERE addr=?1 COLLATE NOCASE AND id>?2 AND origin>=?3 AND blocked=0;",
paramsv![
addr_normalized,
DC_CONTACT_ID_LAST_SPECIAL as i32,
min_origin as u32,
],
)
.await
.context("lookup_id_by_addr: SQL query failed")
let id = context
.sql
.query_get_value(
sqlx::query(
"SELECT id FROM contacts \
WHERE addr=?1 COLLATE NOCASE \
AND id>?2 AND origin>=?3 AND blocked=0;",
)
.bind(addr_normalized)
.bind(DC_CONTACT_ID_LAST_SPECIAL)
.bind(min_origin),
)
.await?
.unwrap_or_default();
Ok(id)
}
/// Lookup a contact and create it if it does not exist yet.
@@ -367,7 +379,7 @@ impl Contact {
let addr = addr_normalize(addr.as_ref()).to_string();
let addr_self = context
.get_config(Config::ConfiguredAddr)
.await
.await?
.unwrap_or_default();
if addr_cmp(&addr, addr_self) {
@@ -419,25 +431,33 @@ impl Contact {
let mut update_addr = false;
let mut row_id = 0;
if let Ok((id, row_name, row_addr, row_origin, row_authname)) = context.sql.query_row(
"SELECT id, name, addr, origin, authname FROM contacts WHERE addr=? COLLATE NOCASE;",
paramsv![addr.to_string()],
|row| {
let row_id = row.get(0)?;
let row_name: String = row.get(1)?;
let row_addr: String = row.get(2)?;
let row_origin: Origin = row.get(3)?;
let row_authname: String = row.get(4)?;
if let Ok((id, row_name, row_addr, row_origin, row_authname)) = context
.sql
.fetch_one(
sqlx::query(
"SELECT id, name, addr, origin, authname \
FROM contacts WHERE addr=? COLLATE NOCASE;",
)
.bind(addr.to_string()),
)
.await
.and_then(|row| {
let row_id = row.try_get(0)?;
let row_name: String = row.try_get(1)?;
let row_addr: String = row.try_get(2)?;
let row_origin: Origin = row.try_get(3)?;
let row_authname: String = row.try_get(4)?;
Ok((row_id, row_name, row_addr, row_origin, row_authname))
},
)
.await {
})
{
let update_name = manual && name != row_name;
let update_authname =
!manual && name != row_authname && !name.is_empty() &&
(origin >= row_origin || origin == Origin::IncomingUnknownFrom || row_authname.is_empty());
let update_authname = !manual
&& name != row_authname
&& !name.is_empty()
&& (origin >= row_origin
|| origin == Origin::IncomingUnknownFrom
|| row_authname.is_empty());
row_id = id;
if origin as i32 >= row_origin as i32 && addr != row_addr {
update_addr = true;
@@ -449,43 +469,55 @@ impl Contact {
row_name
};
context
.sql
.execute(
"UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;",
paramsv![
new_name,
if update_addr { addr.to_string() } else { row_addr },
if origin > row_origin {
origin
} else {
row_origin
},
if update_authname {
name.to_string()
} else {
row_authname
},
row_id
],
)
.await
.ok();
let query = sqlx::query(
"UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;",
)
.bind(&new_name)
.bind(if update_addr {
addr.to_string()
} else {
row_addr
})
.bind(if origin > row_origin {
origin
} else {
row_origin
})
.bind(if update_authname {
name.to_string()
} else {
row_authname
})
.bind(row_id);
context.sql.execute(query).await.ok();
if update_name {
// Update the contact name also if it is used as a group name.
// This is one of the few duplicated data, however, getting the chat list is easier this way.
let chat_id = context.sql.query_get_value::<i32>(
context,
"SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)",
paramsv![Chattype::Single, row_id]
).await;
let chat_id = context.sql.query_get_value::<_, u32>(
sqlx::query(
"SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)"
).bind(Chattype::Single).bind(row_id)
).await?;
if let Some(chat_id) = chat_id {
match context.sql.execute("UPDATE chats SET name=? WHERE id=? AND name!=?1", paramsv![new_name, chat_id]).await {
match context
.sql
.execute(
sqlx::query("UPDATE chats SET name=?1 WHERE id=?2 AND name!=?3")
.bind(&new_name)
.bind(chat_id)
.bind(&new_name),
)
.await
{
Err(err) => warn!(context, "Can't update chat name: {}", err),
Ok(count) => if count > 0 {
// Chat name updated
context.emit_event(EventType::ChatModified(ChatId::new(chat_id as u32)));
Ok(count) => {
if count > 0 {
// Chat name updated
context
.emit_event(EventType::ChatModified(ChatId::new(chat_id)));
}
}
}
}
@@ -499,21 +531,26 @@ impl Contact {
if context
.sql
.execute(
"INSERT INTO contacts (name, addr, origin, authname) VALUES(?, ?, ?, ?);",
paramsv![
if update_name { name.to_string() } else { "".to_string() },
addr,
origin,
if update_authname { name.to_string() } else { "".to_string() }
],
sqlx::query(
"INSERT INTO contacts (name, addr, origin, authname) VALUES(?, ?, ?, ?);",
)
.bind(if update_name {
name.to_string()
} else {
"".to_string()
})
.bind(&addr)
.bind(origin)
.bind(if update_authname {
name.to_string()
} else {
"".to_string()
}),
)
.await
.is_ok()
{
row_id = context
.sql
.get_rowid(context, "contacts", "addr", &addr)
.await?;
row_id = context.sql.get_rowid("contacts", "addr", &addr).await?;
sth_modified = Modifier::Created;
info!(context, "added contact id={} addr={}", row_id, &addr);
} else {
@@ -521,7 +558,7 @@ impl Contact {
}
}
Ok((row_id, sth_modified))
Ok((u32::try_from(row_id)?, sth_modified))
}
/// Add a number of contacts.
@@ -584,7 +621,7 @@ impl Contact {
) -> Result<Vec<u32>> {
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await
.await?
.unwrap_or_default();
let mut add_self = false;
@@ -600,10 +637,12 @@ impl Contact {
.map(|s| s.as_ref().to_string())
.unwrap_or_default()
);
context
let mut rows = context
.sql
.query_map(
"SELECT c.id FROM contacts c \
.fetch(
sqlx::query(
"SELECT c.id FROM contacts c \
LEFT JOIN acpeerstates ps ON c.addr=ps.addr \
WHERE c.addr!=?1 \
AND c.id>?2 \
@@ -612,27 +651,23 @@ impl Contact {
AND (iif(c.name='',c.authname,c.name) LIKE ?4 OR c.addr LIKE ?5) \
AND (1=?6 OR LENGTH(ps.verified_key_fingerprint)!=0) \
ORDER BY LOWER(iif(c.name='',c.authname,c.name)||c.addr),c.id;",
paramsv![
self_addr,
DC_CONTACT_ID_LAST_SPECIAL as i32,
Origin::IncomingReplyTo,
s3str_like_cmd,
s3str_like_cmd,
if flag_verified_only { 0i32 } else { 1i32 },
],
|row| row.get::<_, i32>(0),
|ids| {
for id in ids {
ret.push(id? as u32);
}
Ok(())
},
)
.bind(&self_addr)
.bind(DC_CONTACT_ID_LAST_SPECIAL)
.bind(Origin::IncomingReplyTo)
.bind(&s3str_like_cmd)
.bind(&s3str_like_cmd)
.bind(if flag_verified_only { 0i32 } else { 1i32 }),
)
.await?;
.await?
.map(|row| row?.try_get(0));
while let Some(id) = rows.next().await {
ret.push(id?);
}
let self_name = context
.get_config(Config::Displayname)
.await
.await?
.unwrap_or_default();
let self_name2 = stock_str::self_msg(context);
@@ -649,25 +684,27 @@ impl Contact {
} else {
add_self = true;
context
let mut rows = context
.sql
.query_map(
"SELECT id FROM contacts
.fetch(
sqlx::query(
"SELECT id FROM contacts
WHERE addr!=?1
AND id>?2
AND origin>=?3
AND blocked=0
ORDER BY LOWER(iif(name='',authname,name)||addr),id;",
paramsv![self_addr, DC_CONTACT_ID_LAST_SPECIAL as i32, 0x100],
|row| row.get::<_, i32>(0),
|ids| {
for id in ids {
ret.push(id? as u32);
}
Ok(())
},
)
.bind(self_addr)
.bind(DC_CONTACT_ID_LAST_SPECIAL)
.bind(Origin::IncomingReplyTo),
)
.await?;
.await?
.map(|row| row?.try_get(0));
while let Some(id) = rows.next().await {
ret.push(id?);
}
}
if flag_add_self && add_self {
@@ -683,41 +720,55 @@ impl Contact {
// from the users perspective,
// there is not much difference in an email- and a mailinglist-address)
async fn update_blocked_mailinglist_contacts(context: &Context) -> Result<()> {
let blocked_mailinglists = context
let mut rows = context
.sql
.query_map(
"SELECT name, grpid FROM chats WHERE type=? AND blocked=?;",
paramsv![Chattype::Mailinglist, Blocked::Manually],
|row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
|rows| {
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
.fetch(
sqlx::query("SELECT name, grpid FROM chats WHERE type=? AND blocked=?;")
.bind(Chattype::Mailinglist)
.bind(Blocked::Manually),
)
.await?;
for (name, grpid) in blocked_mailinglists {
while let Some(row) = rows.next().await {
let row = row?;
let name = row.try_get::<String, _>(0)?;
let grpid = row.try_get::<String, _>(1)?;
if !context
.sql
.exists("SELECT id FROM contacts WHERE addr=?;", paramsv![grpid])
.exists(sqlx::query("SELECT COUNT(id) FROM contacts WHERE addr=?;").bind(&grpid))
.await?
{
context
.sql
.execute("INSERT INTO contacts (addr) VALUES (?);", paramsv![grpid])
.execute(sqlx::query("INSERT INTO contacts (addr) VALUES (?);").bind(&grpid))
.await?;
}
// always do an update in case the blocking is reset or name is changed
context
.sql
.execute(
"UPDATE contacts SET name=?, origin=?, blocked=1 WHERE addr=?;",
paramsv![name, Origin::MailinglistAddress, grpid],
sqlx::query("UPDATE contacts SET name=?, origin=?, blocked=1 WHERE addr=?;")
.bind(name)
.bind(Origin::MailinglistAddress)
.bind(&grpid),
)
.await?;
}
Ok(())
}
pub async fn get_blocked_cnt(context: &Context) -> Result<usize> {
let count = context
.sql
.count(
sqlx::query("SELECT COUNT(*) FROM contacts WHERE id>? AND blocked!=0")
.bind(DC_CONTACT_ID_LAST_SPECIAL),
)
.await?;
Ok(count as usize)
}
/// Get blocked contacts.
pub async fn get_all_blocked(context: &Context) -> Result<Vec<u32>> {
if let Err(e) = Contact::update_blocked_mailinglist_contacts(context).await {
@@ -727,19 +778,19 @@ impl Contact {
);
}
let ret = context
let list = context
.sql
.query_map(
.fetch(
sqlx::query(
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY LOWER(iif(name='',authname,name)||addr),id;",
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
|row| row.get::<_, u32>(0),
|ids| {
ids.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
).bind(DC_CONTACT_ID_LAST_SPECIAL)
)
.await?
.map(|row| row?.try_get::<u32, _>(0))
.collect::<sqlx::Result<Vec<_>>>()
.await?;
Ok(ret)
Ok(list)
}
/// Returns a textual summary of the encryption state for the contact.
@@ -755,7 +806,7 @@ impl Contact {
let mut ret = String::new();
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
let loginparam = LoginParam::from_database(context, "configured_").await;
let loginparam = LoginParam::from_database(context, "configured_").await?;
let peerstate = Peerstate::from_addr(context, &contact.addr).await?;
if let Some(peerstate) = peerstate.filter(|peerstate| {
@@ -822,26 +873,23 @@ impl Contact {
"Can not delete special contact"
);
let count_contacts: i32 = context
let count_contacts = context
.sql
.query_get_value(
context,
"SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;",
paramsv![contact_id as i32],
.count(
sqlx::query("SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;")
.bind(contact_id),
)
.await
.unwrap_or_default();
.await?;
let count_msgs: i32 = if count_contacts > 0 {
let count_msgs = if count_contacts > 0 {
context
.sql
.query_get_value(
context,
"SELECT COUNT(*) FROM msgs WHERE from_id=? OR to_id=?;",
paramsv![contact_id as i32, contact_id as i32],
.count(
sqlx::query("SELECT COUNT(*) FROM msgs WHERE from_id=? OR to_id=?;")
.bind(contact_id)
.bind(contact_id),
)
.await
.unwrap_or_default()
.await?
} else {
0
};
@@ -849,10 +897,7 @@ impl Contact {
if count_msgs == 0 {
match context
.sql
.execute(
"DELETE FROM contacts WHERE id=?;",
paramsv![contact_id as i32],
)
.execute(sqlx::query("DELETE FROM contacts WHERE id=?;").bind(contact_id as i32))
.await
{
Ok(_) => {
@@ -889,8 +934,9 @@ impl Contact {
context
.sql
.execute(
"UPDATE contacts SET param=? WHERE id=?",
paramsv![self.param.to_string(), self.id as i32],
sqlx::query("UPDATE contacts SET param=? WHERE id=?")
.bind(self.param.to_string())
.bind(self.id as i32),
)
.await?;
Ok(())
@@ -901,8 +947,9 @@ impl Contact {
context
.sql
.execute(
"UPDATE contacts SET status=? WHERE id=?",
paramsv![self.status, self.id as i32],
sqlx::query("UPDATE contacts SET status=? WHERE id=?")
.bind(&self.status)
.bind(self.id as i32),
)
.await?;
Ok(())
@@ -967,17 +1014,17 @@ impl Contact {
/// Get the contact's profile image.
/// This is the image set by each remote user on their own
/// using dc_set_config(context, "selfavatar", image).
pub async fn get_profile_image(&self, context: &Context) -> Option<PathBuf> {
pub async fn get_profile_image(&self, context: &Context) -> Result<Option<PathBuf>> {
if self.id == DC_CONTACT_ID_SELF {
if let Some(p) = context.get_config(Config::Selfavatar).await {
return Some(PathBuf::from(p));
if let Some(p) = context.get_config(Config::Selfavatar).await? {
return Ok(Some(PathBuf::from(p)));
}
} else if let Some(image_rel) = self.param.get(Param::ProfileImage) {
if !image_rel.is_empty() {
return Some(dc_get_abs_path(context, image_rel));
return Ok(Some(dc_get_abs_path(context, image_rel)));
}
}
None
Ok(None)
}
/// Get a color for the contact.
@@ -1065,20 +1112,19 @@ impl Contact {
false
}
pub async fn get_real_cnt(context: &Context) -> usize {
pub async fn get_real_cnt(context: &Context) -> Result<usize> {
if !context.sql.is_open().await {
return 0;
return Ok(0);
}
context
let count = context
.sql
.query_get_value::<isize>(
context,
"SELECT COUNT(*) FROM contacts WHERE id>?;",
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
.count(
sqlx::query("SELECT COUNT(*) FROM contacts WHERE id>?;")
.bind(DC_CONTACT_ID_LAST_SPECIAL),
)
.await
.unwrap_or_default() as usize
.await?;
Ok(count)
}
pub async fn real_exists_by_id(context: &Context, contact_id: u32) -> bool {
@@ -1088,10 +1134,7 @@ impl Contact {
context
.sql
.exists(
"SELECT id FROM contacts WHERE id=?;",
paramsv![contact_id as i32],
)
.exists(sqlx::query("SELECT COUNT(*) FROM contacts WHERE id=?;").bind(contact_id))
.await
.unwrap_or_default()
}
@@ -1100,8 +1143,10 @@ impl Contact {
context
.sql
.execute(
"UPDATE contacts SET origin=? WHERE id=? AND origin<?;",
paramsv![origin, contact_id as i32, origin],
sqlx::query("UPDATE contacts SET origin=? WHERE id=? AND origin<?;")
.bind(origin)
.bind(contact_id)
.bind(origin),
)
.await
.is_ok()
@@ -1155,8 +1200,9 @@ async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: boo
&& context
.sql
.execute(
"UPDATE contacts SET blocked=? WHERE id=?;",
paramsv![new_blocking as i32, contact_id as i32],
sqlx::query("UPDATE contacts SET blocked=? WHERE id=?;")
.bind(new_blocking as i32)
.bind(contact_id),
)
.await
.is_ok()
@@ -1166,9 +1212,24 @@ async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: boo
// (Maybe, beside normal chats (type=100) we should also block group chats with only this user.
// However, I'm not sure about this point; it may be confusing if the user wants to add other people;
// this would result in recreating the same group...)
if context.sql.execute(
"UPDATE chats SET blocked=? WHERE type=? AND id IN (SELECT chat_id FROM chats_contacts WHERE contact_id=?);",
paramsv![new_blocking, 100, contact_id as i32]).await.is_ok()
if context
.sql
.execute(
sqlx::query(
r#"
UPDATE chats
SET blocked=?
WHERE type=? AND id IN (
SELECT chat_id FROM chats_contacts WHERE contact_id=?
);
"#,
)
.bind(new_blocking)
.bind(Chattype::Single)
.bind(contact_id),
)
.await
.is_ok()
{
Contact::mark_noticed(context, contact_id).await;
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
@@ -1299,7 +1360,7 @@ impl Context {
pub async fn is_self_addr(&self, addr: &str) -> Result<bool> {
let self_addr = self
.get_config(Config::ConfiguredAddr)
.await
.await?
.ok_or_else(|| format_err!("Not configured"))?;
Ok(addr_cmp(self_addr, addr))

View File

@@ -6,12 +6,14 @@ use std::ops::Deref;
use std::time::{Instant, SystemTime};
use anyhow::{bail, ensure, Result};
use async_std::prelude::*;
use async_std::{
channel::{self, Receiver, Sender},
path::{Path, PathBuf},
sync::{Arc, Mutex, RwLock},
task,
};
use sqlx::Row;
use crate::chat::{get_chat_cnt, ChatId};
use crate::config::Config;
@@ -89,8 +91,9 @@ pub struct RunningState {
pub fn get_info() -> BTreeMap<&'static str, String> {
let mut res = BTreeMap::new();
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
res.insert("sqlite_version", rusqlite::version().to_string());
res.insert("sqlite_version", crate::sql::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
}
@@ -270,68 +273,62 @@ impl Context {
* UI chat/message related API
******************************************************************************/
pub async fn get_info(&self) -> BTreeMap<&'static str, String> {
pub async fn get_info(&self) -> Result<BTreeMap<&'static str, String>> {
let unset = "0";
let l = LoginParam::from_database(self, "").await;
let l2 = LoginParam::from_database(self, "configured_").await;
let displayname = self.get_config(Config::Displayname).await;
let chats = get_chat_cnt(self).await as usize;
let l = LoginParam::from_database(self, "").await?;
let l2 = LoginParam::from_database(self, "configured_").await?;
let displayname = self.get_config(Config::Displayname).await?;
let chats = get_chat_cnt(self).await? as usize;
let real_msgs = message::get_real_msg_cnt(self).await as usize;
let deaddrop_msgs = message::get_deaddrop_msg_cnt(self).await as usize;
let contacts = Contact::get_real_cnt(self).await as usize;
let is_configured = self.get_config_int(Config::Configured).await;
let contacts = Contact::get_real_cnt(self).await? as usize;
let is_configured = self.get_config_int(Config::Configured).await?;
let dbversion = self
.sql
.get_raw_config_int(self, "dbversion")
.await
.get_raw_config_int("dbversion")
.await?
.unwrap_or_default();
let journal_mode = self
.sql
.query_get_value(self, "PRAGMA journal_mode;", paramsv![])
.await
.query_get_value("PRAGMA journal_mode;")
.await?
.unwrap_or_else(|| "unknown".to_string());
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await;
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await;
let bcc_self = self.get_config_int(Config::BccSelf).await;
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await?;
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await?;
let bcc_self = self.get_config_int(Config::BccSelf).await?;
let prv_key_cnt: Option<isize> = self
.sql
.query_get_value(self, "SELECT COUNT(*) FROM keypairs;", paramsv![])
.await;
let prv_key_cnt = self.sql.count("SELECT COUNT(*) FROM keypairs;").await?;
let pub_key_cnt: Option<isize> = self
.sql
.query_get_value(self, "SELECT COUNT(*) FROM acpeerstates;", paramsv![])
.await;
let pub_key_cnt = self.sql.count("SELECT COUNT(*) FROM acpeerstates;").await?;
let fingerprint_str = match SignedPublicKey::load_self(self).await {
Ok(key) => key.fingerprint().hex(),
Err(err) => format!("<key failure: {}>", err),
};
let inbox_watch = self.get_config_int(Config::InboxWatch).await;
let sentbox_watch = self.get_config_int(Config::SentboxWatch).await;
let mvbox_watch = self.get_config_int(Config::MvboxWatch).await;
let mvbox_move = self.get_config_int(Config::MvboxMove).await;
let sentbox_move = self.get_config_int(Config::SentboxMove).await;
let inbox_watch = self.get_config_int(Config::InboxWatch).await?;
let sentbox_watch = self.get_config_int(Config::SentboxWatch).await?;
let mvbox_watch = self.get_config_int(Config::MvboxWatch).await?;
let mvbox_move = self.get_config_int(Config::MvboxMove).await?;
let sentbox_move = self.get_config_int(Config::SentboxMove).await?;
let folders_configured = self
.sql
.get_raw_config_int(self, "folders_configured")
.await
.get_raw_config_int("folders_configured")
.await?
.unwrap_or_default();
let configured_sentbox_folder = self
.get_config(Config::ConfiguredSentboxFolder)
.await
.await?
.unwrap_or_else(|| "<unset>".to_string());
let configured_mvbox_folder = self
.get_config(Config::ConfiguredMvboxFolder)
.await
.await?
.unwrap_or_else(|| "<unset>".to_string());
let mut res = get_info();
// insert values
res.insert("bot", self.get_config_int(Config::Bot).await.to_string());
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", real_msgs.to_string());
res.insert("messages_in_contact_requests", deaddrop_msgs.to_string());
@@ -344,7 +341,7 @@ impl Context {
res.insert(
"selfavatar",
self.get_config(Config::Selfavatar)
.await
.await?
.unwrap_or_else(|| "<unset>".to_string()),
);
res.insert("is_configured", is_configured.to_string());
@@ -353,12 +350,12 @@ impl Context {
res.insert(
"fetch_existing_msgs",
self.get_config_int(Config::FetchExistingMsgs)
.await
.await?
.to_string(),
);
res.insert(
"show_emails",
self.get_config_int(Config::ShowEmails).await.to_string(),
self.get_config_int(Config::ShowEmails).await?.to_string(),
);
res.insert("inbox_watch", inbox_watch.to_string());
res.insert("sentbox_watch", sentbox_watch.to_string());
@@ -372,57 +369,51 @@ impl Context {
res.insert("e2ee_enabled", e2ee_enabled.to_string());
res.insert(
"key_gen_type",
self.get_config_int(Config::KeyGenType).await.to_string(),
self.get_config_int(Config::KeyGenType).await?.to_string(),
);
res.insert("bcc_self", bcc_self.to_string());
res.insert(
"private_key_count",
prv_key_cnt.unwrap_or_default().to_string(),
);
res.insert(
"public_key_count",
pub_key_cnt.unwrap_or_default().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(
"webrtc_instance",
self.get_config(Config::WebrtcInstance)
.await
.await?
.unwrap_or_else(|| "<unset>".to_string()),
);
res.insert(
"media_quality",
self.get_config_int(Config::MediaQuality).await.to_string(),
self.get_config_int(Config::MediaQuality).await?.to_string(),
);
res.insert(
"delete_device_after",
self.get_config_int(Config::DeleteDeviceAfter)
.await
.await?
.to_string(),
);
res.insert(
"delete_server_after",
self.get_config_int(Config::DeleteServerAfter)
.await
.await?
.to_string(),
);
res.insert(
"last_housekeeping",
self.get_config_int(Config::LastHousekeeping)
.await
.await?
.to_string(),
);
res.insert(
"scan_all_folders_debounce_secs",
self.get_config_int(Config::ScanAllFoldersDebounceSecs)
.await
.await?
.to_string(),
);
let elapsed = self.creation_time.elapsed();
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));
res
Ok(res)
}
/// Get a list of fresh, unmuted messages in any chat but deaddrop.
@@ -432,10 +423,10 @@ impl Context {
/// Moreover, the number of returned messages
/// can be used for a badge counter on the app icon.
pub async fn get_fresh_msgs(&self) -> Result<Vec<MsgId>> {
let ret = self
let list = self
.sql
.query_map(
concat!(
.fetch(
sqlx::query(concat!(
"SELECT m.id",
" FROM msgs m",
" LEFT JOIN contacts ct",
@@ -449,51 +440,38 @@ impl Context {
" AND c.blocked=0",
" AND NOT(c.muted_until=-1 OR c.muted_until>?)",
" ORDER BY m.timestamp DESC,m.id DESC;"
),
paramsv![MessageState::InFresh, time()],
|row| row.get::<_, MsgId>(0),
|rows| {
let mut ret = Vec::new();
for row in rows {
ret.push(row?);
}
Ok(ret)
},
))
.bind(MessageState::InFresh)
.bind(time()),
)
.await?
.map(|row| row?.try_get("id"))
.collect::<sqlx::Result<_>>()
.await?;
Ok(ret)
Ok(list)
}
/// Searches for messages containing the query string.
///
/// If `chat_id` is provided this searches only for messages in this chat, if `chat_id`
/// is `None` this searches messages from all chats.
pub async fn search_msgs(&self, chat_id: Option<ChatId>, query: impl AsRef<str>) -> Vec<MsgId> {
pub async fn search_msgs(
&self,
chat_id: Option<ChatId>,
query: impl AsRef<str>,
) -> Result<Vec<MsgId>> {
let real_query = query.as_ref().trim();
if real_query.is_empty() {
return Vec::new();
return Ok(Vec::new());
}
let str_like_in_text = format!("%{}%", real_query);
let str_like_beg = format!("{}%", real_query);
let do_query = |query, params| {
self.sql.query_map(
query,
params,
|row| row.get::<_, MsgId>("id"),
|rows| {
let mut ret = Vec::new();
for id in rows {
ret.push(id?);
}
Ok(ret)
},
)
};
if let Some(chat_id) = chat_id {
do_query(
"SELECT m.id AS id, m.timestamp AS timestamp
let list = if let Some(chat_id) = chat_id {
self.sql
.fetch(
sqlx::query(
"SELECT m.id AS id, m.timestamp AS timestamp
FROM msgs m
LEFT JOIN contacts ct
ON m.from_id=ct.id
@@ -502,13 +480,24 @@ impl Context {
AND ct.blocked=0
AND (txt LIKE ? OR ct.name LIKE ?)
ORDER BY m.timestamp,m.id;",
paramsv![chat_id, str_like_in_text, str_like_beg],
)
.await
.unwrap_or_default()
)
.bind(chat_id)
.bind(str_like_in_text)
.bind(str_like_beg),
)
.await?
.map(|row| {
let row = row?;
let id = row.try_get::<MsgId, _>("id")?;
Ok(id)
})
.collect::<sqlx::Result<Vec<MsgId>>>()
.await?
} else {
do_query(
"SELECT m.id AS id, m.timestamp AS timestamp
self.sql
.fetch(
sqlx::query(
"SELECT m.id AS id, m.timestamp AS timestamp
FROM msgs m
LEFT JOIN contacts ct
ON m.from_id=ct.id
@@ -520,31 +509,45 @@ impl Context {
AND ct.blocked=0
AND (m.txt LIKE ? OR ct.name LIKE ?)
ORDER BY m.timestamp DESC,m.id DESC;",
paramsv![str_like_in_text, str_like_beg],
)
.await
.unwrap_or_default()
}
)
.bind(str_like_in_text)
.bind(str_like_beg),
)
.await?
.map(|row| {
let row = row?;
let id = row.try_get::<MsgId, _>("id")?;
Ok(id)
})
.collect::<sqlx::Result<Vec<MsgId>>>()
.await?
};
Ok(list)
}
pub async fn is_inbox(&self, folder_name: impl AsRef<str>) -> bool {
self.get_config(Config::ConfiguredInboxFolder).await
== Some(folder_name.as_ref().to_string())
pub async fn is_inbox(&self, folder_name: impl AsRef<str>) -> Result<bool> {
let inbox = self.get_config(Config::ConfiguredInboxFolder).await?;
Ok(inbox == Some(folder_name.as_ref().to_string()))
}
pub async fn is_sentbox(&self, folder_name: impl AsRef<str>) -> bool {
self.get_config(Config::ConfiguredSentboxFolder).await
== Some(folder_name.as_ref().to_string())
pub async fn is_sentbox(&self, folder_name: impl AsRef<str>) -> Result<bool> {
let sentbox = self.get_config(Config::ConfiguredSentboxFolder).await?;
Ok(sentbox == Some(folder_name.as_ref().to_string()))
}
pub async fn is_mvbox(&self, folder_name: impl AsRef<str>) -> bool {
self.get_config(Config::ConfiguredMvboxFolder).await
== Some(folder_name.as_ref().to_string())
pub async fn is_mvbox(&self, folder_name: impl AsRef<str>) -> Result<bool> {
let mvbox = self.get_config(Config::ConfiguredMvboxFolder).await?;
Ok(mvbox == Some(folder_name.as_ref().to_string()))
}
pub async fn is_spam_folder(&self, folder_name: impl AsRef<str>) -> bool {
self.get_config(Config::ConfiguredSpamFolder).await
== Some(folder_name.as_ref().to_string())
pub async fn is_spam_folder(&self, folder_name: impl AsRef<str>) -> Result<bool> {
let is_spam = self.get_config(Config::ConfiguredSpamFolder).await?
== Some(folder_name.as_ref().to_string());
Ok(is_spam)
}
pub fn derive_blobdir(dbfile: &PathBuf) -> PathBuf {
@@ -620,7 +623,7 @@ mod tests {
}
async fn receive_msg(t: &TestContext, chat: &Chat) {
let members = get_chat_contacts(t, chat.id).await;
let members = get_chat_contacts(t, chat.id).await.unwrap();
let contact = Contact::load_from_db(t, *members.first().unwrap())
.await
.unwrap();
@@ -651,43 +654,49 @@ mod tests {
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
receive_msg(&t, &bob).await;
assert_eq!(get_chat_msgs(&t, bob.id, 0, None).await.len(), 1);
assert_eq!(bob.id.get_fresh_msg_cnt(&t).await, 1);
assert_eq!(get_chat_msgs(&t, bob.id, 0, None).await.unwrap().len(), 1);
assert_eq!(bob.id.get_fresh_msg_cnt(&t).await.unwrap(), 1);
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
receive_msg(&t, &claire).await;
receive_msg(&t, &claire).await;
assert_eq!(get_chat_msgs(&t, claire.id, 0, None).await.len(), 2);
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await, 2);
assert_eq!(
get_chat_msgs(&t, claire.id, 0, None).await.unwrap().len(),
2
);
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2);
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 3);
receive_msg(&t, &dave).await;
receive_msg(&t, &dave).await;
receive_msg(&t, &dave).await;
assert_eq!(get_chat_msgs(&t, dave.id, 0, None).await.len(), 3);
assert_eq!(dave.id.get_fresh_msg_cnt(&t).await, 3);
assert_eq!(get_chat_msgs(&t, dave.id, 0, None).await.unwrap().len(), 3);
assert_eq!(dave.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6);
// mute one of the chats
set_muted(&t, claire.id, MuteDuration::Forever)
.await
.unwrap();
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await, 2);
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2);
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 4); // muted claires messages are no longer counted
// receive more messages
receive_msg(&t, &bob).await;
receive_msg(&t, &claire).await;
receive_msg(&t, &dave).await;
assert_eq!(get_chat_msgs(&t, claire.id, 0, None).await.len(), 3);
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await, 3);
assert_eq!(
get_chat_msgs(&t, claire.id, 0, None).await.unwrap().len(),
3
);
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6); // muted claire is not counted
// unmute claire again
set_muted(&t, claire.id, MuteDuration::NotMuted)
.await
.unwrap();
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await, 3);
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 9); // claire is counted again
}
@@ -696,7 +705,7 @@ mod tests {
let t = TestContext::new_alice().await;
let bob = t.create_chat_with_contact("", "bob@g.it").await;
receive_msg(&t, &bob).await;
assert_eq!(get_chat_msgs(&t, bob.id, 0, None).await.len(), 1);
assert_eq!(get_chat_msgs(&t, bob.id, 0, None).await.unwrap().len(), 1);
// chat is unmuted by default, here and in the following assert(),
// we check mainly that the SQL-statements in is_muted() and get_fresh_msgs()
@@ -720,8 +729,9 @@ mod tests {
// we need to modify the database directly
t.sql
.execute(
"UPDATE chats SET muted_until=? WHERE id=?;",
paramsv![time() - 3600, bob.id],
sqlx::query("UPDATE chats SET muted_until=? WHERE id=?;")
.bind(time() - 3600)
.bind(bob.id),
)
.await
.unwrap();
@@ -738,10 +748,7 @@ mod tests {
// to test get_fresh_msgs() with invalid mute_until (everything < -1),
// that results in "muted forever" by definition.
t.sql
.execute(
"UPDATE chats SET muted_until=-2 WHERE id=?;",
paramsv![bob.id],
)
.execute(sqlx::query("UPDATE chats SET muted_until=-2 WHERE id=?;").bind(bob.id))
.await
.unwrap();
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
@@ -811,7 +818,7 @@ mod tests {
async fn test_get_info() {
let t = TestContext::new().await;
let info = t.get_info().await;
let info = t.get_info().await.unwrap();
assert!(info.get("database_dir").is_some());
}
@@ -851,7 +858,7 @@ mod tests {
"smtp_certificate_checks",
];
let t = TestContext::new().await;
let info = t.get_info().await;
let info = t.get_info().await.unwrap();
for key in Config::iter() {
let key: String = key.to_string();
if !skip_from_get_info.contains(&&*key)

File diff suppressed because it is too large Load Diff

View File

@@ -632,14 +632,6 @@ impl FromStr for EmailAddress {
}
}
impl rusqlite::types::ToSql for EmailAddress {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let val = rusqlite::types::Value::Text(self.to_string());
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
}
/// Makes sure that a user input that is not supposed to contain newlines does not contain newlines.
pub(crate) fn improve_single_line_input(input: impl AsRef<str>) -> String {
input
@@ -1053,7 +1045,9 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await;
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
assert_eq!(msgs.len(), 1);
// the message should be added only once a day - test that an hour later and nearly a day later
@@ -1063,7 +1057,9 @@ mod tests {
get_provider_update_timestamp(),
)
.await;
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await;
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
assert_eq!(msgs.len(), 1);
maybe_warn_on_bad_time(
@@ -1072,7 +1068,9 @@ mod tests {
get_provider_update_timestamp(),
)
.await;
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await;
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
assert_eq!(msgs.len(), 1);
// next day, there should be another device message
@@ -1085,7 +1083,9 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
assert_eq!(device_chat_id, chats.get_chat_id(0));
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await;
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
assert_eq!(msgs.len(), 2);
}
@@ -1115,7 +1115,9 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await;
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
assert_eq!(msgs.len(), 1);
// do not repeat the warning every day ...
@@ -1135,7 +1137,9 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await;
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
let test_len = msgs.len();
assert!(test_len == 1 || test_len == 2);
@@ -1150,7 +1154,9 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await;
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
assert_eq!(msgs.len(), test_len + 1);
}
}

View File

@@ -26,9 +26,9 @@ pub struct EncryptHelper {
impl EncryptHelper {
pub async fn new(context: &Context) -> Result<EncryptHelper> {
let prefer_encrypt =
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await)
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await?)
.unwrap_or_default();
let addr = match context.get_config(Config::ConfiguredAddr).await {
let addr = match context.get_config(Config::ConfiguredAddr).await? {
None => {
bail!("addr not configured!");
}
@@ -329,7 +329,7 @@ fn contains_report(mail: &ParsedMail<'_>) -> bool {
pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await
.await?
.ok_or_else(|| {
format_err!(concat!(
"Failed to get self address, ",

View File

@@ -61,9 +61,10 @@ use std::num::ParseIntError;
use std::str::FromStr;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use anyhow::{ensure, Error};
use anyhow::{ensure, Context as _, Error};
use async_std::task;
use serde::{Deserialize, Serialize};
use sqlx::Row;
use crate::chat::{lookup_by_contact_id, send_msg, ChatId};
use crate::constants::{
@@ -120,28 +121,41 @@ impl FromStr for Timer {
}
}
impl rusqlite::types::ToSql for Timer {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let val = rusqlite::types::Value::Integer(match self {
Self::Disabled => 0,
Self::Enabled { duration } => i64::from(*duration),
});
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
impl sqlx::Type<sqlx::Sqlite> for Timer {
fn type_info() -> sqlx::sqlite::SqliteTypeInfo {
<i64 as sqlx::Type<_>>::type_info()
}
fn compatible(ty: &sqlx::sqlite::SqliteTypeInfo) -> bool {
<i64 as sqlx::Type<_>>::compatible(ty)
}
}
impl rusqlite::types::FromSql for Timer {
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
i64::column_result(value).and_then(|value| {
if value == 0 {
Ok(Self::Disabled)
} else if let Ok(duration) = u32::try_from(value) {
Ok(Self::Enabled { duration })
} else {
Err(rusqlite::types::FromSqlError::OutOfRange(value))
}
})
impl<'q> sqlx::Encode<'q, sqlx::Sqlite> for Timer {
fn encode_by_ref(
&self,
args: &mut Vec<sqlx::sqlite::SqliteArgumentValue<'q>>,
) -> sqlx::encode::IsNull {
args.push(sqlx::sqlite::SqliteArgumentValue::Int64(
self.to_u32() as i64
));
sqlx::encode::IsNull::No
}
}
impl<'r> sqlx::Decode<'r, sqlx::Sqlite> for Timer {
fn decode(value: sqlx::sqlite::SqliteValueRef<'r>) -> Result<Self, sqlx::error::BoxDynError> {
let value: i64 = sqlx::Decode::decode(value)?;
if value == 0 {
Ok(Self::Disabled)
} else if let Ok(duration) = u32::try_from(value) {
Ok(Self::Enabled { duration })
} else {
Err(Box::new(sqlx::Error::Decode(Box::new(
crate::error::OutOfRangeError,
))))
}
}
}
@@ -150,9 +164,8 @@ impl ChatId {
pub async fn get_ephemeral_timer(self, context: &Context) -> Result<Timer, Error> {
let timer = context
.sql
.query_get_value_result(
"SELECT ephemeral_timer FROM chats WHERE id=?;",
paramsv![self],
.query_get_value(
sqlx::query("SELECT ephemeral_timer FROM chats WHERE id=?;").bind(self),
)
.await?;
Ok(timer.unwrap_or_default())
@@ -172,10 +185,13 @@ impl ChatId {
context
.sql
.execute(
"UPDATE chats
sqlx::query(
"UPDATE chats
SET ephemeral_timer=?
WHERE id=?;",
paramsv![timer, self],
)
.bind(timer)
.bind(self),
)
.await?;
@@ -214,44 +230,45 @@ pub(crate) async fn stock_ephemeral_timer_changed(
from_id: u32,
) -> String {
match timer {
Timer::Disabled => stock_str::msg_ephemeral_timer_disabled(context, from_id).await,
Timer::Disabled => stock_str::msg_ephemeral_timer_disabled(context, from_id as u32).await,
Timer::Enabled { duration } => match duration {
0..=59 => {
stock_str::msg_ephemeral_timer_enabled(context, timer.to_string(), from_id).await
stock_str::msg_ephemeral_timer_enabled(context, timer.to_string(), from_id as u32)
.await
}
60 => stock_str::msg_ephemeral_timer_minute(context, from_id).await,
60 => stock_str::msg_ephemeral_timer_minute(context, from_id as u32).await,
61..=3599 => {
stock_str::msg_ephemeral_timer_minutes(
context,
format!("{}", (f64::from(duration) / 6.0).round() / 10.0),
from_id,
from_id as u32,
)
.await
}
3600 => stock_str::msg_ephemeral_timer_hour(context, from_id).await,
3600 => stock_str::msg_ephemeral_timer_hour(context, from_id as u32).await,
3601..=86399 => {
stock_str::msg_ephemeral_timer_hours(
context,
format!("{}", (f64::from(duration) / 360.0).round() / 10.0),
from_id,
from_id as u32,
)
.await
}
86400 => stock_str::msg_ephemeral_timer_day(context, from_id).await,
86400 => stock_str::msg_ephemeral_timer_day(context, from_id as u32).await,
86401..=604_799 => {
stock_str::msg_ephemeral_timer_days(
context,
format!("{}", (f64::from(duration) / 8640.0).round() / 10.0),
from_id,
from_id as u32,
)
.await
}
604_800 => stock_str::msg_ephemeral_timer_week(context, from_id).await,
604_800 => stock_str::msg_ephemeral_timer_week(context, from_id as u32).await,
_ => {
stock_str::msg_ephemeral_timer_weeks(
context,
format!("{}", (f64::from(duration) / 60480.0).round() / 10.0),
from_id,
from_id as u32,
)
.await
}
@@ -261,33 +278,38 @@ pub(crate) async fn stock_ephemeral_timer_changed(
impl MsgId {
/// Returns ephemeral message timer value for the message.
pub(crate) async fn ephemeral_timer(self, context: &Context) -> crate::sql::Result<Timer> {
pub(crate) async fn ephemeral_timer(self, context: &Context) -> anyhow::Result<Timer> {
let res = match context
.sql
.query_get_value_result(
"SELECT ephemeral_timer FROM msgs WHERE id=?",
paramsv![self],
.query_get_value::<_, i64>(
sqlx::query("SELECT ephemeral_timer FROM msgs WHERE id=?").bind(self),
)
.await?
{
None | Some(0) => Timer::Disabled,
Some(duration) => Timer::Enabled { duration },
Some(duration) => Timer::Enabled {
duration: u32::try_from(duration)?,
},
};
Ok(res)
}
/// Starts ephemeral message timer for the message if it is not started yet.
pub(crate) async fn start_ephemeral_timer(self, context: &Context) -> crate::sql::Result<()> {
pub(crate) async fn start_ephemeral_timer(self, context: &Context) -> anyhow::Result<()> {
if let Timer::Enabled { duration } = self.ephemeral_timer(context).await? {
let ephemeral_timestamp = time() + i64::from(duration);
context
.sql
.execute(
"UPDATE msgs SET ephemeral_timestamp = ? \
sqlx::query(
"UPDATE msgs SET ephemeral_timestamp = ? \
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?) \
AND id = ?",
paramsv![ephemeral_timestamp, ephemeral_timestamp, self],
)
.bind(ephemeral_timestamp)
.bind(ephemeral_timestamp)
.bind(self),
)
.await?;
schedule_ephemeral_task(context).await;
@@ -308,20 +330,29 @@ pub(crate) async fn delete_expired_messages(context: &Context) -> Result<bool, E
let mut updated = context
.sql
.execute(
// If you change which information is removed here, also change MsgId::trash() and
// which information dc_receive_imf::add_parts() still adds to the db if the chat_id is TRASH
"UPDATE msgs \
SET chat_id=?, txt='', subject='', txt_raw='', mime_headers='', from_id=0, to_id=0, param='' \
WHERE \
ephemeral_timestamp != 0 \
AND ephemeral_timestamp <= ? \
AND chat_id != ?",
paramsv![DC_CHAT_ID_TRASH, time(), DC_CHAT_ID_TRASH],
sqlx::query(
// If you change which information is removed here, also change MsgId::trash() and
// which information dc_receive_imf::add_parts() still adds to the db if the chat_id is TRASH
r#"
UPDATE msgs
SET
chat_id=?, txt='', subject='', txt_raw='',
mime_headers='', from_id=0, to_id=0, param=''
WHERE
ephemeral_timestamp != 0
AND ephemeral_timestamp <= ?
AND chat_id != ?
"#,
)
.bind(DC_CHAT_ID_TRASH)
.bind(time())
.bind(DC_CHAT_ID_TRASH),
)
.await?
.await
.context("update failed")?
> 0;
if let Some(delete_device_after) = context.get_config_delete_device_after().await {
if let Some(delete_device_after) = context.get_config_delete_device_after().await? {
let self_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
.await
.unwrap_or_default()
@@ -340,21 +371,22 @@ pub(crate) async fn delete_expired_messages(context: &Context) -> Result<bool, E
let rows_modified = context
.sql
.execute(
"UPDATE msgs \
sqlx::query(
"UPDATE msgs \
SET txt = 'DELETED', chat_id = ? \
WHERE timestamp < ? \
AND chat_id > ? \
AND chat_id != ? \
AND chat_id != ?",
paramsv![
DC_CHAT_ID_TRASH,
threshold_timestamp,
DC_CHAT_ID_LAST_SPECIAL,
self_chat_id,
device_chat_id
],
)
.bind(DC_CHAT_ID_TRASH)
.bind(threshold_timestamp)
.bind(DC_CHAT_ID_LAST_SPECIAL)
.bind(self_chat_id)
.bind(device_chat_id),
)
.await?;
.await
.context("deleted update failed")?;
updated |= rows_modified > 0;
}
@@ -376,14 +408,18 @@ pub(crate) async fn delete_expired_messages(context: &Context) -> Result<bool, E
pub async fn schedule_ephemeral_task(context: &Context) {
let ephemeral_timestamp: Option<i64> = match context
.sql
.query_get_value_result(
"SELECT ephemeral_timestamp \
FROM msgs \
WHERE ephemeral_timestamp != 0 \
AND chat_id != ? \
ORDER BY ephemeral_timestamp ASC \
LIMIT 1",
paramsv![DC_CHAT_ID_TRASH], // Trash contains already deleted messages, skip them
.query_get_value(
sqlx::query(
r#"
SELECT ephemeral_timestamp
FROM msgs
WHERE ephemeral_timestamp != 0
AND chat_id != ?
ORDER BY ephemeral_timestamp ASC
LIMIT 1;
"#,
)
.bind(DC_CHAT_ID_TRASH), // Trash contains already deleted messages, skip them
)
.await
{
@@ -439,25 +475,34 @@ pub async fn schedule_ephemeral_task(context: &Context) {
pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> sql::Result<Option<MsgId>> {
let now = time();
let threshold_timestamp = match context.get_config_delete_server_after().await {
let threshold_timestamp = match context.get_config_delete_server_after().await? {
None => 0,
Some(delete_server_after) => now - delete_server_after,
};
context
let row = context
.sql
.query_row_optional(
"SELECT id FROM msgs \
.fetch_optional(
sqlx::query(
"SELECT id FROM msgs \
WHERE ( \
timestamp < ? \
OR (ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?) \
) \
AND server_uid != 0 \
LIMIT 1",
paramsv![threshold_timestamp, now],
|row| row.get::<_, MsgId>(0),
)
.bind(threshold_timestamp)
.bind(now),
)
.await
.await?;
if let Some(row) = row {
let msg_id = row.try_get(0)?;
Ok(Some(msg_id))
} else {
Ok(None)
}
}
/// Start ephemeral timers for seen messages if they are not started
@@ -473,17 +518,17 @@ pub(crate) async fn start_ephemeral_timers(context: &Context) -> sql::Result<()>
context
.sql
.execute(
"UPDATE msgs \
sqlx::query(
"UPDATE msgs \
SET ephemeral_timestamp = ? + ephemeral_timer \
WHERE ephemeral_timer > 0 \
AND ephemeral_timestamp = 0 \
AND state NOT IN (?, ?, ?)",
paramsv![
time(),
MessageState::InFresh,
MessageState::InNoticed,
MessageState::OutDraft
],
)
.bind(time())
.bind(MessageState::InFresh)
.bind(MessageState::InNoticed)
.bind(MessageState::OutDraft),
)
.await?;
@@ -717,7 +762,7 @@ mod tests {
}
async fn check_msg_was_deleted(t: &TestContext, chat: &Chat, msg_id: MsgId) {
let chat_items = chat::get_chat_msgs(t, chat.id, 0, None).await;
let chat_items = chat::get_chat_msgs(t, chat.id, 0, None).await.unwrap();
// Check that the chat is empty except for possibly info messages:
for item in &chat_items {
if let ChatItem::Message { msg_id } = item {
@@ -733,8 +778,9 @@ mod tests {
assert!(msg.text.is_none_or_empty(), msg.text);
let rawtxt: Option<String> = t
.sql
.query_get_value(t, "SELECT txt_raw FROM msgs WHERE id=?;", paramsv![msg_id])
.await;
.query_get_value(sqlx::query("SELECT txt_raw FROM msgs WHERE id=?;").bind(msg_id))
.await
.unwrap();
assert!(rawtxt.is_none_or_empty(), rawtxt);
}
}

View File

@@ -1,5 +1,9 @@
//! # Error handling
#[derive(Debug, thiserror::Error)]
#[error("Out of Range")]
pub struct OutOfRangeError;
#[macro_export]
macro_rules! ensure_eq {
($left:expr, $right:expr) => ({

View File

@@ -13,12 +13,12 @@ use std::pin::Pin;
use anyhow::Result;
use lettre_email::mime::{self, Mime};
use crate::context::Context;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::message::{Message, MsgId};
use crate::mimeparser::parse_message_id;
use crate::param::Param::SendHtml;
use crate::plaintext::PlainText;
use crate::{context::Context, message};
use lettre_email::PartBuilder;
use mailparse::ParsedContentType;
@@ -244,32 +244,20 @@ impl MsgId {
/// this is the case at least when `Message.has_html()` returns true
/// (we do not save raw mime unconditionally in the database to save space).
/// The corresponding ffi-function is `dc_get_msg_html()`.
pub async fn get_html(self, context: &Context) -> Option<String> {
let rawmime: Option<String> = context
.sql
.query_get_value(
context,
"SELECT mime_headers FROM msgs WHERE id=?;",
paramsv![self],
)
.await;
pub async fn get_html(self, context: &Context) -> Result<Option<String>> {
let rawmime = message::get_mime_headers(context, self).await?;
if let Some(rawmime) = rawmime {
if !rawmime.is_empty() {
match HtmlMsgParser::from_bytes(context, rawmime.as_bytes()).await {
Err(err) => {
warn!(context, "get_html: parser error: {}", err);
None
}
Ok(parser) => Some(parser.html),
if !rawmime.is_empty() {
match HtmlMsgParser::from_bytes(context, rawmime.as_bytes()).await {
Err(err) => {
warn!(context, "get_html: parser error: {}", err);
Ok(None)
}
} else {
warn!(context, "get_html: empty mime for {}", self);
None
Ok(parser) => Ok(Some(parser.html)),
}
} else {
warn!(context, "get_html: no mime for {}", self);
None
Ok(None)
}
}
}
@@ -439,7 +427,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
async fn test_get_html_empty() {
let t = TestContext::new().await;
let msg_id = MsgId::new_unset();
assert!(msg_id.get_html(&t).await.is_none())
assert!(msg_id.get_html(&t).await.unwrap().is_none())
}
#[async_std::test]
@@ -460,7 +448,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert!(!msg.is_forwarded());
assert!(msg.get_text().unwrap().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&alice).await.unwrap();
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
// alice: create chat with bob and forward received html-message there
@@ -474,7 +462,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert!(msg.is_forwarded());
assert!(msg.get_text().unwrap().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&alice).await.unwrap();
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
// bob: check that bob also got the html-part of the forwarded message
@@ -487,7 +475,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert!(msg.is_forwarded());
assert!(msg.get_text().unwrap().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&bob).await.unwrap();
let html = msg.get_id().get_html(&bob).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
}
@@ -517,7 +505,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
// receive the message on another device
let alice = TestContext::new_alice().await;
assert_eq!(alice.get_config_int(Config::ShowEmails).await, 0); // set to "1" above, make sure it is another db
assert_eq!(alice.get_config_int(Config::ShowEmails).await.unwrap(), 0); // set to "1" above, make sure it is another db
alice.recv_msg(&msg).await;
let chat = alice.get_self_chat().await;
let msg = alice.get_last_msg_in(chat.get_id()).await;
@@ -527,7 +515,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert!(msg.is_forwarded());
assert!(msg.get_text().unwrap().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&alice).await.unwrap();
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
}
@@ -549,7 +537,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert_eq!(msg.get_text(), Some("plain text".to_string()));
assert!(!msg.is_forwarded());
assert!(msg.mime_modified);
let html = msg.get_id().get_html(&alice).await.unwrap();
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
assert!(html.contains("<b>html</b> text"));
// let bob receive the message
@@ -559,7 +547,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert_eq!(msg.get_text(), Some("plain text".to_string()));
assert!(!msg.is_forwarded());
assert!(msg.mime_modified);
let html = msg.get_id().get_html(&bob).await.unwrap();
let html = msg.get_id().get_html(&bob).await.unwrap().unwrap();
assert!(html.contains("<b>html</b> text"));
}
}

View File

@@ -229,7 +229,7 @@ impl Imap {
let addr: &str = config.addr.as_ref();
if let Some(token) =
dc_get_oauth2_access_token(context, addr, imap_pw, true).await
dc_get_oauth2_access_token(context, addr, imap_pw, true).await?
{
let auth = OAuth2 {
user: imap_user.into(),
@@ -267,7 +267,7 @@ impl Imap {
let lock = context.wrong_pw_warning_mutex.lock().await;
if self.login_failed_once
&& context.get_config_bool(Config::NotifyAboutWrongPw).await
&& context.get_config_bool(Config::NotifyAboutWrongPw).await?
{
if let Err(e) = context.set_config(Config::NotifyAboutWrongPw, None).await {
warn!(context, "{}", e);
@@ -339,11 +339,11 @@ impl Imap {
if self.is_connected() && !self.should_reconnect() {
return Ok(());
}
if !context.is_configured().await {
if !context.is_configured().await? {
bail!("IMAP Connect without configured params");
}
let param = LoginParam::from_database(context, "configured_").await;
let param = LoginParam::from_database(context, "configured_").await?;
// the trailing underscore is correct
if let Err(err) = self
@@ -521,24 +521,29 @@ impl Imap {
// Write collected UIDs to SQLite database.
context
.sql
.with_conn(move |mut conn| {
let conn2 = &mut conn;
let tx = conn2.transaction()?;
tx.execute(
"UPDATE msgs SET server_uid=0 WHERE server_folder=?",
params![folder],
)?;
for (uid, rfc724_mid) in &msg_ids {
// This may detect previously undetected moved
// messages, so we update server_folder too.
tx.execute(
"UPDATE msgs \
SET server_folder=?,server_uid=? WHERE rfc724_mid=?",
params![folder, uid, rfc724_mid],
)?;
}
tx.commit()?;
Ok(())
.transaction(|conn| {
Box::pin(async move {
sqlx::query("UPDATE msgs SET server_uid=0 WHERE server_folder=?")
.bind(&folder)
.execute(&mut *conn)
.await?;
for (uid, rfc724_mid) in &msg_ids {
// This may detect previously undetected moved
// messages, so we update server_folder too.
sqlx::query(
"UPDATE msgs \
SET server_folder=?,server_uid=? WHERE rfc724_mid=?",
)
.bind(&folder)
.bind(uid)
.bind(rfc724_mid)
.execute(&mut *conn)
.await?;
}
Ok(())
})
})
.await?;
Ok(())
@@ -655,7 +660,7 @@ impl Imap {
folder: S,
fetch_existing_msgs: bool,
) -> Result<bool> {
let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await)
let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?)
.unwrap_or_default();
let new_emails = self
@@ -754,7 +759,7 @@ impl Imap {
let session = self.session.as_mut().unwrap();
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await
.await?
.ok_or_else(|| format_err!("Not configured"))?;
let search_command = format!("FROM \"{}\"", self_addr);
@@ -1276,10 +1281,7 @@ impl Imap {
context: &Context,
create_mvbox: bool,
) -> Result<()> {
let folders_configured = context
.sql
.get_raw_config_int(context, "folders_configured")
.await;
let folders_configured = context.sql.get_raw_config_int("folders_configured").await?;
if folders_configured.unwrap_or_default() >= DC_FOLDERS_CONFIGURED_VERSION {
return Ok(());
}
@@ -1407,7 +1409,7 @@ impl Imap {
}
context
.sql
.set_raw_config_int(context, "folders_configured", DC_FOLDERS_CONFIGURED_VERSION)
.set_raw_config_int("folders_configured", DC_FOLDERS_CONFIGURED_VERSION)
.await?;
}
info!(context, "FINISHED configuring IMAP-folders.");
@@ -1519,7 +1521,7 @@ async fn precheck_imf(
"[move] detected bcc-self {} as {}/{}", rfc724_mid, server_folder, server_uid
);
let delete_server_after = context.get_config_delete_server_after().await;
let delete_server_after = context.get_config_delete_server_after().await?;
if delete_server_after != Some(0) {
if msg_id
@@ -1729,9 +1731,15 @@ pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32)
context
.sql
.execute(
"INSERT INTO imap_sync (folder, uidvalidity, uid_next) VALUES (?,?,?)
sqlx::query(
"INSERT INTO imap_sync (folder, uidvalidity, uid_next) VALUES (?,?,?)
ON CONFLICT(folder) DO UPDATE SET uid_next=? WHERE folder=?;",
paramsv![folder, 0u32, uid_next, uid_next, folder],
)
.bind(folder)
.bind(0i32)
.bind(uid_next as i64)
.bind(uid_next as i64)
.bind(folder),
)
.await?;
Ok(())
@@ -1745,10 +1753,7 @@ pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32)
async fn get_uid_next(context: &Context, folder: &str) -> Result<u32> {
Ok(context
.sql
.query_get_value_result(
"SELECT uid_next FROM imap_sync WHERE folder=?;",
paramsv![folder],
)
.query_get_value(sqlx::query("SELECT uid_next FROM imap_sync WHERE folder=?;").bind(folder))
.await?
.unwrap_or(0))
}
@@ -1761,9 +1766,15 @@ pub(crate) async fn set_uidvalidity(
context
.sql
.execute(
"INSERT INTO imap_sync (folder, uidvalidity, uid_next) VALUES (?,?,?)
sqlx::query(
"INSERT INTO imap_sync (folder, uidvalidity, uid_next) VALUES (?,?,?)
ON CONFLICT(folder) DO UPDATE SET uidvalidity=? WHERE folder=?;",
paramsv![folder, uidvalidity, 0u32, uidvalidity, folder],
)
.bind(folder)
.bind(uidvalidity as i32)
.bind(0i32)
.bind(uidvalidity as i32)
.bind(folder),
)
.await?;
Ok(())
@@ -1772,26 +1783,28 @@ pub(crate) async fn set_uidvalidity(
async fn get_uidvalidity(context: &Context, folder: &str) -> Result<u32> {
Ok(context
.sql
.query_get_value_result(
"SELECT uidvalidity FROM imap_sync WHERE folder=?;",
paramsv![folder],
.query_get_value(
sqlx::query("SELECT uidvalidity FROM imap_sync WHERE folder=?;").bind(folder),
)
.await?
.unwrap_or(0))
}
/// Deprecated, use get_uid_next() and get_uidvalidity()
pub async fn get_config_last_seen_uid<S: AsRef<str>>(context: &Context, folder: S) -> (u32, u32) {
pub async fn get_config_last_seen_uid<S: AsRef<str>>(
context: &Context,
folder: S,
) -> Result<(u32, u32)> {
let key = format!("imap.mailbox.{}", folder.as_ref());
if let Some(entry) = context.sql.get_raw_config(context, &key).await {
if let Some(entry) = context.sql.get_raw_config(&key).await? {
// the entry has the format `imap.mailbox.<folder>=<uidvalidity>:<lastseenuid>`
let mut parts = entry.split(':');
(
Ok((
parts.next().unwrap_or_default().parse().unwrap_or(0),
parts.next().unwrap_or_default().parse().unwrap_or(0),
)
))
} else {
(0, 0)
Ok((0, 0))
}
}

View File

@@ -17,7 +17,7 @@ impl Imap {
let elapsed_secs = last_scan.elapsed().as_secs();
let debounce_secs = context
.get_config_u64(Config::ScanAllFoldersDebounceSecs)
.await;
.await?;
if elapsed_secs < debounce_secs {
return Ok(());
@@ -95,8 +95,8 @@ async fn get_watched_folders(context: &Context) -> Vec<String> {
(Config::InboxWatch, Config::ConfiguredInboxFolder),
];
for (watched, configured) in folder_watched_configured {
if context.get_config_bool(*watched).await {
if let Some(folder) = context.get_config(*configured).await {
if context.get_config_bool(*watched).await.unwrap_or_default() {
if let Ok(Some(folder)) = context.get_config(*configured).await {
res.push(folder);
}
}

View File

@@ -10,6 +10,7 @@ use async_std::{
prelude::*,
};
use rand::{thread_rng, Rng};
use sqlx::Row;
use crate::chat;
use crate::chat::delete_and_reset_all_device_msgs;
@@ -38,7 +39,7 @@ const DBFILE_BACKUP_NAME: &str = "dc_database_backup.sqlite";
const BLOBS_BACKUP_NAME: &str = "blobs_backup";
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(i32)]
#[repr(u32)]
pub enum ImexMode {
/// Export all private keys and all public keys of the user to the
/// directory given as `param1`. The default key is written to the files `public-key-default.asc`
@@ -170,8 +171,8 @@ pub async fn has_backup_old(context: &Context, dir_name: impl AsRef<Path>) -> Re
match sql.open(context, &path, true).await {
Ok(_) => {
let curr_backup_time = sql
.get_raw_config_int(context, "backup_time")
.await
.get_raw_config_int("backup_time")
.await?
.unwrap_or_default();
if curr_backup_time > newest_backup_time {
newest_backup_path = Some(path);
@@ -271,7 +272,7 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
bail!("Passphrase must be at least 2 chars long.");
};
let private_key = SignedSecretKey::load_self(context).await?;
let ac_headers = match context.get_config_bool(Config::E2eeEnabled).await {
let ac_headers = match context.get_config_bool(Config::E2eeEnabled).await? {
false => None,
true => Some(("Autocrypt-Prefer-Encrypt", "mutual")),
};
@@ -333,7 +334,7 @@ pub fn create_setup_code(_context: &Context) -> String {
}
async fn maybe_add_bcc_self_device_msg(context: &Context) -> Result<()> {
if !context.sql.get_raw_config_bool(context, "bcc_self").await {
if !context.sql.get_raw_config_bool("bcc_self").await? {
let mut msg = Message::new(Viewtype::Text);
// TODO: define this as a stockstring once the wording is settled.
msg.text = Some(
@@ -394,7 +395,7 @@ async fn set_self_key(
};
context
.sql
.set_raw_config_int(context, "e2ee_enabled", e2ee_enabled)
.set_raw_config_int("e2ee_enabled", e2ee_enabled)
.await?;
}
None => {
@@ -404,7 +405,7 @@ async fn set_self_key(
}
};
let self_addr = context.get_config(Config::ConfiguredAddr).await;
let self_addr = context.get_config(Config::ConfiguredAddr).await?;
ensure!(self_addr.is_some(), "Missing self addr");
let addr = EmailAddress::new(&self_addr.unwrap_or_default())?;
let keypair = pgp::KeyPair {
@@ -493,7 +494,7 @@ async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) ->
);
ensure!(
!context.is_configured().await,
!context.is_configured().await?,
"Cannot import backups to accounts in use."
);
ensure!(
@@ -564,7 +565,7 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef<Path>
);
ensure!(
!context.is_configured().await,
!context.is_configured().await?,
"Cannot import backups to accounts in use."
);
ensure!(
@@ -594,9 +595,9 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef<Path>
let total_files_cnt = context
.sql
.query_get_value::<isize>(context, "SELECT COUNT(*) FROM backup_blobs;", paramsv![])
.await
.unwrap_or_default() as usize;
.count("SELECT COUNT(*) FROM backup_blobs;")
.await?;
info!(
context,
"***IMPORT-in-progress: total_files_cnt={:?}", total_files_cnt,
@@ -606,29 +607,25 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef<Path>
// consuming too much memory.
let file_ids = context
.sql
.query_map(
"SELECT id FROM backup_blobs ORDER BY id",
paramsv![],
|row| row.get(0),
|ids| {
ids.collect::<std::result::Result<Vec<i64>, _>>()
.map_err(Into::into)
},
)
.fetch("SELECT id FROM backup_blobs ORDER BY id")
.await?
.map(|row| row?.try_get(0))
.collect::<sqlx::Result<Vec<i64>>>()
.await?;
let mut all_files_extracted = true;
for (processed_files_cnt, file_id) in file_ids.into_iter().enumerate() {
// Load a single blob into memory
let (file_name, file_blob) = context
let row = context
.sql
.query_row(
"SELECT file_name, file_content FROM backup_blobs WHERE id = ?",
paramsv![file_id],
|row| Ok((row.get::<_, String>(0)?, row.get::<_, Vec<u8>>(1)?)),
.fetch_one(
sqlx::query("SELECT file_name, file_content FROM backup_blobs WHERE id = ?")
.bind(file_id),
)
.await?;
let file_name: String = row.try_get(0)?;
let file_blob: &[u8] = row.try_get(1)?;
if context.shall_stop_ongoing().await {
all_files_extracted = false;
break;
@@ -646,16 +643,13 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef<Path>
}
let path_filename = context.get_blobdir().join(file_name);
dc_write_file(context, &path_filename, &file_blob).await?;
dc_write_file(context, &path_filename, file_blob).await?;
}
if all_files_extracted {
// only delete backup_blobs if all files were successfully extracted
context
.sql
.execute("DROP TABLE backup_blobs;", paramsv![])
.await?;
context.sql.execute("VACUUM;", paramsv![]).await.ok();
context.sql.execute("DROP TABLE backup_blobs;").await?;
context.sql.execute("VACUUM;").await.ok();
Ok(())
} else {
bail!("received stop signal");
@@ -674,13 +668,13 @@ async fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
context
.sql
.set_raw_config_int(context, "backup_time", now as i32)
.set_raw_config_int("backup_time", now as i32)
.await?;
sql::housekeeping(context).await.ok_or_log(context);
context
.sql
.execute("VACUUM;", paramsv![])
.execute("VACUUM;")
.await
.map_err(|e| warn!(context, "Vacuum failed, exporting anyway {}", e));
@@ -833,29 +827,24 @@ async fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()
async fn export_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
let mut export_errors = 0;
let keys = context
let mut keys = context
.sql
.query_map(
"SELECT id, public_key, private_key, is_default FROM keypairs;",
paramsv![],
|row| {
let id = row.get(0)?;
let public_key_blob: Vec<u8> = row.get(1)?;
let public_key = SignedPublicKey::from_slice(&public_key_blob);
let private_key_blob: Vec<u8> = row.get(2)?;
let private_key = SignedSecretKey::from_slice(&private_key_blob);
let is_default: i32 = row.get(3)?;
.fetch("SELECT id, public_key, private_key, is_default FROM keypairs;")
.await?
.map(|row| -> sqlx::Result<_> {
let row = row?;
let id = row.try_get(0)?;
let public_key_blob: &[u8] = row.try_get(1)?;
let public_key = SignedPublicKey::from_slice(public_key_blob);
let private_key_blob: &[u8] = row.try_get(2)?;
let private_key = SignedSecretKey::from_slice(private_key_blob);
let is_default: i32 = row.try_get(3)?;
Ok((id, public_key, private_key, is_default))
},
|keys| {
keys.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await?;
Ok((id, public_key, private_key, is_default))
});
for (id, public_key, private_key, is_default) in keys {
while let Some(parts) = keys.next().await {
let (id, public_key, private_key, is_default) = parts?;
let id = Some(id).filter(|_| is_default != 0);
if let Ok(key) = public_key {
if export_key_to_asc_file(context, &dir, id, &key)

View File

@@ -7,10 +7,11 @@ use std::{fmt, time::Duration};
use anyhow::{bail, ensure, format_err, Context as _, Error, Result};
use async_smtp::smtp::response::{Category, Code, Detail};
use async_std::prelude::*;
use async_std::task::sleep;
use deltachat_derive::{FromSql, ToSql};
use itertools::Itertools;
use rand::{thread_rng, Rng};
use sqlx::Row;
use crate::dc_tools::{dc_delete_file, dc_read_file, time};
use crate::ephemeral::load_imap_deletion_msgid;
@@ -36,10 +37,8 @@ use crate::{scheduler::InterruptInfo, sql};
const JOB_RETRIES: u32 = 17;
/// Thread IDs
#[derive(
Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
)]
#[repr(i32)]
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, sqlx::Type)]
#[repr(u32)]
pub(crate) enum Thread {
Unknown = 0,
Imap = 100,
@@ -76,19 +75,9 @@ impl Default for Thread {
}
#[derive(
Debug,
Display,
Copy,
Clone,
PartialEq,
Eq,
PartialOrd,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Debug, Display, Copy, Clone, PartialEq, Eq, PartialOrd, FromPrimitive, ToPrimitive, sqlx::Type,
)]
#[repr(i32)]
#[repr(u32)]
pub enum Action {
Unknown = 0,
@@ -184,7 +173,7 @@ impl Job {
if self.job_id != 0 {
context
.sql
.execute("DELETE FROM jobs WHERE id=?;", paramsv![self.job_id as i32])
.execute(sqlx::query("DELETE FROM jobs WHERE id=?;").bind(self.job_id as i32))
.await?;
}
@@ -203,26 +192,24 @@ impl Job {
context
.sql
.execute(
"UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;",
paramsv![
self.desired_timestamp,
self.tries as i64,
self.param.to_string(),
self.job_id as i32,
],
sqlx::query(
"UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;",
)
.bind(self.desired_timestamp)
.bind(self.tries as i64)
.bind(self.param.to_string())
.bind(self.job_id as i32),
)
.await?;
} else {
context.sql.execute(
"INSERT INTO jobs (added_timestamp, thread, action, foreign_id, param, desired_timestamp) VALUES (?,?,?,?,?,?);",
paramsv![
self.added_timestamp,
thread,
self.action,
self.foreign_id,
self.param.to_string(),
self.desired_timestamp
]
sqlx::query("INSERT INTO jobs (added_timestamp, thread, action, foreign_id, param, desired_timestamp) VALUES (?,?,?,?,?,?);")
.bind(self.added_timestamp)
.bind(thread)
.bind(self.action)
.bind(self.foreign_id)
.bind(self.param.to_string())
.bind(self.desired_timestamp)
).await?;
}
@@ -253,7 +240,7 @@ impl Job {
let status = match smtp.send(context, recipients, message, job_id).await {
Err(crate::smtp::send::Error::SendError(err)) => {
// Remote error, retry later.
warn!(context, "SMTP failed to send: {}", err);
warn!(context, "SMTP failed to send: {:?}", err);
self.pending_error = Some(err.to_string());
let res = match err {
@@ -339,6 +326,12 @@ impl Job {
error!(context, "SMTP job failed because SMTP has no transport");
Status::Finished(Err(format_err!("SMTP has not transport")))
}
Err(crate::smtp::send::Error::Other(err)) => {
// Local error, job is invalid, do not retry.
smtp.disconnect().await;
warn!(context, "unable to load job: {}", err);
Status::Finished(Err(err))
}
Ok(()) => {
job_try!(success_cb().await);
Status::Finished(Ok(()))
@@ -387,11 +380,21 @@ impl Job {
/* if there is a msg-id and it does not exist in the db, cancel sending.
this happends if dc_delete_msgs() was called
before the generated mime was sent out */
if 0 != self.foreign_id && !message::exists(context, MsgId::new(self.foreign_id)).await {
return Status::Finished(Err(format_err!(
"Not sending Message {} as it was deleted",
self.foreign_id
)));
if 0 != self.foreign_id {
match message::exists(context, MsgId::new(self.foreign_id)).await {
Ok(exists) => {
if !exists {
return Status::Finished(Err(format_err!(
"Not sending Message {} as it was deleted",
self.foreign_id
)));
}
}
Err(err) => {
warn!(context, "failed to check message existence: {:?}", err);
return Status::RetryLater;
}
}
};
let foreign_id = self.foreign_id;
@@ -399,7 +402,7 @@ impl Job {
async move {
// smtp success, update db ASAP, then delete smtp file
if 0 != foreign_id {
set_delivered(context, MsgId::new(foreign_id)).await;
set_delivered(context, MsgId::new(foreign_id)).await?;
}
// now also delete the generated file
dc_delete_file(context, filename).await;
@@ -416,44 +419,38 @@ impl Job {
contact_id: u32,
) -> sql::Result<(Vec<u32>, Vec<String>)> {
// Extract message IDs from job parameters
let res: Vec<(u32, MsgId)> = context
let mut rows = context
.sql
.query_map(
"SELECT id, param FROM jobs WHERE foreign_id=? AND id!=?",
paramsv![contact_id, self.job_id],
|row| {
let job_id: u32 = row.get(0)?;
let params_str: String = row.get(1)?;
let params: Params = params_str.parse().unwrap_or_default();
Ok((job_id, params))
},
|jobs| {
let res = jobs
.filter_map(|row| {
let (job_id, params) = row.ok()?;
let msg_id = params.get_msg_id()?;
Some((job_id, msg_id))
})
.collect();
Ok(res)
},
.fetch(
sqlx::query("SELECT id, param FROM jobs WHERE foreign_id=? AND id!=?")
.bind(contact_id)
.bind(self.job_id),
)
.await?;
// Load corresponding RFC724 message IDs
let mut job_ids = Vec::new();
let mut rfc724_mids = Vec::new();
for (job_id, msg_id) in res {
if let Ok(Message { rfc724_mid, .. }) = Message::load_from_db(context, msg_id).await {
job_ids.push(job_id);
rfc724_mids.push(rfc724_mid);
while let Some(row) = rows.next().await {
let row = row?;
let job_id: u32 = row.try_get(0)?;
let params_str: String = row.try_get(1)?;
let params: Params = params_str.parse().unwrap_or_default();
if let Some(msg_id) = params.get_msg_id() {
if let Ok(Message { rfc724_mid, .. }) = Message::load_from_db(context, msg_id).await
{
job_ids.push(job_id);
rfc724_mids.push(rfc724_mid);
}
}
}
Ok((job_ids, rfc724_mids))
}
async fn send_mdn(&mut self, context: &Context, smtp: &mut Smtp) -> Status {
if !context.get_config_bool(Config::MdnsEnabled).await {
let mdns_enabled = job_try!(context.get_config_bool(Config::MdnsEnabled).await);
if !mdns_enabled {
// User has disabled MDNs after job scheduling but before
// execution.
return Status::Finished(Err(format_err!("MDNs are disabled")));
@@ -539,7 +536,13 @@ impl Job {
);
return Status::Finished(Ok(()));
}
Ok(Some(config)) => context.get_config(config).await,
Ok(Some(config)) => match context.get_config(config).await {
Ok(folder) => folder,
Err(err) => {
warn!(context, "failed to load config: {}", err);
return Status::RetryLater;
}
},
};
if let Some(dest_folder) = dest_folder {
@@ -657,7 +660,7 @@ impl Job {
/// Then, Fetch the last messages DC_FETCH_EXISTING_MSGS_COUNT emails from the server
/// and show them in the chat list.
async fn fetch_existing_msgs(&mut self, context: &Context, imap: &mut Imap) -> Status {
if context.get_config_bool(Config::Bot).await {
if job_try!(context.get_config_bool(Config::Bot).await) {
return Status::Finished(Ok(())); // Bots don't want those messages
}
if let Err(err) = imap.connect_configured(context).await {
@@ -669,13 +672,13 @@ impl Job {
add_all_recipients_as_contacts(context, imap, Config::ConfiguredMvboxFolder).await;
add_all_recipients_as_contacts(context, imap, Config::ConfiguredInboxFolder).await;
if context.get_config_bool(Config::FetchExistingMsgs).await {
if job_try!(context.get_config_bool(Config::FetchExistingMsgs).await) {
for config in &[
Config::ConfiguredMvboxFolder,
Config::ConfiguredInboxFolder,
Config::ConfiguredSentboxFolder,
] {
if let Some(folder) = context.get_config(*config).await {
if let Some(folder) = job_try!(context.get_config(*config).await) {
if let Err(e) = imap.fetch_new_messages(context, folder, true).await {
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
warn!(context, "Could not fetch messages, retrying: {:#}", e);
@@ -688,7 +691,7 @@ impl Job {
// Make sure that if there now is a chat with a contact (created by an outgoing
// message), then group contact requests from this contact should also be unblocked.
// See https://github.com/deltachat/deltachat-core-rust/issues/2097.
for item in chat::get_chat_msgs(context, ChatId::new(DC_CHAT_ID_DEADDROP), 0, None).await {
for item in job_try!(chat::get_chat_msgs(context, DC_CHAT_ID_DEADDROP, 0, None).await) {
if let ChatItem::Message { msg_id } = item {
let msg = match Message::load_from_db(context, msg_id).await {
Err(e) => {
@@ -736,26 +739,21 @@ impl Job {
return Status::RetryLater;
}
if let Some(sentbox_folder) = &context.get_config(Config::ConfiguredSentboxFolder).await {
job_try!(
imap.resync_folder_uids(context, sentbox_folder.to_string())
.await
);
let sentbox_folder = job_try!(context.get_config(Config::ConfiguredSentboxFolder).await);
if let Some(sentbox_folder) = sentbox_folder {
job_try!(imap.resync_folder_uids(context, sentbox_folder).await);
}
if let Some(inbox_folder) = &context.get_config(Config::ConfiguredInboxFolder).await {
job_try!(
imap.resync_folder_uids(context, inbox_folder.to_string())
.await
);
let inbox_folder = job_try!(context.get_config(Config::ConfiguredInboxFolder).await);
if let Some(inbox_folder) = inbox_folder {
job_try!(imap.resync_folder_uids(context, inbox_folder).await);
}
if let Some(mvbox_folder) = &context.get_config(Config::ConfiguredMvboxFolder).await {
job_try!(
imap.resync_folder_uids(context, mvbox_folder.to_string())
.await
);
let mvbox_folder = job_try!(context.get_config(Config::ConfiguredMvboxFolder).await);
if let Some(mvbox_folder) = mvbox_folder {
job_try!(imap.resync_folder_uids(context, mvbox_folder).await);
}
Status::Finished(Ok(()))
}
@@ -803,11 +801,13 @@ impl Job {
// the name sent in the From field by the user.
if msg.param.get_bool(Param::WantsMdn).unwrap_or_default()
&& !msg.is_system_message()
&& context.get_config_bool(Config::MdnsEnabled).await
{
if let Err(err) = send_mdn(context, &msg).await {
warn!(context, "could not send out mdn for {}: {}", msg.id, err);
return Status::Finished(Err(err));
let mdns_enabled = job_try!(context.get_config_bool(Config::MdnsEnabled).await);
if mdns_enabled {
if let Err(err) = send_mdn(context, &msg).await {
warn!(context, "could not send out mdn for {}: {}", msg.id, err);
return Status::Finished(Err(err));
}
}
}
Status::Finished(Ok(()))
@@ -820,50 +820,46 @@ impl Job {
pub async fn kill_action(context: &Context, action: Action) -> bool {
context
.sql
.execute("DELETE FROM jobs WHERE action=?;", paramsv![action])
.execute(sqlx::query("DELETE FROM jobs WHERE action=?;").bind(action))
.await
.is_ok()
}
/// Remove jobs with specified IDs.
async fn kill_ids(context: &Context, job_ids: &[u32]) -> sql::Result<()> {
context
.sql
.execute(
format!(
"DELETE FROM jobs WHERE id IN({})",
job_ids.iter().map(|_| "?").join(",")
),
job_ids.iter().map(|i| i as &dyn crate::ToSql).collect(),
)
.await?;
let q = format!(
"DELETE FROM jobs WHERE id IN({})",
job_ids.iter().map(|_| "?").join(",")
);
let mut query = sqlx::query(&q);
for id in job_ids {
query = query.bind(*id);
}
context.sql.execute(query).await?;
Ok(())
}
pub async fn action_exists(context: &Context, action: Action) -> bool {
context
.sql
.exists("SELECT id FROM jobs WHERE action=?;", paramsv![action])
.exists(sqlx::query("SELECT COUNT(*) FROM jobs WHERE action=?;").bind(action))
.await
.unwrap_or_default()
}
async fn set_delivered(context: &Context, msg_id: MsgId) {
async fn set_delivered(context: &Context, msg_id: MsgId) -> Result<()> {
message::update_msg_state(context, msg_id, MessageState::OutDelivered).await;
let chat_id: ChatId = context
.sql
.query_get_value(
context,
"SELECT chat_id FROM msgs WHERE id=?",
paramsv![msg_id],
)
.await
.query_get_value(sqlx::query("SELECT chat_id FROM msgs WHERE id=?").bind(msg_id))
.await?
.unwrap_or_default();
context.emit_event(EventType::MsgDelivered { chat_id, msg_id });
Ok(())
}
async fn add_all_recipients_as_contacts(context: &Context, imap: &mut Imap, folder: Config) {
let mailbox = if let Some(m) = context.get_config(folder).await {
let mailbox = if let Ok(Some(m)) = context.get_config(folder).await {
m
} else {
return;
@@ -933,14 +929,14 @@ pub async fn send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<Job
let from = context
.get_config(Config::ConfiguredAddr)
.await
.await?
.unwrap_or_default();
let lowercase_from = from.to_lowercase();
// Send BCC to self if it is enabled and we are not going to
// delete it immediately.
if context.get_config_bool(Config::BccSelf).await
&& context.get_config_delete_server_after().await != Some(0)
if context.get_config_bool(Config::BccSelf).await?
&& context.get_config_delete_server_after().await? != Some(0)
&& !recipients
.iter()
.any(|x| x.to_lowercase() == lowercase_from)
@@ -954,7 +950,7 @@ pub async fn send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<Job
context,
"message {} has no recipient, skipping smtp-send", msg_id
);
set_delivered(context, msg_id).await;
set_delivered(context, msg_id).await?;
return Ok(None);
}
@@ -1022,7 +1018,7 @@ pub async fn send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<Job
msg.subject = rendered_msg.subject.clone();
msg.update_subject(context).await;
let job = create(Action::SendMsgToSmtp, msg_id.to_u32() as i32, param, 0)?;
let job = create(Action::SendMsgToSmtp, msg_id.to_u32(), param, 0)?;
Ok(Some(job))
}
@@ -1200,13 +1196,13 @@ pub(crate) async fn schedule_resync(context: &Context) {
}
/// Creates a job.
pub fn create(action: Action, foreign_id: i32, param: Params, delay_seconds: i64) -> Result<Job> {
pub fn create(action: Action, foreign_id: u32, param: Params, delay_seconds: i64) -> Result<Job> {
ensure!(
action != Action::Unknown,
"Invalid action passed to job_add"
);
Ok(Job::new(action, foreign_id as u32, param, delay_seconds))
Ok(Job::new(action, foreign_id, param, delay_seconds))
}
/// Adds a job to the database, scheduling it.
@@ -1245,7 +1241,13 @@ pub async fn add(context: &Context, job: Job) {
}
async fn load_housekeeping_job(context: &Context) -> Option<Job> {
let last_time = context.get_config_i64(Config::LastHousekeeping).await;
let last_time = match context.get_config_i64(Config::LastHousekeeping).await {
Ok(last_time) => last_time,
Err(err) => {
warn!(context, "failed to load housekeeping config: {:?}", err);
return None;
}
};
let next_time = last_time + (60 * 60 * 24);
if next_time <= time() {
@@ -1280,65 +1282,77 @@ pub(crate) async fn load_next(
sleep(Duration::from_millis(500)).await;
}
let query;
let params;
let t = time();
let m;
let thread_i = thread as i64;
if let Some(msg_id) = info.msg_id {
query = r#"
let get_query = || {
if let Some(msg_id) = info.msg_id {
sqlx::query(
r#"
SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
FROM jobs
WHERE thread=? AND foreign_id=?
ORDER BY action DESC, added_timestamp
LIMIT 1;
"#;
m = msg_id;
params = paramsv![thread_i, m];
} else if !info.probe_network {
// processing for first-try and after backoff-timeouts:
// process jobs in the order they were added.
query = r#"
"#,
)
.bind(thread_i)
.bind(msg_id)
} else if !info.probe_network {
// processing for first-try and after backoff-timeouts:
// process jobs in the order they were added.
sqlx::query(
r#"
SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
FROM jobs
WHERE thread=? AND desired_timestamp<=?
ORDER BY action DESC, added_timestamp
LIMIT 1;
"#;
params = paramsv![thread_i, t];
} else {
// processing after call to dc_maybe_network():
// process _all_ pending jobs that failed before
// in the order of their backoff-times.
query = r#"
"#,
)
.bind(thread_i)
.bind(t)
} else {
// processing after call to dc_maybe_network():
// process _all_ pending jobs that failed before
// in the order of their backoff-times.
sqlx::query(
r#"
SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
FROM jobs
WHERE thread=? AND tries>0
ORDER BY desired_timestamp, action DESC
LIMIT 1;
"#;
params = paramsv![thread_i];
"#,
)
.bind(thread_i)
}
};
let job = loop {
let job_res = context
.sql
.query_row_optional(query, params.clone(), |row| {
let job = Job {
job_id: row.get("id")?,
action: row.get("action")?,
foreign_id: row.get("foreign_id")?,
desired_timestamp: row.get("desired_timestamp")?,
added_timestamp: row.get("added_timestamp")?,
tries: row.get("tries")?,
param: row.get::<_, String>("param")?.parse().unwrap_or_default(),
pending_error: None,
};
Ok(job)
})
.await;
.fetch_optional(get_query())
.await
.and_then(|row| {
if let Some(row) = row {
Ok(Some(Job {
job_id: row.try_get("id")?,
action: row.try_get("action")?,
foreign_id: row.try_get("foreign_id")?,
desired_timestamp: row.try_get("desired_timestamp")?,
added_timestamp: row.try_get("added_timestamp")?,
tries: row.try_get::<i64, _>("tries")? as u32,
param: row
.try_get::<String, _>("param")?
.parse()
.unwrap_or_default(),
pending_error: None,
}))
} else {
Ok(None)
}
});
match job_res {
Ok(job) => break job,
@@ -1349,15 +1363,18 @@ LIMIT 1;
// TODO: improve by only doing a single query
match context
.sql
.query_row(query, params.clone(), |row| row.get::<_, i32>(0))
.fetch_one(get_query())
.await
.and_then(|row| row.try_get::<i32, _>(0).map_err(Into::into))
{
Ok(id) => {
context
if let Err(err) = context
.sql
.execute("DELETE FROM jobs WHERE id=?;", paramsv![id])
.execute(sqlx::query("DELETE FROM jobs WHERE id=?;").bind(id))
.await
.ok();
{
warn!(context, "failed to delete job {}: {:?}", id, err);
}
}
Err(err) => {
error!(context, "failed to retrieve invalid job from DB: {}", err);
@@ -1399,22 +1416,22 @@ mod tests {
use crate::test_utils::TestContext;
async fn insert_job(context: &Context, foreign_id: i64) {
async fn insert_job(context: &Context, foreign_id: i64, valid: bool) {
let now = time();
context
.sql
.execute(
"INSERT INTO jobs
sqlx::query(
"INSERT INTO jobs
(added_timestamp, thread, action, foreign_id, param, desired_timestamp)
VALUES (?, ?, ?, ?, ?, ?);",
paramsv![
now,
Thread::from(Action::MoveMsg),
Action::MoveMsg,
foreign_id,
Params::new().to_string(),
now
],
)
.bind(now)
.bind(Thread::from(Action::MoveMsg))
.bind(if valid { Action::MoveMsg as i32 } else { -1 })
.bind(foreign_id)
.bind(Params::new().to_string())
.bind(now),
)
.await
.unwrap();
@@ -1426,7 +1443,7 @@ mod tests {
// fails to load from the database instead of failing to load
// all jobs.
let t = TestContext::new().await;
insert_job(&t, -1).await; // This can not be loaded into Job struct.
insert_job(&t, 1, false).await; // This can not be loaded into Job struct.
let jobs = load_next(
&t,
Thread::from(Action::MoveMsg),
@@ -1436,7 +1453,7 @@ mod tests {
// The housekeeping job should be loaded as we didn't run housekeeping in the last day:
assert!(jobs.unwrap().action == Action::Housekeeping);
insert_job(&t, 1).await;
insert_job(&t, 1, true).await;
let jobs = load_next(
&t,
Thread::from(Action::MoveMsg),
@@ -1450,7 +1467,7 @@ mod tests {
async fn test_load_next_job_one() {
let t = TestContext::new().await;
insert_job(&t, 1).await;
insert_job(&t, 1, true).await;
let jobs = load_next(
&t,

View File

@@ -9,6 +9,7 @@ use num_traits::FromPrimitive;
use pgp::composed::Deserializable;
use pgp::ser::Serialize;
use pgp::types::{KeyTrait, SecretKeyTrait};
use sqlx::Row;
use thiserror::Error;
use crate::config::Config;
@@ -41,6 +42,10 @@ pub enum Error {
InvalidConfiguredAddr(#[from] InvalidEmailError),
#[error("no data provided")]
Empty,
#[error("db: {}", _0)]
Sql(#[from] sqlx::Error),
#[error("{0}")]
Other(#[from] anyhow::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
@@ -118,24 +123,21 @@ impl DcKey for SignedPublicKey {
async fn load_self(context: &Context) -> Result<Self::KeyType> {
match context
.sql
.query_row(
.fetch_optional(
r#"
SELECT public_key
FROM keypairs
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
AND is_default=1;
"#,
paramsv![],
|row| row.get::<_, Vec<u8>>(0),
)
.await
.await?
{
Ok(bytes) => Self::from_slice(&bytes),
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
Some(row) => Self::from_slice(row.try_get(0)?),
None => {
let keypair = generate_keypair(context).await?;
Ok(keypair.public)
}
Err(err) => Err(err.into()),
}
}
@@ -163,24 +165,21 @@ impl DcKey for SignedSecretKey {
async fn load_self(context: &Context) -> Result<Self::KeyType> {
match context
.sql
.query_row(
.fetch_optional(
r#"
SELECT private_key
FROM keypairs
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
AND is_default=1;
"#,
paramsv![],
|row| row.get::<_, Vec<u8>>(0),
)
.await
.await?
{
Ok(bytes) => Self::from_slice(&bytes),
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
Some(row) => Self::from_slice(row.try_get(0)?),
None => {
let keypair = generate_keypair(context).await?;
Ok(keypair.secret)
}
Err(err) => Err(err.into()),
}
}
@@ -221,7 +220,7 @@ impl DcSecretKey for SignedSecretKey {
async fn generate_keypair(context: &Context) -> Result<KeyPair> {
let addr = context
.get_config(Config::ConfiguredAddr)
.await
.await?
.ok_or(Error::NoConfiguredAddr)?;
let addr = EmailAddress::new(&addr)?;
let _guard = context.generating_key_mutex.lock().await;
@@ -229,26 +228,27 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
// Check if the key appeared while we were waiting on the lock.
match context
.sql
.query_row(
r#"
.fetch_optional(
sqlx::query(
r#"
SELECT public_key, private_key
FROM keypairs
WHERE addr=?1
AND is_default=1;
"#,
paramsv![addr],
|row| Ok((row.get::<_, Vec<u8>>(0)?, row.get::<_, Vec<u8>>(1)?)),
)
.bind(addr.to_string()),
)
.await
.await?
{
Ok((pub_bytes, sec_bytes)) => Ok(KeyPair {
Some(row) => Ok(KeyPair {
addr,
public: SignedPublicKey::from_slice(&pub_bytes)?,
secret: SignedSecretKey::from_slice(&sec_bytes)?,
public: SignedPublicKey::from_slice(row.try_get(0)?)?,
secret: SignedSecretKey::from_slice(row.try_get(1)?)?,
}),
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
None => {
let start = std::time::SystemTime::now();
let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await)
let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await?)
.unwrap_or_default();
info!(context, "Generating keypair with type {}", keytype);
let keypair =
@@ -262,7 +262,6 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
);
Ok(keypair)
}
Err(err) => Err(err.into()),
}
}
@@ -320,15 +319,16 @@ pub async fn store_self_keypair(
context
.sql
.execute(
"DELETE FROM keypairs WHERE public_key=? OR private_key=?;",
paramsv![public_key, secret_key],
sqlx::query("DELETE FROM keypairs WHERE public_key=? OR private_key=?;")
.bind(&public_key)
.bind(&secret_key),
)
.await
.map_err(|err| SaveKeyError::new("failed to remove old use of key", err))?;
if default == KeyPairUse::Default {
context
.sql
.execute("UPDATE keypairs SET is_default=0;", paramsv![])
.execute("UPDATE keypairs SET is_default=0;")
.await
.map_err(|err| SaveKeyError::new("failed to clear default", err))?;
}
@@ -340,13 +340,18 @@ pub async fn store_self_keypair(
let addr = keypair.addr.to_string();
let t = time();
let params = paramsv![addr, is_default, public_key, secret_key, t];
context
.sql
.execute(
"INSERT INTO keypairs (addr, is_default, public_key, private_key, created)
sqlx::query(
"INSERT INTO keypairs (addr, is_default, public_key, private_key, created)
VALUES (?,?,?,?,?);",
params,
)
.bind(addr)
.bind(is_default)
.bind(&public_key)
.bind(&secret_key)
.bind(t),
)
.await
.map_err(|err| SaveKeyError::new("failed to insert keypair", err))?;
@@ -620,7 +625,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
let nrows = || async {
ctx.sql
.query_get_value::<u32>(&ctx, "SELECT COUNT(*) FROM keypairs;", paramsv![])
.count("SELECT COUNT(*) FROM keypairs;")
.await
.unwrap()
};

View File

@@ -1,11 +1,11 @@
#![forbid(unsafe_code)]
#![deny(
clippy::correctness,
missing_debug_implementations,
clippy::all,
clippy::indexing_slicing,
clippy::wildcard_imports,
clippy::needless_borrow
clippy::needless_borrow,
unsafe_code
)]
#![allow(clippy::match_bool, clippy::eval_order_dependence)]
@@ -13,16 +13,10 @@
extern crate num_derive;
#[macro_use]
extern crate smallvec;
#[macro_use]
extern crate rusqlite;
extern crate strum;
#[macro_use]
extern crate strum_macros;
pub trait ToSql: rusqlite::ToSql + Send + Sync {}
impl<T: rusqlite::ToSql + Send + Sync> ToSql for T {}
#[macro_use]
pub mod log;
#[macro_use]

View File

@@ -1,8 +1,11 @@
//! Location handling
use std::convert::TryFrom;
use anyhow::{ensure, Error};
use async_std::prelude::*;
use bitflags::bitflags;
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
use sqlx::Row;
use crate::chat::{self, ChatId};
use crate::config::Config;
@@ -198,15 +201,15 @@ pub async fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds:
if context
.sql
.execute(
"UPDATE chats \
sqlx::query(
"UPDATE chats \
SET locations_send_begin=?, \
locations_send_until=? \
WHERE id=?",
paramsv![
if 0 != seconds { now } else { 0 },
if 0 != seconds { now + seconds } else { 0 },
chat_id,
],
)
.bind(if 0 != seconds { now } else { 0 })
.bind(if 0 != seconds { now + seconds } else { 0 })
.bind(chat_id),
)
.await
.is_ok()
@@ -259,16 +262,17 @@ pub async fn is_sending_locations_to_chat(context: &Context, chat_id: Option<Cha
Some(chat_id) => context
.sql
.exists(
"SELECT id FROM chats WHERE id=? AND locations_send_until>?;",
paramsv![chat_id, time()],
sqlx::query("SELECT COUNT(id) FROM chats WHERE id=? AND locations_send_until>?;")
.bind(chat_id)
.bind(time()),
)
.await
.unwrap_or_default(),
None => context
.sql
.exists(
"SELECT id FROM chats WHERE locations_send_until>?;",
paramsv![time()],
sqlx::query("SELECT COUNT(id) FROM chats WHERE locations_send_until>?;")
.bind(time()),
)
.await
.unwrap_or_default(),
@@ -281,28 +285,29 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64
}
let mut continue_streaming = false;
if let Ok(chats) = context
if let Ok(mut chats) = context
.sql
.query_map(
"SELECT id FROM chats WHERE locations_send_until>?;",
paramsv![time()],
|row| row.get::<_, i32>(0),
|chats| chats.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.fetch(sqlx::query("SELECT id FROM chats WHERE locations_send_until>?;").bind(time()))
.await
.map(|rows| rows.map(|row| row?.try_get::<i32, _>(0)))
{
for chat_id in chats {
while let Some(chat_id) = chats.next().await {
let chat_id = match chat_id {
Ok(id) => id,
Err(_) => break,
};
if let Err(err) = context.sql.execute(
sqlx::query(
"INSERT INTO locations \
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
paramsv![
latitude,
longitude,
accuracy,
time(),
chat_id,
DC_CONTACT_ID_SELF,
]
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);"
)
.bind(latitude)
.bind(longitude)
.bind(accuracy)
.bind(time())
.bind(chat_id)
.bind(DC_CONTACT_ID_SELF)
).await {
warn!(context, "failed to store location {:?}", err);
} else {
@@ -324,10 +329,11 @@ pub async fn get_range(
contact_id: Option<u32>,
timestamp_from: i64,
mut timestamp_to: i64,
) -> Vec<Location> {
) -> Result<Vec<Location>, Error> {
if timestamp_to == 0 {
timestamp_to = time() + 10;
}
let (disable_chat_id, chat_id) = match chat_id {
Some(chat_id) => (0, chat_id),
None => (1, ChatId::new(0)), // this ChatId is unused
@@ -336,56 +342,52 @@ pub async fn get_range(
Some(contact_id) => (0, contact_id),
None => (1, 0), // this contact_id is unused
};
context
let list = context
.sql
.query_map(
"SELECT l.id, l.latitude, l.longitude, l.accuracy, l.timestamp, l.independent, \
.fetch(
sqlx::query(
"SELECT l.id, l.latitude, l.longitude, l.accuracy, l.timestamp, l.independent, \
COALESCE(m.id, 0) AS msg_id, l.from_id, l.chat_id, COALESCE(m.txt, '') AS txt \
FROM locations l LEFT JOIN msgs m ON l.id=m.location_id WHERE (? OR l.chat_id=?) \
AND (? OR l.from_id=?) \
AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \
ORDER BY l.timestamp DESC, l.id DESC, msg_id DESC;",
paramsv![
disable_chat_id,
chat_id,
disable_contact_id,
contact_id as i32,
timestamp_from,
timestamp_to,
],
|row| {
let msg_id = row.get(6)?;
let txt: String = row.get(9)?;
let marker = if msg_id != 0 && is_marker(&txt) {
Some(txt)
} else {
None
};
let loc = Location {
location_id: row.get(0)?,
latitude: row.get(1)?,
longitude: row.get(2)?,
accuracy: row.get(3)?,
timestamp: row.get(4)?,
independent: row.get(5)?,
msg_id,
contact_id: row.get(7)?,
chat_id: row.get(8)?,
marker,
};
Ok(loc)
},
|locations| {
let mut ret = Vec::new();
for location in locations {
ret.push(location?);
}
Ok(ret)
},
)
.bind(disable_chat_id)
.bind(chat_id)
.bind(disable_contact_id)
.bind(contact_id as i64)
.bind(timestamp_from)
.bind(timestamp_to),
)
.await
.unwrap_or_default()
.await?
.map(|row| {
let row = row?;
let msg_id = row.try_get(6)?;
let txt: String = row.try_get(9)?;
let marker = if msg_id != 0 && is_marker(&txt) {
Some(txt)
} else {
None
};
let loc = Location {
location_id: row.try_get(0)?,
latitude: row.try_get(1)?,
longitude: row.try_get(2)?,
accuracy: row.try_get(3)?,
timestamp: row.try_get(4)?,
independent: row.try_get(5)?,
msg_id,
contact_id: row.try_get(7)?,
chat_id: row.try_get(8)?,
marker,
};
Ok(loc)
})
.collect::<sqlx::Result<_>>()
.await?;
Ok(list)
}
fn is_marker(txt: &str) -> bool {
@@ -399,10 +401,7 @@ fn is_marker(txt: &str) -> bool {
/// Deletes all locations from the database.
pub async fn delete_all(context: &Context) -> Result<(), Error> {
context
.sql
.execute("DELETE FROM locations;", paramsv![])
.await?;
context.sql.execute("DELETE FROM locations;").await?;
context.emit_event(EventType::LocationChanged(None));
Ok(())
}
@@ -412,19 +411,23 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await
.await?
.unwrap_or_default();
let (locations_send_begin, locations_send_until, locations_last_sent) = context.sql.query_row(
"SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;",
paramsv![chat_id], |row| {
let send_begin: i64 = row.get(0)?;
let send_until: i64 = row.get(1)?;
let last_sent: i64 = row.get(2)?;
let (locations_send_begin, locations_send_until, locations_last_sent) = {
let row = context.sql.fetch_one(
sqlx::query(
"SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;"
)
.bind(chat_id)
).await?;
Ok((send_begin, send_until, last_sent))
})
.await?;
let send_begin: i64 = row.try_get(0)?;
let send_until: i64 = row.try_get(1)?;
let last_sent: i64 = row.try_get(2)?;
(send_begin, send_until, last_sent)
};
let now = time();
let mut location_count = 0;
@@ -435,40 +438,41 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)
self_addr,
);
context.sql.query_map(
"SELECT id, latitude, longitude, accuracy, timestamp \
let mut rows = context.sql.fetch(
sqlx::query(
"SELECT id, latitude, longitude, accuracy, timestamp \
FROM locations WHERE from_id=? \
AND timestamp>=? \
AND (timestamp>=? OR timestamp=(SELECT MAX(timestamp) FROM locations WHERE from_id=?)) \
AND independent=0 \
GROUP BY timestamp \
ORDER BY timestamp;",
paramsv![DC_CONTACT_ID_SELF, locations_send_begin, locations_last_sent, DC_CONTACT_ID_SELF],
|row| {
let location_id: i32 = row.get(0)?;
let latitude: f64 = row.get(1)?;
let longitude: f64 = row.get(2)?;
let accuracy: f64 = row.get(3)?;
let timestamp = get_kml_timestamp(row.get(4)?);
Ok((location_id, latitude, longitude, accuracy, timestamp))
},
|rows| {
for row in rows {
let (location_id, latitude, longitude, accuracy, timestamp) = row?;
ret += &format!(
"<Placemark><Timestamp><when>{}</when></Timestamp><Point><coordinates accuracy=\"{}\">{},{}</coordinates></Point></Placemark>\n",
timestamp,
accuracy,
longitude,
latitude
);
location_count += 1;
last_added_location_id = location_id as u32;
}
Ok(())
}
ORDER BY timestamp;"
)
.bind(DC_CONTACT_ID_SELF)
.bind(locations_send_begin)
.bind(locations_last_sent)
.bind(DC_CONTACT_ID_SELF)
).await?;
while let Some(row) = rows.next().await {
let row = row?;
let location_id: u32 = row.try_get(0)?;
let latitude: f64 = row.try_get(1)?;
let longitude: f64 = row.try_get(2)?;
let accuracy: f64 = row.try_get(3)?;
let timestamp = get_kml_timestamp(row.try_get(4)?);
ret += &format!(
"<Placemark><Timestamp><when>{}</when></Timestamp><Point><coordinates accuracy=\"{}\">{},{}</coordinates></Point></Placemark>\n",
timestamp,
accuracy,
longitude,
latitude
);
location_count += 1;
last_added_location_id = location_id;
}
ret += "</Document>\n</kml>";
}
@@ -509,8 +513,9 @@ pub async fn set_kml_sent_timestamp(
context
.sql
.execute(
"UPDATE chats SET locations_last_sent=? WHERE id=?;",
paramsv![timestamp, chat_id],
sqlx::query("UPDATE chats SET locations_last_sent=? WHERE id=?;")
.bind(timestamp)
.bind(chat_id),
)
.await?;
Ok(())
@@ -524,8 +529,9 @@ pub async fn set_msg_location_id(
context
.sql
.execute(
"UPDATE msgs SET location_id=? WHERE id=?;",
paramsv![location_id, msg_id],
sqlx::query("UPDATE msgs SET location_id=? WHERE id=?;")
.bind(location_id)
.bind(msg_id),
)
.await?;
@@ -544,6 +550,11 @@ pub async fn save(
let mut newest_timestamp = 0;
let mut newest_location_id = 0;
let stmt_test = "SELECT COUNT(*) FROM locations WHERE timestamp=? AND from_id=?";
let stmt_insert = "INSERT INTO locations\
(timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \
VALUES (?,?,?,?,?,?,?);";
for location in locations {
let &Location {
timestamp,
@@ -552,53 +563,42 @@ pub async fn save(
accuracy,
..
} = location;
let (loc_id, ts) = context
let exists = context
.sql
.with_conn(move |mut conn| {
let mut stmt_test = conn
.prepare_cached("SELECT id FROM locations WHERE timestamp=? AND from_id=?")?;
let mut stmt_insert = conn.prepare_cached(
"INSERT INTO locations\
(timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \
VALUES (?,?,?,?,?,?,?);",
)?;
let exists = stmt_test.exists(paramsv![timestamp, contact_id as i32])?;
if independent || !exists {
stmt_insert.execute(paramsv![
timestamp,
contact_id as i32,
chat_id,
latitude,
longitude,
accuracy,
independent,
])?;
if timestamp > newest_timestamp {
// okay to drop, as we use cached prepared statements
drop(stmt_test);
drop(stmt_insert);
newest_timestamp = timestamp;
newest_location_id = crate::sql::get_rowid2(
&mut conn,
"locations",
"timestamp",
timestamp,
"from_id",
contact_id as i32,
)?;
}
}
Ok((newest_location_id, newest_timestamp))
})
.exists(sqlx::query(stmt_test).bind(timestamp).bind(contact_id))
.await?;
newest_timestamp = ts;
newest_location_id = loc_id;
if independent || !exists {
context
.sql
.execute(
sqlx::query(stmt_insert)
.bind(timestamp)
.bind(contact_id)
.bind(chat_id)
.bind(latitude)
.bind(longitude)
.bind(accuracy)
.bind(independent),
)
.await?;
if timestamp > newest_timestamp {
newest_timestamp = timestamp;
newest_location_id = context
.sql
.get_rowid2(
"locations",
"timestamp",
timestamp,
"from_id",
contact_id as i64,
)
.await?;
}
}
}
Ok(newest_location_id)
Ok(u32::try_from(newest_location_id)?)
}
pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> job::Status {
@@ -611,15 +611,21 @@ pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> j
let rows = context
.sql
.query_map(
"SELECT id, locations_send_begin, locations_last_sent \
.fetch(
sqlx::query(
"SELECT id, locations_send_begin, locations_last_sent \
FROM chats \
WHERE locations_send_until>?;",
paramsv![now],
|row| {
let chat_id: ChatId = row.get(0)?;
let locations_send_begin: i64 = row.get(1)?;
let locations_last_sent: i64 = row.get(2)?;
)
.bind(now),
)
.await
.map(|rows| {
rows.map(|row| -> sqlx::Result<Option<_>> {
let row = row?;
let chat_id: ChatId = row.try_get(0)?;
let locations_send_begin: i64 = row.try_get(1)?;
let locations_last_sent: i64 = row.try_get(2)?;
continue_streaming = true;
// be a bit tolerant as the timer may not align exactly with time(NULL)
@@ -628,64 +634,55 @@ pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> j
} else {
Ok(Some((chat_id, locations_send_begin, locations_last_sent)))
}
},
|rows| {
rows.filter_map(|v| v.transpose())
.collect::<Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await;
if rows.is_ok() {
let msgs = context
.sql
.with_conn(move |conn| {
let rows = rows.unwrap();
let mut stmt_locations = conn.prepare_cached(
"SELECT id \
FROM locations \
WHERE from_id=? \
AND timestamp>=? \
AND timestamp>? \
AND independent=0 \
ORDER BY timestamp;",
)?;
let mut msgs = Vec::new();
for (chat_id, locations_send_begin, locations_last_sent) in &rows {
if !stmt_locations
.exists(paramsv![
DC_CONTACT_ID_SELF,
*locations_send_begin,
*locations_last_sent,
])
.unwrap_or_default()
{
// if there is no new location, there's nothing to send.
// however, maybe we want to bypass this test eg. 15 minutes
} else {
// pending locations are attached automatically to every message,
// so also to this empty text message.
// DC_CMD_LOCATION is only needed to create a nicer subject.
//
// for optimisation and to avoid flooding the sending queue,
// we could sending these messages only if we're really online.
// the easiest way to determine this, is to check for an empty message queue.
// (might not be 100%, however, as positions are sent combined later
// and dc_set_location() is typically called periodically, this is ok)
let mut msg = Message::new(Viewtype::Text);
msg.hidden = true;
msg.param.set_cmd(SystemMessage::LocationOnly);
msgs.push((*chat_id, msg));
}
}
Ok(msgs)
})
.await
.unwrap_or_default();
.filter_map(|v| v.transpose())
});
let stmt = "SELECT COUNT(*) \
FROM locations \
WHERE from_id=? \
AND timestamp>=? \
AND timestamp>? \
AND independent=0 \
ORDER BY timestamp;";
if let Ok(mut rows) = rows {
let mut msgs = Vec::new();
while let Some(row) = rows.next().await {
let (chat_id, locations_send_begin, locations_last_sent) = match row {
Ok(row) => row,
Err(_) => break,
};
let exists = context
.sql
.exists(
sqlx::query(stmt)
.bind(DC_CONTACT_ID_SELF)
.bind(locations_send_begin)
.bind(locations_last_sent),
)
.await
.unwrap_or_default(); // TODO: better error handling
if !exists {
// if there is no new location, there's nothing to send.
// however, maybe we want to bypass this test eg. 15 minutes
} else {
// pending locations are attached automatically to every message,
// so also to this empty text message.
// DC_CMD_LOCATION is only needed to create a nicer subject.
//
// for optimisation and to avoid flooding the sending queue,
// we could sending these messages only if we're really online.
// the easiest way to determine this, is to check for an empty message queue.
// (might not be 100%, however, as positions are sent combined later
// and dc_set_location() is typically called periodically, this is ok)
let mut msg = Message::new(Viewtype::Text);
msg.hidden = true;
msg.param.set_cmd(SystemMessage::LocationOnly);
msgs.push((chat_id, msg));
}
}
for (chat_id, mut msg) in msgs.into_iter() {
// TODO: better error handling
@@ -711,16 +708,16 @@ pub(crate) async fn job_maybe_send_locations_ended(
let chat_id = ChatId::new(job.foreign_id);
let (send_begin, send_until) = job_try!(
context
.sql
.query_row(
let (send_begin, send_until) = job_try!(context
.sql
.fetch_one(
sqlx::query(
"SELECT locations_send_begin, locations_send_until FROM chats WHERE id=?",
paramsv![chat_id],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)),
)
.await
);
.bind(chat_id)
)
.await
.and_then(|row| { Ok((row.try_get::<i64, _>(0)?, row.try_get::<i64, _>(1)?)) }));
if !(send_begin != 0 && time() <= send_until) {
// still streaming -
@@ -728,10 +725,19 @@ pub(crate) async fn job_maybe_send_locations_ended(
// do not un-schedule pending DC_MAYBE_SEND_LOC_ENDED jobs
if !(send_begin == 0 && send_until == 0) {
// not streaming, device-message already sent
job_try!(context.sql.execute(
"UPDATE chats SET locations_send_begin=0, locations_send_until=0 WHERE id=?",
paramsv![chat_id],
).await);
job_try!(
context
.sql
.execute(
sqlx::query(
"UPDATE chats \
SET locations_send_begin=0, locations_send_until=0 \
WHERE id=?"
)
.bind(chat_id)
)
.await
);
let stock_str = stock_str::msg_location_disabled(context).await;
chat::add_info_msg(context, chat_id, stock_str).await;

View File

@@ -7,7 +7,7 @@ use crate::provider::{get_provider_by_id, Provider};
use crate::{context::Context, provider::Socket};
#[derive(Copy, Clone, Debug, Display, FromPrimitive, PartialEq, Eq)]
#[repr(i32)]
#[repr(u32)]
#[strum(serialize_all = "snake_case")]
pub enum CertificateChecks {
/// Same as AcceptInvalidCertificates unless overridden by
@@ -54,91 +54,85 @@ pub struct LoginParam {
impl LoginParam {
/// Read the login parameters from the database.
pub async fn from_database(context: &Context, prefix: impl AsRef<str>) -> Self {
pub async fn from_database(
context: &Context,
prefix: impl AsRef<str>,
) -> crate::sql::Result<Self> {
let prefix = prefix.as_ref();
let sql = &context.sql;
let key = format!("{}addr", prefix);
let addr = sql
.get_raw_config(context, key)
.await
.get_raw_config(key)
.await?
.unwrap_or_default()
.trim()
.to_string();
let key = format!("{}mail_server", prefix);
let mail_server = sql.get_raw_config(context, key).await.unwrap_or_default();
let mail_server = sql.get_raw_config(key).await?.unwrap_or_default();
let key = format!("{}mail_port", prefix);
let mail_port = sql
.get_raw_config_int(context, key)
.await
.unwrap_or_default();
let mail_port = sql.get_raw_config_int(key).await?.unwrap_or_default();
let key = format!("{}mail_user", prefix);
let mail_user = sql.get_raw_config(context, key).await.unwrap_or_default();
let mail_user = sql.get_raw_config(key).await?.unwrap_or_default();
let key = format!("{}mail_pw", prefix);
let mail_pw = sql.get_raw_config(context, key).await.unwrap_or_default();
let mail_pw = sql.get_raw_config(key).await?.unwrap_or_default();
let key = format!("{}mail_security", prefix);
let mail_security = sql
.get_raw_config_int(context, key)
.await
.get_raw_config_int(key)
.await?
.and_then(num_traits::FromPrimitive::from_i32)
.unwrap_or_default();
let key = format!("{}imap_certificate_checks", prefix);
let imap_certificate_checks =
if let Some(certificate_checks) = sql.get_raw_config_int(context, key).await {
if let Some(certificate_checks) = sql.get_raw_config_int(key).await? {
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap()
} else {
Default::default()
};
let key = format!("{}send_server", prefix);
let send_server = sql.get_raw_config(context, key).await.unwrap_or_default();
let send_server = sql.get_raw_config(key).await?.unwrap_or_default();
let key = format!("{}send_port", prefix);
let send_port = sql
.get_raw_config_int(context, key)
.await
.unwrap_or_default();
let send_port = sql.get_raw_config_int(key).await?.unwrap_or_default();
let key = format!("{}send_user", prefix);
let send_user = sql.get_raw_config(context, key).await.unwrap_or_default();
let send_user = sql.get_raw_config(key).await?.unwrap_or_default();
let key = format!("{}send_pw", prefix);
let send_pw = sql.get_raw_config(context, key).await.unwrap_or_default();
let send_pw = sql.get_raw_config(key).await?.unwrap_or_default();
let key = format!("{}send_security", prefix);
let send_security = sql
.get_raw_config_int(context, key)
.await
.get_raw_config_int(key)
.await?
.and_then(num_traits::FromPrimitive::from_i32)
.unwrap_or_default();
let key = format!("{}smtp_certificate_checks", prefix);
let smtp_certificate_checks =
if let Some(certificate_checks) = sql.get_raw_config_int(context, key).await {
if let Some(certificate_checks) = sql.get_raw_config_int(key).await? {
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap()
} else {
Default::default()
};
let key = format!("{}server_flags", prefix);
let server_flags = sql
.get_raw_config_int(context, key)
.await
.unwrap_or_default();
let server_flags = sql.get_raw_config_int(key).await?.unwrap_or_default();
let key = format!("{}provider", prefix);
let provider = sql
.get_raw_config(context, key)
.await
.get_raw_config(key)
.await?
.and_then(|provider_id| get_provider_by_id(&provider_id));
LoginParam {
Ok(LoginParam {
addr,
imap: ServerLoginParam {
server: mail_server,
@@ -158,7 +152,7 @@ impl LoginParam {
},
provider,
server_flags,
}
})
}
/// Save this loginparam to the database.
@@ -171,63 +165,54 @@ impl LoginParam {
let sql = &context.sql;
let key = format!("{}addr", prefix);
sql.set_raw_config(context, key, Some(&self.addr)).await?;
sql.set_raw_config(key, Some(&self.addr)).await?;
let key = format!("{}mail_server", prefix);
sql.set_raw_config(context, key, Some(&self.imap.server))
.await?;
sql.set_raw_config(key, Some(&self.imap.server)).await?;
let key = format!("{}mail_port", prefix);
sql.set_raw_config_int(context, key, self.imap.port as i32)
.await?;
sql.set_raw_config_int(key, self.imap.port as i32).await?;
let key = format!("{}mail_user", prefix);
sql.set_raw_config(context, key, Some(&self.imap.user))
.await?;
sql.set_raw_config(key, Some(&self.imap.user)).await?;
let key = format!("{}mail_pw", prefix);
sql.set_raw_config(context, key, Some(&self.imap.password))
.await?;
sql.set_raw_config(key, Some(&self.imap.password)).await?;
let key = format!("{}mail_security", prefix);
sql.set_raw_config_int(context, key, self.imap.security as i32)
sql.set_raw_config_int(key, self.imap.security as i32)
.await?;
let key = format!("{}imap_certificate_checks", prefix);
sql.set_raw_config_int(context, key, self.imap.certificate_checks as i32)
sql.set_raw_config_int(key, self.imap.certificate_checks as i32)
.await?;
let key = format!("{}send_server", prefix);
sql.set_raw_config(context, key, Some(&self.smtp.server))
.await?;
sql.set_raw_config(key, Some(&self.smtp.server)).await?;
let key = format!("{}send_port", prefix);
sql.set_raw_config_int(context, key, self.smtp.port as i32)
.await?;
sql.set_raw_config_int(key, self.smtp.port as i32).await?;
let key = format!("{}send_user", prefix);
sql.set_raw_config(context, key, Some(&self.smtp.user))
.await?;
sql.set_raw_config(key, Some(&self.smtp.user)).await?;
let key = format!("{}send_pw", prefix);
sql.set_raw_config(context, key, Some(&self.smtp.password))
.await?;
sql.set_raw_config(key, Some(&self.smtp.password)).await?;
let key = format!("{}send_security", prefix);
sql.set_raw_config_int(context, key, self.smtp.security as i32)
sql.set_raw_config_int(key, self.smtp.security as i32)
.await?;
let key = format!("{}smtp_certificate_checks", prefix);
sql.set_raw_config_int(context, key, self.smtp.certificate_checks as i32)
sql.set_raw_config_int(key, self.smtp.certificate_checks as i32)
.await?;
let key = format!("{}server_flags", prefix);
sql.set_raw_config_int(context, key, self.server_flags)
.await?;
sql.set_raw_config_int(key, self.server_flags).await?;
if let Some(provider) = self.provider {
let key = format!("{}provider", prefix);
sql.set_raw_config(context, key, Some(provider.id)).await?;
sql.set_raw_config(key, Some(provider.id)).await?;
}
Ok(())

View File

@@ -1,5 +1,3 @@
use deltachat_derive::{FromSql, ToSql};
use crate::key::Fingerprint;
/// An object containing a set of values.
@@ -22,9 +20,7 @@ pub struct Lot {
}
#[repr(u8)]
#[derive(
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
pub enum Meaning {
None = 0,
Text1Draft = 1,
@@ -68,10 +64,8 @@ impl Lot {
}
}
#[repr(i32)]
#[derive(
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(u32)]
pub enum LotState {
// Default
Undefined = 0,

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,11 @@
use std::convert::TryInto;
use anyhow::{bail, ensure, format_err, Error};
use async_std::prelude::*;
use chrono::TimeZone;
use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder};
use sqlx::Row;
use crate::blob::BlobObject;
use crate::chat::{self, Chat};
use crate::config::Config;
@@ -20,11 +28,6 @@ use crate::param::Param;
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::simplify::escape_message_footer_marks;
use crate::stock_str;
use anyhow::Context as _;
use anyhow::{bail, ensure, format_err, Error};
use chrono::TimeZone;
use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder};
use std::convert::TryInto;
// attachments of 25 mb brutto should work on the majority of providers
// (brutto examples: web.de=50, 1&1=40, t-online.de=32, gmail=25, posteo=50, yahoo=25, all-inkl=100).
@@ -92,12 +95,12 @@ impl<'a> MimeFactory<'a> {
let from_addr = context
.get_config(Config::ConfiguredAddr)
.await
.await?
.unwrap_or_default();
let config_displayname = context
.get_config(Config::Displayname)
.await
.await?
.unwrap_or_default();
let (from_displayname, sender_displayname) =
if let Some(override_name) = msg.param.get(Param::OverrideSenderDisplayname) {
@@ -112,52 +115,42 @@ impl<'a> MimeFactory<'a> {
if chat.is_self_talk() {
recipients.push((from_displayname.to_string(), from_addr.to_string()));
} else {
context
let mut rows = context
.sql
.query_map(
"SELECT c.authname, c.addr \
.fetch(
sqlx::query(
"SELECT c.authname, c.addr \
FROM chats_contacts cc \
LEFT JOIN contacts c ON cc.contact_id=c.id \
WHERE cc.chat_id=? AND cc.contact_id>9;",
paramsv![msg.chat_id],
|row| {
let authname: String = row.get(0)?;
let addr: String = row.get(1)?;
Ok((authname, addr))
},
|rows| {
for row in rows {
let (authname, addr) = row?;
if !recipients_contain_addr(&recipients, &addr) {
recipients.push((authname, addr));
}
}
Ok(())
},
)
.bind(msg.chat_id),
)
.await?;
while let Some(row) = rows.next().await {
let row = row?;
let authname: String = row.try_get(0)?;
let addr: String = row.try_get(1)?;
if !recipients_contain_addr(&recipients, &addr) {
recipients.push((authname, addr));
}
}
if !msg.is_system_message() && context.get_config_bool(Config::MdnsEnabled).await {
if !msg.is_system_message() && context.get_config_bool(Config::MdnsEnabled).await? {
req_mdn = true;
}
}
let (in_reply_to, references) = context
let row = context
.sql
.query_row(
"SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?",
paramsv![msg.id],
|row| {
let in_reply_to: String = row.get(0)?;
let references: String = row.get(1)?;
Ok((
render_rfc724_mid_list(&in_reply_to),
render_rfc724_mid_list(&references),
))
},
.fetch_one(
sqlx::query("SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?")
.bind(msg.id),
)
.await
.context("Can't get mime_in_reply_to, mime_references")?;
.await?;
let (in_reply_to, references) = (
render_rfc724_mid_list(row.try_get(0)?),
render_rfc724_mid_list(row.try_get(1)?),
);
let default_str = stock_str::status_line(context).await;
let factory = MimeFactory {
@@ -166,7 +159,7 @@ impl<'a> MimeFactory<'a> {
sender_displayname,
selfstatus: context
.get_config(Config::Selfstatus)
.await
.await?
.unwrap_or(default_str),
recipients,
timestamp: msg.timestamp_sort,
@@ -191,16 +184,16 @@ impl<'a> MimeFactory<'a> {
let contact = Contact::load_from_db(context, msg.from_id).await?;
let from_addr = context
.get_config(Config::ConfiguredAddr)
.await
.await?
.unwrap_or_default();
let from_displayname = context
.get_config(Config::Displayname)
.await
.await?
.unwrap_or_default();
let default_str = stock_str::status_line(context).await;
let selfstatus = context
.get_config(Config::Selfstatus)
.await
.await?
.unwrap_or(default_str);
let timestamp = dc_create_smeared_timestamp(context).await;
@@ -232,7 +225,7 @@ impl<'a> MimeFactory<'a> {
) -> Result<Vec<(Option<Peerstate>, &str)>, Error> {
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await
.await?
.ok_or_else(|| format_err!("Not configured"))?;
let mut res = Vec::new();
@@ -309,18 +302,18 @@ impl<'a> MimeFactory<'a> {
}
}
async fn should_do_gossip(&self, context: &Context) -> bool {
async fn should_do_gossip(&self, context: &Context) -> Result<bool, Error> {
match &self.loaded {
Loaded::Message { chat } => {
// beside key- and member-changes, force re-gossip every 48 hours
let gossiped_timestamp = chat.get_gossiped_timestamp(context).await;
let gossiped_timestamp = chat.get_gossiped_timestamp(context).await?;
if time() > gossiped_timestamp + (2 * 24 * 60 * 60) {
return true;
Ok(true)
} else {
Ok(self.msg.param.get_cmd() == SystemMessage::MemberAddedToGroup)
}
self.msg.param.get_cmd() == SystemMessage::MemberAddedToGroup
}
Loaded::MDN { .. } => false,
Loaded::MDN { .. } => Ok(false),
}
}
@@ -357,7 +350,7 @@ impl<'a> MimeFactory<'a> {
async fn subject_str(&self, context: &Context) -> anyhow::Result<String> {
let quoted_msg_subject = self.msg.quoted_message(context).await?.map(|m| m.subject);
Ok(match self.loaded {
let subject = match self.loaded {
Loaded::Message { ref chat } => {
if self.msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage {
return Ok(stock_str::ac_setup_msg_subject(context).await);
@@ -387,16 +380,18 @@ impl<'a> MimeFactory<'a> {
if let Some(last_subject) = parent_subject {
format!("Re: {}", remove_subject_prefix(last_subject))
} else {
let self_name = match context.get_config(Config::Displayname).await {
let self_name = match context.get_config(Config::Displayname).await? {
Some(name) => name,
None => context.get_config(Config::Addr).await.unwrap_or_default(),
None => context.get_config(Config::Addr).await?.unwrap_or_default(),
};
stock_str::subject_for_new_contact(context, self_name).await
}
}
Loaded::MDN { .. } => stock_str::read_rcpt(context).await,
})
};
Ok(subject)
}
pub fn recipients(&self) -> Vec<String> {
@@ -567,7 +562,7 @@ impl<'a> MimeFactory<'a> {
let outer_message = if is_encrypted {
// Add gossip headers in chats with multiple recipients
if peerstates.len() > 1 && self.should_do_gossip(context).await {
if peerstates.len() > 1 && self.should_do_gossip(context).await? {
for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) {
if peerstate.peek_key(min_verified).is_some() {
if let Some(header) = peerstate.render_gossip_header(min_verified) {
@@ -966,7 +961,9 @@ impl<'a> MimeFactory<'a> {
// for simplificity and to avoid conversion errors, we're generating the HTML-part from the original message.
if self.msg.has_html() {
let html = if let Some(orig_msg_id) = self.msg.param.get_int(Param::Forwarded) {
MsgId::new(orig_msg_id.try_into()?).get_html(context).await
MsgId::new(orig_msg_id.try_into()?)
.get_html(context)
.await?
} else {
self.msg.param.get(Param::SendHtml).map(|s| s.to_string())
};
@@ -1009,7 +1006,7 @@ impl<'a> MimeFactory<'a> {
}
if self.attach_selfavatar {
match context.get_config(Config::Selfavatar).await {
match context.get_config(Config::Selfavatar).await? {
Some(path) => match build_selfavatar_file(context, &path) {
Ok((part, filename)) => {
parts.push(part);
@@ -1137,14 +1134,14 @@ async fn build_body_file(
// etc.
let filename_to_send: String = match msg.viewtype {
Viewtype::Voice => chrono::Utc
.timestamp(msg.timestamp_sort as i64, 0)
.timestamp(msg.timestamp_sort, 0)
.format(&format!("voice-message_%Y-%m-%d_%H-%M-%S.{}", &suffix))
.to_string(),
Viewtype::Image | Viewtype::Gif => format!(
"{}.{}",
if base_name.is_empty() {
chrono::Utc
.timestamp(msg.timestamp_sort as i64, 0)
.timestamp(msg.timestamp_sort, 0)
.format("image_%Y-%m-%d_%H-%M-%S")
.to_string()
} else {
@@ -1155,7 +1152,7 @@ async fn build_body_file(
Viewtype::Video => format!(
"video_{}.{}",
chrono::Utc
.timestamp(msg.timestamp_sort as i64, 0)
.timestamp(msg.timestamp_sort, 0)
.format("%Y-%m-%d_%H-%M-%S")
.to_string(),
&suffix

View File

@@ -4,7 +4,6 @@ use std::pin::Pin;
use anyhow::{bail, Result};
use charset::Charset;
use deltachat_derive::{FromSql, ToSql};
use lettre_email::mime::{self, Mime};
use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo};
use once_cell::sync::Lazy;
@@ -103,10 +102,8 @@ pub(crate) enum MailinglistType {
None,
}
#[derive(
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
)]
#[repr(i32)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(u32)]
pub enum SystemMessage {
Unknown = 0,
GroupNameChanged = 2,
@@ -1215,10 +1212,16 @@ impl MimeMessage {
for original_message_id in
std::iter::once(&report.original_message_id).chain(&report.additional_message_ids)
{
if let Some((chat_id, msg_id)) =
message::handle_mdn(context, from_id, original_message_id, sent_timestamp).await
match message::handle_mdn(context, from_id, original_message_id, sent_timestamp)
.await
{
context.emit_event(EventType::MsgRead { chat_id, msg_id });
Ok(Some((chat_id, msg_id))) => {
context.emit_event(EventType::MsgRead { chat_id, msg_id });
}
Ok(None) => {}
Err(err) => {
warn!(context, "failed to handle_mdn: {:#}", err);
}
}
}
}
@@ -1245,9 +1248,8 @@ impl MimeMessage {
{
context
.sql
.query_get_value_result(
"SELECT timestamp FROM msgs WHERE rfc724_mid=?",
paramsv![field],
.query_get_value(
sqlx::query("SELECT timestamp FROM msgs WHERE rfc724_mid=?").bind(field),
)
.await?
} else {
@@ -1918,8 +1920,9 @@ mod tests {
.ctx
.sql
.execute(
"INSERT INTO msgs (rfc724_mid, timestamp) VALUES(?,?)",
paramsv!["Gr.beZgAF2Nn0-.oyaJOpeuT70@example.org", timestamp],
sqlx::query("INSERT INTO msgs (rfc724_mid, timestamp) VALUES(?,?)")
.bind("Gr.beZgAF2Nn0-.oyaJOpeuT70@example.org")
.bind(timestamp),
)
.await
.expect("Failed to write to the database");

View File

@@ -2,6 +2,7 @@
use std::collections::HashMap;
use anyhow::Result;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use serde::Deserialize;
@@ -58,11 +59,7 @@ pub async fn dc_get_oauth2_url(
if let Some(oauth2) = Oauth2::from_address(addr).await {
if context
.sql
.set_raw_config(
context,
"oauth2_pending_redirect_uri",
Some(redirect_uri.as_ref()),
)
.set_raw_config("oauth2_pending_redirect_uri", Some(redirect_uri.as_ref()))
.await
.is_err()
{
@@ -82,31 +79,25 @@ pub async fn dc_get_oauth2_access_token(
addr: impl AsRef<str>,
code: impl AsRef<str>,
regenerate: bool,
) -> Option<String> {
) -> Result<Option<String>> {
if let Some(oauth2) = Oauth2::from_address(addr).await {
let lock = context.oauth2_mutex.lock().await;
// read generated token
if !regenerate && !is_expired(context).await {
let access_token = context
.sql
.get_raw_config(context, "oauth2_access_token")
.await;
if !regenerate && !is_expired(context).await? {
let access_token = context.sql.get_raw_config("oauth2_access_token").await?;
if access_token.is_some() {
// success
return access_token;
return Ok(access_token);
}
}
// generate new token: build & call auth url
let refresh_token = context
.sql
.get_raw_config(context, "oauth2_refresh_token")
.await;
let refresh_token = context.sql.get_raw_config("oauth2_refresh_token").await?;
let refresh_token_for = context
.sql
.get_raw_config(context, "oauth2_refresh_token_for")
.await
.get_raw_config("oauth2_refresh_token_for")
.await?
.unwrap_or_else(|| "unset".into());
let (redirect_uri, token_url, update_redirect_uri_on_success) =
@@ -115,8 +106,8 @@ pub async fn dc_get_oauth2_access_token(
(
context
.sql
.get_raw_config(context, "oauth2_pending_redirect_uri")
.await
.get_raw_config("oauth2_pending_redirect_uri")
.await?
.unwrap_or_else(|| "unset".into()),
oauth2.init_token,
true,
@@ -129,8 +120,8 @@ pub async fn dc_get_oauth2_access_token(
(
context
.sql
.get_raw_config(context, "oauth2_redirect_uri")
.await
.get_raw_config("oauth2_redirect_uri")
.await?
.unwrap_or_else(|| "unset".into()),
oauth2.refresh_token,
false,
@@ -166,7 +157,7 @@ pub async fn dc_get_oauth2_access_token(
let mut req = surf::post(post_url).build();
if let Err(err) = req.body_form(&post_param) {
warn!(context, "Error calling OAuth2 at {}: {:?}", token_url, err);
return None;
return Ok(None);
}
let client = surf::Client::new();
@@ -176,7 +167,7 @@ pub async fn dc_get_oauth2_access_token(
context,
"Failed to parse OAuth2 JSON response from {}: error: {:?}", token_url, parsed
);
return None;
return Ok(None);
}
// update refresh_token if given, typically on the first round, but we update it later as well.
@@ -184,14 +175,12 @@ pub async fn dc_get_oauth2_access_token(
if let Some(ref token) = response.refresh_token {
context
.sql
.set_raw_config(context, "oauth2_refresh_token", Some(token))
.await
.ok();
.set_raw_config("oauth2_refresh_token", Some(token))
.await?;
context
.sql
.set_raw_config(context, "oauth2_refresh_token_for", Some(code.as_ref()))
.await
.ok();
.set_raw_config("oauth2_refresh_token_for", Some(code.as_ref()))
.await?;
}
// after that, save the access token.
@@ -199,9 +188,8 @@ pub async fn dc_get_oauth2_access_token(
if let Some(ref token) = response.access_token {
context
.sql
.set_raw_config(context, "oauth2_access_token", Some(token))
.await
.ok();
.set_raw_config("oauth2_access_token", Some(token))
.await?;
let expires_in = response
.expires_in
// refresh a bit before
@@ -209,16 +197,14 @@ pub async fn dc_get_oauth2_access_token(
.unwrap_or_else(|| 0);
context
.sql
.set_raw_config_int64(context, "oauth2_timestamp_expires", expires_in)
.await
.ok();
.set_raw_config_int64("oauth2_timestamp_expires", expires_in)
.await?;
if update_redirect_uri_on_success {
context
.sql
.set_raw_config(context, "oauth2_redirect_uri", Some(redirect_uri.as_ref()))
.await
.ok();
.set_raw_config("oauth2_redirect_uri", Some(redirect_uri.as_ref()))
.await?;
}
} else {
warn!(context, "Failed to find OAuth2 access token");
@@ -226,11 +212,11 @@ pub async fn dc_get_oauth2_access_token(
drop(lock);
response.access_token
Ok(response.access_token)
} else {
warn!(context, "Internal OAuth2 error: 2");
None
Ok(None)
}
}
@@ -238,27 +224,33 @@ pub async fn dc_get_oauth2_addr(
context: &Context,
addr: impl AsRef<str>,
code: impl AsRef<str>,
) -> Option<String> {
let oauth2 = Oauth2::from_address(addr.as_ref()).await?;
oauth2.get_userinfo?;
) -> Result<Option<String>> {
let oauth2 = match Oauth2::from_address(addr.as_ref()).await {
Some(o) => o,
None => return Ok(None),
};
if oauth2.get_userinfo.is_none() {
return Ok(None);
}
if let Some(access_token) =
dc_get_oauth2_access_token(context, addr.as_ref(), code.as_ref(), false).await
dc_get_oauth2_access_token(context, addr.as_ref(), code.as_ref(), false).await?
{
let addr_out = oauth2.get_addr(context, access_token).await;
if addr_out.is_none() {
// regenerate
if let Some(access_token) = dc_get_oauth2_access_token(context, addr, code, true).await
if let Some(access_token) =
dc_get_oauth2_access_token(context, addr, code, true).await?
{
oauth2.get_addr(context, access_token).await
Ok(oauth2.get_addr(context, access_token).await)
} else {
None
Ok(None)
}
} else {
addr_out
Ok(addr_out)
}
} else {
None
Ok(None)
}
}
@@ -317,21 +309,21 @@ impl Oauth2 {
}
}
async fn is_expired(context: &Context) -> bool {
async fn is_expired(context: &Context) -> Result<bool, crate::sql::Error> {
let expire_timestamp = context
.sql
.get_raw_config_int64(context, "oauth2_timestamp_expires")
.await
.get_raw_config_int64("oauth2_timestamp_expires")
.await?
.unwrap_or_default();
if expire_timestamp <= 0 {
return false;
return Ok(false);
}
if expire_timestamp > time() {
return false;
return Ok(false);
}
true
Ok(true)
}
fn replace_in_uri(uri: impl AsRef<str>, key: impl AsRef<str>, value: impl AsRef<str>) -> String {
@@ -399,7 +391,7 @@ mod tests {
let ctx = TestContext::new().await;
let addr = "dignifiedquire@gmail.com";
let code = "fail";
let res = dc_get_oauth2_addr(&ctx.ctx, addr, code).await;
let res = dc_get_oauth2_addr(&ctx.ctx, addr, code).await.unwrap();
// this should fail as it is an invalid password
assert_eq!(res, None);
}
@@ -419,7 +411,9 @@ mod tests {
let ctx = TestContext::new().await;
let addr = "dignifiedquire@gmail.com";
let code = "fail";
let res = dc_get_oauth2_access_token(&ctx.ctx, addr, code, false).await;
let res = dc_get_oauth2_access_token(&ctx.ctx, addr, code, false)
.await
.unwrap();
// this should fail as it is an invalid password
assert_eq!(res, None);
}

View File

@@ -333,7 +333,7 @@ impl Params {
pub fn get_msg_id(&self) -> Option<MsgId> {
self.get(Param::MsgId)
.and_then(|x| x.parse::<u32>().ok())
.and_then(|x| x.parse().ok())
.map(MsgId::new)
}

View File

@@ -5,6 +5,7 @@ use std::fmt;
use anyhow::{bail, Result};
use num_traits::FromPrimitive;
use sqlx::Row;
use crate::aheader::{Aheader, EncryptPreference};
use crate::chat;
@@ -139,12 +140,15 @@ impl Peerstate {
}
pub async fn from_addr(context: &Context, addr: &str) -> Result<Option<Peerstate>> {
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
let query = sqlx::query(
"SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
verified_key, verified_key_fingerprint \
FROM acpeerstates \
WHERE addr=? COLLATE NOCASE;";
Self::from_stmt(context, query, paramsv![addr]).await
WHERE addr=? COLLATE NOCASE;",
)
.bind(addr);
Self::from_stmt(context, query).await
}
pub async fn from_fingerprint(
@@ -152,72 +156,75 @@ impl Peerstate {
_sql: &Sql,
fingerprint: &Fingerprint,
) -> Result<Option<Peerstate>> {
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
let fp = fingerprint.hex();
let query = sqlx::query(
"SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
verified_key, verified_key_fingerprint \
FROM acpeerstates \
WHERE public_key_fingerprint=? COLLATE NOCASE \
OR gossip_key_fingerprint=? COLLATE NOCASE \
ORDER BY public_key_fingerprint=? DESC;";
let fp = fingerprint.hex();
Self::from_stmt(context, query, paramsv![fp, fp, fp]).await
ORDER BY public_key_fingerprint=? DESC;",
)
.bind(&fp)
.bind(&fp)
.bind(&fp);
Self::from_stmt(context, query).await
}
async fn from_stmt(
context: &Context,
query: &str,
params: Vec<&dyn crate::ToSql>,
) -> Result<Option<Peerstate>> {
let peerstate = context
.sql
.query_row_optional(query, params, |row| {
/* all the above queries start with this: SELECT
addr, last_seen, last_seen_autocrypt, prefer_encrypted,
public_key, gossip_timestamp, gossip_key, public_key_fingerprint,
gossip_key_fingerprint, verified_key, verified_key_fingerprint
*/
async fn from_stmt<'e, 'q, E>(context: &Context, query: E) -> Result<Option<Peerstate>>
where
'q: 'e,
E: 'q + sqlx::Execute<'q, sqlx::Sqlite>,
{
if let Some(row) = context.sql.fetch_optional(query).await? {
// all the above queries start with this: SELECT
// addr, last_seen, last_seen_autocrypt, prefer_encrypted,
// public_key, gossip_timestamp, gossip_key, public_key_fingerprint,
// gossip_key_fingerprint, verified_key, verified_key_fingerprint
let res = Peerstate {
addr: row.get(0)?,
last_seen: row.get(1)?,
last_seen_autocrypt: row.get(2)?,
prefer_encrypt: EncryptPreference::from_i32(row.get(3)?).unwrap_or_default(),
public_key: row
.get(4)
.ok()
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok()),
public_key_fingerprint: row
.get::<_, Option<String>>(7)?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default(),
gossip_key: row
.get(6)
.ok()
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok()),
gossip_key_fingerprint: row
.get::<_, Option<String>>(8)?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default(),
gossip_timestamp: row.get(5)?,
verified_key: row
.get(9)
.ok()
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok()),
verified_key_fingerprint: row
.get::<_, Option<String>>(10)?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default(),
to_save: None,
fingerprint_changed: false,
};
let peerstate = Peerstate {
addr: row.try_get(0)?,
last_seen: row.try_get(1)?,
last_seen_autocrypt: row.try_get(2)?,
prefer_encrypt: EncryptPreference::from_i32(row.try_get(3)?).unwrap_or_default(),
public_key: row
.try_get::<&[u8], _>(4)
.ok()
.and_then(|blob| SignedPublicKey::from_slice(blob).ok()),
public_key_fingerprint: row
.try_get::<Option<String>, _>(7)?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default(),
gossip_key: row
.try_get::<&[u8], _>(6)
.ok()
.and_then(|blob| SignedPublicKey::from_slice(blob).ok()),
gossip_key_fingerprint: row
.try_get::<Option<String>, _>(8)?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default(),
gossip_timestamp: row.try_get(5)?,
verified_key: row
.try_get::<&[u8], _>(9)
.ok()
.and_then(|blob| SignedPublicKey::from_slice(blob).ok()),
verified_key_fingerprint: row
.try_get::<Option<String>, _>(10)?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default(),
to_save: None,
fingerprint_changed: false,
};
Ok(res)
})
.await?;
Ok(peerstate)
Ok(Some(peerstate))
} else {
Ok(None)
}
}
pub fn recalc_fingerprint(&mut self) {
@@ -266,9 +273,8 @@ impl Peerstate {
if self.fingerprint_changed {
if let Some(contact_id) = context
.sql
.query_get_value_result(
"SELECT id FROM contacts WHERE addr=?;",
paramsv![self.addr],
.query_get_value(
sqlx::query("SELECT id FROM contacts WHERE addr=?;").bind(&self.addr),
)
.await?
{
@@ -429,42 +435,59 @@ impl Peerstate {
pub async fn save_to_db(&self, sql: &Sql, create: bool) -> crate::sql::Result<()> {
if self.to_save == Some(ToSave::All) || create {
sql.execute(
if create {
"INSERT INTO acpeerstates (last_seen, last_seen_autocrypt, prefer_encrypted, \
public_key, gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
verified_key, verified_key_fingerprint, addr \
) VALUES(?,?,?,?,?,?,?,?,?,?,?)"
(if create {
sqlx::query(
"INSERT INTO acpeerstates ( \
last_seen, \
last_seen_autocrypt, \
prefer_encrypted, \
public_key, \
gossip_timestamp, \
gossip_key, \
public_key_fingerprint, \
gossip_key_fingerprint, \
verified_key, \
verified_key_fingerprint, \
addr \
) VALUES(?,?,?,?,?,?,?,?,?,?,?)",
)
} else {
"UPDATE acpeerstates \
SET last_seen=?, last_seen_autocrypt=?, prefer_encrypted=?, \
public_key=?, gossip_timestamp=?, gossip_key=?, public_key_fingerprint=?, gossip_key_fingerprint=?, \
verified_key=?, verified_key_fingerprint=? \
WHERE addr=?"
},
paramsv![
self.last_seen,
self.last_seen_autocrypt,
self.prefer_encrypt as i64,
self.public_key.as_ref().map(|k| k.to_bytes()),
self.gossip_timestamp,
self.gossip_key.as_ref().map(|k| k.to_bytes()),
self.public_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.verified_key.as_ref().map(|k| k.to_bytes()),
self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.addr,
],
).await?;
sqlx::query(
"UPDATE acpeerstates \
SET last_seen=?, \
last_seen_autocrypt=?, \
prefer_encrypted=?, \
public_key=?, \
gossip_timestamp=?, \
gossip_key=?, \
public_key_fingerprint=?, \
gossip_key_fingerprint=?, \
verified_key=?, \
verified_key_fingerprint=? \
WHERE addr=?",
)
})
.bind(self.last_seen)
.bind(self.last_seen_autocrypt)
.bind(self.prefer_encrypt as i64)
.bind(self.public_key.as_ref().map(|k| k.to_bytes()))
.bind(self.gossip_timestamp)
.bind(self.gossip_key.as_ref().map(|k| k.to_bytes()))
.bind(self.public_key_fingerprint.as_ref().map(|fp| fp.hex()))
.bind(self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()))
.bind(self.verified_key.as_ref().map(|k| k.to_bytes()))
.bind(self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()))
.bind(&self.addr),
)
.await?;
} else if self.to_save == Some(ToSave::Timestamps) {
sql.execute(
"UPDATE acpeerstates SET last_seen=?, last_seen_autocrypt=?, gossip_timestamp=? \
WHERE addr=?;",
paramsv![
self.last_seen,
self.last_seen_autocrypt,
self.gossip_timestamp,
self.addr
],
sqlx::query("UPDATE acpeerstates SET last_seen=?, last_seen_autocrypt=?, gossip_timestamp=? \
WHERE addr=?;").bind(
self.last_seen).bind(
self.last_seen_autocrypt).bind(
self.gossip_timestamp).bind(
&self.addr)
)
.await?;
}
@@ -481,12 +504,6 @@ impl Peerstate {
}
}
impl From<crate::key::FingerprintError> for rusqlite::Error {
fn from(_source: crate::key::FingerprintError) -> Self {
Self::InvalidColumnType(0, "Invalid fingerprint".into(), rusqlite::types::Type::Text)
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -619,7 +636,7 @@ mod tests {
// can be loaded without errors.
ctx.ctx
.sql
.execute("INSERT INTO acpeerstates (addr) VALUES(?)", paramsv![addr])
.execute(sqlx::query("INSERT INTO acpeerstates (addr) VALUES(?)").bind(addr))
.await
.expect("Failed to write to the database");

View File

@@ -791,20 +791,39 @@ mod tests {
async fn test_set_config_from_qr() {
let ctx = TestContext::new().await;
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await.is_none());
assert!(ctx
.ctx
.get_config(Config::WebrtcInstance)
.await
.unwrap()
.is_none());
let res = set_config_from_qr(&ctx.ctx, "badqr:https://example.org/").await;
assert!(!res.is_ok());
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await.is_none());
assert!(ctx
.ctx
.get_config(Config::WebrtcInstance)
.await
.unwrap()
.is_none());
let res = set_config_from_qr(&ctx.ctx, "https://no.qr").await;
assert!(!res.is_ok());
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await.is_none());
assert!(ctx
.ctx
.get_config(Config::WebrtcInstance)
.await
.unwrap()
.is_none());
let res = set_config_from_qr(&ctx.ctx, "dcwebrtc:https://example.org/").await;
assert!(res.is_ok());
assert_eq!(
ctx.ctx.get_config(Config::WebrtcInstance).await.unwrap(),
ctx.ctx
.get_config(Config::WebrtcInstance)
.await
.unwrap()
.unwrap(),
"https://example.org/"
);
@@ -812,7 +831,11 @@ mod tests {
set_config_from_qr(&ctx.ctx, "DCWEBRTC:basicwebrtc:https://foo.bar/?$ROOM&test").await;
assert!(res.is_ok());
assert_eq!(
ctx.ctx.get_config(Config::WebrtcInstance).await.unwrap(),
ctx.ctx
.get_config(Config::WebrtcInstance)
.await
.unwrap()
.unwrap(),
"basicwebrtc:https://foo.bar/?$ROOM&test"
);
}

View File

@@ -77,7 +77,11 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
Some(job) => {
// Let the fetch run, but return back to the job afterwards.
jobs_loaded = 0;
if ctx.get_config_bool(Config::InboxWatch).await {
if ctx
.get_config_bool(Config::InboxWatch)
.await
.unwrap_or_default()
{
info!(ctx, "postponing imap-job {} to run fetch...", job);
fetch(&ctx, &mut connection).await;
}
@@ -93,7 +97,11 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
maybe_add_time_based_warnings(&ctx).await;
info = if ctx.get_config_bool(Config::InboxWatch).await {
info = if ctx
.get_config_bool(Config::InboxWatch)
.await
.unwrap_or_default()
{
fetch_idle(&ctx, &mut connection, Config::ConfiguredInboxFolder).await
} else {
if let Err(err) = connection.scan_folders(&ctx).await {
@@ -121,7 +129,7 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
async fn fetch(ctx: &Context, connection: &mut Imap) {
match ctx.get_config(Config::ConfiguredInboxFolder).await {
Some(watch_folder) => {
Ok(Some(watch_folder)) => {
if let Err(err) = connection.connect_configured(ctx).await {
error_network!(ctx, "{}", err);
return;
@@ -133,16 +141,23 @@ async fn fetch(ctx: &Context, connection: &mut Imap) {
warn!(ctx, "{:#}", err);
}
}
None => {
Ok(None) => {
warn!(ctx, "Can not fetch inbox folder, not set");
connection.fake_idle(ctx, None).await;
}
Err(err) => {
warn!(
ctx,
"Can not fetch inbox folder, failed to get config: {:?}", err
);
connection.fake_idle(ctx, None).await;
}
}
}
async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> InterruptInfo {
match ctx.get_config(folder).await {
Some(watch_folder) => {
Ok(Some(watch_folder)) => {
// connect and fake idle if unable to connect
if let Err(err) = connection.connect_configured(ctx).await {
warn!(ctx, "imap connection failed: {}", err);
@@ -178,10 +193,17 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
connection.fake_idle(ctx, Some(watch_folder)).await
}
}
None => {
Ok(None) => {
warn!(ctx, "Can not watch {} folder, not set", folder);
connection.fake_idle(ctx, None).await
}
Err(err) => {
warn!(
ctx,
"Can not watch {} folder, failed to retrieve config: {:?}", folder, err
);
connection.fake_idle(ctx, None).await
}
}
}
@@ -299,7 +321,11 @@ impl Scheduler {
}))
};
if ctx.get_config_bool(Config::MvboxWatch).await {
if ctx
.get_config_bool(Config::MvboxWatch)
.await
.unwrap_or_default()
{
let ctx = ctx.clone();
mvbox_handle = Some(task::spawn(async move {
simple_imap_loop(
@@ -317,7 +343,11 @@ impl Scheduler {
.expect("mvbox start send, missing receiver");
}
if ctx.get_config_bool(Config::SentboxWatch).await {
if ctx
.get_config_bool(Config::SentboxWatch)
.await
.unwrap_or_default()
{
let ctx = ctx.clone();
sentbox_handle = Some(task::spawn(async move {
simple_imap_loop(

View File

@@ -191,7 +191,7 @@ impl BobState {
let chat_id = chat::create_by_contact_id(context, invite.contact_id())
.await
.map_err(JoinError::UnknownContact)?;
if fingerprint_equals_sender(context, invite.fingerprint(), chat_id).await {
if fingerprint_equals_sender(context, invite.fingerprint(), chat_id).await? {
// The scanned fingerprint matches Alice's key, we can proceed to step 4b.
info!(context, "Taking securejoin protocol shortcut");
let state = Self {
@@ -300,7 +300,7 @@ impl BobState {
self.next = SecureJoinStep::Terminated;
return Ok(Some(BobHandshakeStage::Terminated(reason)));
}
if !fingerprint_equals_sender(context, self.invite.fingerprint(), self.chat_id).await {
if !fingerprint_equals_sender(context, self.invite.fingerprint(), self.chat_id).await? {
self.next = SecureJoinStep::Terminated;
return Ok(Some(BobHandshakeStage::Terminated("Fingerprint mismatch")));
}

View File

@@ -173,9 +173,16 @@ pub async fn dc_get_securejoin_qr(context: &Context, group: Option<ChatId>) -> O
let invitenumber = token::lookup_or_new(context, token::Namespace::InviteNumber, group).await;
let auth = token::lookup_or_new(context, token::Namespace::Auth, group).await;
let self_addr = match context.get_config(Config::ConfiguredAddr).await {
Some(addr) => addr,
None => {
error!(context, "Not configured, cannot generate QR code.",);
Ok(Some(addr)) => addr,
Ok(None) => {
error!(context, "Not configured, cannot generate QR code.");
return None;
}
Err(err) => {
error!(
context,
"Unable to retrieve configuration, cannot generate QR code: {:?}", err
);
return None;
}
};
@@ -183,6 +190,7 @@ pub async fn dc_get_securejoin_qr(context: &Context, group: Option<ChatId>) -> O
let self_name = context
.get_config(Config::Displayname)
.await
.ok()?
.unwrap_or_default();
let fingerprint: Fingerprint = match get_self_fingerprint(context).await {
@@ -263,6 +271,8 @@ pub enum JoinError {
MissingChat(#[source] sql::Error),
#[error("Ongoing sender dropped (this is a bug)")]
OngoingSenderDropped,
#[error("Other")]
Other(#[from] anyhow::Error),
}
/// Take a scanned QR-code and do the setup-contact/join-group/invite handshake.
@@ -290,6 +300,7 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
info!(context, "Requesting secure-join ...",);
let qr_scan = check_qr(context, &qr).await;
let invite = QrInvite::try_from(qr_scan)?;
match context.bob.start_protocol(context, invite.clone()).await? {
@@ -390,11 +401,11 @@ async fn send_handshake_msg(
Ok(())
}
async fn chat_id_2_contact_id(context: &Context, contact_chat_id: ChatId) -> u32 {
if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await[..] {
contact_id
async fn chat_id_2_contact_id(context: &Context, contact_chat_id: ChatId) -> Result<u32, Error> {
if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await?[..] {
Ok(contact_id)
} else {
0
Ok(0)
}
}
@@ -402,8 +413,8 @@ async fn fingerprint_equals_sender(
context: &Context,
fingerprint: &Fingerprint,
contact_chat_id: ChatId,
) -> bool {
if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await[..] {
) -> Result<bool, Error> {
if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await?[..] {
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
let peerstate = match Peerstate::from_addr(context, contact.get_addr()).await {
Ok(peerstate) => peerstate,
@@ -414,7 +425,7 @@ async fn fingerprint_equals_sender(
contact.get_addr(),
err
);
return false;
return Ok(false);
}
};
@@ -422,12 +433,12 @@ async fn fingerprint_equals_sender(
if peerstate.public_key_fingerprint.is_some()
&& fingerprint == peerstate.public_key_fingerprint.as_ref().unwrap()
{
return true;
return Ok(true);
}
}
}
}
false
Ok(false)
}
/// What to do with a Secure-Join handshake message after it was handled.
@@ -552,7 +563,7 @@ pub(crate) async fn handle_securejoin_handshake(
Some(mut bobstate) => match bobstate.handle_message(context, mime_message).await {
Some(BobHandshakeStage::Terminated(why)) => {
could_not_establish_secure_connection(context, bobstate.chat_id(), why)
.await;
.await?;
Ok(HandshakeMessage::Done)
}
Some(_stage) => {
@@ -581,7 +592,7 @@ pub(crate) async fn handle_securejoin_handshake(
contact_chat_id,
"Fingerprint not provided.",
)
.await;
.await?;
return Ok(HandshakeMessage::Ignore);
}
};
@@ -591,16 +602,16 @@ pub(crate) async fn handle_securejoin_handshake(
contact_chat_id,
"Auth not encrypted.",
)
.await;
.await?;
return Ok(HandshakeMessage::Ignore);
}
if !fingerprint_equals_sender(context, &fingerprint, contact_chat_id).await {
if !fingerprint_equals_sender(context, &fingerprint, contact_chat_id).await? {
could_not_establish_secure_connection(
context,
contact_chat_id,
"Fingerprint mismatch on inviter-side.",
)
.await;
.await?;
return Ok(HandshakeMessage::Ignore);
}
info!(context, "Fingerprint verified.",);
@@ -613,13 +624,13 @@ pub(crate) async fn handle_securejoin_handshake(
contact_chat_id,
"Auth not provided.",
)
.await;
.await?;
return Ok(HandshakeMessage::Ignore);
}
};
if !token::exists(context, token::Namespace::Auth, auth_0).await {
could_not_establish_secure_connection(context, contact_chat_id, "Auth invalid.")
.await;
.await?;
return Ok(HandshakeMessage::Ignore);
}
if mark_peer_as_verified(context, &fingerprint).await.is_err() {
@@ -628,12 +639,12 @@ pub(crate) async fn handle_securejoin_handshake(
contact_chat_id,
"Fingerprint mismatch on inviter-side.",
)
.await;
.await?;
return Ok(HandshakeMessage::Ignore);
}
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited).await;
info!(context, "Auth verified.",);
secure_connection_established(context, contact_chat_id).await;
secure_connection_established(context, contact_chat_id).await?;
emit_event!(context, EventType::ContactsChanged(Some(contact_id)));
inviter_progress!(context, contact_id, 600);
if join_vg {
@@ -693,12 +704,12 @@ pub(crate) async fn handle_securejoin_handshake(
Some(mut bobstate) => match bobstate.handle_message(context, mime_message).await {
Some(BobHandshakeStage::Terminated(why)) => {
could_not_establish_secure_connection(context, bobstate.chat_id(), why)
.await;
.await?;
Ok(HandshakeMessage::Done)
}
Some(BobHandshakeStage::Completed) => {
// Can only be BobHandshakeStage::Completed
secure_connection_established(context, bobstate.chat_id()).await;
secure_connection_established(context, bobstate.chat_id()).await?;
Ok(retval)
}
Some(_) => {
@@ -812,7 +823,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
contact_chat_id,
"Message not encrypted correctly.",
)
.await;
.await?;
return Ok(HandshakeMessage::Ignore);
}
let fingerprint: Fingerprint = match mime_message.get(HeaderDef::SecureJoinFingerprint)
@@ -824,7 +835,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
contact_chat_id,
"Fingerprint not provided, please update Delta Chat on all your devices.",
)
.await;
.await?;
return Ok(HandshakeMessage::Ignore);
}
};
@@ -834,7 +845,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
contact_chat_id,
format!("Fingerprint mismatch on observing {}.", step).as_ref(),
)
.await;
.await?;
return Ok(HandshakeMessage::Ignore);
}
Ok(if step.as_str() == "vg-member-added" {
@@ -847,8 +858,11 @@ pub(crate) async fn observe_securejoin_on_other_device(
}
}
async fn secure_connection_established(context: &Context, contact_chat_id: ChatId) {
let contact_id: u32 = chat_id_2_contact_id(context, contact_chat_id).await;
async fn secure_connection_established(
context: &Context,
contact_chat_id: ChatId,
) -> Result<(), Error> {
let contact_id = chat_id_2_contact_id(context, contact_chat_id).await?;
let contact = Contact::get_by_id(context, contact_id).await;
let addr = if let Ok(ref contact) = contact {
@@ -860,14 +874,16 @@ async fn secure_connection_established(context: &Context, contact_chat_id: ChatI
chat::add_info_msg(context, contact_chat_id, msg).await;
emit_event!(context, EventType::ChatModified(contact_chat_id));
info!(context, "StockMessage::ContactVerified posted to 1:1 chat");
Ok(())
}
async fn could_not_establish_secure_connection(
context: &Context,
contact_chat_id: ChatId,
details: &str,
) {
let contact_id = chat_id_2_contact_id(context, contact_chat_id).await;
) -> Result<(), Error> {
let contact_id = chat_id_2_contact_id(context, contact_chat_id).await?;
let contact = Contact::get_by_id(context, contact_id).await;
let msg = stock_str::contact_not_verified(
context,
@@ -884,6 +900,8 @@ async fn could_not_establish_secure_connection(
context,
"StockMessage::ContactNotVerified posted to 1:1 chat ({})", details
);
Ok(())
}
async fn mark_peer_as_verified(context: &Context, fingerprint: &Fingerprint) -> Result<(), Error> {
@@ -1061,6 +1079,7 @@ mod tests {
let chat = alice.create_chat(&bob).await;
let msg_id = chat::get_chat_msgs(&alice.ctx, chat.get_id(), 0x1, None)
.await
.unwrap()
.into_iter()
.filter_map(|item| match item {
chat::ChatItem::Message { msg_id } => Some(msg_id),
@@ -1109,6 +1128,7 @@ mod tests {
let chat = bob.create_chat(&alice).await;
let msg_id = chat::get_chat_msgs(&bob.ctx, chat.get_id(), 0x1, None)
.await
.unwrap()
.into_iter()
.filter_map(|item| match item {
chat::ChatItem::Message { msg_id } => Some(msg_id),

View File

@@ -22,25 +22,24 @@ const SMTP_TIMEOUT: u64 = 30;
pub enum Error {
#[error("Bad parameters")]
BadParameters,
#[error("Invalid login address {address}: {error}")]
InvalidLoginAddress {
address: String,
#[source]
error: error::Error,
},
#[error("SMTP: failed to connect: {0}")]
ConnectionFailure(#[source] smtp::error::Error),
#[error("SMTP: failed to setup connection {0:?}")]
ConnectionSetupFailure(#[source] smtp::error::Error),
#[error("SMTP: oauth2 error {address}")]
Oauth2Error { address: String },
#[error("TLS error")]
#[error("TLS error {0}")]
Tls(#[from] async_native_tls::Error),
#[error("Sql {0}")]
Sql(#[from] crate::sql::Error),
#[error("{0}")]
Other(#[from] anyhow::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
@@ -100,7 +99,7 @@ impl Smtp {
return Ok(());
}
let lp = LoginParam::from_database(context, "configured_").await;
let lp = LoginParam::from_database(context, "configured_").await?;
let res = self
.connect(
context,
@@ -164,7 +163,7 @@ impl Smtp {
let (creds, mechanism) = if oauth2 {
// oauth2
let send_pw = &lp.password;
let access_token = dc_get_oauth2_access_token(context, addr, send_pw, false).await;
let access_token = dc_get_oauth2_access_token(context, addr, send_pw, false).await?;
if access_token.is_none() {
return Err(Error::Oauth2Error {
address: addr.to_string(),

View File

@@ -15,12 +15,12 @@ pub type Result<T> = std::result::Result<T, Error>;
pub enum Error {
#[error("Envelope error: {}", _0)]
EnvelopeError(#[from] async_smtp::error::Error),
#[error("Send error: {}", _0)]
SendError(#[from] async_smtp::smtp::error::Error),
#[error("SMTP has no transport")]
NoTransport,
#[error("{}", _0)]
Other(#[from] anyhow::Error),
}
impl Smtp {
@@ -36,7 +36,7 @@ impl Smtp {
let message_len_bytes = message.len();
let mut chunk_size = DEFAULT_MAX_SMTP_RCPT_TO;
if let Some(provider) = context.get_configured_provider().await {
if let Some(provider) = context.get_configured_provider().await? {
if let Some(max_smtp_rcpt_to) = provider.max_smtp_rcpt_to {
chunk_size = max_smtp_rcpt_to as usize;
}

1665
src/sql.rs

File diff suppressed because it is too large Load Diff

19
src/sql/error.rs Normal file
View File

@@ -0,0 +1,19 @@
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Sqlx: {0:?}")]
Sqlx(#[from] sqlx::Error),
#[error("Sqlite: Connection closed")]
SqlNoConnection,
#[error("Sqlite: Already open")]
SqlAlreadyOpen,
#[error("Sqlite: Failed to open")]
SqlFailedToOpen,
#[error("{0}")]
Io(#[from] std::io::Error),
// #[error("{0:?}")]
// BlobError(#[from] crate::blob::BlobError),
#[error("{0}")]
Other(#[from] anyhow::Error),
}
pub type Result<T> = std::result::Result<T, Error>;

505
src/sql/migrations.rs Normal file
View File

@@ -0,0 +1,505 @@
use async_std::prelude::*;
use super::{Result, Sql};
use crate::config::Config;
use crate::constants::ShowEmails;
use crate::context::Context;
use crate::dc_tools::EmailAddress;
use crate::imap;
use crate::provider::get_provider_by_domain;
const DBVERSION: i32 = 68;
const VERSION_CFG: &str = "dbversion";
const TABLES: &str = include_str!("./tables.sql");
pub async fn run(context: &Context, sql: &Sql) -> Result<(bool, bool, bool)> {
let mut recalc_fingerprints = false;
let mut exists_before_update = false;
let mut dbversion_before_update = DBVERSION;
if !sql.table_exists("config").await? {
info!(context, "First time init: creating tables",);
sql.transaction(move |conn| {
Box::pin(async move {
sqlx::query(TABLES)
.execute_many(&mut *conn)
.await
.collect::<std::result::Result<Vec<_>, _>>()
.await?;
// set raw config inside the transaction
sqlx::query("INSERT INTO config (keyname, value) VALUES (?, ?);")
.bind(VERSION_CFG)
.bind(format!("{}", dbversion_before_update))
.execute(&mut *conn)
.await?;
Ok(())
})
})
.await?;
} else {
exists_before_update = true;
dbversion_before_update = sql
.get_raw_config_int(VERSION_CFG)
.await?
.unwrap_or_default();
}
let dbversion = dbversion_before_update;
let mut update_icons = !exists_before_update;
let mut disable_server_delete = false;
if dbversion < 1 {
info!(context, "[migration] v1");
sql.execute_migration(
r#"
CREATE TABLE leftgrps ( id INTEGER PRIMARY KEY, grpid TEXT DEFAULT '');
CREATE INDEX leftgrps_index1 ON leftgrps (grpid);"#,
1,
)
.await?;
}
if dbversion < 2 {
info!(context, "[migration] v2");
sql.execute_migration(
"ALTER TABLE contacts ADD COLUMN authname TEXT DEFAULT '';",
2,
)
.await?;
}
if dbversion < 7 {
info!(context, "[migration] v7");
sql.execute_migration(
"CREATE TABLE keypairs (\
id INTEGER PRIMARY KEY, \
addr TEXT DEFAULT '' COLLATE NOCASE, \
is_default INTEGER DEFAULT 0, \
private_key, \
public_key, \
created INTEGER DEFAULT 0);",
7,
)
.await?;
}
if dbversion < 10 {
info!(context, "[migration] v10");
sql.execute_migration(
"CREATE TABLE acpeerstates (\
id INTEGER PRIMARY KEY, \
addr TEXT DEFAULT '' COLLATE NOCASE, \
last_seen INTEGER DEFAULT 0, \
last_seen_autocrypt INTEGER DEFAULT 0, \
public_key, \
prefer_encrypted INTEGER DEFAULT 0); \
CREATE INDEX acpeerstates_index1 ON acpeerstates (addr);",
10,
)
.await?;
}
if dbversion < 12 {
info!(context, "[migration] v12");
sql.execute_migration(
r#"
CREATE TABLE msgs_mdns ( msg_id INTEGER, contact_id INTEGER);
CREATE INDEX msgs_mdns_index1 ON msgs_mdns (msg_id);"#,
12,
)
.await?;
}
if dbversion < 17 {
info!(context, "[migration] v17");
sql.execute_migration(
r#"
ALTER TABLE chats ADD COLUMN archived INTEGER DEFAULT 0;
CREATE INDEX chats_index2 ON chats (archived);
-- 'starred' column is not used currently
-- (dropping is not easily doable and stop adding it will make reusing it complicated)
ALTER TABLE msgs ADD COLUMN starred INTEGER DEFAULT 0;
CREATE INDEX msgs_index5 ON msgs (starred);"#,
17,
)
.await?;
}
if dbversion < 18 {
info!(context, "[migration] v18");
sql.execute_migration(
r#"
ALTER TABLE acpeerstates ADD COLUMN gossip_timestamp INTEGER DEFAULT 0;
ALTER TABLE acpeerstates ADD COLUMN gossip_key;"#,
18,
)
.await?;
}
if dbversion < 27 {
info!(context, "[migration] v27");
// chat.id=1 and chat.id=2 are the old deaddrops,
// the current ones are defined by chats.blocked=2
sql.execute_migration(
r#"
DELETE FROM msgs WHERE chat_id=1 OR chat_id=2;"
CREATE INDEX chats_contacts_index2 ON chats_contacts (contact_id);"
ALTER TABLE msgs ADD COLUMN timestamp_sent INTEGER DEFAULT 0;")
ALTER TABLE msgs ADD COLUMN timestamp_rcvd INTEGER DEFAULT 0;"#,
27,
)
.await?;
}
if dbversion < 34 {
info!(context, "[migration] v34");
sql.execute_migration(
r#"
ALTER TABLE msgs ADD COLUMN hidden INTEGER DEFAULT 0;
ALTER TABLE msgs_mdns ADD COLUMN timestamp_sent INTEGER DEFAULT 0;
ALTER TABLE acpeerstates ADD COLUMN public_key_fingerprint TEXT DEFAULT '';
ALTER TABLE acpeerstates ADD COLUMN gossip_key_fingerprint TEXT DEFAULT '';
CREATE INDEX acpeerstates_index3 ON acpeerstates (public_key_fingerprint);
CREATE INDEX acpeerstates_index4 ON acpeerstates (gossip_key_fingerprint);"#,
34,
)
.await?;
recalc_fingerprints = true;
}
if dbversion < 39 {
info!(context, "[migration] v39");
sql.execute_migration(
r#"
CREATE TABLE tokens (
id INTEGER PRIMARY KEY,
namespc INTEGER DEFAULT 0,
foreign_id INTEGER DEFAULT 0,
token TEXT DEFAULT '',
timestamp INTEGER DEFAULT 0
);
ALTER TABLE acpeerstates ADD COLUMN verified_key;
ALTER TABLE acpeerstates ADD COLUMN verified_key_fingerprint TEXT DEFAULT '';
CREATE INDEX acpeerstates_index5 ON acpeerstates (verified_key_fingerprint);"#,
38,
)
.await?;
}
if dbversion < 40 {
info!(context, "[migration] v40");
sql.execute_migration("ALTER TABLE jobs ADD COLUMN thread INTEGER DEFAULT 0;", 40)
.await?;
}
if dbversion < 44 {
info!(context, "[migration] v44");
sql.execute_migration("ALTER TABLE msgs ADD COLUMN mime_headers TEXT;", 44)
.await?;
}
if dbversion < 46 {
info!(context, "[migration] v46");
sql.execute_migration(
r#"
ALTER TABLE msgs ADD COLUMN mime_in_reply_to TEXT;
ALTER TABLE msgs ADD COLUMN mime_references TEXT;"#,
46,
)
.await?;
}
if dbversion < 47 {
info!(context, "[migration] v47");
sql.execute_migration("ALTER TABLE jobs ADD COLUMN tries INTEGER DEFAULT 0;", 47)
.await?;
}
if dbversion < 48 {
info!(context, "[migration] v48");
// NOTE: move_state is not used anymore
sql.execute_migration(
"ALTER TABLE msgs ADD COLUMN move_state INTEGER DEFAULT 1;",
48,
)
.await?;
}
if dbversion < 49 {
info!(context, "[migration] v49");
sql.execute_migration(
"ALTER TABLE chats ADD COLUMN gossiped_timestamp INTEGER DEFAULT 0;",
49,
)
.await?;
}
if dbversion < 50 {
info!(context, "[migration] v50");
// installations <= 0.100.1 used DC_SHOW_EMAILS_ALL implicitly;
// keep this default and use DC_SHOW_EMAILS_NO
// only for new installations
if exists_before_update {
sql.set_raw_config_int("show_emails", ShowEmails::All as i32)
.await?;
}
sql.set_db_version(50).await?;
}
if dbversion < 53 {
info!(context, "[migration] v53");
// the messages containing _only_ locations
// are also added to the database as _hidden_.
sql.execute_migration(
r#"
CREATE TABLE locations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
latitude REAL DEFAULT 0.0,
longitude REAL DEFAULT 0.0,
accuracy REAL DEFAULT 0.0,
timestamp INTEGER DEFAULT 0,
chat_id INTEGER DEFAULT 0,
from_id INTEGER DEFAULT 0
);"
CREATE INDEX locations_index1 ON locations (from_id);
CREATE INDEX locations_index2 ON locations (timestamp);
ALTER TABLE chats ADD COLUMN locations_send_begin INTEGER DEFAULT 0;
ALTER TABLE chats ADD COLUMN locations_send_until INTEGER DEFAULT 0;
ALTER TABLE chats ADD COLUMN locations_last_sent INTEGER DEFAULT 0;
CREATE INDEX chats_index3 ON chats (locations_send_until);"#,
53,
)
.await?;
}
if dbversion < 54 {
info!(context, "[migration] v54");
sql.execute_migration(
r#"
ALTER TABLE msgs ADD COLUMN location_id INTEGER DEFAULT 0;
CREATE INDEX msgs_index6 ON msgs (location_id);"#,
54,
)
.await?;
}
if dbversion < 55 {
info!(context, "[migration] v55");
sql.execute_migration(
"ALTER TABLE locations ADD COLUMN independent INTEGER DEFAULT 0;",
55,
)
.await?;
}
if dbversion < 59 {
info!(context, "[migration] v59");
// records in the devmsglabels are kept when the message is deleted.
// so, msg_id may or may not exist.
sql.execute_migration(
r#"
CREATE TABLE devmsglabels (id INTEGER PRIMARY KEY AUTOINCREMENT, label TEXT, msg_id INTEGER DEFAULT 0);",
CREATE INDEX devmsglabels_index1 ON devmsglabels (label);"#, 59)
.await?;
if exists_before_update && sql.get_raw_config_int("bcc_self").await?.is_none() {
sql.set_raw_config_int("bcc_self", 1).await?;
}
}
if dbversion < 60 {
info!(context, "[migration] v60");
sql.execute_migration(
"ALTER TABLE chats ADD COLUMN created_timestamp INTEGER DEFAULT 0;",
60,
)
.await?;
}
if dbversion < 61 {
info!(context, "[migration] v61");
sql.execute_migration(
"ALTER TABLE contacts ADD COLUMN selfavatar_sent INTEGER DEFAULT 0;",
61,
)
.await?;
update_icons = true;
}
if dbversion < 62 {
info!(context, "[migration] v62");
sql.execute_migration(
"ALTER TABLE chats ADD COLUMN muted_until INTEGER DEFAULT 0;",
62,
)
.await?;
}
if dbversion < 63 {
info!(context, "[migration] v63");
sql.execute_migration("UPDATE chats SET grpid='' WHERE type=100", 63)
.await?;
}
if dbversion < 64 {
info!(context, "[migration] v64");
sql.execute_migration("ALTER TABLE msgs ADD COLUMN error TEXT DEFAULT '';", 64)
.await?;
}
if dbversion < 65 {
info!(context, "[migration] v65");
sql.execute_migration(
r#"
ALTER TABLE chats ADD COLUMN ephemeral_timer INTEGER;
ALTER TABLE msgs ADD COLUMN ephemeral_timer INTEGER DEFAULT 0;
ALTER TABLE msgs ADD COLUMN ephemeral_timestamp INTEGER DEFAULT 0;"#,
65,
)
.await?;
}
if dbversion < 66 {
info!(context, "[migration] v66");
update_icons = true;
sql.set_db_version(66).await?;
}
if dbversion < 67 {
info!(context, "[migration] v67");
for prefix in &["", "configured_"] {
if let Some(server_flags) = sql
.get_raw_config_int(format!("{}server_flags", prefix))
.await?
{
let imap_socket_flags = server_flags & 0x700;
let key = format!("{}mail_security", prefix);
match imap_socket_flags {
0x100 => sql.set_raw_config_int(key, 2).await?, // STARTTLS
0x200 => sql.set_raw_config_int(key, 1).await?, // SSL/TLS
0x400 => sql.set_raw_config_int(key, 3).await?, // Plain
_ => sql.set_raw_config_int(key, 0).await?,
}
let smtp_socket_flags = server_flags & 0x70000;
let key = format!("{}send_security", prefix);
match smtp_socket_flags {
0x10000 => sql.set_raw_config_int(key, 2).await?, // STARTTLS
0x20000 => sql.set_raw_config_int(key, 1).await?, // SSL/TLS
0x40000 => sql.set_raw_config_int(key, 3).await?, // Plain
_ => sql.set_raw_config_int(key, 0).await?,
}
}
}
sql.set_db_version(67).await?;
}
if dbversion < 68 {
info!(context, "[migration] v68");
// the index is used to speed up get_fresh_msg_cnt() (see comment there for more details) and marknoticed_chat()
sql.execute_migration(
"CREATE INDEX IF NOT EXISTS msgs_index7 ON msgs (state, hidden, chat_id);",
68,
)
.await?;
}
if dbversion < 69 {
info!(context, "[migration] v69");
sql.execute_migration(
r#"
ALTER TABLE chats ADD COLUMN protected INTEGER DEFAULT 0;
-- 120=group, 130=old verified group
UPDATE chats SET protected=1, type=120 WHERE type=130;"#,
69,
)
.await?;
}
if dbversion < 71 {
info!(context, "[migration] v71");
if let Some(addr) = context.get_config(Config::ConfiguredAddr).await? {
if let Ok(domain) = addr.parse::<EmailAddress>().map(|email| email.domain) {
context
.set_config(
Config::ConfiguredProvider,
get_provider_by_domain(&domain).map(|provider| provider.id),
)
.await?;
} else {
warn!(context, "Can't parse configured address: {:?}", addr);
}
}
sql.set_db_version(71).await?;
}
if dbversion < 72 {
info!(context, "[migration] v72");
if !sql.col_exists("msgs", "mime_modified").await? {
sql.execute_migration(
r#"
ALTER TABLE msgs ADD COLUMN mime_modified INTEGER DEFAULT 0;"#,
72,
)
.await?;
}
}
if dbversion < 73 {
use Config::*;
info!(context, "[migration] v73");
sql.execute(
r#"
CREATE TABLE imap_sync (folder TEXT PRIMARY KEY, uidvalidity INTEGER DEFAULT 0, uid_next INTEGER DEFAULT 0);"#,
)
.await?;
for c in &[
ConfiguredInboxFolder,
ConfiguredSentboxFolder,
ConfiguredMvboxFolder,
] {
if let Some(folder) = context.get_config(*c).await? {
let (uid_validity, last_seen_uid) =
imap::get_config_last_seen_uid(context, &folder).await?;
if last_seen_uid > 0 {
imap::set_uid_next(context, &folder, last_seen_uid + 1).await?;
imap::set_uidvalidity(context, &folder, uid_validity).await?;
}
}
}
if exists_before_update {
disable_server_delete = true;
// Don't disable server delete if it was on by default (Nauta):
if let Some(provider) = context.get_configured_provider().await? {
if let Some(defaults) = &provider.config_defaults {
if defaults.iter().any(|d| d.key == Config::DeleteServerAfter) {
disable_server_delete = false;
}
}
}
}
sql.set_db_version(73).await?;
}
if dbversion < 74 {
info!(context, "[migration] v74");
sql.execute_migration("UPDATE contacts SET name='' WHERE name=authname", 74)
.await?;
}
if dbversion < 75 {
info!(context, "[migration] v75");
sql.execute_migration(
"ALTER TABLE contacts ADD COLUMN status TEXT DEFAULT '';",
74,
)
.await?;
}
if dbversion < 76 {
info!(context, "[migration] v76");
sql.execute_migration("ALTER TABLE msgs ADD COLUMN subject TEXT DEFAULT '';", 76)
.await?;
}
Ok((recalc_fingerprints, update_icons, disable_server_delete))
}
impl Sql {
async fn set_db_version(&self, version: i32) -> Result<()> {
self.set_raw_config_int(VERSION_CFG, version).await?;
Ok(())
}
async fn execute_migration(&self, query: &'static str, version: i32) -> Result<()> {
let query = sqlx::query(query);
self.transaction(move |conn| {
Box::pin(async move {
query
.execute_many(&mut *conn)
.await
.collect::<std::result::Result<Vec<_>, _>>()
.await?;
// set raw config inside the transaction
sqlx::query("UPDATE config SET value=? WHERE keyname=?;")
.bind(format!("{}", version))
.bind(VERSION_CFG)
.execute(&mut *conn)
.await?;
Ok(())
})
})
.await?;
Ok(())
}
}

866
src/sql/mod.rs Normal file
View File

@@ -0,0 +1,866 @@
//! # SQLite wrapper
use std::collections::HashSet;
use std::path::Path;
use std::pin::Pin;
use std::time::Duration;
use anyhow::Context as _;
use async_std::prelude::*;
use async_std::sync::RwLock;
use sqlx::{
pool::PoolOptions,
sqlite::{Sqlite, SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqliteSynchronous},
Execute, Executor, Row,
};
use crate::chat::{add_device_msg, update_device_icon, update_saved_messages_icon};
use crate::config::Config;
use crate::constants::{Viewtype, DC_CHAT_ID_TRASH};
use crate::context::Context;
use crate::dc_tools::{dc_delete_file, time};
use crate::ephemeral::start_ephemeral_timers;
use crate::message::Message;
use crate::param::{Param, Params};
use crate::peerstate::Peerstate;
use crate::stock_str;
mod error;
mod migrations;
pub use self::error::*;
/// A wrapper around the underlying Sqlite3 object.
///
/// We maintain two different pools to sqlite, on for reading, one for writing.
/// This can go away once https://github.com/launchbadge/sqlx/issues/459 is implemented.
#[derive(Debug)]
pub struct Sql {
/// Writer pool, must only have 1 connection in it.
writer: RwLock<Option<SqlitePool>>,
/// Reader pool, maintains multiple connections for reading data.
reader: RwLock<Option<SqlitePool>>,
}
impl Default for Sql {
fn default() -> Self {
Self {
writer: RwLock::new(None),
reader: RwLock::new(None),
}
}
}
impl Drop for Sql {
fn drop(&mut self) {
async_std::task::block_on(self.close());
}
}
impl Sql {
pub fn new() -> Sql {
Self::default()
}
/// Checks if there is currently a connection to the underlying Sqlite database.
pub async fn is_open(&self) -> bool {
// in read only mode the writer does not exists
self.reader.read().await.is_some()
}
/// Closes all underlying Sqlite connections.
pub async fn close(&self) {
if let Some(sql) = self.writer.write().await.take() {
sql.close().await;
}
if let Some(sql) = self.reader.write().await.take() {
sql.close().await;
}
}
async fn new_writer_pool(dbfile: impl AsRef<Path>) -> sqlx::Result<SqlitePool> {
let config = SqliteConnectOptions::new()
.journal_mode(SqliteJournalMode::Wal)
.filename(dbfile.as_ref())
.read_only(false)
.busy_timeout(Duration::from_secs(100))
.create_if_missing(true)
.statement_cache_capacity(0) // XXX workaround for https://github.com/launchbadge/sqlx/issues/1147
.synchronous(SqliteSynchronous::Normal);
PoolOptions::<Sqlite>::new()
.max_connections(1)
.after_connect(|conn| {
Box::pin(async move {
let q = r#"
PRAGMA secure_delete=on;
PRAGMA temp_store=memory; -- Avoid SQLITE_IOERR_GETTEMPPATH errors on Android
"#;
conn.execute_many(sqlx::query(q))
.collect::<std::result::Result<Vec<_>, _>>()
.await?;
Ok(())
})
})
.connect_with(config)
.await
}
async fn new_reader_pool(dbfile: impl AsRef<Path>, readonly: bool) -> sqlx::Result<SqlitePool> {
let config = SqliteConnectOptions::new()
.journal_mode(SqliteJournalMode::Wal)
.filename(dbfile.as_ref())
.read_only(readonly)
.busy_timeout(Duration::from_secs(100))
.statement_cache_capacity(0) // XXX workaround for https://github.com/launchbadge/sqlx/issues/1147
.synchronous(SqliteSynchronous::Normal);
PoolOptions::<Sqlite>::new()
.max_connections(10)
.after_connect(|conn| {
Box::pin(async move {
let q = r#"
PRAGMA temp_store=memory; -- Avoid SQLITE_IOERR_GETTEMPPATH errors on Android
PRAGMA query_only=1; -- Protect against writes even in read-write mode
"#;
conn.execute_many(sqlx::query(q))
.collect::<std::result::Result<Vec<_>, _>>()
.await?;
Ok(())
})
})
.connect_with(config)
.await
}
/// Opens the provided database and runs any necessary migrations.
/// If a database is already open, this will return an error.
pub async fn open(
&self,
context: &Context,
dbfile: impl AsRef<Path>,
readonly: bool,
) -> anyhow::Result<()> {
if self.is_open().await {
error!(
context,
"Cannot open, database \"{:?}\" already opened.",
dbfile.as_ref(),
);
return Err(Error::SqlAlreadyOpen.into());
}
// Open write pool
if !readonly {
*self.writer.write().await = Some(Self::new_writer_pool(&dbfile).await?);
}
// Open read pool
*self.reader.write().await = Some(Self::new_reader_pool(&dbfile, readonly).await?);
if !readonly {
// (1) update low-level database structure.
// this should be done before updates that use high-level objects that
// rely themselves on the low-level structure.
let (recalc_fingerprints, update_icons, disable_server_delete) =
migrations::run(context, self).await?;
// (2) updates that require high-level objects
// the structure is complete now and all objects are usable
if recalc_fingerprints {
info!(context, "[migration] recalc fingerprints");
let mut rows = self.fetch("SELECT addr FROM acpeerstates;").await?;
while let Some(row) = rows.next().await {
let row = row?;
let addr = row.try_get(0)?;
if let Some(ref mut peerstate) = Peerstate::from_addr(context, addr).await? {
peerstate.recalc_fingerprint();
peerstate.save_to_db(self, false).await?;
}
}
}
if update_icons {
update_saved_messages_icon(context).await?;
update_device_icon(context).await?;
}
if disable_server_delete {
// We now always watch all folders and delete messages there if delete_server is enabled.
// So, for people who have delete_server enabled, disable it and add a hint to the devicechat:
if context.get_config_delete_server_after().await?.is_some() {
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(stock_str::delete_server_turned_off(context).await);
add_device_msg(context, None, Some(&mut msg)).await?;
context
.set_config(Config::DeleteServerAfter, Some("0"))
.await?;
}
}
}
info!(context, "Opened {:?}.", dbfile.as_ref());
Ok(())
}
/// Execute the given query, returning the number of affected rows.
pub async fn execute<'e, 'q, E>(&self, query: E) -> Result<u64>
where
'q: 'e,
E: 'q + Execute<'q, Sqlite>,
{
let lock = self.writer.read().await;
let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?;
let rows = pool.execute(query).await?;
Ok(rows.rows_affected())
}
/// Execute many queries.
pub async fn execute_many<'e, 'q, E>(&self, query: E) -> Result<()>
where
'q: 'e,
E: 'q + Execute<'q, Sqlite>,
{
let lock = self.writer.read().await;
let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?;
pool.execute_many(query)
.collect::<sqlx::Result<Vec<_>>>()
.await?;
Ok(())
}
/// Fetch the given query.
pub async fn fetch<'e, 'q, E>(
&self,
query: E,
) -> Result<impl Stream<Item = sqlx::Result<<Sqlite as sqlx::Database>::Row>> + 'e + Send>
where
'q: 'e,
E: 'q + Execute<'q, Sqlite>,
{
let lock = self.reader.read().await;
let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?;
let rows = pool.fetch(query);
Ok(rows)
}
/// Fetch exactly one row, errors if no row is found.
pub async fn fetch_one<'e, 'q, E>(&self, query: E) -> Result<<Sqlite as sqlx::Database>::Row>
where
'q: 'e,
E: 'q + Execute<'q, Sqlite>,
{
let lock = self.reader.read().await;
let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?;
let row = pool.fetch_one(query).await?;
Ok(row)
}
/// Fetches at most one row.
pub async fn fetch_optional<'e, 'q, E>(
&self,
query: E,
) -> Result<Option<<Sqlite as sqlx::Database>::Row>>
where
'q: 'e,
E: 'q + Execute<'q, Sqlite>,
{
let lock = self.reader.read().await;
let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?;
let row = pool.fetch_optional(query).await?;
Ok(row)
}
/// Used for executing `SELECT COUNT` statements only. Returns the resulting count.
pub async fn count<'e, 'q, E>(&self, query: E) -> Result<usize>
where
'q: 'e,
E: 'q + Execute<'q, Sqlite>,
{
use std::convert::TryFrom;
let row = self.fetch_one(query).await?;
let count: i64 = row.try_get(0)?;
Ok(usize::try_from(count).map_err::<anyhow::Error, _>(Into::into)?)
}
/// Used for executing `SELECT COUNT` statements only. Returns `true`, if the count is at least
/// one, `false` otherwise.
pub async fn exists<'e, 'q, E>(&self, query: E) -> Result<bool>
where
'q: 'e,
E: 'q + Execute<'q, Sqlite>,
{
let count = self.count(query).await?;
Ok(count > 0)
}
/// Execute the function inside a transaction.
///
/// If the function returns an error, the transaction will be rolled back. If it does not return an
/// error, the transaction will be committed.
pub async fn transaction<F, R>(&self, callback: F) -> Result<R>
where
F: for<'c> FnOnce(
&'c mut sqlx::Transaction<'_, Sqlite>,
) -> Pin<Box<dyn Future<Output = Result<R>> + 'c + Send>>
+ 'static
+ Send
+ Sync,
R: Send,
{
let lock = self.writer.read().await;
let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?;
let mut transaction = pool.begin().await?;
let ret = callback(&mut transaction).await;
match ret {
Ok(ret) => {
transaction.commit().await?;
Ok(ret)
}
Err(err) => {
transaction.rollback().await?;
Err(err)
}
}
}
/// Query the database if the requested table already exists.
pub async fn table_exists(&self, name: impl AsRef<str>) -> Result<bool> {
let q = format!("PRAGMA table_info(\"{}\")", name.as_ref());
let lock = self.reader.read().await;
let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?;
let mut rows = pool.fetch(sqlx::query(&q));
if let Some(first_row) = rows.next().await {
Ok(first_row.is_ok())
} else {
Ok(false)
}
}
/// Check if a column exists in a given table.
pub async fn col_exists(
&self,
table_name: impl AsRef<str>,
col_name: impl AsRef<str>,
) -> Result<bool> {
let q = format!("PRAGMA table_info(\"{}\")", table_name.as_ref());
let lock = self.reader.read().await;
let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?;
let mut rows = pool.fetch(sqlx::query(&q));
while let Some(row) = rows.next().await {
let row = row?;
// `PRAGMA table_info` returns one row per column,
// each row containing 0=cid, 1=name, 2=type, 3=notnull, 4=dflt_value
let curr_name: &str = row.try_get(1)?;
if col_name.as_ref() == curr_name {
return Ok(true);
}
}
Ok(false)
}
/// Executes a query which is expected to return one row and one
/// column. If the query does not return a value or returns SQL
/// `NULL`, returns `Ok(None)`.
pub async fn query_get_value<'e, 'q, E, T>(&self, query: E) -> Result<Option<T>>
where
'q: 'e,
E: 'q + Execute<'q, Sqlite>,
T: for<'r> sqlx::Decode<'r, Sqlite> + sqlx::Type<Sqlite>,
{
let res = self
.fetch_optional(query)
.await?
.map(|row| row.get::<T, _>(0));
Ok(res)
}
/// Set private configuration options.
///
/// Setting `None` deletes the value. On failure an error message
/// will already have been logged.
pub async fn set_raw_config(&self, key: impl AsRef<str>, value: Option<&str>) -> Result<()> {
if !self.is_open().await {
return Err(Error::SqlNoConnection);
}
let key = key.as_ref();
if let Some(value) = value {
let exists = self
.exists(sqlx::query("SELECT COUNT(*) FROM config WHERE keyname=?;").bind(key))
.await?;
if exists {
self.execute(
sqlx::query("UPDATE config SET value=? WHERE keyname=?;")
.bind(value)
.bind(key),
)
.await?;
} else {
self.execute(
sqlx::query("INSERT INTO config (keyname, value) VALUES (?, ?);")
.bind(key)
.bind(value),
)
.await?;
}
} else {
self.execute(sqlx::query("DELETE FROM config WHERE keyname=?;").bind(key))
.await?;
}
Ok(())
}
/// Get configuration options from the database.
pub async fn get_raw_config(&self, key: impl AsRef<str>) -> Result<Option<String>> {
if !self.is_open().await || key.as_ref().is_empty() {
return Err(Error::SqlNoConnection);
}
let value = self
.query_get_value(
sqlx::query("SELECT value FROM config WHERE keyname=?;").bind(key.as_ref()),
)
.await
.context(format!("failed to fetch raw config: {}", key.as_ref()))?;
Ok(value)
}
pub async fn set_raw_config_int(&self, key: impl AsRef<str>, value: i32) -> Result<()> {
self.set_raw_config(key, Some(&format!("{}", value))).await
}
pub async fn get_raw_config_int(&self, key: impl AsRef<str>) -> Result<Option<i32>> {
self.get_raw_config(key)
.await
.map(|s| s.and_then(|s| s.parse().ok()))
}
pub async fn get_raw_config_bool(&self, key: impl AsRef<str>) -> Result<bool> {
// Not the most obvious way to encode bool as string, but it is matter
// of backward compatibility.
let res = self.get_raw_config_int(key).await?;
Ok(res.unwrap_or_default() > 0)
}
pub async fn set_raw_config_bool<T>(&self, key: T, value: bool) -> Result<()>
where
T: AsRef<str>,
{
let value = if value { Some("1") } else { None };
self.set_raw_config(key, value).await
}
pub async fn set_raw_config_int64(&self, key: impl AsRef<str>, value: i64) -> Result<()> {
self.set_raw_config(key, Some(&format!("{}", value))).await
}
pub async fn get_raw_config_int64(&self, key: impl AsRef<str>) -> Result<Option<i64>> {
self.get_raw_config(key)
.await
.map(|s| s.and_then(|r| r.parse().ok()))
}
/// Alternative to sqlite3_last_insert_rowid() which MUST NOT be used due to race conditions, see comment above.
/// the ORDER BY ensures, this function always returns the most recent id,
/// eg. if a Message-ID is split into different messages.
pub async fn get_rowid(
&self,
table: impl AsRef<str>,
field: impl AsRef<str>,
value: impl AsRef<str>,
) -> Result<i64> {
// alternative to sqlite3_last_insert_rowid() which MUST NOT be used due to race conditions, see comment above.
// the ORDER BY ensures, this function always returns the most recent id,
// eg. if a Message-ID is split into different messages.
let query = format!(
"SELECT id FROM {} WHERE {}=? ORDER BY id DESC",
table.as_ref(),
field.as_ref(),
);
self.query_get_value(sqlx::query(&query).bind(value.as_ref()))
.await
.map(|id| id.unwrap_or_default())
}
/// Fetches the rowid by restricting the rows through two different key, value settings.
pub async fn get_rowid2(
&self,
table: impl AsRef<str>,
field: impl AsRef<str>,
value: i64,
field2: impl AsRef<str>,
value2: i64,
) -> Result<i64> {
let query = format!(
"SELECT id FROM {} WHERE {}={} AND {}={} ORDER BY id DESC",
table.as_ref(),
field.as_ref(),
value,
field2.as_ref(),
value2,
);
self.query_get_value(sqlx::query(&query))
.await
.map(|id| id.unwrap_or_default())
}
}
pub async fn housekeeping(context: &Context) -> Result<()> {
if let Err(err) = crate::ephemeral::delete_expired_messages(context).await {
warn!(context, "Failed to delete expired messages: {}", err);
}
let mut files_in_use = HashSet::new();
let mut unreferenced_count = 0;
info!(context, "Start housekeeping...");
maybe_add_from_param(
&context.sql,
&mut files_in_use,
"SELECT param FROM msgs WHERE chat_id!=3 AND type!=10;",
Param::File,
)
.await?;
maybe_add_from_param(
&context.sql,
&mut files_in_use,
"SELECT param FROM jobs;",
Param::File,
)
.await?;
maybe_add_from_param(
&context.sql,
&mut files_in_use,
"SELECT param FROM chats;",
Param::ProfileImage,
)
.await?;
maybe_add_from_param(
&context.sql,
&mut files_in_use,
"SELECT param FROM contacts;",
Param::ProfileImage,
)
.await?;
let mut rows = context.sql.fetch("SELECT value FROM config;").await?;
while let Some(row) = rows.next().await {
let row: String = row?.try_get(0)?;
maybe_add_file(&mut files_in_use, row);
}
info!(context, "{} files in use.", files_in_use.len(),);
/* go through directory and delete unused files */
let p = context.get_blobdir();
match async_std::fs::read_dir(p).await {
Ok(mut dir_handle) => {
/* avoid deletion of files that are just created to build a message object */
let diff = std::time::Duration::from_secs(60 * 60);
let keep_files_newer_than = std::time::SystemTime::now().checked_sub(diff).unwrap();
while let Some(entry) = dir_handle.next().await {
if entry.is_err() {
break;
}
let entry = entry.unwrap();
let name_f = entry.file_name();
let name_s = name_f.to_string_lossy();
if is_file_in_use(&files_in_use, None, &name_s)
|| is_file_in_use(&files_in_use, Some(".increation"), &name_s)
|| is_file_in_use(&files_in_use, Some(".waveform"), &name_s)
|| is_file_in_use(&files_in_use, Some("-preview.jpg"), &name_s)
{
continue;
}
unreferenced_count += 1;
if let Ok(stats) = async_std::fs::metadata(entry.path()).await {
let recently_created =
stats.created().is_ok() && stats.created().unwrap() > keep_files_newer_than;
let recently_modified = stats.modified().is_ok()
&& stats.modified().unwrap() > keep_files_newer_than;
let recently_accessed = stats.accessed().is_ok()
&& stats.accessed().unwrap() > keep_files_newer_than;
if recently_created || recently_modified || recently_accessed {
info!(
context,
"Housekeeping: Keeping new unreferenced file #{}: {:?}",
unreferenced_count,
entry.file_name(),
);
continue;
}
}
info!(
context,
"Housekeeping: Deleting unreferenced file #{}: {:?}",
unreferenced_count,
entry.file_name()
);
let path = entry.path();
dc_delete_file(context, path).await;
}
}
Err(err) => {
warn!(
context,
"Housekeeping: Cannot open {}. ({})",
context.get_blobdir().display(),
err
);
}
}
if let Err(err) = start_ephemeral_timers(context).await {
warn!(
context,
"Housekeeping: cannot start ephemeral timers: {}", err
);
}
if let Err(err) = prune_tombstones(&context.sql).await {
warn!(
context,
"Housekeeping: Cannot prune message tombstones: {}", err
);
}
if let Err(e) = context
.set_config(Config::LastHousekeeping, Some(&time().to_string()))
.await
{
warn!(context, "Can't set config: {}", e);
}
info!(context, "Housekeeping done.");
Ok(())
}
#[allow(clippy::indexing_slicing)]
fn is_file_in_use(files_in_use: &HashSet<String>, namespc_opt: Option<&str>, name: &str) -> bool {
let name_to_check = if let Some(namespc) = namespc_opt {
let name_len = name.len();
let namespc_len = namespc.len();
if name_len <= namespc_len || !name.ends_with(namespc) {
return false;
}
&name[..name_len - namespc_len]
} else {
name
};
files_in_use.contains(name_to_check)
}
fn maybe_add_file(files_in_use: &mut HashSet<String>, file: impl AsRef<str>) {
if let Some(file) = file.as_ref().strip_prefix("$BLOBDIR/") {
files_in_use.insert(file.to_string());
}
}
async fn maybe_add_from_param(
sql: &Sql,
files_in_use: &mut HashSet<String>,
query: &str,
param_id: Param,
) -> Result<()> {
let mut rows = sql.fetch(query).await?;
while let Some(row) = rows.next().await {
let row: String = row?.try_get(0)?;
let param: Params = row.parse().unwrap_or_default();
if let Some(file) = param.get(param_id) {
maybe_add_file(files_in_use, file);
}
}
Ok(())
}
/// Removes from the database locally deleted messages that also don't
/// have a server UID.
async fn prune_tombstones(sql: &Sql) -> Result<()> {
sql.execute(
sqlx::query(
"DELETE FROM msgs \
WHERE (chat_id = ? OR hidden) \
AND server_uid = 0",
)
.bind(DC_CHAT_ID_TRASH),
)
.await?;
Ok(())
}
/// Returns the SQLite version as a string; e.g., `"3.16.2"` for version 3.16.2.
pub fn version() -> &'static str {
#[allow(unsafe_code)]
let cstr = unsafe { std::ffi::CStr::from_ptr(libsqlite3_sys::sqlite3_libversion()) };
cstr.to_str()
.expect("SQLite version string is not valid UTF8 ?!")
}
#[cfg(test)]
mod test {
use async_std::fs::File;
use crate::config::Config;
use crate::{test_utils::TestContext, Event, EventType};
use super::*;
#[test]
fn test_maybe_add_file() {
let mut files = Default::default();
maybe_add_file(&mut files, "$BLOBDIR/hello");
maybe_add_file(&mut files, "$BLOBDIR/world.txt");
maybe_add_file(&mut files, "world2.txt");
maybe_add_file(&mut files, "$BLOBDIR");
assert!(files.contains("hello"));
assert!(files.contains("world.txt"));
assert!(!files.contains("world2.txt"));
assert!(!files.contains("$BLOBDIR"));
}
#[test]
fn test_is_file_in_use() {
let mut files = Default::default();
maybe_add_file(&mut files, "$BLOBDIR/hello");
maybe_add_file(&mut files, "$BLOBDIR/world.txt");
maybe_add_file(&mut files, "world2.txt");
assert!(is_file_in_use(&files, None, "hello"));
assert!(!is_file_in_use(&files, Some(".txt"), "hello"));
assert!(is_file_in_use(&files, Some("-suffix"), "world.txt-suffix"));
}
#[async_std::test]
async fn test_table_exists() {
let t = TestContext::new().await;
assert!(t.ctx.sql.table_exists("msgs").await.unwrap());
assert!(!t.ctx.sql.table_exists("foobar").await.unwrap());
}
#[async_std::test]
async fn test_col_exists() {
let t = TestContext::new().await;
assert!(t.ctx.sql.col_exists("msgs", "mime_modified").await.unwrap());
assert!(!t.ctx.sql.col_exists("msgs", "foobar").await.unwrap());
assert!(!t.ctx.sql.col_exists("foobar", "foobar").await.unwrap());
}
#[async_std::test]
async fn test_housekeeping_db_closed() {
let t = TestContext::new().await;
let avatar_src = t.dir.path().join("avatar.png");
let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png");
File::create(&avatar_src)
.await
.unwrap()
.write_all(avatar_bytes)
.await
.unwrap();
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
.await
.unwrap();
t.add_event_sink(move |event: Event| async move {
match event.typ {
EventType::Info(s) => assert!(
!s.contains("Keeping new unreferenced file"),
"File {} was almost deleted, only reason it was kept is that it was created recently (as the tests don't run for a long time)",
s
),
EventType::Error(s) => panic!(s),
_ => {}
}
})
.await;
let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
assert_eq!(avatar_bytes, &async_std::fs::read(&a).await.unwrap()[..]);
t.sql.close().await;
housekeeping(&t).await.unwrap_err(); // housekeeping should fail as the db is closed
t.sql.open(&t, &t.get_dbfile(), false).await.unwrap();
let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
assert_eq!(avatar_bytes, &async_std::fs::read(&a).await.unwrap()[..]);
}
/// Regression test.
///
/// Previously the code checking for existence of `config` table
/// checked it with `PRAGMA table_info("config")` but did not
/// drain `SqlitePool.fetch` result, only using the first row
/// returned. As a result, prepared statement for `PRAGMA` was not
/// finalized early enough, leaving reader connection in a broken
/// state after reopening the database, when `config` table
/// existed and `PRAGMA` returned non-empty result.
///
/// Statements were not finalized due to a bug in sqlx:
/// https://github.com/launchbadge/sqlx/issues/1147
#[async_std::test]
async fn test_db_reopen() -> Result<()> {
use tempfile::tempdir;
// The context is used only for logging.
let t = TestContext::new().await;
// Create a separate empty database for testing.
let dir = tempdir()?;
let dbfile = dir.path().join("testdb.sqlite");
let sql = Sql::new();
// Create database with all the tables.
sql.open(&t, &dbfile, false).await.unwrap();
sql.close().await;
// Reopen the database
sql.open(&t, &dbfile, false).await?;
sql.execute(
sqlx::query("INSERT INTO config (keyname, value) VALUES (?, ?);")
.bind("foo")
.bind("bar"),
)
.await?;
let value: Option<String> = sql
.query_get_value(sqlx::query("SELECT value FROM config WHERE keyname=?;").bind("foo"))
.await?;
assert_eq!(value.unwrap(), "bar");
Ok(())
}
}

185
src/sql/tables.sql Normal file
View File

@@ -0,0 +1,185 @@
CREATE TABLE config (
id INTEGER PRIMARY KEY,
keyname TEXT,
value TEXT
);
CREATE INDEX config_index1 ON config (keyname);
CREATE TABLE contacts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT DEFAULT '',
addr TEXT DEFAULT '' COLLATE NOCASE,
origin INTEGER DEFAULT 0,
blocked INTEGER DEFAULT 0,
last_seen INTEGER DEFAULT 0,
param TEXT DEFAULT '',
authname TEXT DEFAULT '',
selfavatar_sent INTEGER DEFAULT 0
);
CREATE INDEX contacts_index1 ON contacts (name COLLATE NOCASE);
CREATE INDEX contacts_index2 ON contacts (addr COLLATE NOCASE);
INSERT INTO contacts (id,name,origin) VALUES
(1,'self',262144), (2,'info',262144), (3,'rsvd',262144),
(4,'rsvd',262144), (5,'device',262144), (6,'rsvd',262144),
(7,'rsvd',262144), (8,'rsvd',262144), (9,'rsvd',262144);
CREATE TABLE chats (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type INTEGER DEFAULT 0,
name TEXT DEFAULT '',
draft_timestamp INTEGER DEFAULT 0,
draft_txt TEXT DEFAULT '',
blocked INTEGER DEFAULT 0,
grpid TEXT DEFAULT '',
param TEXT DEFAULT '',
archived INTEGER DEFAULT 0,
gossiped_timestamp INTEGER DEFAULT 0,
locations_send_begin INTEGER DEFAULT 0,
locations_send_until INTEGER DEFAULT 0,
locations_last_sent INTEGER DEFAULT 0,
created_timestamp INTEGER DEFAULT 0,
muted_until INTEGER DEFAULT 0,
ephemeral_timer INTEGER
);
CREATE INDEX chats_index1 ON chats (grpid);
CREATE INDEX chats_index2 ON chats (archived);
CREATE INDEX chats_index3 ON chats (locations_send_until);
INSERT INTO chats (id,type,name) VALUES
(1,120,'deaddrop'), (2,120,'rsvd'), (3,120,'trash'),
(4,120,'msgs_in_creation'), (5,120,'starred'), (6,120,'archivedlink'),
(7,100,'rsvd'), (8,100,'rsvd'), (9,100,'rsvd');
CREATE TABLE chats_contacts (chat_id INTEGER, contact_id INTEGER);
CREATE INDEX chats_contacts_index1 ON chats_contacts (chat_id);
CREATE INDEX chats_contacts_index2 ON chats_contacts (contact_id);
CREATE TABLE msgs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
rfc724_mid TEXT DEFAULT '',
server_folder TEXT DEFAULT '',
server_uid INTEGER DEFAULT 0,
chat_id INTEGER DEFAULT 0,
from_id INTEGER DEFAULT 0,
to_id INTEGER DEFAULT 0,
timestamp INTEGER DEFAULT 0,
type INTEGER DEFAULT 0,
state INTEGER DEFAULT 0,
msgrmsg INTEGER DEFAULT 1,
bytes INTEGER DEFAULT 0,
txt TEXT DEFAULT '',
txt_raw TEXT DEFAULT '',
param TEXT DEFAULT '',
starred INTEGER DEFAULT 0,
timestamp_sent INTEGER DEFAULT 0,
timestamp_rcvd INTEGER DEFAULT 0,
hidden INTEGER DEFAULT 0,
mime_headers TEXT,
mime_in_reply_to TEXT,
mime_references TEXT,
move_state INTEGER DEFAULT 1,
location_id INTEGER DEFAULT 0,
error TEXT DEFAULT '',
-- Timer value in seconds. For incoming messages this
-- timer starts when message is read, so we want to have
-- the value stored here until the timer starts.
ephemeral_timer INTEGER DEFAULT 0,
-- Timestamp indicating when the message should be
-- deleted. It is convenient to store it here because UI
-- needs this value to display how much time is left until
-- the message is deleted.
ephemeral_timestamp INTEGER DEFAULT 0
);
CREATE INDEX msgs_index1 ON msgs (rfc724_mid);
CREATE INDEX msgs_index2 ON msgs (chat_id);
CREATE INDEX msgs_index3 ON msgs (timestamp);
CREATE INDEX msgs_index4 ON msgs (state);
CREATE INDEX msgs_index5 ON msgs (starred);
CREATE INDEX msgs_index6 ON msgs (location_id);
CREATE INDEX msgs_index7 ON msgs (state, hidden, chat_id);
INSERT INTO msgs (id,msgrmsg,txt) VALUES
(1,0,'marker1'), (2,0,'rsvd'), (3,0,'rsvd'),
(4,0,'rsvd'), (5,0,'rsvd'), (6,0,'rsvd'), (7,0,'rsvd'),
(8,0,'rsvd'), (9,0,'daymarker');
CREATE TABLE jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
added_timestamp INTEGER,
desired_timestamp INTEGER DEFAULT 0,
action INTEGER,
foreign_id INTEGER,
param TEXT DEFAULT '',
thread INTEGER DEFAULT 0,
tries INTEGER DEFAULT 0
);
CREATE INDEX jobs_index1 ON jobs (desired_timestamp);
CREATE TABLE leftgrps (
id INTEGER PRIMARY KEY,
grpid TEXT DEFAULT ''
);
CREATE INDEX leftgrps_index1 ON leftgrps (grpid);
CREATE TABLE keypairs (
id INTEGER PRIMARY KEY,
addr TEXT DEFAULT '' COLLATE NOCASE,
is_default INTEGER DEFAULT 0,
private_key,
public_key,
created INTEGER DEFAULT 0
);
CREATE TABLE acpeerstates (
id INTEGER PRIMARY KEY,
addr TEXT DEFAULT '' COLLATE NOCASE,
last_seen INTEGER DEFAULT 0,
last_seen_autocrypt INTEGER DEFAULT 0,
public_key,
prefer_encrypted INTEGER DEFAULT 0,
gossip_timestamp INTEGER DEFAULT 0,
gossip_key,
public_key_fingerprint TEXT DEFAULT '',
gossip_key_fingerprint TEXT DEFAULT '',
verified_key,
verified_key_fingerprint TEXT DEFAULT ''
);
CREATE INDEX acpeerstates_index1 ON acpeerstates (addr);
CREATE INDEX acpeerstates_index3 ON acpeerstates (public_key_fingerprint);
CREATE INDEX acpeerstates_index4 ON acpeerstates (gossip_key_fingerprint);
CREATE INDEX acpeerstates_index5 ON acpeerstates (verified_key_fingerprint);
CREATE TABLE msgs_mdns (
msg_id INTEGER,
contact_id INTEGER,
timestamp_sent INTEGER DEFAULT 0
);
CREATE INDEX msgs_mdns_index1 ON msgs_mdns (msg_id);
CREATE TABLE tokens (
id INTEGER PRIMARY KEY,
namespc INTEGER DEFAULT 0,
foreign_id INTEGER DEFAULT 0,
token TEXT DEFAULT '',
timestamp INTEGER DEFAULT 0
);
CREATE TABLE locations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
latitude REAL DEFAULT 0.0,
longitude REAL DEFAULT 0.0,
accuracy REAL DEFAULT 0.0,
timestamp INTEGER DEFAULT 0,
chat_id INTEGER DEFAULT 0,
from_id INTEGER DEFAULT 0,
independent INTEGER DEFAULT 0
);
CREATE INDEX locations_index1 ON locations (from_id);
CREATE INDEX locations_index2 ON locations (timestamp);
CREATE TABLE devmsglabels (
id INTEGER PRIMARY KEY AUTOINCREMENT,
label TEXT,
msg_id INTEGER DEFAULT 0
);
CREATE INDEX devmsglabels_index1 ON devmsglabels (label);

View File

@@ -898,15 +898,15 @@ impl Context {
}
pub(crate) async fn update_device_chats(&self) -> Result<(), Error> {
if self.get_config_bool(Config::Bot).await {
if self.get_config_bool(Config::Bot).await? {
return Ok(());
}
// create saved-messages chat; we do this only once, if the user has deleted the chat,
// he can recreate it manually (make sure we do not re-add it when configure() was called a second time)
if !self.sql.get_raw_config_bool(self, "self-chat-added").await {
if !self.sql.get_raw_config_bool("self-chat-added").await? {
self.sql
.set_raw_config_bool(self, "self-chat-added", true)
.set_raw_config_bool("self-chat-added", true)
.await?;
chat::create_by_contact_id(self, DC_CONTACT_ID_SELF).await?;
}
@@ -1061,10 +1061,16 @@ mod tests {
};
// delete self-talk first; this adds a message to device-chat about how self-talk can be restored
let device_chat_msgs_before = chat::get_chat_msgs(&t, device_chat_id, 0, None).await.len();
let device_chat_msgs_before = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap()
.len();
self_talk_id.delete(&t).await.ok();
assert_eq!(
chat::get_chat_msgs(&t, device_chat_id, 0, None).await.len(),
chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap()
.len(),
device_chat_msgs_before + 1
);

View File

@@ -15,6 +15,7 @@ use async_std::{channel, pin::Pin};
use async_std::{future::Future, task};
use chat::ChatItem;
use once_cell::sync::Lazy;
use sqlx::Row;
use tempfile::{tempdir, TempDir};
use crate::chat::{self, Chat, ChatId};
@@ -85,6 +86,7 @@ impl TestContext {
async fn new_named(name: Option<String>) -> Self {
use rand::Rng;
pretty_env_logger::try_init().ok();
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
@@ -95,9 +97,10 @@ impl TestContext {
}
let ctx = Context::new("FakeOS".into(), dbfile.into(), id)
.await
.unwrap();
.expect("failed to create context");
let events = ctx.get_event_emitter();
let event_sinks: Arc<RwLock<Vec<Box<EventSink>>>> = Arc::new(RwLock::new(Vec::new()));
let sinks = Arc::clone(&event_sinks);
let (poison_sender, poison_receiver) = channel::bounded(1);
@@ -114,6 +117,7 @@ impl TestContext {
while let Some(event) = events.recv().await {
{
log::debug!("{:?}", event);
let sinks = sinks.read().await;
for sink in sinks.iter() {
sink(event.clone()).await;
@@ -224,22 +228,25 @@ impl TestContext {
let row = self
.ctx
.sql
.query_row(
r#"
.fetch_one(
sqlx::query(
r#"
SELECT id, foreign_id, param
FROM jobs
WHERE action=?
ORDER BY desired_timestamp DESC;
"#,
paramsv![Action::SendMsgToSmtp],
|row| {
let id: i64 = row.get(0)?;
let foreign_id: i64 = row.get(1)?;
let param: String = row.get(2)?;
Ok((id, foreign_id, param))
},
)
.bind(Action::SendMsgToSmtp),
)
.await;
.await
.and_then(|row| {
let id: u32 = row.try_get(0)?;
let foreign_id: u32 = row.try_get(1)?;
let param: String = row.try_get(2)?;
Ok((id, foreign_id, param))
});
if let Ok(row) = row {
break row;
}
@@ -249,7 +256,7 @@ impl TestContext {
panic!("no sent message found in jobs table");
}
};
let id = MsgId::new(foreign_id as u32);
let id = MsgId::new(foreign_id);
let params = Params::from_str(&raw_params).unwrap();
let blob_path = params
.get_blob(Param::File, &self.ctx, false)
@@ -259,7 +266,7 @@ impl TestContext {
.to_abs_path();
self.ctx
.sql
.execute("DELETE FROM jobs WHERE id=?;", paramsv![rowid])
.execute(sqlx::query("DELETE FROM jobs WHERE id=?;").bind(rowid))
.await
.expect("failed to remove job");
update_msg_state(&self.ctx, id, MessageState::OutDelivered).await;
@@ -302,7 +309,9 @@ impl TestContext {
///
/// Panics on errors or if the most recent message is a marker.
pub async fn get_last_msg_in(&self, chat_id: ChatId) -> Message {
let msgs = chat::get_chat_msgs(&self.ctx, chat_id, 0, None).await;
let msgs = chat::get_chat_msgs(&self.ctx, chat_id, 0, None)
.await
.unwrap();
let msg_id = if let ChatItem::Message { msg_id } = msgs.last().unwrap() {
msg_id
} else {
@@ -313,13 +322,17 @@ impl TestContext {
/// Gets the most recent message over all chats.
pub async fn get_last_msg(&self) -> Message {
let chats = Chatlist::try_load(&self.ctx, 0, None, None).await.unwrap();
let chats = Chatlist::try_load(&self.ctx, 0, None, None)
.await
.expect("failed to load chatlist");
// 0 is correct in the next line (as opposed to `chats.len() - 1`, which would be the last element):
// The chatlist describes what you see when you open DC, a list of chats and in each of them
// the first words of the last message. To get the last message overall, we look at the chat at the top of the
// list, which has the index 0.
let msg_id = chats.get_msg_id(0).unwrap();
Message::load_from_db(&self.ctx, msg_id).await.unwrap()
Message::load_from_db(&self.ctx, msg_id)
.await
.expect("failed to load msg")
}
/// Creates or returns an existing 1:1 [`Chat`] with another account.
@@ -333,8 +346,14 @@ impl TestContext {
.ctx
.get_config(Config::Displayname)
.await
.unwrap_or_default()
.unwrap_or_default(),
other.ctx.get_config(Config::ConfiguredAddr).await.unwrap(),
other
.ctx
.get_config(Config::ConfiguredAddr)
.await
.unwrap()
.unwrap(),
Origin::ManuallyCreated,
)
.await
@@ -394,7 +413,7 @@ impl TestContext {
#[allow(dead_code)]
#[allow(clippy::clippy::indexing_slicing)]
pub async fn print_chat(&self, chat_id: ChatId) {
let msglist = chat::get_chat_msgs(self, chat_id, 0x1, None).await;
let msglist = chat::get_chat_msgs(self, chat_id, 0x1, None).await.unwrap();
let msglist: Vec<MsgId> = msglist
.into_iter()
.map(|x| match x {
@@ -405,7 +424,7 @@ impl TestContext {
.collect();
let sel_chat = Chat::load_from_db(self, chat_id).await.unwrap();
let members = chat::get_chat_contacts(self, sel_chat.id).await;
let members = chat::get_chat_contacts(self, sel_chat.id).await.unwrap();
let subtitle = if sel_chat.is_device_talk() {
"device-talk".to_string()
} else if sel_chat.get_type() == Chattype::Single && !members.is_empty() {
@@ -428,7 +447,7 @@ impl TestContext {
} else {
""
},
match sel_chat.get_profile_image(self).await {
match sel_chat.get_profile_image(self).await.unwrap() {
Some(icon) => match icon.to_str() {
Some(icon) => format!(" Icon: {}", icon),
_ => " Icon: Err".to_string(),
@@ -563,7 +582,7 @@ pub(crate) async fn get_chat_msg(
index: usize,
asserted_msgs_count: usize,
) -> Message {
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await;
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await.unwrap();
assert_eq!(msgs.len(), asserted_msgs_count);
let msg_id = if let ChatItem::Message { msg_id } = msgs[index] {
msg_id

View File

@@ -4,17 +4,13 @@
//!
//! Tokens are used in countermitm verification protocols.
use deltachat_derive::{FromSql, ToSql};
use crate::chat::ChatId;
use crate::context::Context;
use crate::dc_tools::{dc_create_id, time};
/// Token namespace
#[derive(
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
)]
#[repr(i32)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, sqlx::Type)]
#[repr(u32)]
pub enum Namespace {
Unknown = 0,
Auth = 110,
@@ -30,26 +26,36 @@ impl Default for Namespace {
/// Creates a new token and saves it into the database.
///
/// Returns created token.
pub async fn save(context: &Context, namespace: Namespace, chat: Option<ChatId>) -> String {
pub async fn save(context: &Context, namespace: Namespace, foreign_id: Option<ChatId>) -> String {
let token = dc_create_id();
match chat {
Some(chat_id) => context
match foreign_id {
Some(foreign_id) => context
.sql
.execute(
"INSERT INTO tokens (namespc, foreign_id, token, timestamp) VALUES (?, ?, ?, ?);",
paramsv![namespace, chat_id, token, time()],
sqlx::query(
"INSERT INTO tokens (namespc, foreign_id, token, timestamp) VALUES (?, ?, ?, ?);"
)
.bind(namespace)
.bind(foreign_id)
.bind(&token)
.bind(time()),
)
.await
.ok(),
None => context
.sql
.execute(
"INSERT INTO tokens (namespc, token, timestamp) VALUES (?, ?, ?);",
paramsv![namespace, token, time()],
sqlx::query(
"INSERT INTO tokens (namespc, token, timestamp) VALUES (?, ?, ?);"
)
.bind(namespace)
.bind(&token)
.bind(time()),
)
.await
.ok(),
};
token
}
@@ -57,50 +63,51 @@ pub async fn lookup(
context: &Context,
namespace: Namespace,
chat: Option<ChatId>,
) -> Option<String> {
match chat {
) -> crate::sql::Result<Option<String>> {
let token = match chat {
Some(chat_id) => {
context
.sql
.query_get_value::<String>(
context,
"SELECT token FROM tokens WHERE namespc=? AND foreign_id=?;",
paramsv![namespace, chat_id],
.query_get_value(
sqlx::query("SELECT token FROM tokens WHERE namespc=? AND foreign_id=?;")
.bind(namespace)
.bind(chat_id),
)
.await
.await?
}
// foreign_id is declared as `INTEGER DEFAULT 0` in the schema.
None => {
context
.sql
.query_get_value::<String>(
context,
"SELECT token FROM tokens WHERE namespc=? AND foreign_id=0;",
paramsv![namespace],
.query_get_value(
sqlx::query("SELECT token FROM tokens WHERE namespc=? AND foreign_id=0;")
.bind(namespace),
)
.await
.await?
}
}
};
Ok(token)
}
pub async fn lookup_or_new(
context: &Context,
namespace: Namespace,
chat: Option<ChatId>,
foreign_id: Option<ChatId>,
) -> String {
if let Some(token) = lookup(context, namespace, chat).await {
if let Ok(Some(token)) = lookup(context, namespace, foreign_id).await {
return token;
}
save(context, namespace, chat).await
save(context, namespace, foreign_id).await
}
pub async fn exists(context: &Context, namespace: Namespace, token: &str) -> bool {
context
.sql
.exists(
"SELECT id FROM tokens WHERE namespc=? AND token=?;",
paramsv![namespace, token],
sqlx::query("SELECT COUNT(*) FROM tokens WHERE namespc=? AND token=?;")
.bind(namespace)
.bind(token),
)
.await
.unwrap_or_default()