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

885
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,8 +12,6 @@ debug = 0
lto = true lto = true
[dependencies] [dependencies]
deltachat_derive = { path = "./deltachat_derive" }
libc = "0.2.51" libc = "0.2.51"
pgp = { version = "0.7.0", default-features = false } pgp = { version = "0.7.0", default-features = false }
hex = "0.4.0" hex = "0.4.0"
@@ -40,9 +38,6 @@ indexmap = "1.3.0"
kamadak-exif = "0.5" kamadak-exif = "0.5"
once_cell = "1.4.1" once_cell = "1.4.1"
regex = "1.1.6" regex = "1.1.6"
rusqlite = { version = "0.24", features = ["bundled"] }
r2d2_sqlite = "0.17.0"
r2d2 = "0.8.5"
strum = "0.19.0" strum = "0.19.0"
strum_macros = "0.19.0" strum_macros = "0.19.0"
backtrace = "0.3.33" backtrace = "0.3.33"
@@ -66,6 +61,9 @@ async-std-resolver = "0.19.5"
async-tar = "0.3.0" async-tar = "0.3.0"
uuid = { version = "0.8", features = ["serde", "v4"] } uuid = { version = "0.8", features = ["serde", "v4"] }
rust-hsluv = "0.1.4" rust-hsluv = "0.1.4"
sqlx = { git = "https://github.com/dignifiedquire/sqlx", branch = "fix-pool-time-out", features = ["runtime-async-std-native-tls", "sqlite"] }
# keep in sync with sqlx
libsqlite3-sys = { version = "0.20.1", default-features = false, features = [ "pkg-config", "vcpkg", "bundled" ] }
pretty_env_logger = { version = "0.4.0", optional = true } pretty_env_logger = { version = "0.4.0", optional = true }
log = {version = "0.4.8", optional = true } log = {version = "0.4.8", optional = true }
@@ -73,6 +71,7 @@ rustyline = { version = "4.1.0", optional = true }
ansi_term = { version = "0.12.1", optional = true } ansi_term = { version = "0.12.1", optional = true }
dirs = { version = "3.0.1", optional=true } dirs = { version = "3.0.1", optional=true }
toml = "0.5.6" toml = "0.5.6"
num_cpus = "1.13.0"
[dev-dependencies] [dev-dependencies]
@@ -84,11 +83,11 @@ async-std = { version = "1.6.4", features = ["unstable", "attributes"] }
futures-lite = "1.7.0" futures-lite = "1.7.0"
criterion = "0.3" criterion = "0.3"
ansi_term = "0.12.0" ansi_term = "0.12.0"
log = "0.4.11"
[workspace] [workspace]
members = [ members = [
"deltachat-ffi", "deltachat-ffi",
"deltachat_derive",
] ]
[[example]] [[example]]

View File

@@ -17,7 +17,7 @@ $ curl https://sh.rustup.rs -sSf | sh
Compile and run Delta Chat Core command line utility, using `cargo`: Compile and run Delta Chat Core command line utility, using `cargo`:
``` ```
$ RUST_LOG=info cargo run --example repl --features repl -- ~/deltachat-db $ RUST_LOG=repl=info cargo run --example repl --features repl -- ~/deltachat-db
``` ```
where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist. where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist.
@@ -95,7 +95,7 @@ $ cargo build -p deltachat_ffi --release
- `DCC_MIME_DEBUG`: if set outgoing and incoming message will be printed - `DCC_MIME_DEBUG`: if set outgoing and incoming message will be printed
- `RUST_LOG=info,async_imap=trace,async_smtp=trace`: enable IMAP and - `RUST_LOG=repl=info,async_imap=trace,async_smtp=trace`: enable IMAP and
SMTP tracing in addition to info messages. SMTP tracing in addition to info messages.
### Expensive tests ### Expensive tests

View File

@@ -156,7 +156,14 @@ pub unsafe extern "C" fn dc_get_config(
} }
let ctx = &*context; let ctx = &*context;
match config::Config::from_str(&to_string_lossy(key)) { match config::Config::from_str(&to_string_lossy(key)) {
Ok(key) => block_on(async move { ctx.get_config(key).await.unwrap_or_default().strdup() }), Ok(key) => block_on(async move {
ctx.get_config(key)
.await
.log_err(ctx, "Can't get config")
.unwrap_or_default()
.unwrap_or_default()
.strdup()
}),
Err(_) => { Err(_) => {
warn!(ctx, "dc_get_config(): invalid key"); warn!(ctx, "dc_get_config(): invalid key");
"".strdup() "".strdup()
@@ -225,8 +232,13 @@ pub unsafe extern "C" fn dc_get_info(context: *const dc_context_t) -> *mut libc:
} }
let ctx = &*context; let ctx = &*context;
block_on(async move { block_on(async move {
let info = ctx.get_info().await; match ctx.get_info().await {
render_info(info).unwrap_or_default().strdup() Ok(info) => render_info(info).unwrap_or_default().strdup(),
Err(err) => {
warn!(ctx, "failed to get info: {}", err);
"".strdup()
}
}
}) })
} }
@@ -283,7 +295,12 @@ pub unsafe extern "C" fn dc_is_configured(context: *mut dc_context_t) -> libc::c
} }
let ctx = &*context; let ctx = &*context;
block_on(async move { ctx.is_configured().await as libc::c_int }) block_on(async move {
ctx.is_configured()
.await
.log_err(ctx, "failed to get configured state")
.unwrap_or_default() as libc::c_int
})
} }
#[no_mangle] #[no_mangle]
@@ -768,7 +785,12 @@ pub unsafe extern "C" fn dc_set_draft(
Some(&mut ffi_msg.message) Some(&mut ffi_msg.message)
}; };
block_on(ChatId::new(chat_id).set_draft(&ctx, msg)) block_on(async move {
ChatId::new(chat_id)
.set_draft(&ctx, msg)
.await
.unwrap_or_log_default(ctx, "failed to set draft");
});
} }
#[no_mangle] #[no_mangle]
@@ -863,6 +885,7 @@ pub unsafe extern "C" fn dc_get_chat_msgs(
Box::into_raw(Box::new( Box::into_raw(Box::new(
chat::get_chat_msgs(&ctx, ChatId::new(chat_id), flags, marker_flag) chat::get_chat_msgs(&ctx, ChatId::new(chat_id), flags, marker_flag)
.await .await
.unwrap_or_log_default(ctx, "failed to get chat msgs")
.into(), .into(),
)) ))
}) })
@@ -876,7 +899,12 @@ pub unsafe extern "C" fn dc_get_msg_cnt(context: *mut dc_context_t, chat_id: u32
} }
let ctx = &*context; let ctx = &*context;
block_on(async move { ChatId::new(chat_id).get_msg_cnt(&ctx).await as libc::c_int }) block_on(async move {
ChatId::new(chat_id)
.get_msg_cnt(&ctx)
.await
.unwrap_or_log_default(ctx, "failed to get msg count") as libc::c_int
})
} }
#[no_mangle] #[no_mangle]
@@ -890,7 +918,12 @@ pub unsafe extern "C" fn dc_get_fresh_msg_cnt(
} }
let ctx = &*context; let ctx = &*context;
block_on(async move { ChatId::new(chat_id).get_fresh_msg_cnt(&ctx).await as libc::c_int }) block_on(async move {
ChatId::new(chat_id)
.get_fresh_msg_cnt(&ctx)
.await
.unwrap_or_log_default(ctx, "failed to get fresh msg cnt") as libc::c_int
})
} }
#[no_mangle] #[no_mangle]
@@ -988,6 +1021,7 @@ pub unsafe extern "C" fn dc_get_chat_media(
or_msg_type3, or_msg_type3,
) )
.await .await
.unwrap_or_log_default(ctx, "Failed get_chat_media")
.into(), .into(),
)) ))
}) })
@@ -1029,7 +1063,7 @@ pub unsafe extern "C" fn dc_get_next_media(
or_msg_type3, or_msg_type3,
) )
.await .await
.map(|msg_id| msg_id.to_u32()) .map(|msg_id| msg_id.map(|id| id.to_u32()).unwrap_or_default())
.unwrap_or(0) .unwrap_or(0)
}) })
} }
@@ -1122,7 +1156,11 @@ pub unsafe extern "C" fn dc_get_chat_contacts(
let ctx = &*context; let ctx = &*context;
block_on(async move { block_on(async move {
let arr = dc_array_t::from(chat::get_chat_contacts(&ctx, ChatId::new(chat_id)).await); let arr = dc_array_t::from(
chat::get_chat_contacts(&ctx, ChatId::new(chat_id))
.await
.unwrap_or_log_default(ctx, "Failed get_chat_contacts"),
);
Box::into_raw(Box::new(arr)) Box::into_raw(Box::new(arr))
}) })
} }
@@ -1148,6 +1186,7 @@ pub unsafe extern "C" fn dc_search_msgs(
let arr = dc_array_t::from( let arr = dc_array_t::from(
ctx.search_msgs(chat_id, to_string_lossy(query)) ctx.search_msgs(chat_id, to_string_lossy(query))
.await .await
.unwrap_or_log_default(ctx, "Failed search_msgs")
.iter() .iter()
.map(|msg_id| msg_id.to_u32()) .map(|msg_id| msg_id.to_u32())
.collect::<Vec<u32>>(), .collect::<Vec<u32>>(),
@@ -1261,7 +1300,8 @@ pub unsafe extern "C" fn dc_set_chat_name(
chat_id: u32, chat_id: u32,
name: *const libc::c_char, name: *const libc::c_char,
) -> libc::c_int { ) -> libc::c_int {
if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL as u32 || name.is_null() { if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL.to_u32() || name.is_null()
{
eprintln!("ignoring careless call to dc_set_chat_name()"); eprintln!("ignoring careless call to dc_set_chat_name()");
return 0; return 0;
} }
@@ -1281,7 +1321,7 @@ pub unsafe extern "C" fn dc_set_chat_profile_image(
chat_id: u32, chat_id: u32,
image: *const libc::c_char, image: *const libc::c_char,
) -> libc::c_int { ) -> libc::c_int {
if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL as u32 { if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL.to_u32() {
eprintln!("ignoring careless call to dc_set_chat_profile_image()"); eprintln!("ignoring careless call to dc_set_chat_profile_image()");
return 0; return 0;
} }
@@ -1406,7 +1446,12 @@ pub unsafe extern "C" fn dc_get_msg_info(
} }
let ctx = &*context; let ctx = &*context;
block_on(message::get_msg_info(&ctx, MsgId::new(msg_id))).strdup() block_on(async move {
message::get_msg_info(&ctx, MsgId::new(msg_id))
.await
.unwrap_or_log_default(ctx, "failed to get msg id")
.strdup()
})
} }
#[no_mangle] #[no_mangle]
@@ -1420,7 +1465,9 @@ pub unsafe extern "C" fn dc_get_msg_html(
} }
let ctx = &*context; let ctx = &*context;
block_on(MsgId::new(msg_id).get_html(&ctx)).strdup() block_on(MsgId::new(msg_id).get_html(&ctx))
.unwrap_or_log_default(ctx, "Failed get_msg_html")
.strdup()
} }
#[no_mangle] #[no_mangle]
@@ -1435,10 +1482,13 @@ pub unsafe extern "C" fn dc_get_mime_headers(
let ctx = &*context; let ctx = &*context;
block_on(async move { block_on(async move {
message::get_mime_headers(&ctx, MsgId::new(msg_id)) let mime = message::get_mime_headers(&ctx, MsgId::new(msg_id))
.await .await
.map(|s| s.strdup()) .unwrap_or_log_default(ctx, "failed to get mime headers");
.unwrap_or_else(ptr::null_mut) if mime.is_empty() {
return ptr::null_mut();
}
mime.strdup()
}) })
} }
@@ -1468,7 +1518,7 @@ pub unsafe extern "C" fn dc_forward_msgs(
if context.is_null() if context.is_null()
|| msg_ids.is_null() || msg_ids.is_null()
|| msg_cnt <= 0 || msg_cnt <= 0
|| chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL as u32 || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL.to_u32()
{ {
eprintln!("ignoring careless call to dc_forward_msgs()"); eprintln!("ignoring careless call to dc_forward_msgs()");
return; return;
@@ -1564,14 +1614,12 @@ pub unsafe extern "C" fn dc_lookup_contact_id_by_addr(
} }
let ctx = &*context; let ctx = &*context;
block_on(Contact::lookup_id_by_addr( block_on(async move {
&ctx, Contact::lookup_id_by_addr(&ctx, to_string_lossy(addr), Origin::IncomingReplyTo)
to_string_lossy(addr), .await
Origin::IncomingReplyTo, .unwrap_or_log_default(ctx, "failed to lookup id")
)) .unwrap_or(0)
.ok() })
.flatten()
.unwrap_or_default()
} }
#[no_mangle] #[no_mangle]
@@ -1645,8 +1693,7 @@ pub unsafe extern "C" fn dc_get_blocked_cnt(context: *mut dc_context_t) -> libc:
block_on(async move { block_on(async move {
Contact::get_all_blocked(&ctx) Contact::get_all_blocked(&ctx)
.await .await
.log_err(&ctx, "Can't get blocked count") .unwrap_or_log_default(ctx, "failed to get blocked count")
.unwrap_or_default()
.len() as libc::c_int .len() as libc::c_int
}) })
} }
@@ -1931,7 +1978,7 @@ pub unsafe extern "C" fn dc_send_locations_to_chat(
chat_id: u32, chat_id: u32,
seconds: libc::c_int, seconds: libc::c_int,
) { ) {
if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL as u32 || seconds < 0 { if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL.to_u32() || seconds < 0 {
eprintln!("ignoring careless call to dc_send_locations_to_chat()"); eprintln!("ignoring careless call to dc_send_locations_to_chat()");
return; return;
} }
@@ -2011,7 +2058,8 @@ pub unsafe extern "C" fn dc_get_locations(
timestamp_begin as i64, timestamp_begin as i64,
timestamp_end as i64, timestamp_end as i64,
) )
.await; .await
.unwrap_or_log_default(ctx, "Failed get_locations");
Box::into_raw(Box::new(dc_array_t::from(res))) Box::into_raw(Box::new(dc_array_t::from(res)))
}) })
} }
@@ -2392,8 +2440,12 @@ pub unsafe extern "C" fn dc_chat_get_profile_image(chat: *mut dc_chat_t) -> *mut
block_on(async move { block_on(async move {
match ffi_chat.chat.get_profile_image(&ctx).await { match ffi_chat.chat.get_profile_image(&ctx).await {
Some(p) => p.to_string_lossy().strdup(), Ok(Some(p)) => p.to_string_lossy().strdup(),
None => ptr::null_mut(), Ok(None) => ptr::null_mut(),
Err(err) => {
error!(ctx, "failed to get profile image: {:?}", err);
ptr::null_mut()
}
} }
}) })
} }
@@ -2407,7 +2459,7 @@ pub unsafe extern "C" fn dc_chat_get_color(chat: *mut dc_chat_t) -> u32 {
let ffi_chat = &*chat; let ffi_chat = &*chat;
let ctx = &*ffi_chat.context; let ctx = &*ffi_chat.context;
block_on(ffi_chat.chat.get_color(&ctx)) block_on(ffi_chat.chat.get_color(&ctx)).unwrap_or_log_default(ctx, "Failed get_color")
} }
#[no_mangle] #[no_mangle]
@@ -3318,6 +3370,7 @@ pub unsafe extern "C" fn dc_contact_get_profile_image(
.contact .contact
.get_profile_image(&ctx) .get_profile_image(&ctx)
.await .await
.unwrap_or_log_default(ctx, "failed to get profile image")
.map(|p| p.to_string_lossy().strdup()) .map(|p| p.to_string_lossy().strdup())
.unwrap_or_else(std::ptr::null_mut) .unwrap_or_else(std::ptr::null_mut)
}) })

