diff --git a/CHANGELOG.md b/CHANGELOG.md index 329f52089d..f6ea5ea5b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [1.123.0] - 2023-09-22 + +### API-Changes + +- Make it possible to import secret key from a file with `DC_IMEX_IMPORT_SELF_KEYS`. +- [**breaking**] Make `dc_jsonrpc_blocking_call` accept JSON-RPC request. + +### Fixes + +- `lookup_chat_by_reply()`: Skip not fully downloaded and undecipherable messages ([#4676](https://github.com/deltachat/deltachat-core-rust/pull/4676)). +- `lookup_chat_by_reply()`: Skip undecipherable parent messages created by older versions ([#4676](https://github.com/deltachat/deltachat-core-rust/pull/4676)). +- imex: Use "default" in the filename of the default key. + +### Miscellaneous Tasks + +- Update OpenSSL from 3.1.2 to 3.1.3. + ## [1.122.0] - 2023-09-12 ### API-Changes @@ -2816,3 +2833,4 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed [1.120.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.1...v1.120.0 [1.121.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.120.0...v1.121.0 [1.122.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.121.0...v1.122.0 +[1.123.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.122.0...v1.123.0 diff --git a/Cargo.lock b/Cargo.lock index 13458b6afe..c16fb130b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1085,7 +1085,7 @@ dependencies = [ [[package]] name = "deltachat" -version = "1.122.0" +version = "1.123.0" dependencies = [ "ansi_term", "anyhow", @@ -1162,7 +1162,7 @@ dependencies = [ [[package]] name = "deltachat-jsonrpc" -version = "1.122.0" +version = "1.123.0" dependencies = [ "anyhow", "async-channel", @@ -1186,7 +1186,7 @@ dependencies = [ [[package]] name = "deltachat-repl" -version = "1.122.0" +version = "1.123.0" dependencies = [ "ansi_term", "anyhow", @@ -1201,7 +1201,7 @@ dependencies = [ [[package]] name = "deltachat-rpc-server" -version = "1.122.0" +version = "1.123.0" dependencies = [ "anyhow", "deltachat", @@ -1226,7 +1226,7 @@ dependencies = [ [[package]] name = "deltachat_ffi" -version = "1.122.0" +version = "1.123.0" dependencies = [ "anyhow", "deltachat", @@ -3057,9 +3057,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.1.3+3.1.2" +version = "300.1.5+3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd2c101a165fff9935e34def4669595ab1c7847943c42be86e21503e482be107" +checksum = "559068e4c12950d7dcaa1857a61725c0d38d4fc03ff8e070ab31a75d6e316491" dependencies = [ "cc", ] diff --git a/Cargo.toml b/Cargo.toml index 12b380bdbf..229451cf5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat" -version = "1.122.0" +version = "1.123.0" edition = "2021" license = "MPL-2.0" rust-version = "1.67" diff --git a/deltachat-ffi/Cargo.toml b/deltachat-ffi/Cargo.toml index 0611e8b793..b544dbb57d 100644 --- a/deltachat-ffi/Cargo.toml +++ b/deltachat-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat_ffi" -version = "1.122.0" +version = "1.123.0" description = "Deltachat FFI" edition = "2018" readme = "README.md" diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 0d79156685..c413d53ed8 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -2273,6 +2273,7 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co * * - **DC_IMEX_IMPORT_SELF_KEYS** (2) - Import private keys found in the directory given as `param1`. * The last imported key is made the default keys unless its name contains the string `legacy`. Public keys are not imported. + * If `param1` is a filename, import the private key from the file and make it the default. * * While dc_imex() returns immediately, the started job may take a while, * you can stop it using dc_stop_ongoing_process(). During execution of the job, @@ -5770,12 +5771,11 @@ char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance); * * @memberof dc_jsonrpc_instance_t * @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init(). - * @param method JSON-RPC method name, e.g. `check_email_validity`. - * @param params JSON-RPC method parameters, e.g. `["alice@example.org"]`. + * @param input JSON-RPC request. * @return JSON-RPC response as string, must be freed using dc_str_unref() after usage. - * On error, NULL is returned. + * If there is no response, NULL is returned. */ -char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *method, const char *params); +char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *input); /** * @class dc_event_emitter_t diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 4144f13b34..c759d60680 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -4986,7 +4986,7 @@ pub unsafe extern "C" fn dc_accounts_get_event_emitter( #[cfg(feature = "jsonrpc")] mod jsonrpc { use deltachat_jsonrpc::api::CommandApi; - use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcServer, RpcSession}; + use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession}; use super::*; @@ -5062,25 +5062,24 @@ mod jsonrpc { #[no_mangle] pub unsafe extern "C" fn dc_jsonrpc_blocking_call( jsonrpc_instance: *mut dc_jsonrpc_instance_t, - method: *const libc::c_char, - params: *const libc::c_char, + input: *const libc::c_char, ) -> *mut libc::c_char { if jsonrpc_instance.is_null() { eprintln!("ignoring careless call to dc_jsonrpc_blocking_call()"); return ptr::null_mut(); } let api = &*jsonrpc_instance; - let method = to_string_lossy(method); - let params = to_string_lossy(params); - let params: Option = match serde_json::from_str(¶ms) { - Ok(params) => Some(params), - Err(_) => None, - }; - let params = params.map(yerpc::Params::into_value).unwrap_or_default(); - let res = block_on(api.handle.server().handle_request(method, params)); + let input = to_string_lossy(input); + let res = block_on(api.handle.process_incoming(&input)); match res { - Ok(res) => res.to_string().strdup(), - Err(_) => ptr::null_mut(), + Some(message) => { + if let Ok(message) = serde_json::to_string(&message) { + message.strdup() + } else { + ptr::null_mut() + } + } + None => ptr::null_mut(), } } } diff --git a/deltachat-jsonrpc/Cargo.toml b/deltachat-jsonrpc/Cargo.toml index 6732af2ed6..bab37cd56b 100644 --- a/deltachat-jsonrpc/Cargo.toml +++ b/deltachat-jsonrpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-jsonrpc" -version = "1.122.0" +version = "1.123.0" description = "DeltaChat JSON-RPC API" edition = "2021" default-run = "deltachat-jsonrpc-server" diff --git a/deltachat-jsonrpc/src/api/types/message.rs b/deltachat-jsonrpc/src/api/types/message.rs index 2d4acec7e9..4bd691e3ad 100644 --- a/deltachat-jsonrpc/src/api/types/message.rs +++ b/deltachat-jsonrpc/src/api/types/message.rs @@ -318,6 +318,7 @@ pub enum DownloadState { Done, Available, Failure, + Undecipherable, InProgress, } @@ -327,6 +328,7 @@ impl From for DownloadState { download::DownloadState::Done => DownloadState::Done, download::DownloadState::Available => DownloadState::Available, download::DownloadState::Failure => DownloadState::Failure, + download::DownloadState::Undecipherable => DownloadState::Undecipherable, download::DownloadState::InProgress => DownloadState::InProgress, } } diff --git a/deltachat-jsonrpc/typescript/package.json b/deltachat-jsonrpc/typescript/package.json index 51473e5659..7f0307837b 100644 --- a/deltachat-jsonrpc/typescript/package.json +++ b/deltachat-jsonrpc/typescript/package.json @@ -55,5 +55,5 @@ }, "type": "module", "types": "dist/deltachat.d.ts", - "version": "1.122.0" + "version": "1.123.0" } diff --git a/deltachat-repl/Cargo.toml b/deltachat-repl/Cargo.toml index 35935518a9..ab623efbb3 100644 --- a/deltachat-repl/Cargo.toml +++ b/deltachat-repl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-repl" -version = "1.122.0" +version = "1.123.0" license = "MPL-2.0" edition = "2021" diff --git a/deltachat-repl/src/cmdline.rs b/deltachat-repl/src/cmdline.rs index 080d8d07b7..5500c03066 100644 --- a/deltachat-repl/src/cmdline.rs +++ b/deltachat-repl/src/cmdline.rs @@ -188,6 +188,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef, msg: &Message) { DownloadState::Available => " [⬇ Download available]", DownloadState::InProgress => " [⬇ Download in progress...]️", DownloadState::Failure => " [⬇ Download failed]", + DownloadState::Undecipherable => " [⬇ Decryption failed]", }; let temp2 = timestamp_to_str(msg.get_timestamp()); diff --git a/deltachat-rpc-server/Cargo.toml b/deltachat-rpc-server/Cargo.toml index 5a59e2a528..13cdf7ce7a 100644 --- a/deltachat-rpc-server/Cargo.toml +++ b/deltachat-rpc-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-rpc-server" -version = "1.122.0" +version = "1.123.0" description = "DeltaChat JSON-RPC server" edition = "2021" readme = "README.md" diff --git a/package.json b/package.json index a2cd51844f..31743b7386 100644 --- a/package.json +++ b/package.json @@ -60,5 +60,5 @@ "test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit" }, "types": "node/dist/index.d.ts", - "version": "1.122.0" + "version": "1.123.0" } diff --git a/python/tests/test_4_lowlevel.py b/python/tests/test_4_lowlevel.py index 5f1ec1003a..23ef3dbe3f 100644 --- a/python/tests/test_4_lowlevel.py +++ b/python/tests/test_4_lowlevel.py @@ -1,3 +1,4 @@ +import json from queue import Queue import deltachat as dc @@ -227,10 +228,26 @@ def test_jsonrpc_blocking_call(tmp_path): lib.dc_accounts_unref, ) jsonrpc = ffi.gc(lib.dc_jsonrpc_init(accounts), lib.dc_jsonrpc_unref) - res = from_optional_dc_charpointer( - lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice@example.org"]'), + res = json.loads( + from_optional_dc_charpointer( + lib.dc_jsonrpc_blocking_call( + jsonrpc, + json.dumps( + {"jsonrpc": "2.0", "method": "check_email_validity", "params": ["alice@example.org"], "id": "123"}, + ).encode("utf-8"), + ), + ), ) - assert res == "true" - - res = from_optional_dc_charpointer(lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice"]')) - assert res == "false" + assert res == {"jsonrpc": "2.0", "id": "123", "result": True} + + res = json.loads( + from_optional_dc_charpointer( + lib.dc_jsonrpc_blocking_call( + jsonrpc, + json.dumps( + {"jsonrpc": "2.0", "method": "check_email_validity", "params": ["alice"], "id": "456"}, + ).encode("utf-8"), + ), + ), + ) + assert res == {"jsonrpc": "2.0", "id": "456", "result": False} diff --git a/release-date.in b/release-date.in index 05f0fc2f90..d6d1f7fd54 100644 --- a/release-date.in +++ b/release-date.in @@ -1 +1 @@ -2023-09-12 \ No newline at end of file +2023-09-22 \ No newline at end of file diff --git a/src/chat.rs b/src/chat.rs index 103702e76a..c185d42891 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -23,6 +23,7 @@ use crate::constants::{ use crate::contact::{Contact, ContactId, Origin, VerifiedStatus}; use crate::context::Context; use crate::debug_logging::maybe_set_logging_xdc; +use crate::download::DownloadState; use crate::ephemeral::Timer as EphemeralTimer; use crate::events::EventType; use crate::html::new_html_mimepart; @@ -1052,11 +1053,14 @@ impl ChatId { T: Send + 'static, { let sql = &context.sql; + // Do not reply to not fully downloaded messages. Such a message could be a group chat + // message that we assigned to 1:1 chat. let query = format!( "SELECT {fields} \ - FROM msgs WHERE chat_id=? AND state NOT IN (?, ?) AND NOT hidden \ + FROM msgs WHERE chat_id=? AND state NOT IN (?, ?) AND NOT hidden AND download_state={} \ ORDER BY timestamp DESC, id DESC \ - LIMIT 1;" + LIMIT 1;", + DownloadState::Done as u32, ); let row = sql .query_row_optional( @@ -1081,34 +1085,17 @@ impl ChatId { self, context: &Context, ) -> Result> { - if let Some((rfc724_mid, mime_in_reply_to, mime_references, error)) = self - .parent_query( - context, - "rfc724_mid, mime_in_reply_to, mime_references, error", - |row: &rusqlite::Row| { - let rfc724_mid: String = row.get(0)?; - let mime_in_reply_to: String = row.get(1)?; - let mime_references: String = row.get(2)?; - let error: String = row.get(3)?; - Ok((rfc724_mid, mime_in_reply_to, mime_references, error)) - }, - ) - .await? - { - if !error.is_empty() { - // Do not reply to error messages. - // - // An error message could be a group chat message that we failed to decrypt and - // assigned to 1:1 chat. A reply to it will show up as a reply to group message - // on the other side. To avoid such situations, it is better not to reply to - // error messages at all. - Ok(None) - } else { - Ok(Some((rfc724_mid, mime_in_reply_to, mime_references))) - } - } else { - Ok(None) - } + self.parent_query( + context, + "rfc724_mid, mime_in_reply_to, mime_references", + |row: &rusqlite::Row| { + let rfc724_mid: String = row.get(0)?; + let mime_in_reply_to: String = row.get(1)?; + let mime_references: String = row.get(2)?; + Ok((rfc724_mid, mime_in_reply_to, mime_references)) + }, + ) + .await } /// Returns multi-line text summary of encryption preferences of all chat contacts. diff --git a/src/download.rs b/src/download.rs index 2f3ea499de..4146cd9668 100644 --- a/src/download.rs +++ b/src/download.rs @@ -59,6 +59,9 @@ pub enum DownloadState { /// Failed to fully download the message. Failure = 20, + /// Undecipherable message. + Undecipherable = 30, + /// Full download of the message is in progress. InProgress = 1000, } @@ -80,7 +83,9 @@ impl MsgId { pub async fn download_full(self, context: &Context) -> Result<()> { let msg = Message::load_from_db(context, self).await?; match msg.download_state() { - DownloadState::Done => return Err(anyhow!("Nothing to download.")), + DownloadState::Done | DownloadState::Undecipherable => { + return Err(anyhow!("Nothing to download.")) + } DownloadState::InProgress => return Err(anyhow!("Download already in progress.")), DownloadState::Available | DownloadState::Failure => { self.update_download_state(context, DownloadState::InProgress) diff --git a/src/imex.rs b/src/imex.rs index 7f2ce009e2..0b61de71cb 100644 --- a/src/imex.rs +++ b/src/imex.rs @@ -588,63 +588,74 @@ async fn export_backup_inner( Ok(()) } -/******************************************************************************* - * Classic key import - ******************************************************************************/ -async fn import_self_keys(context: &Context, dir: &Path) -> Result<()> { - /* hint: even if we switch to import Autocrypt Setup Files, we should leave the possibility to import - plain ASC keys, at least keys without a password, if we do not want to implement a password entry function. - Importing ASC keys is useful to use keys in Delta Chat used by any other non-Autocrypt-PGP implementation. - - Maybe we should make the "default" key handlong also a little bit smarter - (currently, the last imported key is the standard key unless it contains the string "legacy" in its name) */ - let mut set_default: bool; +/// Imports secret key from a file. +async fn import_secret_key(context: &Context, path: &Path, set_default: bool) -> Result<()> { + let buf = read_file(context, &path).await?; + let armored = std::string::String::from_utf8_lossy(&buf); + set_self_key(context, &armored, set_default, false).await?; + Ok(()) +} + +/// Imports secret keys from the provided file or directory. +/// +/// If provided path is a file, ASCII-armored secret key is read from the file +/// and set as the default key. +/// +/// If provided path is a directory, all files with .asc extension +/// containing secret keys are imported and the last successfully +/// imported which does not contain "legacy" in its filename +/// is set as the default. +async fn import_self_keys(context: &Context, path: &Path) -> Result<()> { + let attr = tokio::fs::metadata(path).await?; + + if attr.is_file() { + info!( + context, + "Importing secret key from {} as the default key.", + path.display() + ); + let set_default = true; + import_secret_key(context, path, set_default).await?; + return Ok(()); + } + let mut imported_cnt = 0; - let dir_name = dir.to_string_lossy(); - let mut dir_handle = tokio::fs::read_dir(&dir).await?; + let mut dir_handle = tokio::fs::read_dir(&path).await?; while let Ok(Some(entry)) = dir_handle.next_entry().await { let entry_fn = entry.file_name(); let name_f = entry_fn.to_string_lossy(); - let path_plus_name = dir.join(&entry_fn); - match get_filesuffix_lc(&name_f) { - Some(suffix) => { - if suffix != "asc" { - continue; - } - set_default = if name_f.contains("legacy") { - info!(context, "found legacy key '{}'", path_plus_name.display()); - false - } else { - true - } - } - None => { + let path_plus_name = path.join(&entry_fn); + if let Some(suffix) = get_filesuffix_lc(&name_f) { + if suffix != "asc" { continue; } - } + } else { + continue; + }; + let set_default = !name_f.contains("legacy"); info!( context, - "considering key file: {}", + "Considering key file: {}.", path_plus_name.display() ); - match read_file(context, &path_plus_name).await { - Ok(buf) => { - let armored = std::string::String::from_utf8_lossy(&buf); - if let Err(err) = set_self_key(context, &armored, set_default, false).await { - info!(context, "set_self_key: {}", err); - continue; - } - } - Err(_) => continue, + if let Err(err) = import_secret_key(context, &path_plus_name, set_default).await { + warn!( + context, + "Failed to import secret key from {}: {:#}.", + path_plus_name.display(), + err + ); + continue; } + imported_cnt += 1; } ensure!( imported_cnt > 0, - "No private keys found in \"{}\".", - dir_name + "No private keys found in {}.", + path.display() ); Ok(()) } @@ -675,7 +686,8 @@ async fn export_self_keys(context: &Context, dir: &Path) -> Result<()> { .await?; for (id, public_key, private_key, is_default) in keys { - let id = Some(id).filter(|_| is_default != 0); + let id = Some(id).filter(|_| is_default == 0); + if let Ok(key) = public_key { if let Err(err) = export_key_to_asc_file(context, dir, id, &key).await { error!(context, "Failed to export public key: {:#}.", err); @@ -871,14 +883,35 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_export_and_import_key() { + let export_dir = tempfile::tempdir().unwrap(); + let context = TestContext::new_alice().await; - let blobdir = context.ctx.get_blobdir(); - if let Err(err) = imex(&context.ctx, ImexMode::ExportSelfKeys, blobdir, None).await { + if let Err(err) = imex( + &context.ctx, + ImexMode::ExportSelfKeys, + export_dir.path(), + None, + ) + .await + { panic!("got error on export: {err:#}"); } let context2 = TestContext::new_alice().await; - if let Err(err) = imex(&context2.ctx, ImexMode::ImportSelfKeys, blobdir, None).await { + if let Err(err) = imex( + &context2.ctx, + ImexMode::ImportSelfKeys, + export_dir.path(), + None, + ) + .await + { + panic!("got error on import: {err:#}"); + } + + let keyfile = export_dir.path().join("private-key-default.asc"); + let context3 = TestContext::new_alice().await; + if let Err(err) = imex(&context3.ctx, ImexMode::ImportSelfKeys, &keyfile, None).await { panic!("got error on import: {err:#}"); } } diff --git a/src/mimeparser.rs b/src/mimeparser.rs index b358a5a88f..5af928aaba 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -438,6 +438,8 @@ impl MimeMessage { typ: Viewtype::Text, msg_raw: Some(txt.clone()), msg: txt, + // Don't change the error prefix for now, + // receive_imf.rs:lookup_chat_by_reply() checks it. error: Some(format!("Decrypting failed: {err:#}")), ..Default::default() }; diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 05cf0c4994..76ed7cc431 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1254,6 +1254,8 @@ RETURNING id ephemeral_timestamp, if is_partial_download.is_some() { DownloadState::Available + } else if mime_parser.decrypting_failed { + DownloadState::Undecipherable } else { DownloadState::Done }, @@ -1454,11 +1456,18 @@ async fn lookup_chat_by_reply( if let Some(parent) = parent { let parent_chat = Chat::load_from_db(context, parent.chat_id).await?; - if parent.error.is_some() { - // If the parent msg is undecipherable, then it may have been assigned to the wrong chat - // (undecipherable group msgs often get assigned to the 1:1 chat with the sender). - // We don't have any way of finding out whether a msg is undecipherable, so we check for - // error.is_some() instead. + if parent.download_state != DownloadState::Done + // TODO (2023-09-12): Added for backward compatibility with versions that did not have + // `DownloadState::Undecipherable`. Remove eventually with the comment in + // `MimeMessage::from_bytes()`. + || parent + .error + .as_ref() + .filter(|e| e.starts_with("Decrypting failed:")) + .is_some() + { + // If the parent msg is not fully downloaded or undecipherable, it may have been + // assigned to the wrong chat (they often get assigned to the 1:1 chat with the sender). return Ok(None); }