View File

@@ -1,13 +0,0 @@
[package]
name = "deltachat_derive"
version = "2.0.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"
[lib]
proc-macro = true
[dependencies]
syn = "1.0.13"
quote = "1.0.2"

View File

@@ -1,43 +0,0 @@
#![recursion_limit = "128"]
extern crate proc_macro;
use crate::proc_macro::TokenStream;
use quote::quote;
// For now, assume (not check) that these macroses are applied to enum without
// data. If this assumption is violated, compiler error will point to
// generated code, which is not very user-friendly.
#[proc_macro_derive(ToSql)]
pub fn to_sql_derive(input: TokenStream) -> TokenStream {
let ast: syn::DeriveInput = syn::parse(input).unwrap();
let name = &ast.ident;
let gen = quote! {
impl rusqlite::types::ToSql for #name {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let num = *self as i64;
let value = rusqlite::types::Value::Integer(num);
let output = rusqlite::types::ToSqlOutput::Owned(value);
std::result::Result::Ok(output)
}
}
};
gen.into()
}
#[proc_macro_derive(FromSql)]
pub fn from_sql_derive(input: TokenStream) -> TokenStream {
let ast: syn::DeriveInput = syn::parse(input).unwrap();
let name = &ast.ident;
let gen = quote! {
impl rusqlite::types::FromSql for #name {
fn column_result(col: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
let inner = rusqlite::types::FromSql::column_result(col)?;
Ok(num_traits::FromPrimitive::from_i64(inner).unwrap_or_default())
}
}
};
gen.into()
}

View File

@@ -32,17 +32,13 @@ use std::time::{Duration, SystemTime};
async fn reset_tables(context: &Context, bits: i32) { async fn reset_tables(context: &Context, bits: i32) {
println!("Resetting tables ({})...", bits); println!("Resetting tables ({})...", bits);
if 0 != bits & 1 { if 0 != bits & 1 {
context context.sql().execute("DELETE FROM jobs;").await.unwrap();
.sql()
.execute("DELETE FROM jobs;", paramsv![])
.await
.unwrap();
println!("(1) Jobs reset."); println!("(1) Jobs reset.");
} }
if 0 != bits & 2 { if 0 != bits & 2 {
context context
.sql() .sql()
.execute("DELETE FROM acpeerstates;", paramsv![]) .execute("DELETE FROM acpeerstates;")
.await .await
.unwrap(); .unwrap();
println!("(2) Peerstates reset."); println!("(2) Peerstates reset.");
@@ -50,7 +46,7 @@ async fn reset_tables(context: &Context, bits: i32) {
if 0 != bits & 4 { if 0 != bits & 4 {
context context
.sql() .sql()
.execute("DELETE FROM keypairs;", paramsv![]) .execute("DELETE FROM keypairs;")
.await .await
.unwrap(); .unwrap();
println!("(4) Private keypairs reset."); println!("(4) Private keypairs reset.");
@@ -58,35 +54,34 @@ async fn reset_tables(context: &Context, bits: i32) {
if 0 != bits & 8 { if 0 != bits & 8 {
context context
.sql() .sql()
.execute("DELETE FROM contacts WHERE id>9;", paramsv![]) .execute("DELETE FROM contacts WHERE id>9;")
.await .await
.unwrap(); .unwrap();
context context
.sql() .sql()
.execute("DELETE FROM chats WHERE id>9;", paramsv![]) .execute("DELETE FROM chats WHERE id>9;")
.await .await
.unwrap(); .unwrap();
context context
.sql() .sql()
.execute("DELETE FROM chats_contacts;", paramsv![]) .execute("DELETE FROM chats_contacts;")
.await .await
.unwrap(); .unwrap();
context context
.sql() .sql()
.execute("DELETE FROM msgs WHERE id>9;", paramsv![]) .execute("DELETE FROM msgs WHERE id>9;")
.await .await
.unwrap(); .unwrap();
context context
.sql() .sql()
.execute( .execute(
"DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';", "DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';",
paramsv![],
) )
.await .await
.unwrap(); .unwrap();
context context
.sql() .sql()
.execute("DELETE FROM leftgrps;", paramsv![]) .execute("DELETE FROM leftgrps;")
.await .await
.unwrap(); .unwrap();
println!("(8) Rest but server config reset."); println!("(8) Rest but server config reset.");
@@ -120,11 +115,11 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
real_spec = spec.to_string(); real_spec = spec.to_string();
context context
.sql() .sql()
.set_raw_config(context, "import_spec", Some(&real_spec)) .set_raw_config("import_spec", Some(&real_spec))
.await .await
.unwrap(); .unwrap();
} else { } else {
let rs = context.sql().get_raw_config(context, "import_spec").await; let rs = context.sql().get_raw_config("import_spec").await.unwrap();
if rs.is_none() { if rs.is_none() {
error!(context, "Import: No file or folder given."); error!(context, "Import: No file or folder given.");
return false; return false;
@@ -201,7 +196,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
contact_id, contact_id,
msgtext.unwrap_or_default(), msgtext.unwrap_or_default(),
if msg.has_html() { "[HAS-HTML]" } else { "" }, if msg.has_html() { "[HAS-HTML]" } else { "" },
if msg.get_from_id() == 1 as libc::c_uint { if msg.get_from_id() == 1 {
"" ""
} else if msg.get_state() == MessageState::InSeen { } else if msg.get_state() == MessageState::InSeen {
"[SEEN]" "[SEEN]"
@@ -292,7 +287,7 @@ async fn log_contactlist(context: &Context, contacts: &[u32]) {
let peerstate = Peerstate::from_addr(context, &addr) let peerstate = Peerstate::from_addr(context, &addr)
.await .await
.expect("peerstate error"); .expect("peerstate error");
if peerstate.is_some() && *contact_id != 1 as libc::c_uint { if peerstate.is_some() && *contact_id != 1 {
line2 = format!( line2 = format!(
", prefer-encrypt={}", ", prefer-encrypt={}",
peerstate.as_ref().unwrap().prefer_encrypt peerstate.as_ref().unwrap().prefer_encrypt
@@ -543,7 +538,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
chat_prefix(&chat), chat_prefix(&chat),
chat.get_id(), chat.get_id(),
chat.get_name(), chat.get_name(),
chat.get_id().get_fresh_msg_cnt(&context).await, chat.get_id().get_fresh_msg_cnt(&context).await?,
if chat.is_muted() { "🔇" } else { "" }, if chat.is_muted() { "🔇" } else { "" },
match chat.visibility { match chat.visibility {
ChatVisibility::Normal => "", ChatVisibility::Normal => "",
@@ -605,7 +600,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(sel_chat.is_some(), "Failed to select chat"); ensure!(sel_chat.is_some(), "Failed to select chat");
let sel_chat = sel_chat.as_ref().unwrap(); let sel_chat = sel_chat.as_ref().unwrap();
let msglist = chat::get_chat_msgs(&context, sel_chat.get_id(), 0x1, None).await; let msglist = chat::get_chat_msgs(&context, sel_chat.get_id(), 0x1, None).await?;
let msglist: Vec<MsgId> = msglist let msglist: Vec<MsgId> = msglist
.into_iter() .into_iter()
.map(|x| match x { .map(|x| match x {
@@ -615,7 +610,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}) })
.collect(); .collect();
let members = chat::get_chat_contacts(&context, sel_chat.id).await; let members = chat::get_chat_contacts(&context, sel_chat.id).await?;
let subtitle = if sel_chat.is_device_talk() { let subtitle = if sel_chat.is_device_talk() {
"device-talk".to_string() "device-talk".to_string()
} else if sel_chat.get_type() == Chattype::Single && !members.is_empty() { } else if sel_chat.get_type() == Chattype::Single && !members.is_empty() {
@@ -638,7 +633,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
} else { } else {
"" ""
}, },
match sel_chat.get_profile_image(&context).await { match sel_chat.get_profile_image(&context).await? {
Some(icon) => match icon.to_str() { Some(icon) => match icon.to_str() {
Some(icon) => format!(" Icon: {}", icon), Some(icon) => format!(" Icon: {}", icon),
_ => " Icon: Err".to_string(), _ => " Icon: Err".to_string(),
@@ -658,14 +653,14 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!( println!(
"{} messages.", "{} messages.",
sel_chat.get_id().get_msg_cnt(&context).await sel_chat.get_id().get_msg_cnt(&context).await?
); );
chat::marknoticed_chat(&context, sel_chat.get_id()).await?; chat::marknoticed_chat(&context, sel_chat.get_id()).await?;
} }
"createchat" => { "createchat" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing."); ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id: libc::c_int = arg1.parse()?; let contact_id: u32 = arg1.parse()?;
let chat_id = chat::create_by_contact_id(&context, contact_id as u32).await?; let chat_id = chat::create_by_contact_id(&context, contact_id).await?;
println!("Single#{} created successfully.", chat_id,); println!("Single#{} created successfully.", chat_id,);
} }
@@ -716,11 +711,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(sel_chat.is_some(), "No chat selected"); ensure!(sel_chat.is_some(), "No chat selected");
ensure!(!arg1.is_empty(), "Argument <contact-id> missing."); ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id_0: libc::c_int = arg1.parse()?; let contact_id_0: u32 = arg1.parse()?;
if chat::add_contact_to_chat( if chat::add_contact_to_chat(
&context, &context,
sel_chat.as_ref().unwrap().get_id(), sel_chat.as_ref().unwrap().get_id(),
contact_id_0 as u32, contact_id_0,
) )
.await .await
{ {
@@ -732,11 +727,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"removemember" => { "removemember" => {
ensure!(sel_chat.is_some(), "No chat selected."); ensure!(sel_chat.is_some(), "No chat selected.");
ensure!(!arg1.is_empty(), "Argument <contact-id> missing."); ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id_1: libc::c_int = arg1.parse()?; let contact_id_1: u32 = arg1.parse()?;
chat::remove_contact_from_chat( chat::remove_contact_from_chat(
&context, &context,
sel_chat.as_ref().unwrap().get_id(), sel_chat.as_ref().unwrap().get_id(),
contact_id_1 as u32, contact_id_1,
) )
.await?; .await?;
@@ -762,7 +757,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(sel_chat.is_some(), "No chat selected."); ensure!(sel_chat.is_some(), "No chat selected.");
let contacts = let contacts =
chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await; chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await?;
println!("Memberlist:"); println!("Memberlist:");
log_contactlist(&context, &contacts).await; log_contactlist(&context, &contacts).await;
@@ -787,7 +782,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
0, 0,
0, 0,
) )
.await; .await?;
let default_marker = "-".to_string(); let default_marker = "-".to_string();
for location in &locations { for location in &locations {
let marker = location.marker.as_ref().unwrap_or(&default_marker); let marker = location.marker.as_ref().unwrap_or(&default_marker);
@@ -899,7 +894,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
None None
}; };
let msglist = context.search_msgs(chat, arg1).await; let msglist = context.search_msgs(chat, arg1).await?;
log_msglist(&context, &msglist).await?; log_msglist(&context, &msglist).await?;
println!("{} messages.", msglist.len()); println!("{} messages.", msglist.len());
@@ -915,7 +910,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
.unwrap() .unwrap()
.get_id() .get_id()
.set_draft(&context, Some(&mut draft)) .set_draft(&context, Some(&mut draft))
.await; .await?;
println!("Draft saved."); println!("Draft saved.");
} else { } else {
sel_chat sel_chat
@@ -923,7 +918,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
.unwrap() .unwrap()
.get_id() .get_id()
.set_draft(&context, None) .set_draft(&context, None)
.await; .await?;
println!("Draft deleted."); println!("Draft deleted.");
} }
} }
@@ -946,7 +941,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
Viewtype::Gif, Viewtype::Gif,
Viewtype::Video, Viewtype::Video,
) )
.await; .await?;
println!("{} images or videos: ", images.len()); println!("{} images or videos: ", images.len());
for (i, data) in images.iter().enumerate() { for (i, data) in images.iter().enumerate() {
if 0 == i { if 0 == i {
@@ -1012,7 +1007,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"msginfo" => { "msginfo" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing."); ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let id = MsgId::new(arg1.parse()?); let id = MsgId::new(arg1.parse()?);
let res = message::get_msg_info(&context, id).await; let res = message::get_msg_info(&context, id).await?;
println!("{}", res); println!("{}", res);
} }
"html" => { "html" => {
@@ -1021,7 +1016,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let file = dirs::home_dir() let file = dirs::home_dir()
.unwrap_or_default() .unwrap_or_default()
.join(format!("msg-{}.html", id.to_u32())); .join(format!("msg-{}.html", id.to_u32()));
let html = id.get_html(&context).await.unwrap_or_default(); let html = id.get_html(&context).await?.unwrap_or_default();
fs::write(&file, html)?; fs::write(&file, html)?;
println!("HTML written to: {:#?}", file); println!("HTML written to: {:#?}", file);
} }
@@ -1081,14 +1076,14 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"contactinfo" => { "contactinfo" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing."); ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id = arg1.parse()?; let contact_id: u32 = arg1.parse()?;
let contact = Contact::get_by_id(&context, contact_id).await?; let contact = Contact::get_by_id(&context, contact_id).await?;
let name_n_addr = contact.get_name_n_addr(); let name_n_addr = contact.get_name_n_addr();
let mut res = format!( let mut res = format!(
"Contact info for: {}:\nIcon: {}\n", "Contact info for: {}:\nIcon: {}\n",
name_n_addr, name_n_addr,
match contact.get_profile_image(&context).await { match contact.get_profile_image(&context).await? {
Some(image) => image.to_str().unwrap().to_string(), Some(image) => image.to_str().unwrap().to_string(),
None => "NoIcon".to_string(), None => "NoIcon".to_string(),
} }
@@ -1177,7 +1172,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
// let r = context.emit_event(event, 0 as libc::uintptr_t, 0 as libc::uintptr_t); // let r = context.emit_event(event, 0 as libc::uintptr_t, 0 as libc::uintptr_t);
// println!( // println!(
// "Sending event {:?}({}), received value {}.", // "Sending event {:?}({}), received value {}.",
// event, event as usize, r as libc::c_int, // event, event as usize, r,
// ); // );
// } // }
"fileinfo" => { "fileinfo" => {

View File

@@ -390,7 +390,7 @@ async fn handle_cmd(
ctx.configure().await?; ctx.configure().await?;
} }
"oauth2" => { "oauth2" => {
if let Some(addr) = ctx.get_config(config::Config::Addr).await { if let Some(addr) = ctx.get_config(config::Config::Addr).await? {
let oauth2_url = let oauth2_url =
dc_get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await; dc_get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await;
if oauth2_url.is_none() { if oauth2_url.is_none() {

View File

@@ -135,15 +135,25 @@ impl Accounts {
let old_id = self.config.get_selected_account().await; let old_id = self.config.get_selected_account().await;
// create new account // 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_dbfile = account_config.dbfile().into();
let new_blobdir = Context::derive_blobdir(&new_dbfile); let new_blobdir = Context::derive_blobdir(&new_dbfile);
let res = { let res = {
fs::create_dir_all(&account_config.dir).await?; fs::create_dir_all(&account_config.dir)
fs::rename(&dbfile, &new_dbfile).await?; .await
fs::rename(&blobdir, &new_blobdir).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(()) Ok(())
}; };
@@ -502,7 +512,10 @@ mod tests {
let ctx = accounts.get_selected_account().await; let ctx = accounts.get_selected_account().await;
assert_eq!( assert_eq!(
"me@mail.com", "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 blob_abs = self.to_abs_path();
let img_wh = 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() .unwrap_or_default()
{ {
MediaQuality::Balanced => BALANCED_AVATAR_SIZE, MediaQuality::Balanced => BALANCED_AVATAR_SIZE,
@@ -403,7 +403,7 @@ impl<'a> BlobObject<'a> {
} }
let img_wh = 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() .unwrap_or_default()
{ {
MediaQuality::Balanced => BALANCED_IMAGE_SIZE, MediaQuality::Balanced => BALANCED_IMAGE_SIZE,
@@ -514,6 +514,10 @@ pub enum BlobError {
WrongBlobdir { blobdir: PathBuf, src: PathBuf }, WrongBlobdir { blobdir: PathBuf, src: PathBuf },
#[error("Blob has a badname {}", .blobname.display())] #[error("Blob has a badname {}", .blobname.display())]
WrongName { blobname: PathBuf }, WrongName { blobname: PathBuf },
#[error("Sql: {0}")]
Sql(#[from] crate::sql::Error),
#[error("{0}")]
Other(#[from] anyhow::Error),
} }
#[cfg(test)] #[cfg(test)]

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,9 @@
//! # Constants //! # Constants
use deltachat_derive::{FromSql, ToSql};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::chat::ChatId;
pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION").to_string()); pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION").to_string());
#[derive( #[derive(
@@ -14,12 +15,11 @@ pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION")
Eq, Eq,
FromPrimitive, FromPrimitive,
ToPrimitive, ToPrimitive,
FromSql,
ToSql,
Serialize, Serialize,
Deserialize, Deserialize,
sqlx::Type,
)] )]
#[repr(u8)] #[repr(i8)]
pub enum Blocked { pub enum Blocked {
Not = 0, Not = 0,
Manually = 1, Manually = 1,
@@ -32,9 +32,7 @@ impl Default for Blocked {
} }
} }
#[derive( #[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
)]
#[repr(u8)] #[repr(u8)]
pub enum ShowEmails { pub enum ShowEmails {
Off = 0, Off = 0,
@@ -48,9 +46,7 @@ impl Default for ShowEmails {
} }
} }
#[derive( #[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
)]
#[repr(u8)] #[repr(u8)]
pub enum MediaQuality { pub enum MediaQuality {
Balanced = 0, Balanced = 0,
@@ -63,9 +59,7 @@ impl Default for MediaQuality {
} }
} }
#[derive( #[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
)]
#[repr(u8)] #[repr(u8)]
pub enum KeyGenType { pub enum KeyGenType {
Default = 0, Default = 0,
@@ -79,9 +73,7 @@ impl Default for KeyGenType {
} }
} }
#[derive( #[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
)]
#[repr(i8)] #[repr(i8)]
pub enum VideochatType { pub enum VideochatType {
Unknown = 0, Unknown = 0,
@@ -122,15 +114,15 @@ pub const DC_RESEND_USER_AVATAR_DAYS: i64 = 14;
pub const DC_OUTDATED_WARNING_DAYS: i64 = 365; pub const DC_OUTDATED_WARNING_DAYS: i64 = 365;
/// virtual chat showing all messages belonging to chats flagged with chats.blocked=2 /// 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) /// 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 /// 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 /// 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. /// 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( #[derive(
Debug, Debug,
@@ -141,11 +133,10 @@ pub const DC_CHAT_ID_LAST_SPECIAL: u32 = 9;
Eq, Eq,
FromPrimitive, FromPrimitive,
ToPrimitive, ToPrimitive,
FromSql,
ToSql,
IntoStaticStr, IntoStaticStr,
Serialize, Serialize,
Deserialize, Deserialize,
sqlx::Type,
)] )]
#[repr(u32)] #[repr(u32)]
pub enum Chattype { pub enum Chattype {
@@ -256,12 +247,11 @@ pub const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;
Eq, Eq,
FromPrimitive, FromPrimitive,
ToPrimitive, ToPrimitive,
FromSql,
ToSql,
Serialize, Serialize,
Deserialize, Deserialize,
sqlx::Type,
)] )]
#[repr(i32)] #[repr(u32)]
pub enum Viewtype { pub enum Viewtype {
Unknown = 0, Unknown = 0,

View File

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

View File

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

View File

@@ -26,9 +26,9 @@ pub struct EncryptHelper {
impl EncryptHelper { impl EncryptHelper {
pub async fn new(context: &Context) -> Result<EncryptHelper> { pub async fn new(context: &Context) -> Result<EncryptHelper> {
let prefer_encrypt = 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(); .unwrap_or_default();
let addr = match context.get_config(Config::ConfiguredAddr).await { let addr = match context.get_config(Config::ConfiguredAddr).await? {
None => { None => {
bail!("addr not configured!"); 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> { pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
let self_addr = context let self_addr = context
.get_config(Config::ConfiguredAddr) .get_config(Config::ConfiguredAddr)
.await .await?
.ok_or_else(|| { .ok_or_else(|| {
format_err!(concat!( format_err!(concat!(
"Failed to get self address, ", "Failed to get self address, ",

View File

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

View File

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

View File

@@ -13,12 +13,12 @@ use std::pin::Pin;
use anyhow::Result; use anyhow::Result;
use lettre_email::mime::{self, Mime}; use lettre_email::mime::{self, Mime};
use crate::context::Context;
use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::message::{Message, MsgId}; use crate::message::{Message, MsgId};
use crate::mimeparser::parse_message_id; use crate::mimeparser::parse_message_id;
use crate::param::Param::SendHtml; use crate::param::Param::SendHtml;
use crate::plaintext::PlainText; use crate::plaintext::PlainText;
use crate::{context::Context, message};
use lettre_email::PartBuilder; use lettre_email::PartBuilder;
use mailparse::ParsedContentType; use mailparse::ParsedContentType;
@@ -244,32 +244,20 @@ impl MsgId {
/// this is the case at least when `Message.has_html()` returns true /// 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). /// (we do not save raw mime unconditionally in the database to save space).
/// The corresponding ffi-function is `dc_get_msg_html()`. /// The corresponding ffi-function is `dc_get_msg_html()`.
pub async fn get_html(self, context: &Context) -> Option<String> { pub async fn get_html(self, context: &Context) -> Result<Option<String>> {
let rawmime: Option<String> = context let rawmime = message::get_mime_headers(context, self).await?;
.sql
.query_get_value(
context,
"SELECT mime_headers FROM msgs WHERE id=?;",
paramsv![self],
)
.await;
if let Some(rawmime) = rawmime { if !rawmime.is_empty() {
if !rawmime.is_empty() { match HtmlMsgParser::from_bytes(context, rawmime.as_bytes()).await {
match HtmlMsgParser::from_bytes(context, rawmime.as_bytes()).await { Err(err) => {
Err(err) => { warn!(context, "get_html: parser error: {}", err);
warn!(context, "get_html: parser error: {}", err); Ok(None)
None
}
Ok(parser) => Some(parser.html),
} }
} else { Ok(parser) => Ok(Some(parser.html)),
warn!(context, "get_html: empty mime for {}", self);
None
} }
} else { } else {
warn!(context, "get_html: no mime for {}", self); 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() { async fn test_get_html_empty() {
let t = TestContext::new().await; let t = TestContext::new().await;
let msg_id = MsgId::new_unset(); 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] #[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.is_forwarded());
assert!(msg.get_text().unwrap().contains("this is plain")); assert!(msg.get_text().unwrap().contains("this is plain"));
assert!(msg.has_html()); 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>")); assert!(html.contains("this is <b>html</b>"));
// alice: create chat with bob and forward received html-message there // 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.is_forwarded());
assert!(msg.get_text().unwrap().contains("this is plain")); assert!(msg.get_text().unwrap().contains("this is plain"));
assert!(msg.has_html()); 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>")); assert!(html.contains("this is <b>html</b>"));
// bob: check that bob also got the html-part of the forwarded message // 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.is_forwarded());
assert!(msg.get_text().unwrap().contains("this is plain")); assert!(msg.get_text().unwrap().contains("this is plain"));
assert!(msg.has_html()); 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>")); 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 // receive the message on another device
let alice = TestContext::new_alice().await; 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; alice.recv_msg(&msg).await;
let chat = alice.get_self_chat().await; let chat = alice.get_self_chat().await;
let msg = alice.get_last_msg_in(chat.get_id()).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.is_forwarded());
assert!(msg.get_text().unwrap().contains("this is plain")); assert!(msg.get_text().unwrap().contains("this is plain"));
assert!(msg.has_html()); 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>")); 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_eq!(msg.get_text(), Some("plain text".to_string()));
assert!(!msg.is_forwarded()); assert!(!msg.is_forwarded());
assert!(msg.mime_modified); 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")); assert!(html.contains("<b>html</b> text"));
// let bob receive the message // 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_eq!(msg.get_text(), Some("plain text".to_string()));
assert!(!msg.is_forwarded()); assert!(!msg.is_forwarded());
assert!(msg.mime_modified); 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")); assert!(html.contains("<b>html</b> text"));
} }
} }

View File

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

View File

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

View File

@@ -10,6 +10,7 @@ use async_std::{
prelude::*, prelude::*,
}; };
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
use sqlx::Row;
use crate::chat; use crate::chat;
use crate::chat::delete_and_reset_all_device_msgs; 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"; const BLOBS_BACKUP_NAME: &str = "blobs_backup";
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)] #[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(i32)] #[repr(u32)]
pub enum ImexMode { pub enum ImexMode {
/// Export all private keys and all public keys of the user to the /// 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` /// 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 { match sql.open(context, &path, true).await {
Ok(_) => { Ok(_) => {
let curr_backup_time = sql let curr_backup_time = sql
.get_raw_config_int(context, "backup_time") .get_raw_config_int("backup_time")
.await .await?
.unwrap_or_default(); .unwrap_or_default();
if curr_backup_time > newest_backup_time { if curr_backup_time > newest_backup_time {
newest_backup_path = Some(path); 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."); bail!("Passphrase must be at least 2 chars long.");
}; };
let private_key = SignedSecretKey::load_self(context).await?; 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, false => None,
true => Some(("Autocrypt-Prefer-Encrypt", "mutual")), 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<()> { 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); let mut msg = Message::new(Viewtype::Text);
// TODO: define this as a stockstring once the wording is settled. // TODO: define this as a stockstring once the wording is settled.
msg.text = Some( msg.text = Some(
@@ -394,7 +395,7 @@ async fn set_self_key(
}; };
context context
.sql .sql
.set_raw_config_int(context, "e2ee_enabled", e2ee_enabled) .set_raw_config_int("e2ee_enabled", e2ee_enabled)
.await?; .await?;
} }
None => { 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"); ensure!(self_addr.is_some(), "Missing self addr");
let addr = EmailAddress::new(&self_addr.unwrap_or_default())?; let addr = EmailAddress::new(&self_addr.unwrap_or_default())?;
let keypair = pgp::KeyPair { let keypair = pgp::KeyPair {
@@ -493,7 +494,7 @@ async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) ->
); );
ensure!( ensure!(
!context.is_configured().await, !context.is_configured().await?,
"Cannot import backups to accounts in use." "Cannot import backups to accounts in use."
); );
ensure!( ensure!(
@@ -564,7 +565,7 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef<Path>
); );
ensure!( ensure!(
!context.is_configured().await, !context.is_configured().await?,
"Cannot import backups to accounts in use." "Cannot import backups to accounts in use."
); );
ensure!( ensure!(
@@ -594,9 +595,9 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef<Path>
let total_files_cnt = context let total_files_cnt = context
.sql .sql
.query_get_value::<isize>(context, "SELECT COUNT(*) FROM backup_blobs;", paramsv![]) .count("SELECT COUNT(*) FROM backup_blobs;")
.await .await?;
.unwrap_or_default() as usize;
info!( info!(
context, context,
"***IMPORT-in-progress: total_files_cnt={:?}", total_files_cnt, "***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. // consuming too much memory.
let file_ids = context let file_ids = context
.sql .sql
.query_map( .fetch("SELECT id FROM backup_blobs ORDER BY id")
"SELECT id FROM backup_blobs ORDER BY id", .await?
paramsv![], .map(|row| row?.try_get(0))
|row| row.get(0), .collect::<sqlx::Result<Vec<i64>>>()
|ids| {
ids.collect::<std::result::Result<Vec<i64>, _>>()
.map_err(Into::into)
},
)
.await?; .await?;
let mut all_files_extracted = true; let mut all_files_extracted = true;
for (processed_files_cnt, file_id) in file_ids.into_iter().enumerate() { for (processed_files_cnt, file_id) in file_ids.into_iter().enumerate() {
// Load a single blob into memory // Load a single blob into memory
let (file_name, file_blob) = context let row = context
.sql .sql
.query_row( .fetch_one(
"SELECT file_name, file_content FROM backup_blobs WHERE id = ?", sqlx::query("SELECT file_name, file_content FROM backup_blobs WHERE id = ?")
paramsv![file_id], .bind(file_id),
|row| Ok((row.get::<_, String>(0)?, row.get::<_, Vec<u8>>(1)?)),
) )
.await?; .await?;
let file_name: String = row.try_get(0)?;
let file_blob: &[u8] = row.try_get(1)?;
if context.shall_stop_ongoing().await { if context.shall_stop_ongoing().await {
all_files_extracted = false; all_files_extracted = false;
break; 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); 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 { if all_files_extracted {
// only delete backup_blobs if all files were successfully extracted // only delete backup_blobs if all files were successfully extracted
context context.sql.execute("DROP TABLE backup_blobs;").await?;
.sql context.sql.execute("VACUUM;").await.ok();
.execute("DROP TABLE backup_blobs;", paramsv![])
.await?;
context.sql.execute("VACUUM;", paramsv![]).await.ok();
Ok(()) Ok(())
} else { } else {
bail!("received stop signal"); bail!("received stop signal");
@@ -674,13 +668,13 @@ async fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
context context
.sql .sql
.set_raw_config_int(context, "backup_time", now as i32) .set_raw_config_int("backup_time", now as i32)
.await?; .await?;
sql::housekeeping(context).await.ok_or_log(context); sql::housekeeping(context).await.ok_or_log(context);
context context
.sql .sql
.execute("VACUUM;", paramsv![]) .execute("VACUUM;")
.await .await
.map_err(|e| warn!(context, "Vacuum failed, exporting anyway {}", e)); .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<()> { async fn export_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
let mut export_errors = 0; let mut export_errors = 0;
let keys = context let mut keys = context
.sql .sql
.query_map( .fetch("SELECT id, public_key, private_key, is_default FROM keypairs;")
"SELECT id, public_key, private_key, is_default FROM keypairs;", .await?
paramsv![], .map(|row| -> sqlx::Result<_> {
|row| { let row = row?;
let id = row.get(0)?; let id = row.try_get(0)?;
let public_key_blob: Vec<u8> = row.get(1)?; let public_key_blob: &[u8] = row.try_get(1)?;
let public_key = SignedPublicKey::from_slice(&public_key_blob); let public_key = SignedPublicKey::from_slice(public_key_blob);
let private_key_blob: Vec<u8> = row.get(2)?; let private_key_blob: &[u8] = row.try_get(2)?;
let private_key = SignedSecretKey::from_slice(&private_key_blob); let private_key = SignedSecretKey::from_slice(private_key_blob);
let is_default: i32 = row.get(3)?; let is_default: i32 = row.try_get(3)?;
Ok((id, public_key, private_key, is_default)) Ok((id, public_key, private_key, is_default))
}, });
|keys| {
keys.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await?;
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); let id = Some(id).filter(|_| is_default != 0);
if let Ok(key) = public_key { if let Ok(key) = public_key {
if export_key_to_asc_file(context, &dir, id, &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 anyhow::{bail, ensure, format_err, Context as _, Error, Result};
use async_smtp::smtp::response::{Category, Code, Detail}; use async_smtp::smtp::response::{Category, Code, Detail};
use async_std::prelude::*;
use async_std::task::sleep; use async_std::task::sleep;
use deltachat_derive::{FromSql, ToSql};
use itertools::Itertools; use itertools::Itertools;
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
use sqlx::Row;
use crate::dc_tools::{dc_delete_file, dc_read_file, time}; use crate::dc_tools::{dc_delete_file, dc_read_file, time};
use crate::ephemeral::load_imap_deletion_msgid; use crate::ephemeral::load_imap_deletion_msgid;
@@ -36,10 +37,8 @@ use crate::{scheduler::InterruptInfo, sql};
const JOB_RETRIES: u32 = 17; const JOB_RETRIES: u32 = 17;
/// Thread IDs /// Thread IDs
#[derive( #[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, sqlx::Type)]
Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql, #[repr(u32)]
)]
#[repr(i32)]
pub(crate) enum Thread { pub(crate) enum Thread {
Unknown = 0, Unknown = 0,
Imap = 100, Imap = 100,
@@ -76,19 +75,9 @@ impl Default for Thread {
} }
#[derive( #[derive(
Debug, Debug, Display, Copy, Clone, PartialEq, Eq, PartialOrd, FromPrimitive, ToPrimitive, sqlx::Type,
Display,
Copy,
Clone,
PartialEq,
Eq,
PartialOrd,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
)] )]
#[repr(i32)] #[repr(u32)]
pub enum Action { pub enum Action {
Unknown = 0, Unknown = 0,
@@ -184,7 +173,7 @@ impl Job {
if self.job_id != 0 { if self.job_id != 0 {
context context
.sql .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?; .await?;
} }
@@ -203,26 +192,24 @@ impl Job {
context context
.sql .sql
.execute( .execute(
"UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;", sqlx::query(
paramsv![ "UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;",
self.desired_timestamp, )
self.tries as i64, .bind(self.desired_timestamp)
self.param.to_string(), .bind(self.tries as i64)
self.job_id as i32, .bind(self.param.to_string())
], .bind(self.job_id as i32),
) )
.await?; .await?;
} else { } else {
context.sql.execute( context.sql.execute(
"INSERT INTO jobs (added_timestamp, thread, action, foreign_id, param, desired_timestamp) VALUES (?,?,?,?,?,?);", sqlx::query("INSERT INTO jobs (added_timestamp, thread, action, foreign_id, param, desired_timestamp) VALUES (?,?,?,?,?,?);")
paramsv![ .bind(self.added_timestamp)
self.added_timestamp, .bind(thread)
thread, .bind(self.action)
self.action, .bind(self.foreign_id)
self.foreign_id, .bind(self.param.to_string())
self.param.to_string(), .bind(self.desired_timestamp)
self.desired_timestamp
]
).await?; ).await?;
} }
@@ -253,7 +240,7 @@ impl Job {
let status = match smtp.send(context, recipients, message, job_id).await { let status = match smtp.send(context, recipients, message, job_id).await {
Err(crate::smtp::send::Error::SendError(err)) => { Err(crate::smtp::send::Error::SendError(err)) => {
// Remote error, retry later. // 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()); self.pending_error = Some(err.to_string());
let res = match err { let res = match err {
@@ -339,6 +326,12 @@ impl Job {
error!(context, "SMTP job failed because SMTP has no transport"); error!(context, "SMTP job failed because SMTP has no transport");
Status::Finished(Err(format_err!("SMTP has not 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(()) => { Ok(()) => {
job_try!(success_cb().await); job_try!(success_cb().await);
Status::Finished(Ok(())) 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. /* if there is a msg-id and it does not exist in the db, cancel sending.
this happends if dc_delete_msgs() was called this happends if dc_delete_msgs() was called
before the generated mime was sent out */ before the generated mime was sent out */
if 0 != self.foreign_id && !message::exists(context, MsgId::new(self.foreign_id)).await { if 0 != self.foreign_id {
return Status::Finished(Err(format_err!( match message::exists(context, MsgId::new(self.foreign_id)).await {
"Not sending Message {} as it was deleted", Ok(exists) => {
self.foreign_id 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; let foreign_id = self.foreign_id;
@@ -399,7 +402,7 @@ impl Job {
async move { async move {
// smtp success, update db ASAP, then delete smtp file // smtp success, update db ASAP, then delete smtp file
if 0 != foreign_id { 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 // now also delete the generated file
dc_delete_file(context, filename).await; dc_delete_file(context, filename).await;
@@ -416,44 +419,38 @@ impl Job {
contact_id: u32, contact_id: u32,
) -> sql::Result<(Vec<u32>, Vec<String>)> { ) -> sql::Result<(Vec<u32>, Vec<String>)> {
// Extract message IDs from job parameters // Extract message IDs from job parameters
let res: Vec<(u32, MsgId)> = context let mut rows = context
.sql .sql
.query_map( .fetch(
"SELECT id, param FROM jobs WHERE foreign_id=? AND id!=?", sqlx::query("SELECT id, param FROM jobs WHERE foreign_id=? AND id!=?")
paramsv![contact_id, self.job_id], .bind(contact_id)
|row| { .bind(self.job_id),
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)
},
) )
.await?; .await?;
// Load corresponding RFC724 message IDs // Load corresponding RFC724 message IDs
let mut job_ids = Vec::new(); let mut job_ids = Vec::new();
let mut rfc724_mids = 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 { while let Some(row) = rows.next().await {
job_ids.push(job_id); let row = row?;
rfc724_mids.push(rfc724_mid); 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)) Ok((job_ids, rfc724_mids))
} }
async fn send_mdn(&mut self, context: &Context, smtp: &mut Smtp) -> Status { 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 // User has disabled MDNs after job scheduling but before
// execution. // execution.
return Status::Finished(Err(format_err!("MDNs are disabled"))); return Status::Finished(Err(format_err!("MDNs are disabled")));
@@ -539,7 +536,13 @@ impl Job {
); );
return Status::Finished(Ok(())); 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 { 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 /// Then, Fetch the last messages DC_FETCH_EXISTING_MSGS_COUNT emails from the server
/// and show them in the chat list. /// and show them in the chat list.
async fn fetch_existing_msgs(&mut self, context: &Context, imap: &mut Imap) -> Status { 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 return Status::Finished(Ok(())); // Bots don't want those messages
} }
if let Err(err) = imap.connect_configured(context).await { 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::ConfiguredMvboxFolder).await;
add_all_recipients_as_contacts(context, imap, Config::ConfiguredInboxFolder).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 &[ for config in &[
Config::ConfiguredMvboxFolder, Config::ConfiguredMvboxFolder,
Config::ConfiguredInboxFolder, Config::ConfiguredInboxFolder,
Config::ConfiguredSentboxFolder, 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 { 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 {:#}: // We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
warn!(context, "Could not fetch messages, retrying: {:#}", e); 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 // 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. // message), then group contact requests from this contact should also be unblocked.
// See https://github.com/deltachat/deltachat-core-rust/issues/2097. // 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 { if let ChatItem::Message { msg_id } = item {
let msg = match Message::load_from_db(context, msg_id).await { let msg = match Message::load_from_db(context, msg_id).await {
Err(e) => { Err(e) => {
@@ -736,26 +739,21 @@ impl Job {
return Status::RetryLater; return Status::RetryLater;
} }
if let Some(sentbox_folder) = &context.get_config(Config::ConfiguredSentboxFolder).await { let sentbox_folder = job_try!(context.get_config(Config::ConfiguredSentboxFolder).await);
job_try!( if let Some(sentbox_folder) = sentbox_folder {
imap.resync_folder_uids(context, sentbox_folder.to_string()) job_try!(imap.resync_folder_uids(context, sentbox_folder).await);
.await
);
} }
if let Some(inbox_folder) = &context.get_config(Config::ConfiguredInboxFolder).await { let inbox_folder = job_try!(context.get_config(Config::ConfiguredInboxFolder).await);
job_try!( if let Some(inbox_folder) = inbox_folder {
imap.resync_folder_uids(context, inbox_folder.to_string()) job_try!(imap.resync_folder_uids(context, inbox_folder).await);
.await
);
} }
if let Some(mvbox_folder) = &context.get_config(Config::ConfiguredMvboxFolder).await { let mvbox_folder = job_try!(context.get_config(Config::ConfiguredMvboxFolder).await);
job_try!( if let Some(mvbox_folder) = mvbox_folder {
imap.resync_folder_uids(context, mvbox_folder.to_string()) job_try!(imap.resync_folder_uids(context, mvbox_folder).await);
.await
);
} }
Status::Finished(Ok(())) Status::Finished(Ok(()))
} }
@@ -803,11 +801,13 @@ impl Job {
// the name sent in the From field by the user. // the name sent in the From field by the user.
if msg.param.get_bool(Param::WantsMdn).unwrap_or_default() if msg.param.get_bool(Param::WantsMdn).unwrap_or_default()
&& !msg.is_system_message() && !msg.is_system_message()
&& context.get_config_bool(Config::MdnsEnabled).await
{ {
if let Err(err) = send_mdn(context, &msg).await { let mdns_enabled = job_try!(context.get_config_bool(Config::MdnsEnabled).await);
warn!(context, "could not send out mdn for {}: {}", msg.id, err); if mdns_enabled {
return Status::Finished(Err(err)); 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(())) Status::Finished(Ok(()))
@@ -820,50 +820,46 @@ impl Job {
pub async fn kill_action(context: &Context, action: Action) -> bool { pub async fn kill_action(context: &Context, action: Action) -> bool {
context context
.sql .sql
.execute("DELETE FROM jobs WHERE action=?;", paramsv![action]) .execute(sqlx::query("DELETE FROM jobs WHERE action=?;").bind(action))
.await .await
.is_ok() .is_ok()
} }
/// Remove jobs with specified IDs. /// Remove jobs with specified IDs.
async fn kill_ids(context: &Context, job_ids: &[u32]) -> sql::Result<()> { async fn kill_ids(context: &Context, job_ids: &[u32]) -> sql::Result<()> {
context let q = format!(
.sql "DELETE FROM jobs WHERE id IN({})",
.execute( job_ids.iter().map(|_| "?").join(",")
format!( );
"DELETE FROM jobs WHERE id IN({})", let mut query = sqlx::query(&q);
job_ids.iter().map(|_| "?").join(",") for id in job_ids {
), query = query.bind(*id);
job_ids.iter().map(|i| i as &dyn crate::ToSql).collect(), }
) context.sql.execute(query).await?;
.await?;
Ok(()) Ok(())
} }
pub async fn action_exists(context: &Context, action: Action) -> bool { pub async fn action_exists(context: &Context, action: Action) -> bool {
context context
.sql .sql
.exists("SELECT id FROM jobs WHERE action=?;", paramsv![action]) .exists(sqlx::query("SELECT COUNT(*) FROM jobs WHERE action=?;").bind(action))
.await .await
.unwrap_or_default() .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; message::update_msg_state(context, msg_id, MessageState::OutDelivered).await;
let chat_id: ChatId = context let chat_id: ChatId = context
.sql .sql
.query_get_value( .query_get_value(sqlx::query("SELECT chat_id FROM msgs WHERE id=?").bind(msg_id))
context, .await?
"SELECT chat_id FROM msgs WHERE id=?",
paramsv![msg_id],
)
.await
.unwrap_or_default(); .unwrap_or_default();
context.emit_event(EventType::MsgDelivered { chat_id, msg_id }); context.emit_event(EventType::MsgDelivered { chat_id, msg_id });
Ok(())
} }
async fn add_all_recipients_as_contacts(context: &Context, imap: &mut Imap, folder: Config) { 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 m
} else { } else {
return; return;
@@ -933,14 +929,14 @@ pub async fn send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<Job
let from = context let from = context
.get_config(Config::ConfiguredAddr) .get_config(Config::ConfiguredAddr)
.await .await?
.unwrap_or_default(); .unwrap_or_default();
let lowercase_from = from.to_lowercase(); let lowercase_from = from.to_lowercase();
// Send BCC to self if it is enabled and we are not going to // Send BCC to self if it is enabled and we are not going to
// delete it immediately. // delete it immediately.
if context.get_config_bool(Config::BccSelf).await if context.get_config_bool(Config::BccSelf).await?
&& context.get_config_delete_server_after().await != Some(0) && context.get_config_delete_server_after().await? != Some(0)
&& !recipients && !recipients
.iter() .iter()
.any(|x| x.to_lowercase() == lowercase_from) .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, context,
"message {} has no recipient, skipping smtp-send", msg_id "message {} has no recipient, skipping smtp-send", msg_id
); );
set_delivered(context, msg_id).await; set_delivered(context, msg_id).await?;
return Ok(None); 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.subject = rendered_msg.subject.clone();
msg.update_subject(context).await; 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)) Ok(Some(job))
} }
@@ -1200,13 +1196,13 @@ pub(crate) async fn schedule_resync(context: &Context) {
} }
/// Creates a job. /// 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!( ensure!(
action != Action::Unknown, action != Action::Unknown,
"Invalid action passed to job_add" "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. /// 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> { 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); let next_time = last_time + (60 * 60 * 24);
if next_time <= time() { if next_time <= time() {
@@ -1280,65 +1282,77 @@ pub(crate) async fn load_next(
sleep(Duration::from_millis(500)).await; sleep(Duration::from_millis(500)).await;
} }
let query;
let params;
let t = time(); let t = time();
let m;
let thread_i = thread as i64; let thread_i = thread as i64;
if let Some(msg_id) = info.msg_id { let get_query = || {
query = r#" if let Some(msg_id) = info.msg_id {
sqlx::query(
r#"
SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
FROM jobs FROM jobs
WHERE thread=? AND foreign_id=? WHERE thread=? AND foreign_id=?
ORDER BY action DESC, added_timestamp ORDER BY action DESC, added_timestamp
LIMIT 1; LIMIT 1;
"#; "#,
m = msg_id; )
params = paramsv![thread_i, m]; .bind(thread_i)
} else if !info.probe_network { .bind(msg_id)
// processing for first-try and after backoff-timeouts: } else if !info.probe_network {
// process jobs in the order they were added. // processing for first-try and after backoff-timeouts:
query = r#" // process jobs in the order they were added.
sqlx::query(
r#"
SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
FROM jobs FROM jobs
WHERE thread=? AND desired_timestamp<=? WHERE thread=? AND desired_timestamp<=?
ORDER BY action DESC, added_timestamp ORDER BY action DESC, added_timestamp
LIMIT 1; LIMIT 1;
"#; "#,
params = paramsv![thread_i, t]; )
} else { .bind(thread_i)
// processing after call to dc_maybe_network(): .bind(t)
// process _all_ pending jobs that failed before } else {
// in the order of their backoff-times. // processing after call to dc_maybe_network():
query = r#" // 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 SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
FROM jobs FROM jobs
WHERE thread=? AND tries>0 WHERE thread=? AND tries>0
ORDER BY desired_timestamp, action DESC ORDER BY desired_timestamp, action DESC
LIMIT 1; LIMIT 1;
"#; "#,
params = paramsv![thread_i]; )
.bind(thread_i)
}
}; };
let job = loop { let job = loop {
let job_res = context let job_res = context
.sql .sql
.query_row_optional(query, params.clone(), |row| { .fetch_optional(get_query())
let job = Job { .await
job_id: row.get("id")?, .and_then(|row| {
action: row.get("action")?, if let Some(row) = row {
foreign_id: row.get("foreign_id")?, Ok(Some(Job {
desired_timestamp: row.get("desired_timestamp")?, job_id: row.try_get("id")?,
added_timestamp: row.get("added_timestamp")?, action: row.try_get("action")?,
tries: row.get("tries")?, foreign_id: row.try_get("foreign_id")?,
param: row.get::<_, String>("param")?.parse().unwrap_or_default(), desired_timestamp: row.try_get("desired_timestamp")?,
pending_error: None, added_timestamp: row.try_get("added_timestamp")?,
}; tries: row.try_get::<i64, _>("tries")? as u32,
param: row
Ok(job) .try_get::<String, _>("param")?
}) .parse()
.await; .unwrap_or_default(),
pending_error: None,
}))
} else {
Ok(None)
}
});
match job_res { match job_res {
Ok(job) => break job, Ok(job) => break job,
@@ -1349,15 +1363,18 @@ LIMIT 1;
// TODO: improve by only doing a single query // TODO: improve by only doing a single query
match context match context
.sql .sql
.query_row(query, params.clone(), |row| row.get::<_, i32>(0)) .fetch_one(get_query())
.await .await
.and_then(|row| row.try_get::<i32, _>(0).map_err(Into::into))
{ {
Ok(id) => { Ok(id) => {
context if let Err(err) = context
.sql .sql
.execute("DELETE FROM jobs WHERE id=?;", paramsv![id]) .execute(sqlx::query("DELETE FROM jobs WHERE id=?;").bind(id))
.await .await
.ok(); {
warn!(context, "failed to delete job {}: {:?}", id, err);
}
} }
Err(err) => { Err(err) => {
error!(context, "failed to retrieve invalid job from DB: {}", err); error!(context, "failed to retrieve invalid job from DB: {}", err);
@@ -1399,22 +1416,22 @@ mod tests {
use crate::test_utils::TestContext; 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(); let now = time();
context context
.sql .sql
.execute( .execute(
"INSERT INTO jobs sqlx::query(
"INSERT INTO jobs
(added_timestamp, thread, action, foreign_id, param, desired_timestamp) (added_timestamp, thread, action, foreign_id, param, desired_timestamp)
VALUES (?, ?, ?, ?, ?, ?);", VALUES (?, ?, ?, ?, ?, ?);",
paramsv![ )
now, .bind(now)
Thread::from(Action::MoveMsg), .bind(Thread::from(Action::MoveMsg))
Action::MoveMsg, .bind(if valid { Action::MoveMsg as i32 } else { -1 })
foreign_id, .bind(foreign_id)
Params::new().to_string(), .bind(Params::new().to_string())
now .bind(now),
],
) )
.await .await
.unwrap(); .unwrap();
@@ -1426,7 +1443,7 @@ mod tests {
// fails to load from the database instead of failing to load // fails to load from the database instead of failing to load
// all jobs. // all jobs.
let t = TestContext::new().await; 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( let jobs = load_next(
&t, &t,
Thread::from(Action::MoveMsg), 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: // The housekeeping job should be loaded as we didn't run housekeeping in the last day:
assert!(jobs.unwrap().action == Action::Housekeeping); assert!(jobs.unwrap().action == Action::Housekeeping);
insert_job(&t, 1).await; insert_job(&t, 1, true).await;
let jobs = load_next( let jobs = load_next(
&t, &t,
Thread::from(Action::MoveMsg), Thread::from(Action::MoveMsg),
@@ -1450,7 +1467,7 @@ mod tests {
async fn test_load_next_job_one() { async fn test_load_next_job_one() {
let t = TestContext::new().await; let t = TestContext::new().await;
insert_job(&t, 1).await; insert_job(&t, 1, true).await;
let jobs = load_next( let jobs = load_next(
&t, &t,

View File

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

View File

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

View File

@@ -1,8 +1,11 @@
//! Location handling //! Location handling
use std::convert::TryFrom;
use anyhow::{ensure, Error}; use anyhow::{ensure, Error};
use async_std::prelude::*;
use bitflags::bitflags; use bitflags::bitflags;
use quick_xml::events::{BytesEnd, BytesStart, BytesText}; use quick_xml::events::{BytesEnd, BytesStart, BytesText};
use sqlx::Row;
use crate::chat::{self, ChatId}; use crate::chat::{self, ChatId};
use crate::config::Config; use crate::config::Config;
@@ -198,15 +201,15 @@ pub async fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds:
if context if context
.sql .sql
.execute( .execute(
"UPDATE chats \ sqlx::query(
"UPDATE chats \
SET locations_send_begin=?, \ SET locations_send_begin=?, \
locations_send_until=? \ locations_send_until=? \
WHERE id=?", WHERE id=?",
paramsv![ )
if 0 != seconds { now } else { 0 }, .bind(if 0 != seconds { now } else { 0 })
if 0 != seconds { now + seconds } else { 0 }, .bind(if 0 != seconds { now + seconds } else { 0 })
chat_id, .bind(chat_id),
],
) )
.await .await
.is_ok() .is_ok()
@@ -259,16 +262,17 @@ pub async fn is_sending_locations_to_chat(context: &Context, chat_id: Option<Cha
Some(chat_id) => context Some(chat_id) => context
.sql .sql
.exists( .exists(
"SELECT id FROM chats WHERE id=? AND locations_send_until>?;", sqlx::query("SELECT COUNT(id) FROM chats WHERE id=? AND locations_send_until>?;")
paramsv![chat_id, time()], .bind(chat_id)
.bind(time()),
) )
.await .await
.unwrap_or_default(), .unwrap_or_default(),
None => context None => context
.sql .sql
.exists( .exists(
"SELECT id FROM chats WHERE locations_send_until>?;", sqlx::query("SELECT COUNT(id) FROM chats WHERE locations_send_until>?;")
paramsv![time()], .bind(time()),
) )
.await .await
.unwrap_or_default(), .unwrap_or_default(),
@@ -281,28 +285,29 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64
} }
let mut continue_streaming = false; let mut continue_streaming = false;
if let Ok(chats) = context if let Ok(mut chats) = context
.sql .sql
.query_map( .fetch(sqlx::query("SELECT id FROM chats WHERE locations_send_until>?;").bind(time()))
"SELECT id FROM chats WHERE locations_send_until>?;",
paramsv![time()],
|row| row.get::<_, i32>(0),
|chats| chats.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await .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( if let Err(err) = context.sql.execute(
sqlx::query(
"INSERT INTO locations \ "INSERT INTO locations \
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);", (latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);"
paramsv![ )
latitude, .bind(latitude)
longitude, .bind(longitude)
accuracy, .bind(accuracy)
time(), .bind(time())
chat_id, .bind(chat_id)
DC_CONTACT_ID_SELF, .bind(DC_CONTACT_ID_SELF)
]
).await { ).await {
warn!(context, "failed to store location {:?}", err); warn!(context, "failed to store location {:?}", err);
} else { } else {
@@ -324,10 +329,11 @@ pub async fn get_range(
contact_id: Option<u32>, contact_id: Option<u32>,
timestamp_from: i64, timestamp_from: i64,
mut timestamp_to: i64, mut timestamp_to: i64,
) -> Vec<Location> { ) -> Result<Vec<Location>, Error> {
if timestamp_to == 0 { if timestamp_to == 0 {
timestamp_to = time() + 10; timestamp_to = time() + 10;
} }
let (disable_chat_id, chat_id) = match chat_id { let (disable_chat_id, chat_id) = match chat_id {
Some(chat_id) => (0, chat_id), Some(chat_id) => (0, chat_id),
None => (1, ChatId::new(0)), // this ChatId is unused None => (1, ChatId::new(0)), // this ChatId is unused
@@ -336,56 +342,52 @@ pub async fn get_range(
Some(contact_id) => (0, contact_id), Some(contact_id) => (0, contact_id),
None => (1, 0), // this contact_id is unused None => (1, 0), // this contact_id is unused
}; };
context
let list = context
.sql .sql
.query_map( .fetch(
"SELECT l.id, l.latitude, l.longitude, l.accuracy, l.timestamp, l.independent, \ 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 \ 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=?) \ FROM locations l LEFT JOIN msgs m ON l.id=m.location_id WHERE (? OR l.chat_id=?) \
AND (? OR l.from_id=?) \ AND (? OR l.from_id=?) \
AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \ AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \
ORDER BY l.timestamp DESC, l.id DESC, msg_id DESC;", ORDER BY l.timestamp DESC, l.id DESC, msg_id DESC;",
paramsv![ )
disable_chat_id, .bind(disable_chat_id)
chat_id, .bind(chat_id)
disable_contact_id, .bind(disable_contact_id)
contact_id as i32, .bind(contact_id as i64)
timestamp_from, .bind(timestamp_from)
timestamp_to, .bind(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)
},
) )
.await .await?
.unwrap_or_default() .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 { fn is_marker(txt: &str) -> bool {
@@ -399,10 +401,7 @@ fn is_marker(txt: &str) -> bool {
/// Deletes all locations from the database. /// Deletes all locations from the database.
pub async fn delete_all(context: &Context) -> Result<(), Error> { pub async fn delete_all(context: &Context) -> Result<(), Error> {
context context.sql.execute("DELETE FROM locations;").await?;
.sql
.execute("DELETE FROM locations;", paramsv![])
.await?;
context.emit_event(EventType::LocationChanged(None)); context.emit_event(EventType::LocationChanged(None));
Ok(()) Ok(())
} }
@@ -412,19 +411,23 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)
let self_addr = context let self_addr = context
.get_config(Config::ConfiguredAddr) .get_config(Config::ConfiguredAddr)
.await .await?
.unwrap_or_default(); .unwrap_or_default();
let (locations_send_begin, locations_send_until, locations_last_sent) = context.sql.query_row( let (locations_send_begin, locations_send_until, locations_last_sent) = {
"SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;", let row = context.sql.fetch_one(
paramsv![chat_id], |row| { sqlx::query(
let send_begin: i64 = row.get(0)?; "SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;"
let send_until: i64 = row.get(1)?; )
let last_sent: i64 = row.get(2)?; .bind(chat_id)
).await?;
Ok((send_begin, send_until, last_sent)) let send_begin: i64 = row.try_get(0)?;
}) let send_until: i64 = row.try_get(1)?;
.await?; let last_sent: i64 = row.try_get(2)?;
(send_begin, send_until, last_sent)
};
let now = time(); let now = time();
let mut location_count = 0; let mut location_count = 0;
@@ -435,40 +438,41 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)
self_addr, self_addr,
); );
context.sql.query_map( let mut rows = context.sql.fetch(
"SELECT id, latitude, longitude, accuracy, timestamp \ sqlx::query(
"SELECT id, latitude, longitude, accuracy, timestamp \
FROM locations WHERE from_id=? \ FROM locations WHERE from_id=? \
AND timestamp>=? \ AND timestamp>=? \
AND (timestamp>=? OR timestamp=(SELECT MAX(timestamp) FROM locations WHERE from_id=?)) \ AND (timestamp>=? OR timestamp=(SELECT MAX(timestamp) FROM locations WHERE from_id=?)) \
AND independent=0 \ AND independent=0 \
GROUP BY timestamp \ GROUP BY timestamp \
ORDER BY timestamp;", ORDER BY timestamp;"
paramsv![DC_CONTACT_ID_SELF, locations_send_begin, locations_last_sent, DC_CONTACT_ID_SELF], )
|row| { .bind(DC_CONTACT_ID_SELF)
let location_id: i32 = row.get(0)?; .bind(locations_send_begin)
let latitude: f64 = row.get(1)?; .bind(locations_last_sent)
let longitude: f64 = row.get(2)?; .bind(DC_CONTACT_ID_SELF)
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(())
}
).await?; ).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>"; ret += "</Document>\n</kml>";
} }
@@ -509,8 +513,9 @@ pub async fn set_kml_sent_timestamp(
context context
.sql .sql
.execute( .execute(
"UPDATE chats SET locations_last_sent=? WHERE id=?;", sqlx::query("UPDATE chats SET locations_last_sent=? WHERE id=?;")
paramsv![timestamp, chat_id], .bind(timestamp)
.bind(chat_id),
) )
.await?; .await?;
Ok(()) Ok(())
@@ -524,8 +529,9 @@ pub async fn set_msg_location_id(
context context
.sql .sql
.execute( .execute(
"UPDATE msgs SET location_id=? WHERE id=?;", sqlx::query("UPDATE msgs SET location_id=? WHERE id=?;")
paramsv![location_id, msg_id], .bind(location_id)
.bind(msg_id),
) )
.await?; .await?;
@@ -544,6 +550,11 @@ pub async fn save(
let mut newest_timestamp = 0; let mut newest_timestamp = 0;
let mut newest_location_id = 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 { for location in locations {
let &Location { let &Location {
timestamp, timestamp,
@@ -552,53 +563,42 @@ pub async fn save(
accuracy, accuracy,
.. ..
} = location; } = location;
let (loc_id, ts) = context let exists = context
.sql .sql
.with_conn(move |mut conn| { .exists(sqlx::query(stmt_test).bind(timestamp).bind(contact_id))
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))
})
.await?; .await?;
newest_timestamp = ts; if independent || !exists {
newest_location_id = loc_id; 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 { 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 let rows = context
.sql .sql
.query_map( .fetch(
"SELECT id, locations_send_begin, locations_last_sent \ sqlx::query(
"SELECT id, locations_send_begin, locations_last_sent \
FROM chats \ FROM chats \
WHERE locations_send_until>?;", WHERE locations_send_until>?;",
paramsv![now], )
|row| { .bind(now),
let chat_id: ChatId = row.get(0)?; )
let locations_send_begin: i64 = row.get(1)?; .await
let locations_last_sent: i64 = row.get(2)?; .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; continue_streaming = true;
// be a bit tolerant as the timer may not align exactly with time(NULL) // 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 { } else {
Ok(Some((chat_id, locations_send_begin, locations_last_sent))) 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 .filter_map(|v| v.transpose())
.unwrap_or_default(); });
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() { for (chat_id, mut msg) in msgs.into_iter() {
// TODO: better error handling // 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 chat_id = ChatId::new(job.foreign_id);
let (send_begin, send_until) = job_try!( let (send_begin, send_until) = job_try!(context
context .sql
.sql .fetch_one(
.query_row( sqlx::query(
"SELECT locations_send_begin, locations_send_until FROM chats WHERE id=?", "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) { if !(send_begin != 0 && time() <= send_until) {
// still streaming - // 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 // do not un-schedule pending DC_MAYBE_SEND_LOC_ENDED jobs
if !(send_begin == 0 && send_until == 0) { if !(send_begin == 0 && send_until == 0) {
// not streaming, device-message already sent // not streaming, device-message already sent
job_try!(context.sql.execute( job_try!(
"UPDATE chats SET locations_send_begin=0, locations_send_until=0 WHERE id=?", context
paramsv![chat_id], .sql
).await); .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; let stock_str = stock_str::msg_location_disabled(context).await;
chat::add_info_msg(context, chat_id, stock_str).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}; use crate::{context::Context, provider::Socket};
#[derive(Copy, Clone, Debug, Display, FromPrimitive, PartialEq, Eq)] #[derive(Copy, Clone, Debug, Display, FromPrimitive, PartialEq, Eq)]
#[repr(i32)] #[repr(u32)]
#[strum(serialize_all = "snake_case")] #[strum(serialize_all = "snake_case")]
pub enum CertificateChecks { pub enum CertificateChecks {
/// Same as AcceptInvalidCertificates unless overridden by /// Same as AcceptInvalidCertificates unless overridden by
@@ -54,91 +54,85 @@ pub struct LoginParam {
impl LoginParam { impl LoginParam {
/// Read the login parameters from the database. /// 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 prefix = prefix.as_ref();
let sql = &context.sql; let sql = &context.sql;
let key = format!("{}addr", prefix); let key = format!("{}addr", prefix);
let addr = sql let addr = sql
.get_raw_config(context, key) .get_raw_config(key)
.await .await?
.unwrap_or_default() .unwrap_or_default()
.trim() .trim()
.to_string(); .to_string();
let key = format!("{}mail_server", prefix); 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 key = format!("{}mail_port", prefix);
let mail_port = sql let mail_port = sql.get_raw_config_int(key).await?.unwrap_or_default();
.get_raw_config_int(context, key)
.await
.unwrap_or_default();
let key = format!("{}mail_user", prefix); 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 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 key = format!("{}mail_security", prefix);
let mail_security = sql let mail_security = sql
.get_raw_config_int(context, key) .get_raw_config_int(key)
.await .await?
.and_then(num_traits::FromPrimitive::from_i32) .and_then(num_traits::FromPrimitive::from_i32)
.unwrap_or_default(); .unwrap_or_default();
let key = format!("{}imap_certificate_checks", prefix); let key = format!("{}imap_certificate_checks", prefix);
let imap_certificate_checks = 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() num_traits::FromPrimitive::from_i32(certificate_checks).unwrap()
} else { } else {
Default::default() Default::default()
}; };
let key = format!("{}send_server", prefix); 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 key = format!("{}send_port", prefix);
let send_port = sql let send_port = sql.get_raw_config_int(key).await?.unwrap_or_default();
.get_raw_config_int(context, key)
.await
.unwrap_or_default();
let key = format!("{}send_user", prefix); 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 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 key = format!("{}send_security", prefix);
let send_security = sql let send_security = sql
.get_raw_config_int(context, key) .get_raw_config_int(key)
.await .await?
.and_then(num_traits::FromPrimitive::from_i32) .and_then(num_traits::FromPrimitive::from_i32)
.unwrap_or_default(); .unwrap_or_default();
let key = format!("{}smtp_certificate_checks", prefix); let key = format!("{}smtp_certificate_checks", prefix);
let smtp_certificate_checks = 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() num_traits::FromPrimitive::from_i32(certificate_checks).unwrap()
} else { } else {
Default::default() Default::default()
}; };
let key = format!("{}server_flags", prefix); let key = format!("{}server_flags", prefix);
let server_flags = sql let server_flags = sql.get_raw_config_int(key).await?.unwrap_or_default();
.get_raw_config_int(context, key)
.await
.unwrap_or_default();
let key = format!("{}provider", prefix); let key = format!("{}provider", prefix);
let provider = sql let provider = sql
.get_raw_config(context, key) .get_raw_config(key)
.await .await?
.and_then(|provider_id| get_provider_by_id(&provider_id)); .and_then(|provider_id| get_provider_by_id(&provider_id));
LoginParam { Ok(LoginParam {
addr, addr,
imap: ServerLoginParam { imap: ServerLoginParam {
server: mail_server, server: mail_server,
@@ -158,7 +152,7 @@ impl LoginParam {
}, },
provider, provider,
server_flags, server_flags,
} })
} }
/// Save this loginparam to the database. /// Save this loginparam to the database.
@@ -171,63 +165,54 @@ impl LoginParam {
let sql = &context.sql; let sql = &context.sql;
let key = format!("{}addr", prefix); 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); let key = format!("{}mail_server", prefix);
sql.set_raw_config(context, key, Some(&self.imap.server)) sql.set_raw_config(key, Some(&self.imap.server)).await?;
.await?;
let key = format!("{}mail_port", prefix); let key = format!("{}mail_port", prefix);
sql.set_raw_config_int(context, key, self.imap.port as i32) sql.set_raw_config_int(key, self.imap.port as i32).await?;
.await?;
let key = format!("{}mail_user", prefix); let key = format!("{}mail_user", prefix);
sql.set_raw_config(context, key, Some(&self.imap.user)) sql.set_raw_config(key, Some(&self.imap.user)).await?;
.await?;
let key = format!("{}mail_pw", prefix); let key = format!("{}mail_pw", prefix);
sql.set_raw_config(context, key, Some(&self.imap.password)) sql.set_raw_config(key, Some(&self.imap.password)).await?;
.await?;
let key = format!("{}mail_security", prefix); 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?; .await?;
let key = format!("{}imap_certificate_checks", prefix); 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?; .await?;
let key = format!("{}send_server", prefix); let key = format!("{}send_server", prefix);
sql.set_raw_config(context, key, Some(&self.smtp.server)) sql.set_raw_config(key, Some(&self.smtp.server)).await?;
.await?;
let key = format!("{}send_port", prefix); let key = format!("{}send_port", prefix);
sql.set_raw_config_int(context, key, self.smtp.port as i32) sql.set_raw_config_int(key, self.smtp.port as i32).await?;
.await?;
let key = format!("{}send_user", prefix); let key = format!("{}send_user", prefix);
sql.set_raw_config(context, key, Some(&self.smtp.user)) sql.set_raw_config(key, Some(&self.smtp.user)).await?;
.await?;
let key = format!("{}send_pw", prefix); let key = format!("{}send_pw", prefix);
sql.set_raw_config(context, key, Some(&self.smtp.password)) sql.set_raw_config(key, Some(&self.smtp.password)).await?;
.await?;
let key = format!("{}send_security", prefix); 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?; .await?;
let key = format!("{}smtp_certificate_checks", prefix); 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?; .await?;
let key = format!("{}server_flags", prefix); let key = format!("{}server_flags", prefix);
sql.set_raw_config_int(context, key, self.server_flags) sql.set_raw_config_int(key, self.server_flags).await?;
.await?;
if let Some(provider) = self.provider { if let Some(provider) = self.provider {
let key = format!("{}provider", prefix); 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(()) Ok(())

View File

@@ -1,5 +1,3 @@
use deltachat_derive::{FromSql, ToSql};
use crate::key::Fingerprint; use crate::key::Fingerprint;
/// An object containing a set of values. /// An object containing a set of values.
@@ -22,9 +20,7 @@ pub struct Lot {
} }
#[repr(u8)] #[repr(u8)]
#[derive( #[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
)]
pub enum Meaning { pub enum Meaning {
None = 0, None = 0,
Text1Draft = 1, Text1Draft = 1,
@@ -68,10 +64,8 @@ impl Lot {
} }
} }
#[repr(i32)] #[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[derive( #[repr(u32)]
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
)]
pub enum LotState { pub enum LotState {
// Default // Default
Undefined = 0, 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::blob::BlobObject;
use crate::chat::{self, Chat}; use crate::chat::{self, Chat};
use crate::config::Config; use crate::config::Config;
@@ -20,11 +28,6 @@ use crate::param::Param;
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus}; use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::simplify::escape_message_footer_marks; use crate::simplify::escape_message_footer_marks;
use crate::stock_str; 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 // 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). // (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 let from_addr = context
.get_config(Config::ConfiguredAddr) .get_config(Config::ConfiguredAddr)
.await .await?
.unwrap_or_default(); .unwrap_or_default();
let config_displayname = context let config_displayname = context
.get_config(Config::Displayname) .get_config(Config::Displayname)
.await .await?
.unwrap_or_default(); .unwrap_or_default();
let (from_displayname, sender_displayname) = let (from_displayname, sender_displayname) =
if let Some(override_name) = msg.param.get(Param::OverrideSenderDisplayname) { if let Some(override_name) = msg.param.get(Param::OverrideSenderDisplayname) {
@@ -112,52 +115,42 @@ impl<'a> MimeFactory<'a> {
if chat.is_self_talk() { if chat.is_self_talk() {
recipients.push((from_displayname.to_string(), from_addr.to_string())); recipients.push((from_displayname.to_string(), from_addr.to_string()));
} else { } else {
context let mut rows = context
.sql .sql
.query_map( .fetch(
"SELECT c.authname, c.addr \ sqlx::query(
"SELECT c.authname, c.addr \
FROM chats_contacts cc \ FROM chats_contacts cc \
LEFT JOIN contacts c ON cc.contact_id=c.id \ LEFT JOIN contacts c ON cc.contact_id=c.id \
WHERE cc.chat_id=? AND cc.contact_id>9;", WHERE cc.chat_id=? AND cc.contact_id>9;",
paramsv![msg.chat_id], )
|row| { .bind(msg.chat_id),
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(())
},
) )
.await?; .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; req_mdn = true;
} }
} }
let (in_reply_to, references) = context let row = context
.sql .sql
.query_row( .fetch_one(
"SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?", sqlx::query("SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?")
paramsv![msg.id], .bind(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),
))
},
) )
.await .await?;
.context("Can't get mime_in_reply_to, mime_references")?; 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 default_str = stock_str::status_line(context).await;
let factory = MimeFactory { let factory = MimeFactory {
@@ -166,7 +159,7 @@ impl<'a> MimeFactory<'a> {
sender_displayname, sender_displayname,
selfstatus: context selfstatus: context
.get_config(Config::Selfstatus) .get_config(Config::Selfstatus)
.await .await?
.unwrap_or(default_str), .unwrap_or(default_str),
recipients, recipients,
timestamp: msg.timestamp_sort, timestamp: msg.timestamp_sort,
@@ -191,16 +184,16 @@ impl<'a> MimeFactory<'a> {
let contact = Contact::load_from_db(context, msg.from_id).await?; let contact = Contact::load_from_db(context, msg.from_id).await?;
let from_addr = context let from_addr = context
.get_config(Config::ConfiguredAddr) .get_config(Config::ConfiguredAddr)
.await .await?
.unwrap_or_default(); .unwrap_or_default();
let from_displayname = context let from_displayname = context
.get_config(Config::Displayname) .get_config(Config::Displayname)
.await .await?
.unwrap_or_default(); .unwrap_or_default();
let default_str = stock_str::status_line(context).await; let default_str = stock_str::status_line(context).await;
let selfstatus = context let selfstatus = context
.get_config(Config::Selfstatus) .get_config(Config::Selfstatus)
.await .await?
.unwrap_or(default_str); .unwrap_or(default_str);
let timestamp = dc_create_smeared_timestamp(context).await; let timestamp = dc_create_smeared_timestamp(context).await;
@@ -232,7 +225,7 @@ impl<'a> MimeFactory<'a> {
) -> Result<Vec<(Option<Peerstate>, &str)>, Error> { ) -> Result<Vec<(Option<Peerstate>, &str)>, Error> {
let self_addr = context let self_addr = context
.get_config(Config::ConfiguredAddr) .get_config(Config::ConfiguredAddr)
.await .await?
.ok_or_else(|| format_err!("Not configured"))?; .ok_or_else(|| format_err!("Not configured"))?;
let mut res = Vec::new(); 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 { match &self.loaded {
Loaded::Message { chat } => { Loaded::Message { chat } => {
// beside key- and member-changes, force re-gossip every 48 hours // 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) { 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> { async fn subject_str(&self, context: &Context) -> anyhow::Result<String> {
let quoted_msg_subject = self.msg.quoted_message(context).await?.map(|m| m.subject); 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 } => { Loaded::Message { ref chat } => {
if self.msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage { if self.msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage {
return Ok(stock_str::ac_setup_msg_subject(context).await); 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 { if let Some(last_subject) = parent_subject {
format!("Re: {}", remove_subject_prefix(last_subject)) format!("Re: {}", remove_subject_prefix(last_subject))
} else { } else {
let self_name = match context.get_config(Config::Displayname).await { let self_name = match context.get_config(Config::Displayname).await? {
Some(name) => name, 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 stock_str::subject_for_new_contact(context, self_name).await
} }
} }
Loaded::MDN { .. } => stock_str::read_rcpt(context).await, Loaded::MDN { .. } => stock_str::read_rcpt(context).await,
}) };
Ok(subject)
} }
pub fn recipients(&self) -> Vec<String> { pub fn recipients(&self) -> Vec<String> {
@@ -567,7 +562,7 @@ impl<'a> MimeFactory<'a> {
let outer_message = if is_encrypted { let outer_message = if is_encrypted {
// Add gossip headers in chats with multiple recipients // 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()) { for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) {
if peerstate.peek_key(min_verified).is_some() { if peerstate.peek_key(min_verified).is_some() {
if let Some(header) = peerstate.render_gossip_header(min_verified) { 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. // for simplificity and to avoid conversion errors, we're generating the HTML-part from the original message.
if self.msg.has_html() { if self.msg.has_html() {
let html = if let Some(orig_msg_id) = self.msg.param.get_int(Param::Forwarded) { 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 { } else {
self.msg.param.get(Param::SendHtml).map(|s| s.to_string()) self.msg.param.get(Param::SendHtml).map(|s| s.to_string())
}; };
@@ -1009,7 +1006,7 @@ impl<'a> MimeFactory<'a> {
} }
if self.attach_selfavatar { 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) { Some(path) => match build_selfavatar_file(context, &path) {
Ok((part, filename)) => { Ok((part, filename)) => {
parts.push(part); parts.push(part);
@@ -1137,14 +1134,14 @@ async fn build_body_file(
// etc. // etc.
let filename_to_send: String = match msg.viewtype { let filename_to_send: String = match msg.viewtype {
Viewtype::Voice => chrono::Utc 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)) .format(&format!("voice-message_%Y-%m-%d_%H-%M-%S.{}", &suffix))
.to_string(), .to_string(),
Viewtype::Image | Viewtype::Gif => format!( Viewtype::Image | Viewtype::Gif => format!(
"{}.{}", "{}.{}",
if base_name.is_empty() { if base_name.is_empty() {
chrono::Utc chrono::Utc
.timestamp(msg.timestamp_sort as i64, 0) .timestamp(msg.timestamp_sort, 0)
.format("image_%Y-%m-%d_%H-%M-%S") .format("image_%Y-%m-%d_%H-%M-%S")
.to_string() .to_string()
} else { } else {
@@ -1155,7 +1152,7 @@ async fn build_body_file(
Viewtype::Video => format!( Viewtype::Video => format!(
"video_{}.{}", "video_{}.{}",
chrono::Utc chrono::Utc
.timestamp(msg.timestamp_sort as i64, 0) .timestamp(msg.timestamp_sort, 0)
.format("%Y-%m-%d_%H-%M-%S") .format("%Y-%m-%d_%H-%M-%S")
.to_string(), .to_string(),
&suffix &suffix

View File

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

View File

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

View File

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

View File

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

View File

@@ -791,20 +791,39 @@ mod tests {
async fn test_set_config_from_qr() { async fn test_set_config_from_qr() {
let ctx = TestContext::new().await; 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; let res = set_config_from_qr(&ctx.ctx, "badqr:https://example.org/").await;
assert!(!res.is_ok()); 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; let res = set_config_from_qr(&ctx.ctx, "https://no.qr").await;
assert!(!res.is_ok()); 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; let res = set_config_from_qr(&ctx.ctx, "dcwebrtc:https://example.org/").await;
assert!(res.is_ok()); assert!(res.is_ok());
assert_eq!( assert_eq!(
ctx.ctx.get_config(Config::WebrtcInstance).await.unwrap(), ctx.ctx
.get_config(Config::WebrtcInstance)
.await
.unwrap()
.unwrap(),
"https://example.org/" "https://example.org/"
); );
@@ -812,7 +831,11 @@ mod tests {
set_config_from_qr(&ctx.ctx, "DCWEBRTC:basicwebrtc:https://foo.bar/?$ROOM&test").await; set_config_from_qr(&ctx.ctx, "DCWEBRTC:basicwebrtc:https://foo.bar/?$ROOM&test").await;
assert!(res.is_ok()); assert!(res.is_ok());
assert_eq!( 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" "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) => { Some(job) => {
// Let the fetch run, but return back to the job afterwards. // Let the fetch run, but return back to the job afterwards.
jobs_loaded = 0; 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); info!(ctx, "postponing imap-job {} to run fetch...", job);
fetch(&ctx, &mut connection).await; 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; 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 fetch_idle(&ctx, &mut connection, Config::ConfiguredInboxFolder).await
} else { } else {
if let Err(err) = connection.scan_folders(&ctx).await { 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) { async fn fetch(ctx: &Context, connection: &mut Imap) {
match ctx.get_config(Config::ConfiguredInboxFolder).await { match ctx.get_config(Config::ConfiguredInboxFolder).await {
Some(watch_folder) => { Ok(Some(watch_folder)) => {
if let Err(err) = connection.connect_configured(ctx).await { if let Err(err) = connection.connect_configured(ctx).await {
error_network!(ctx, "{}", err); error_network!(ctx, "{}", err);
return; return;
@@ -133,16 +141,23 @@ async fn fetch(ctx: &Context, connection: &mut Imap) {
warn!(ctx, "{:#}", err); warn!(ctx, "{:#}", err);
} }
} }
None => { Ok(None) => {
warn!(ctx, "Can not fetch inbox folder, not set"); warn!(ctx, "Can not fetch inbox folder, not set");
connection.fake_idle(ctx, None).await; 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 { async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> InterruptInfo {
match ctx.get_config(folder).await { match ctx.get_config(folder).await {
Some(watch_folder) => { Ok(Some(watch_folder)) => {
// connect and fake idle if unable to connect // connect and fake idle if unable to connect
if let Err(err) = connection.connect_configured(ctx).await { if let Err(err) = connection.connect_configured(ctx).await {
warn!(ctx, "imap connection failed: {}", err); 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 connection.fake_idle(ctx, Some(watch_folder)).await
} }
} }
None => { Ok(None) => {
warn!(ctx, "Can not watch {} folder, not set", folder); warn!(ctx, "Can not watch {} folder, not set", folder);
connection.fake_idle(ctx, None).await 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(); let ctx = ctx.clone();
mvbox_handle = Some(task::spawn(async move { mvbox_handle = Some(task::spawn(async move {
simple_imap_loop( simple_imap_loop(
@@ -317,7 +343,11 @@ impl Scheduler {
.expect("mvbox start send, missing receiver"); .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(); let ctx = ctx.clone();
sentbox_handle = Some(task::spawn(async move { sentbox_handle = Some(task::spawn(async move {
simple_imap_loop( simple_imap_loop(

View File

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

View File

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

View File

@@ -15,12 +15,12 @@ pub type Result<T> = std::result::Result<T, Error>;
pub enum Error { pub enum Error {
#[error("Envelope error: {}", _0)] #[error("Envelope error: {}", _0)]
EnvelopeError(#[from] async_smtp::error::Error), EnvelopeError(#[from] async_smtp::error::Error),
#[error("Send error: {}", _0)] #[error("Send error: {}", _0)]
SendError(#[from] async_smtp::smtp::error::Error), SendError(#[from] async_smtp::smtp::error::Error),
#[error("SMTP has no transport")] #[error("SMTP has no transport")]
NoTransport, NoTransport,
#[error("{}", _0)]
Other(#[from] anyhow::Error),
} }
impl Smtp { impl Smtp {
@@ -36,7 +36,7 @@ impl Smtp {
let message_len_bytes = message.len(); let message_len_bytes = message.len();
let mut chunk_size = DEFAULT_MAX_SMTP_RCPT_TO; 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 { if let Some(max_smtp_rcpt_to) = provider.max_smtp_rcpt_to {
chunk_size = max_smtp_rcpt_to as usize; 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> { 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(()); return Ok(());
} }
// create saved-messages chat; we do this only once, if the user has deleted the chat, // 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) // 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 self.sql
.set_raw_config_bool(self, "self-chat-added", true) .set_raw_config_bool("self-chat-added", true)
.await?; .await?;
chat::create_by_contact_id(self, DC_CONTACT_ID_SELF).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 // 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(); self_talk_id.delete(&t).await.ok();
assert_eq!( 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 device_chat_msgs_before + 1
); );

View File

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

View File

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