From 1f14767fe92d1e92b008c9884120b5a5b37e337f Mon Sep 17 00:00:00 2001 From: iequidoo Date: Mon, 5 Dec 2022 19:51:55 -0300 Subject: [PATCH 001/132] Revert "Fix misplaced info! message" (#3778) This reverts commit 08de326930d7bb827314f92ddee6da46bfb09220. --- src/mimeparser.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 2e811ebb9..2899a24f5 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -216,12 +216,12 @@ impl MimeMessage { headers.remove("secure-join-fingerprint"); headers.remove("chat-verified"); - let is_thunderbird = headers - .get("user-agent") - .map_or(false, |user_agent| user_agent.contains("Thunderbird")); - if is_thunderbird { - info!(context, "Detected Thunderbird"); - } + let is_thunderbird = if let Some(user_agent) = headers.get("user-agent") { + info!(context, "Detected thunderbird"); + user_agent.contains("Thunderbird") + } else { + false + }; let from = from.context("No from in message")?; let mut decryption_info = From 4cbcd3c606893267a3220115733ab8e8dc4d877a Mon Sep 17 00:00:00 2001 From: iequidoo Date: Mon, 5 Dec 2022 23:01:31 -0300 Subject: [PATCH 002/132] Revert "mimeparser: assume all Thunderbird users prefer encryption" except for the test (#3778) This partially reverts commit b341cfd4d928f13d517ef21ea63b31e9d3f6102c. --- src/decrypt.rs | 11 ++--------- src/mimeparser.rs | 9 +-------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/src/decrypt.rs b/src/decrypt.rs index b3413bb65..457af6851 100644 --- a/src/decrypt.rs +++ b/src/decrypt.rs @@ -5,7 +5,7 @@ use std::collections::HashSet; use anyhow::{Context as _, Result}; use mailparse::ParsedMail; -use crate::aheader::{Aheader, EncryptPreference}; +use crate::aheader::Aheader; use crate::authres::handle_authres; use crate::authres::{self, DkimResults}; use crate::contact::addr_cmp; @@ -61,7 +61,6 @@ pub(crate) async fn prepare_decryption( mail: &ParsedMail<'_>, from: &str, message_time: i64, - is_thunderbird: bool, ) -> Result { if mail.headers.get_header(HeaderDef::ListPost).is_some() { if mail.headers.get_header(HeaderDef::Autocrypt).is_some() { @@ -84,16 +83,10 @@ pub(crate) async fn prepare_decryption( }); } - let mut autocrypt_header = Aheader::from_headers(from, &mail.headers) + let autocrypt_header = Aheader::from_headers(from, &mail.headers) .ok_or_log_msg(context, "Failed to parse Autocrypt header") .flatten(); - if is_thunderbird { - if let Some(autocrypt_header) = &mut autocrypt_header { - autocrypt_header.prefer_encrypt = EncryptPreference::Mutual; - } - } - let dkim_results = handle_authres(context, mail, from, message_time).await?; let peerstate = get_autocrypt_peerstate( diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 2899a24f5..1b68b29de 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -216,16 +216,9 @@ impl MimeMessage { headers.remove("secure-join-fingerprint"); headers.remove("chat-verified"); - let is_thunderbird = if let Some(user_agent) = headers.get("user-agent") { - info!(context, "Detected thunderbird"); - user_agent.contains("Thunderbird") - } else { - false - }; - let from = from.context("No from in message")?; let mut decryption_info = - prepare_decryption(context, &mail, &from.addr, message_time, is_thunderbird).await?; + prepare_decryption(context, &mail, &from.addr, message_time).await?; // Memory location for a possible decrypted message. let mut mail_raw = Vec::new(); From 21f1439ad88ec507e5d0752bc3b8de4b0336df36 Mon Sep 17 00:00:00 2001 From: iequidoo Date: Fri, 9 Dec 2022 14:45:21 -0300 Subject: [PATCH 003/132] Treat attached PGP keys as peer keys with mutual encryption preference (#3778) --- CHANGELOG.md | 1 + src/mimeparser.rs | 96 ++++++++++-- src/peerstate.rs | 9 +- src/receive_imf.rs | 16 ++ ...thunderbird_with_autocrypt_unencrypted.eml | 142 ++++++++++++++++++ 5 files changed, 248 insertions(+), 16 deletions(-) create mode 100644 test-data/message/thunderbird_with_autocrypt_unencrypted.eml diff --git a/CHANGELOG.md b/CHANGELOG.md index cf38ef76d..ae8135008 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ ### Fixes - Set read/write timeouts for IMAP over SOCKS5 #3833 +- Treat attached PGP keys as peer keys with mutual encryption preference #3832 ## 1.103.0 diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 1b68b29de..8c4826633 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -3,6 +3,7 @@ use std::collections::{HashMap, HashSet}; use std::future::Future; use std::pin::Pin; +use std::str; use anyhow::{bail, Context as _, Result}; use deltachat_derive::{FromSql, ToSql}; @@ -10,17 +11,17 @@ use lettre_email::mime::{self, Mime}; use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo}; use once_cell::sync::Lazy; -use crate::aheader::Aheader; +use crate::aheader::{Aheader, EncryptPreference}; use crate::blob::BlobObject; use crate::constants::{DC_DESIRED_TEXT_LINES, DC_DESIRED_TEXT_LINE_LEN}; use crate::contact::{addr_cmp, addr_normalize, ContactId}; use crate::context::Context; -use crate::decrypt::{prepare_decryption, try_decrypt}; +use crate::decrypt::{prepare_decryption, try_decrypt, DecryptionInfo}; use crate::dehtml::dehtml; use crate::events::EventType; use crate::format_flowed::unformat_flowed; use crate::headerdef::{HeaderDef, HeaderDefMap}; -use crate::key::Fingerprint; +use crate::key::{DcKey, Fingerprint, SignedPublicKey}; use crate::message::{self, Viewtype}; use crate::param::{Param, Params}; use crate::peerstate::Peerstate; @@ -52,6 +53,7 @@ pub struct MimeMessage { pub from_is_signed: bool, pub list_post: Option, pub chat_disposition_notification_to: Option, + pub decryption_info: DecryptionInfo, pub decrypting_failed: bool, /// Set of valid signature fingerprints if a message is an @@ -322,6 +324,7 @@ impl MimeMessage { from, from_is_signed, chat_disposition_notification_to, + decryption_info, decrypting_failed: mail.is_err(), // only non-empty if it was a valid autocrypt message @@ -390,8 +393,8 @@ impl MimeMessage { parser.decoded_data = mail_raw; } - crate::peerstate::maybe_do_aeap_transition(context, &mut decryption_info, &parser).await?; - if let Some(peerstate) = decryption_info.peerstate { + crate::peerstate::maybe_do_aeap_transition(context, &mut parser).await?; + if let Some(peerstate) = &parser.decryption_info.peerstate { peerstate .handle_fingerprint_change(context, message_time) .await?; @@ -950,7 +953,7 @@ impl MimeMessage { &filename, is_related, ) - .await; + .await?; } None => { match mime_type.type_() { @@ -1093,9 +1096,18 @@ impl MimeMessage { decoded_data: &[u8], filename: &str, is_related: bool, - ) { + ) -> Result<()> { if decoded_data.is_empty() { - return; + return Ok(()); + } + if let Some(peerstate) = &mut self.decryption_info.peerstate { + if peerstate.prefer_encrypt != EncryptPreference::Mutual + && mime_type.type_() == mime::APPLICATION + && mime_type.subtype().as_str() == "pgp-keys" + && Self::try_set_peer_key_from_file_part(context, peerstate, decoded_data).await? + { + return Ok(()); + } } let msg_type = if context .is_webxdc_file(filename, decoded_data) @@ -1117,7 +1129,7 @@ impl MimeMessage { } else { self.message_kml = parsed; } - return; + return Ok(()); } msg_type } else if filename == "multi-device-sync.json" { @@ -1130,13 +1142,13 @@ impl MimeMessage { warn!(context, "failed to parse sync data: {}", err); }) .ok(); - return; + return Ok(()); } else if filename == "status-update.json" { let serialized = String::from_utf8_lossy(decoded_data) .parse() .unwrap_or_default(); self.webxdc_status_update = Some(serialized); - return; + return Ok(()); } else { msg_type }; @@ -1151,7 +1163,7 @@ impl MimeMessage { context, "Could not add blob for mime part {}, error {}", filename, err ); - return; + return Ok(()); } }; info!(context, "added blobfile: {:?}", blob.as_name()); @@ -1174,6 +1186,66 @@ impl MimeMessage { part.is_related = is_related; self.do_add_single_part(part); + Ok(()) + } + + /// Returns whether a key from the attachment was set as peer's pubkey. + async fn try_set_peer_key_from_file_part( + context: &Context, + peerstate: &mut Peerstate, + decoded_data: &[u8], + ) -> Result { + let key = match str::from_utf8(decoded_data) { + Err(err) => { + warn!(context, "PGP key attachment is not a UTF-8 file: {}", err); + return Ok(false); + } + Ok(key) => key, + }; + let key = match SignedPublicKey::from_asc(key) { + Err(err) => { + warn!( + context, + "PGP key attachment is not an ASCII-armored file: {}", err, + ); + return Ok(false); + } + Ok((key, _)) => key, + }; + if let Err(err) = key.verify() { + warn!(context, "attached PGP key verification failed: {}", err); + return Ok(false); + } + if !key.details.users.iter().any(|user| { + user.id + .id() + .ends_with(&(String::from("<") + &peerstate.addr + ">")) + }) { + return Ok(false); + } + if let Some(curr_key) = &peerstate.public_key { + if key != *curr_key && peerstate.prefer_encrypt != EncryptPreference::Reset { + // We don't want to break the existing Autocrypt setup. Yes, it's unlikely that a + // user have an Autocrypt-capable MUA and also attaches a key, but if that's the + // case, let 'em first disable Autocrypt and then change the key by attaching it. + warn!( + context, + "not using attached PGP key for peer '{}' because another one is already set \ + with prefer-encrypt={}", + peerstate.addr, + peerstate.prefer_encrypt, + ); + return Ok(false); + } + } + info!( + context, + "will use attached PGP key for peer '{}' with mutual encryption", peerstate.addr, + ); + peerstate.public_key = Some(key); + peerstate.prefer_encrypt = EncryptPreference::Mutual; + peerstate.save_to_db(&context.sql).await?; + Ok(true) } fn do_add_single_part(&mut self, mut part: Part) { diff --git a/src/peerstate.rs b/src/peerstate.rs index e9591c789..22e4c8f50 100644 --- a/src/peerstate.rs +++ b/src/peerstate.rs @@ -9,7 +9,6 @@ use crate::chatlist::Chatlist; use crate::constants::Chattype; use crate::contact::{addr_cmp, Contact, Origin}; use crate::context::Context; -use crate::decrypt::DecryptionInfo; use crate::events::EventType; use crate::key::{DcKey, Fingerprint, SignedPublicKey}; use crate::message::Message; @@ -565,10 +564,10 @@ impl Peerstate { /// In `drafts/aeap_mvp.md` there is a "big picture" overview over AEAP. pub async fn maybe_do_aeap_transition( context: &Context, - info: &mut DecryptionInfo, - mime_parser: &crate::mimeparser::MimeMessage, + mime_parser: &mut crate::mimeparser::MimeMessage, ) -> Result<()> { - if let Some(peerstate) = &mut info.peerstate { + let info = &mime_parser.decryption_info; + if let Some(peerstate) = &info.peerstate { // If the from addr is different from the peerstate address we know, // we may want to do an AEAP transition. if !addr_cmp(&peerstate.addr, &mime_parser.from.addr) @@ -588,6 +587,8 @@ pub async fn maybe_do_aeap_transition( && mime_parser.from_is_signed && info.message_time > peerstate.last_seen { + let info = &mut mime_parser.decryption_info; + let peerstate = info.peerstate.as_mut().context("no peerstate??")?; // Add info messages to chats with this (verified) contact // peerstate diff --git a/src/receive_imf.rs b/src/receive_imf.rs index b1398c5a9..d10d4bad1 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -5350,6 +5350,22 @@ Reply from different address Ok(()) } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_thunderbird_autocrypt_unencrypted() -> Result<()> { + let t = TestContext::new_bob().await; + t.set_config(Config::ShowEmails, Some("2")).await?; + + let raw = include_bytes!("../test-data/message/thunderbird_with_autocrypt_unencrypted.eml"); + receive_imf(&t, raw, false).await?; + + let peerstate = Peerstate::from_addr(&t, "alice@example.org") + .await? + .unwrap(); + assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual); + + Ok(()) + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_mua_user_adds_member() -> Result<()> { let t = TestContext::new_alice().await; diff --git a/test-data/message/thunderbird_with_autocrypt_unencrypted.eml b/test-data/message/thunderbird_with_autocrypt_unencrypted.eml new file mode 100644 index 000000000..a8654024a --- /dev/null +++ b/test-data/message/thunderbird_with_autocrypt_unencrypted.eml @@ -0,0 +1,142 @@ +From - Fri, 09 Dec 2022 13:16:11 GMT +X-Mozilla-Status: 0801 +X-Mozilla-Status2: 00000000 +Message-ID: <0c8e3ffc-99ae-eb68-15b5-15c4d85a5c12@example.org> +Date: Fri, 9 Dec 2022 10:16:11 -0300 +MIME-Version: 1.0 +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 + Thunderbird/102.5.1 +Content-Language: en-US +To: bob@example.net +From: Alice +Subject: test message 10:15 +Autocrypt: addr=alice@example.org; keydata= + xsDNBGOTM3UBDADZ819boOPXK/ZPO1EepYUBve2psYO3rZkPu3uhyn7qpI8c0U5IbR+mAXPH + FkKfvSwTtGiPpXaP6/vx0OjTs1aR7We9MrP+1EckbsyQnnDmDGsGxxyn3+a3ar0FcgOBi/kS + j0fPB1tX92/z3MWtOSXYtYOlMotRdIxt/L8CYQSBe8wWpoOKQPNmtvnEuDlJwSlrhRPx6PDm + BgoKv1qi5UOrAoyUPbdnINnSgj14KBNMgiuJQz6+AwVaYitVJ37N6lrCfhWRPZAVDRW5ajLx + W+DuuYUW675xzi2bLlb4jGeFePvS9Rhw2CpkG608cFVFrUCBH91mfb0UnmxIDMcc6JSn0Uqf + PESC+0wK9xokzi07/FZtXyf925oiMpA7ZQ7aSNW6J7kk618xNQRivLhEV1+QofynAzfwAB+C + vqY+VjNZbGKGW7aba84Nx9Wa7g8rbZ5ZvsQmrn38fpWu+2GcUvnGOxn8lYEljnfCthSigjCg + q3T90aSUwDQfedJej9nzM98AEQEAAc0ZQWxpY2UgPGFsaWNlQGV4YW1wbGUub3JnPsLBDQQT + AQgANxYhBL7Y7n4kdUxZWposQeDVBmYZR/R8BQJjkzN2BQm7+B4AAhsDBAsJCAcFFQgJCgsF + FgIDAQAACgkQ4NUGZhlH9HzsWwv+JucjIbwsHfRWDB81R9d0WIQGvYM8sjUETQWmlwEcts/y + yHVLNnyvxn9EUboo9tLg3VmukPYNLyuVJ6WlWRuskZHXy3TdW+1TcAIzO97vReBOXOunDmoT + PoA9IRUFVVwC3ejyFj9timcKVKX6WUyNY7l0x1Voy5gHqswnlVQ0SXsdBQqDwMJqUuRmWE1z + rk15EdF2OvWADlZ0j9TeGHFYcr6lLXZ92sOQbjsm4vmwPGFC5oiolKmoZXNfNa9Ef3HPv9Q0 + XF576hfu7CyIhWXXxCNGzssuTA42Kdxhcpppi+HtzEr1F3jApDG2T5bfMnIN9udu6UgNTdQm + /Qyuamn2vo11fXsdA41Kajrnj2Vtcf6qd4qv4HSgeyGxZw3btjbmwuVAao0x49jXYZhpx00r + iddTfjBhhE1MCPNHK9ypmodWMiF99dZNhAHB434agfkNWHl8z3QwxDLjWhkzNdnHeO1Xg2zq + 3/mKi2mNyb2iGImDp4GAxOQVLYGwXPRe0NeqzsDNBGOTM3YBDADFQ11NReZAL2vdu5avkfs7 + iw7MNI2DANGvouIcQOP0gqSkF0UY/bMmvWXmDV6iTaxe2/+r/t51zZZRnr1KYF/XayoQmxLu + MAKWAUJvltzcYlJwSphCCbh2OpxHBZqrbhHKGZIkj1Is3uVBSFt6gkr9lYDFk+ehhBBNoE50 + nSamJXNpur2A4aZYmIwKWNeU+skzYu4VDUKXet69fmK4bZlF1ydYturcSQtE6fLb8ob7b/52 + C2FJxRNFJQ7el8bozPKX0ZitKCSh9HXKw4TvD+nD8v4tDAmzno9Z66T4o8WYRA5mCYWVpD+W + Qadcikcqx5G7RIiKgRxvcGAx9kMUjMptjErc+1rKcNw7QdpFu6uiSj1602jBM/JvQRvUVa85 + vkQn0u07PjIzH+ZQeKsijdmDaeOZWjE1/XkOVi3btzoOaQQRh14spC+ztl8hV6/9+bDIWXEK + iiQQUi1Kvw7TfaRQprmD1IUyfb69LpwD8MTnBoDyA/PxY1DurQPJMN5yAvsAEQEAAcLA/AQY + AQgAJhYhBL7Y7n4kdUxZWposQeDVBmYZR/R8BQJjkzN3BQm7+B4AAhsMAAoJEODVBmYZR/R8 + 90sL/0+cJmENgLGI+Ji5rMlZe63hDk4w1p+7THf4vmX/Pg27hUTznTeRLs3dhGVYrSPvxgl7 + L4KlTwe1euSBgfWqCpNjh0g5Hvz3X5uSoLerEsGa7PoGTvpnTuWoRzYJLYRkWtuwfQ3SvpeQ + OglT7vgvsSoC1h6MOnWJgTo8yYyYP92Wq7fv867bSpWjjykHcK5DIjEM71+6IJTn5pnhkG2d + dibfHyDZoBj0P8VrJFEkkCzkycANtmhUBDr/vFhYKWy76ZZNgGHg71iFGwXK/kz5dKA6mIUN + AaeyyarAzoaJh0y3UkAPW/evwD/PP9M4y2mP6TDPeFYBZI7o5gCD6q+t1zCMc1M4V+hOJXfs + ISJPE3J/Rq53QnPOmsz9sdyfOxxfePV64gtv3xHBFUafucFiipeHgx4eXmdMNRnzlGeHlhDn + dpFGkJJeA8TCJqfP0DFY/CCW4mT0FvaVcFtJ/CXvmD6qORTlbJg9XZ2FNCA7x0+WJ2mjn/m1 + rhEBN10sGyg93A== +X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0; + attachmentreminder=0; deliveryformat=0 +X-Identity-Key: id3 +Fcc: imap://alice%40example.org@in.example.org/Sent +Content-Type: multipart/signed; micalg=pgp-sha256; + protocol="application/pgp-signature"; + boundary="------------FFBOG29BVxcOkoFV1hnc0RaY" + +This is an OpenPGP/MIME signed message (RFC 4880 and 3156) +--------------FFBOG29BVxcOkoFV1hnc0RaY +Content-Type: multipart/mixed; boundary="------------4cwiD0i5NnTXNSfPNpFwrv6V"; + protected-headers="v1" +From: Alice +To: bob@example.net +Message-ID: <0c8e3ffc-99ae-eb68-15b5-15c4d85a5c12@example.org> +Subject: test message 10:15 + +--------------4cwiD0i5NnTXNSfPNpFwrv6V +Content-Type: multipart/mixed; boundary="------------fbNEFvfS22YOKnkTd1oAl0ak" + +--------------fbNEFvfS22YOKnkTd1oAl0ak +Content-Type: text/plain; charset=UTF-8; format=flowed +Content-Transfer-Encoding: base64 + +MTIzDQoNCg== +--------------fbNEFvfS22YOKnkTd1oAl0ak +Content-Type: application/pgp-keys; name="OpenPGP_0xE0D506661947F47C.asc" +Content-Disposition: attachment; filename="OpenPGP_0xE0D506661947F47C.asc" +Content-Description: OpenPGP public key +Content-Transfer-Encoding: quoted-printable + +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xsDNBGOTM3UBDADZ819boOPXK/ZPO1EepYUBve2psYO3rZkPu3uhyn7qpI8c0U5I +bR+mAXPHFkKfvSwTtGiPpXaP6/vx0OjTs1aR7We9MrP+1EckbsyQnnDmDGsGxxyn +3+a3ar0FcgOBi/kSj0fPB1tX92/z3MWtOSXYtYOlMotRdIxt/L8CYQSBe8wWpoOK +QPNmtvnEuDlJwSlrhRPx6PDmBgoKv1qi5UOrAoyUPbdnINnSgj14KBNMgiuJQz6+ +AwVaYitVJ37N6lrCfhWRPZAVDRW5ajLxW+DuuYUW675xzi2bLlb4jGeFePvS9Rhw +2CpkG608cFVFrUCBH91mfb0UnmxIDMcc6JSn0UqfPESC+0wK9xokzi07/FZtXyf9 +25oiMpA7ZQ7aSNW6J7kk618xNQRivLhEV1+QofynAzfwAB+CvqY+VjNZbGKGW7ab +a84Nx9Wa7g8rbZ5ZvsQmrn38fpWu+2GcUvnGOxn8lYEljnfCthSigjCgq3T90aSU +wDQfedJej9nzM98AEQEAAc0ZQWxpY2UgPGFsaWNlQGV4YW1wbGUub3JnPsLBDQQT +AQgANxYhBL7Y7n4kdUxZWposQeDVBmYZR/R8BQJjkzN2BQm7+B4AAhsDBAsJCAcF +FQgJCgsFFgIDAQAACgkQ4NUGZhlH9HzsWwv+JucjIbwsHfRWDB81R9d0WIQGvYM8 +sjUETQWmlwEcts/yyHVLNnyvxn9EUboo9tLg3VmukPYNLyuVJ6WlWRuskZHXy3Td +W+1TcAIzO97vReBOXOunDmoTPoA9IRUFVVwC3ejyFj9timcKVKX6WUyNY7l0x1Vo +y5gHqswnlVQ0SXsdBQqDwMJqUuRmWE1zrk15EdF2OvWADlZ0j9TeGHFYcr6lLXZ9 +2sOQbjsm4vmwPGFC5oiolKmoZXNfNa9Ef3HPv9Q0XF576hfu7CyIhWXXxCNGzssu +TA42Kdxhcpppi+HtzEr1F3jApDG2T5bfMnIN9udu6UgNTdQm/Qyuamn2vo11fXsd +A41Kajrnj2Vtcf6qd4qv4HSgeyGxZw3btjbmwuVAao0x49jXYZhpx00riddTfjBh +hE1MCPNHK9ypmodWMiF99dZNhAHB434agfkNWHl8z3QwxDLjWhkzNdnHeO1Xg2zq +3/mKi2mNyb2iGImDp4GAxOQVLYGwXPRe0NeqzsDNBGOTM3YBDADFQ11NReZAL2vd +u5avkfs7iw7MNI2DANGvouIcQOP0gqSkF0UY/bMmvWXmDV6iTaxe2/+r/t51zZZR +nr1KYF/XayoQmxLuMAKWAUJvltzcYlJwSphCCbh2OpxHBZqrbhHKGZIkj1Is3uVB +SFt6gkr9lYDFk+ehhBBNoE50nSamJXNpur2A4aZYmIwKWNeU+skzYu4VDUKXet69 +fmK4bZlF1ydYturcSQtE6fLb8ob7b/52C2FJxRNFJQ7el8bozPKX0ZitKCSh9HXK +w4TvD+nD8v4tDAmzno9Z66T4o8WYRA5mCYWVpD+WQadcikcqx5G7RIiKgRxvcGAx +9kMUjMptjErc+1rKcNw7QdpFu6uiSj1602jBM/JvQRvUVa85vkQn0u07PjIzH+ZQ +eKsijdmDaeOZWjE1/XkOVi3btzoOaQQRh14spC+ztl8hV6/9+bDIWXEKiiQQUi1K +vw7TfaRQprmD1IUyfb69LpwD8MTnBoDyA/PxY1DurQPJMN5yAvsAEQEAAcLA/AQY +AQgAJhYhBL7Y7n4kdUxZWposQeDVBmYZR/R8BQJjkzN3BQm7+B4AAhsMAAoJEODV +BmYZR/R890sL/0+cJmENgLGI+Ji5rMlZe63hDk4w1p+7THf4vmX/Pg27hUTznTeR +Ls3dhGVYrSPvxgl7L4KlTwe1euSBgfWqCpNjh0g5Hvz3X5uSoLerEsGa7PoGTvpn +TuWoRzYJLYRkWtuwfQ3SvpeQOglT7vgvsSoC1h6MOnWJgTo8yYyYP92Wq7fv867b +SpWjjykHcK5DIjEM71+6IJTn5pnhkG2ddibfHyDZoBj0P8VrJFEkkCzkycANtmhU +BDr/vFhYKWy76ZZNgGHg71iFGwXK/kz5dKA6mIUNAaeyyarAzoaJh0y3UkAPW/ev +wD/PP9M4y2mP6TDPeFYBZI7o5gCD6q+t1zCMc1M4V+hOJXfsISJPE3J/Rq53QnPO +msz9sdyfOxxfePV64gtv3xHBFUafucFiipeHgx4eXmdMNRnzlGeHlhDndpFGkJJe +A8TCJqfP0DFY/CCW4mT0FvaVcFtJ/CXvmD6qORTlbJg9XZ2FNCA7x0+WJ2mjn/m1 +rhEBN10sGyg93A=3D=3D +=3DDPMe +-----END PGP PUBLIC KEY BLOCK----- + +--------------fbNEFvfS22YOKnkTd1oAl0ak-- + +--------------4cwiD0i5NnTXNSfPNpFwrv6V-- + +--------------FFBOG29BVxcOkoFV1hnc0RaY +Content-Type: application/pgp-signature; name="OpenPGP_signature.asc" +Content-Description: OpenPGP digital signature +Content-Disposition: attachment; filename="OpenPGP_signature" + +-----BEGIN PGP SIGNATURE----- + +wsD5BAABCAAjFiEEvtjufiR1TFlamixB4NUGZhlH9HwFAmOTNRsFAwAAAAAACgkQ4NUGZhlH9Hzw +Iwv/dNC7LDvRGmZ71IaivkUkSTbpGgg0gnCNOuf+B8OxUBQlWPkBmLxyrXbkxsTghFogDsVQeZQQ +DJ182KMgeC//rUN5DPJNrh95YZnav0nUpzW1mkFZjK+PdhbfdXKoXhJIqcw/7lpy/povRYZ20Igg +tIHLa1NlqPPhSx/o2dsEqWeAtXF4e8T/jQSA5+ZQtVrdcTCNQG6zbqlHZuJ7bF1bwuHPgLgDhJ5k ++T2ny80ZtkfLXJl5tQdblAomhBPfEOj+AeLCKsrJFO3WFZOvsuoKMPZpwW1wEh7+QYLABX/lRvqx +IxjH1Tc26vttlOrVH13FKGSeWJELun+b2dP1LPiBQ7DOsrrFNs3fp56Nb7Y+exH5ld0jz0kJZTUD +yPqZpJXTsWkFPE7x1tbH/7goiH8f9DbQrvmqQ2fnCjzf3UJR3ZhG/13YAUEdLVkVzwMItEd6yisg +MP8mlbwm4aDeCiGXO/xhOoBVl6bn1HVSxo7mb0chHVyD1NOfd7qsxem0L/A2 +=MOQB +-----END PGP SIGNATURE----- + +--------------FFBOG29BVxcOkoFV1hnc0RaY-- From 2a2db4f526bba4ef3834ecea74ea22812d00fe99 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 13 Dec 2022 16:57:40 +0000 Subject: [PATCH 004/132] Remove unused pytest-async plugin We use pytest-asyncio instead --- CHANGELOG.md | 1 + deltachat-rpc-client/tox.ini | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae8135008..6daf361a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - If a classical-email-user sends an email to a group and adds new recipients, add the new recipients as group members #3781 +- Remove `pytest-async` plugin #3846 ### API-Changes diff --git a/deltachat-rpc-client/tox.ini b/deltachat-rpc-client/tox.ini index 4aad8010b..bea3b4603 100644 --- a/deltachat-rpc-client/tox.ini +++ b/deltachat-rpc-client/tox.ini @@ -13,7 +13,6 @@ passenv = DCC_NEW_TMP_EMAIL deps = pytest - pytest-async pytest-asyncio aiohttp aiodns From ccd0842df81c1053397e7fa1c92969cc2b197862 Mon Sep 17 00:00:00 2001 From: bjoern Date: Tue, 13 Dec 2022 20:12:29 +0100 Subject: [PATCH 005/132] do not `SELECT *` on old tables to fill new ones (#3842) * do not `SELECT *` on old tables to fill new ones the old table may contain deprecrated columns for whatever reason; as a result the query fails as the statement tries to insert eg. 16 columns into 12 colums (concrete error for acpeerstate that have several deprecated columns) * update CHANGELOG --- CHANGELOG.md | 1 + src/sql/migrations.rs | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6daf361a9..806419520 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ ### Fixes - Set read/write timeouts for IMAP over SOCKS5 #3833 - Treat attached PGP keys as peer keys with mutual encryption preference #3832 +- fix migration of old databases #3842 ## 1.103.0 diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 0b5a46da8..6324f01fb 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -637,7 +637,11 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid); verified_key_fingerprint TEXT DEFAULT '', UNIQUE (addr) -- Only one peerstate per address ); - INSERT OR IGNORE INTO new_acpeerstates SELECT * FROM acpeerstates; + INSERT OR IGNORE INTO new_acpeerstates SELECT + id, addr, last_seen, last_seen_autocrypt, public_key, prefer_encrypted, + gossip_timestamp, gossip_key, public_key_fingerprint, + gossip_key_fingerprint, verified_key, verified_key_fingerprint + FROM acpeerstates; DROP TABLE acpeerstates; ALTER TABLE new_acpeerstates RENAME TO acpeerstates; CREATE INDEX acpeerstates_index1 ON acpeerstates (addr); @@ -652,7 +656,7 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid); if dbversion < 95 { sql.execute_migration( "CREATE TABLE new_chats_contacts (chat_id INTEGER, contact_id INTEGER, UNIQUE(chat_id, contact_id));\ - INSERT OR IGNORE INTO new_chats_contacts SELECT * FROM chats_contacts;\ + INSERT OR IGNORE INTO new_chats_contacts SELECT chat_id, contact_id FROM chats_contacts;\ DROP TABLE chats_contacts;\ ALTER TABLE new_chats_contacts RENAME TO chats_contacts;\ CREATE INDEX chats_contacts_index1 ON chats_contacts (chat_id);\ From 2cd63234c16a8b0265c97f4ce9f25a2b5e7a3bd9 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 13 Dec 2022 14:47:41 +0000 Subject: [PATCH 006/132] Do not allow missing documentation by default --- src/accounts.rs | 2 -- src/chat.rs | 2 ++ src/chatlist.rs | 2 ++ src/config.rs | 2 ++ src/constants.rs | 3 +++ src/contact.rs | 2 ++ src/context.rs | 2 ++ src/download.rs | 2 ++ src/ephemeral.rs | 2 ++ src/events.rs | 2 ++ src/headerdef.rs | 2 ++ src/imex.rs | 2 ++ src/job.rs | 3 +++ src/key.rs | 2 ++ src/lib.rs | 2 ++ src/location.rs | 3 +++ src/log.rs | 2 ++ src/message.rs | 2 ++ src/mimeparser.rs | 2 ++ src/oauth2.rs | 2 ++ src/peerstate.rs | 2 ++ src/pgp.rs | 2 ++ src/plaintext.rs | 2 ++ src/provider.rs | 2 ++ src/qr.rs | 2 ++ src/qr_code_generator.rs | 2 ++ src/quota.rs | 2 ++ src/receive_imf.rs | 2 ++ src/scheduler/connectivity.rs | 2 ++ src/securejoin.rs | 2 ++ src/sql.rs | 2 ++ src/stock_str.rs | 2 ++ src/summary.rs | 2 ++ src/tools.rs | 2 ++ src/webxdc.rs | 2 ++ 35 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/accounts.rs b/src/accounts.rs index 0ec5d1f40..511f9f5b5 100644 --- a/src/accounts.rs +++ b/src/accounts.rs @@ -1,7 +1,5 @@ //! # Account manager module. -#![warn(missing_docs)] - use std::collections::BTreeMap; use std::path::{Path, PathBuf}; diff --git a/src/chat.rs b/src/chat.rs index e92e6d574..1d41bb5ac 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1,5 +1,7 @@ //! # Chat module. +#![allow(missing_docs)] + use std::collections::HashMap; use std::convert::{TryFrom, TryInto}; use std::fmt; diff --git a/src/chatlist.rs b/src/chatlist.rs index 8482213f2..da7d309a4 100644 --- a/src/chatlist.rs +++ b/src/chatlist.rs @@ -1,5 +1,7 @@ //! # Chat list module. +#![allow(missing_docs)] + use anyhow::{ensure, Context as _, Result}; use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility}; diff --git a/src/config.rs b/src/config.rs index 259c3e063..aea06b2a7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,7 @@ //! # Key-value configuration management. +#![allow(missing_docs)] + use anyhow::{ensure, Context as _, Result}; use strum::{EnumProperty as EnumPropertyTrait, IntoEnumIterator}; use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString}; diff --git a/src/constants.rs b/src/constants.rs index bbdad67f7..dde6219f5 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,4 +1,7 @@ //! # Constants. + +#![allow(missing_docs)] + use deltachat_derive::{FromSql, ToSql}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; diff --git a/src/contact.rs b/src/contact.rs index ddaec2306..79f131283 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -1,5 +1,7 @@ //! Contacts module +#![allow(missing_docs)] + use std::cmp::Reverse; use std::collections::BinaryHeap; use std::convert::{TryFrom, TryInto}; diff --git a/src/context.rs b/src/context.rs index d28774df9..a78ea833c 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,5 +1,7 @@ //! Context module. +#![allow(missing_docs)] + use std::collections::{BTreeMap, HashMap}; use std::ffi::OsString; use std::ops::Deref; diff --git a/src/download.rs b/src/download.rs index f9fc9711b..3253e54fe 100644 --- a/src/download.rs +++ b/src/download.rs @@ -1,5 +1,7 @@ //! # Download large messages manually. +#![allow(missing_docs)] + use anyhow::{anyhow, Result}; use deltachat_derive::{FromSql, ToSql}; use serde::{Deserialize, Serialize}; diff --git a/src/ephemeral.rs b/src/ephemeral.rs index a65e1636c..aff6d7e32 100644 --- a/src/ephemeral.rs +++ b/src/ephemeral.rs @@ -62,6 +62,8 @@ //! the database entries which are expired either according to their //! ephemeral message timers or global `delete_server_after` setting. +#![allow(missing_docs)] + use std::convert::{TryFrom, TryInto}; use std::num::ParseIntError; use std::str::FromStr; diff --git a/src/events.rs b/src/events.rs index d0033c4ae..b2744f4d5 100644 --- a/src/events.rs +++ b/src/events.rs @@ -1,5 +1,7 @@ //! # Events specification. +#![allow(missing_docs)] + use std::path::PathBuf; use async_channel::{self as channel, Receiver, Sender, TrySendError}; diff --git a/src/headerdef.rs b/src/headerdef.rs index 231bc3987..30d01222b 100644 --- a/src/headerdef.rs +++ b/src/headerdef.rs @@ -1,5 +1,7 @@ //! # List of email headers. +#![allow(missing_docs)] + use mailparse::{MailHeader, MailHeaderMap}; #[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames, IntoStaticStr)] diff --git a/src/imex.rs b/src/imex.rs index 809c7958e..11bc0c748 100644 --- a/src/imex.rs +++ b/src/imex.rs @@ -1,5 +1,7 @@ //! # Import/export module. +#![allow(missing_docs)] + use std::any::Any; use std::ffi::OsStr; use std::path::{Path, PathBuf}; diff --git a/src/job.rs b/src/job.rs index 438cbbab5..0438e4494 100644 --- a/src/job.rs +++ b/src/job.rs @@ -2,6 +2,9 @@ //! //! This module implements a job queue maintained in the SQLite database //! and job types. + +#![allow(missing_docs)] + use std::fmt; use anyhow::{Context as _, Result}; diff --git a/src/key.rs b/src/key.rs index 7c385a313..fc1ae4847 100644 --- a/src/key.rs +++ b/src/key.rs @@ -1,5 +1,7 @@ //! Cryptographic key module. +#![allow(missing_docs)] + use std::collections::BTreeMap; use std::fmt; use std::io::Cursor; diff --git a/src/lib.rs b/src/lib.rs index f3925a306..a50483843 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ unused, clippy::correctness, missing_debug_implementations, + missing_docs, clippy::all, clippy::indexing_slicing, clippy::wildcard_imports, @@ -34,6 +35,7 @@ extern crate rusqlite; #[macro_use] extern crate strum_macros; +#[allow(missing_docs)] pub trait ToSql: rusqlite::ToSql + Send + Sync {} impl ToSql for T {} diff --git a/src/location.rs b/src/location.rs index f99838d9e..c6845898a 100644 --- a/src/location.rs +++ b/src/location.rs @@ -1,4 +1,7 @@ //! Location handling. + +#![allow(missing_docs)] + use std::convert::TryFrom; use std::time::Duration; diff --git a/src/log.rs b/src/log.rs index 4ae368514..51ada7480 100644 --- a/src/log.rs +++ b/src/log.rs @@ -1,5 +1,7 @@ //! # Logging. +#![allow(missing_docs)] + use crate::context::Context; #[macro_export] diff --git a/src/message.rs b/src/message.rs index 600bbc8f1..5c8f64435 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1,5 +1,7 @@ //! # Messages and their identifiers. +#![allow(missing_docs)] + use std::collections::BTreeSet; use std::path::{Path, PathBuf}; diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 8c4826633..e85c95246 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -1,5 +1,7 @@ //! # MIME message parsing module. +#![allow(missing_docs)] + use std::collections::{HashMap, HashSet}; use std::future::Future; use std::pin::Pin; diff --git a/src/oauth2.rs b/src/oauth2.rs index 1d0296716..d48cb45a6 100644 --- a/src/oauth2.rs +++ b/src/oauth2.rs @@ -1,5 +1,7 @@ //! OAuth 2 module. +#![allow(missing_docs)] + use std::collections::HashMap; use anyhow::Result; diff --git a/src/peerstate.rs b/src/peerstate.rs index 22e4c8f50..931891929 100644 --- a/src/peerstate.rs +++ b/src/peerstate.rs @@ -1,5 +1,7 @@ //! # [Autocrypt Peer State](https://autocrypt.org/level1.html#peer-state-management) module. +#![allow(missing_docs)] + use std::collections::HashSet; use std::fmt; diff --git a/src/pgp.rs b/src/pgp.rs index e9b9e7c84..8a91a46b3 100644 --- a/src/pgp.rs +++ b/src/pgp.rs @@ -1,5 +1,7 @@ //! OpenPGP helper module using [rPGP facilities](https://github.com/rpgp/rpgp). +#![allow(missing_docs)] + use std::collections::{BTreeMap, HashSet}; use std::io; use std::io::Cursor; diff --git a/src/plaintext.rs b/src/plaintext.rs index fd3ba1675..5bdeffed4 100644 --- a/src/plaintext.rs +++ b/src/plaintext.rs @@ -1,5 +1,7 @@ //! Handle plain text together with some attributes. +#![allow(missing_docs)] + use crate::simplify::split_lines; use once_cell::sync::Lazy; diff --git a/src/provider.rs b/src/provider.rs index fb36982a6..4d8ee74d8 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -1,5 +1,7 @@ //! [Provider database](https://providers.delta.chat/) module. +#![allow(missing_docs)] + mod data; use crate::config::Config; diff --git a/src/qr.rs b/src/qr.rs index c12ce8045..6a735216e 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -1,5 +1,7 @@ //! # QR code module. +#![allow(missing_docs)] + mod dclogin_scheme; pub use dclogin_scheme::LoginOptions; diff --git a/src/qr_code_generator.rs b/src/qr_code_generator.rs index b3b4aef11..6dbdfc249 100644 --- a/src/qr_code_generator.rs +++ b/src/qr_code_generator.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use anyhow::Result; use qrcodegen::{QrCode, QrCodeEcc}; diff --git a/src/quota.rs b/src/quota.rs index 1053cd81a..eba52899f 100644 --- a/src/quota.rs +++ b/src/quota.rs @@ -1,5 +1,7 @@ //! # Support for IMAP QUOTA extension. +#![allow(missing_docs)] + use anyhow::{anyhow, Context as _, Result}; use async_imap::types::{Quota, QuotaResource}; use std::collections::BTreeMap; diff --git a/src/receive_imf.rs b/src/receive_imf.rs index d10d4bad1..b477d8686 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1,5 +1,7 @@ //! Internet Message Format reception pipeline. +#![allow(missing_docs)] + use std::cmp::min; use std::collections::HashSet; use std::convert::TryFrom; diff --git a/src/scheduler/connectivity.rs b/src/scheduler/connectivity.rs index 9ca9fb263..630053864 100644 --- a/src/scheduler/connectivity.rs +++ b/src/scheduler/connectivity.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use core::fmt; use std::{ops::Deref, sync::Arc}; diff --git a/src/securejoin.rs b/src/securejoin.rs index 08822a5a1..9663a30a8 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -1,5 +1,7 @@ //! Verified contact protocol implementation as [specified by countermitm project](https://countermitm.readthedocs.io/en/stable/new.html#setup-contact-protocol). +#![allow(missing_docs)] + use std::convert::TryFrom; use anyhow::{bail, Context as _, Error, Result}; diff --git a/src/sql.rs b/src/sql.rs index 4dacb5daf..bed7f0063 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -1,5 +1,7 @@ //! # SQLite wrapper. +#![allow(missing_docs)] + use std::collections::{HashMap, HashSet}; use std::convert::TryFrom; use std::path::Path; diff --git a/src/stock_str.rs b/src/stock_str.rs index e58de954d..2e93962d6 100644 --- a/src/stock_str.rs +++ b/src/stock_str.rs @@ -1,5 +1,7 @@ //! Module to work with translatable stock strings. +#![allow(missing_docs)] + use std::collections::HashMap; use std::sync::Arc; diff --git a/src/summary.rs b/src/summary.rs index 5f6a0537a..cbf301ed7 100644 --- a/src/summary.rs +++ b/src/summary.rs @@ -1,5 +1,7 @@ //! # Message summary for chatlist. +#![allow(missing_docs)] + use crate::chat::Chat; use crate::constants::Chattype; use crate::contact::{Contact, ContactId}; diff --git a/src/tools.rs b/src/tools.rs index ed04bc25a..f5500369e 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -1,6 +1,8 @@ //! Some tools and enhancements to the used libraries, there should be //! no references to Context and other "larger" entities here. +#![allow(missing_docs)] + use core::cmp::{max, min}; use std::borrow::Cow; use std::fmt; diff --git a/src/webxdc.rs b/src/webxdc.rs index a4f185900..7d4bb4d03 100644 --- a/src/webxdc.rs +++ b/src/webxdc.rs @@ -1,5 +1,7 @@ //! # Handle webxdc messages. +#![allow(missing_docs)] + use std::convert::TryFrom; use std::path::Path; From c3a0bb2b77ebeab5466cf4f3be018ca08d442e69 Mon Sep 17 00:00:00 2001 From: iequidoo Date: Thu, 15 Dec 2022 17:31:57 -0300 Subject: [PATCH 007/132] Fix cargo clippy and doc errors after Rust update to 1.66 --- CHANGELOG.md | 2 +- deltachat-ffi/src/lib.rs | 25 ++++++++----------------- src/blob.rs | 2 +- src/chat.rs | 10 +++++----- src/config.rs | 2 +- src/contact.rs | 2 +- src/context.rs | 8 ++++---- src/dehtml.rs | 4 ++-- src/html.rs | 2 +- src/job.rs | 2 +- src/key.rs | 2 +- src/message.rs | 4 ++-- src/param.rs | 2 +- src/sql.rs | 10 +++++----- src/tools.rs | 6 +++--- 15 files changed, 37 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 806419520..fad7f1347 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,6 @@ ### Changes - Don't use deprecated `chrono` functions #3798 - Document accounts manager #3837 - - If a classical-email-user sends an email to a group and adds new recipients, add the new recipients as group members #3781 - Remove `pytest-async` plugin #3846 @@ -16,6 +15,7 @@ - Set read/write timeouts for IMAP over SOCKS5 #3833 - Treat attached PGP keys as peer keys with mutual encryption preference #3832 - fix migration of old databases #3842 +- Fix cargo clippy and doc errors after Rust update to 1.66 #3850 ## 1.103.0 diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index f07e4d53b..a3b772c28 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -2182,7 +2182,7 @@ pub unsafe extern "C" fn dc_imex( eprintln!("ignoring careless call to dc_imex()"); return; } - let what = match imex::ImexMode::from_i32(what_raw as i32) { + let what = match imex::ImexMode::from_i32(what_raw) { Some(what) => what, None => { eprintln!("ignoring invalid argument {} to dc_imex", what_raw); @@ -2253,10 +2253,7 @@ pub unsafe extern "C" fn dc_continue_key_transfer( msg_id: u32, setup_code: *const libc::c_char, ) -> libc::c_int { - if context.is_null() - || msg_id <= constants::DC_MSG_ID_LAST_SPECIAL as u32 - || setup_code.is_null() - { + if context.is_null() || msg_id <= constants::DC_MSG_ID_LAST_SPECIAL || setup_code.is_null() { eprintln!("ignoring careless call to dc_continue_key_transfer()"); return 0; } @@ -2447,15 +2444,9 @@ pub unsafe extern "C" fn dc_get_locations( }; block_on(async move { - let res = location::get_range( - ctx, - chat_id, - contact_id, - timestamp_begin as i64, - timestamp_end as i64, - ) - .await - .unwrap_or_log_default(ctx, "Failed get_locations"); + let res = location::get_range(ctx, chat_id, contact_id, timestamp_begin, timestamp_end) + .await + .unwrap_or_log_default(ctx, "Failed get_locations"); Box::into_raw(Box::new(dc_array_t::from(res))) }) } @@ -2702,7 +2693,7 @@ pub unsafe extern "C" fn dc_chatlist_get_chat_id( } let ffi_list = &*chatlist; let ctx = &*ffi_list.context; - match ffi_list.list.get_chat_id(index as usize) { + match ffi_list.list.get_chat_id(index) { Ok(chat_id) => chat_id.to_u32(), Err(err) => { warn!(ctx, "get_chat_id failed: {}", err); @@ -2722,7 +2713,7 @@ pub unsafe extern "C" fn dc_chatlist_get_msg_id( } let ffi_list = &*chatlist; let ctx = &*ffi_list.context; - match ffi_list.list.get_msg_id(index as usize) { + match ffi_list.list.get_msg_id(index) { Ok(msg_id) => msg_id.map_or(0, |msg_id| msg_id.to_u32()), Err(err) => { warn!(ctx, "get_msg_id failed: {}", err); @@ -2753,7 +2744,7 @@ pub unsafe extern "C" fn dc_chatlist_get_summary( block_on(async move { let summary = ffi_list .list - .get_summary(ctx, index as usize, maybe_chat) + .get_summary(ctx, index, maybe_chat) .await .log_err(ctx, "get_summary failed") .unwrap_or_default(); diff --git a/src/blob.rs b/src/blob.rs index 6cbfdfa31..7b6109f2e 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -746,7 +746,7 @@ mod tests { assert!(file_size(&avatar_blob).await <= 3000); assert!(file_size(&avatar_blob).await > 2000); tokio::task::block_in_place(move || { - let img = image::open(&avatar_blob).unwrap(); + let img = image::open(avatar_blob).unwrap(); assert!(img.width() > 130); assert_eq!(img.width(), img.height()); }); diff --git a/src/chat.rs b/src/chat.rs index 1d41bb5ac..58928cf39 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -768,7 +768,7 @@ impl ChatId { paramsv![self], ) .await?; - Ok(count as usize) + Ok(count) } pub async fn get_fresh_msg_cnt(self, context: &Context) -> Result { @@ -793,7 +793,7 @@ impl ChatId { paramsv![MessageState::InFresh, self], ) .await?; - Ok(count as usize) + Ok(count) } pub(crate) async fn get_param(self, context: &Context) -> Result { @@ -1474,7 +1474,7 @@ impl Chat { new_rfc724_mid, self.id, ContactId::SELF, - to_id as i32, + to_id, timestamp, msg.viewtype, msg.state, @@ -1522,7 +1522,7 @@ impl Chat { new_rfc724_mid, self.id, ContactId::SELF, - to_id as i32, + to_id, timestamp, msg.viewtype, msg.state, @@ -3261,7 +3261,7 @@ pub(crate) async fn get_chat_cnt(context: &Context) -> Result { paramsv![], ) .await?; - Ok(count as usize) + Ok(count) } else { Ok(0) } diff --git a/src/config.rs b/src/config.rs index aea06b2a7..40cddcc8d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -204,7 +204,7 @@ impl Context { let value = match key { Config::Selfavatar => { let rel_path = self.sql.get_raw_config(key.as_ref()).await?; - rel_path.map(|p| get_abs_path(self, &p).to_string_lossy().into_owned()) + rel_path.map(|p| get_abs_path(self, p).to_string_lossy().into_owned()) } Config::SysVersion => Some((*DC_VERSION_STR).clone()), Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)), diff --git a/src/contact.rs b/src/contact.rs index 79f131283..17b74987d 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -855,7 +855,7 @@ impl Contact { paramsv![ContactId::LAST_SPECIAL], ) .await?; - Ok(count as usize) + Ok(count) } /// Get blocked contacts. diff --git a/src/context.rs b/src/context.rs index a78ea833c..7e38a2106 100644 --- a/src/context.rs +++ b/src/context.rs @@ -528,10 +528,10 @@ impl Context { let l2 = LoginParam::load_configured_params(self).await?; let secondary_addrs = self.get_secondary_self_addrs().await?.join(", "); let displayname = self.get_config(Config::Displayname).await?; - let chats = get_chat_cnt(self).await? as usize; - let unblocked_msgs = message::get_unblocked_msg_cnt(self).await as usize; - let request_msgs = message::get_request_msg_cnt(self).await as usize; - let contacts = Contact::get_real_cnt(self).await? as usize; + let chats = get_chat_cnt(self).await?; + let unblocked_msgs = message::get_unblocked_msg_cnt(self).await; + let request_msgs = message::get_request_msg_cnt(self).await; + let contacts = Contact::get_real_cnt(self).await?; let is_configured = self.get_config_int(Config::Configured).await?; let socks5_enabled = self.get_config_int(Config::Socks5Enabled).await?; let dbversion = self diff --git a/src/dehtml.rs b/src/dehtml.rs index 3d39080c6..20075dbab 100644 --- a/src/dehtml.rs +++ b/src/dehtml.rs @@ -20,7 +20,7 @@ struct Dehtml { /// increased at each `
` and decreased at each `
`. This way we know when the quote ends. /// If this is > `0`, then we are inside a `
` divs_since_quote_div: u32, - /// Everything between
and
is usually metadata + /// Everything between `
` and `
` is usually metadata /// If this is > `0`, then we are inside a `
`. divs_since_quoted_content_div: u32, /// All-Inkl just puts the quote into `
`. This count is @@ -42,7 +42,7 @@ impl Dehtml { } fn get_add_text(&self) -> AddText { if self.divs_since_quote_div > 0 && self.divs_since_quoted_content_div == 0 { - AddText::No // Everything between
and
is metadata which we don't want + AddText::No // Everything between `
` and `
` is metadata which we don't want } else { self.add_text } diff --git a/src/html.rs b/src/html.rs index b112fcd2f..ac59793fc 100644 --- a/src/html.rs +++ b/src/html.rs @@ -234,7 +234,7 @@ impl HtmlMsgParser { /// Convert a mime part to a data: url as defined in [RFC 2397](https://tools.ietf.org/html/rfc2397). fn mimepart_to_data_url(mail: &mailparse::ParsedMail<'_>) -> Result { let data = mail.get_body_raw()?; - let data = base64::encode(&data); + let data = base64::encode(data); Ok(format!("data:{};base64,{}", mail.ctype.mimetype, data)) } diff --git a/src/job.rs b/src/job.rs index 0438e4494..e3b513fed 100644 --- a/src/job.rs +++ b/src/job.rs @@ -241,7 +241,7 @@ pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_ info!( context, "job #{} not succeeded on try #{}, retry in {} seconds.", - job.job_id as u32, + job.job_id, tries, time_offset ); diff --git a/src/key.rs b/src/key.rs index fc1ae4847..a67cdab43 100644 --- a/src/key.rs +++ b/src/key.rs @@ -74,7 +74,7 @@ pub trait DcKey: Serialize + Deserializable + KeyTrait + Clone { /// Serialise the key to a base64 string. fn to_base64(&self) -> String { - base64::encode(&DcKey::to_bytes(self)) + base64::encode(DcKey::to_bytes(self)) } /// Serialise the key to ASCII-armored representation. diff --git a/src/message.rs b/src/message.rs index 5c8f64435..2baef9d3f 100644 --- a/src/message.rs +++ b/src/message.rs @@ -579,8 +579,8 @@ impl Message { pub fn has_deviating_timestamp(&self) -> bool { let cnv_to_local = gm2local_offset(); - let sort_timestamp = self.get_sort_timestamp() as i64 + cnv_to_local; - let send_timestamp = self.get_timestamp() as i64 + cnv_to_local; + let sort_timestamp = self.get_sort_timestamp() + cnv_to_local; + let send_timestamp = self.get_timestamp() + cnv_to_local; sort_timestamp / 86400 != send_timestamp / 86400 } diff --git a/src/param.rs b/src/param.rs index 981a1f0dd..69e587e18 100644 --- a/src/param.rs +++ b/src/param.rs @@ -131,7 +131,7 @@ pub enum Param { /// For Chats Selftalk = b'K', - /// For Chats: On sending a new message we set the subject to "Re: ". + /// For Chats: On sending a new message we set the subject to `Re: `. /// Usually we just use the subject of the parent message, but if the parent message /// is deleted, we use the LastSubject of the chat. LastSubject = b't', diff --git a/src/sql.rs b/src/sql.rs index bed7f0063..f24008390 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -236,13 +236,13 @@ impl Sql { // When auto_vacuum is INCREMENTAL, it is possible to // use PRAGMA incremental_vacuum to return unused // database pages to the filesystem. - conn.pragma_update(None, "auto_vacuum", &"INCREMENTAL".to_string())?; + conn.pragma_update(None, "auto_vacuum", "INCREMENTAL".to_string())?; // journal_mode is persisted, it is sufficient to change it only for one handle. - conn.pragma_update(None, "journal_mode", &"WAL".to_string())?; + conn.pragma_update(None, "journal_mode", "WAL".to_string())?; // Default synchronous=FULL is much slower. NORMAL is sufficient for WAL mode. - conn.pragma_update(None, "synchronous", &"NORMAL".to_string())?; + conn.pragma_update(None, "synchronous", "NORMAL".to_string())?; Ok(()) })?; } @@ -459,7 +459,7 @@ impl Sql { let conn = self.get_conn().await?; tokio::task::block_in_place(move || { let mut exists = false; - conn.pragma(None, "table_info", &name.to_string(), |_row| { + conn.pragma(None, "table_info", name.to_string(), |_row| { // will only be executed if the info was found exists = true; Ok(()) @@ -476,7 +476,7 @@ impl Sql { let mut exists = false; // `PRAGMA table_info` returns one row per column, // each row containing 0=cid, 1=name, 2=type, 3=notnull, 4=dflt_value - conn.pragma(None, "table_info", &table_name.to_string(), |row| { + conn.pragma(None, "table_info", table_name.to_string(), |row| { let curr_name: String = row.get(1)?; if col_name == curr_name { exists = true; diff --git a/src/tools.rs b/src/tools.rs index f5500369e..187a38ae4 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -273,7 +273,7 @@ async fn maybe_warn_on_outdated(context: &Context, now: i64, approx_compile_time /// IDs generated by this function are 66 bit wide and are returned as 11 base64 characters. /// /// Additional information when used as a message-id or group-id: -/// - for OUTGOING messages this ID is written to the header as `Chat-Group-ID:` and is added to the message ID as Gr..@ +/// - for OUTGOING messages this ID is written to the header as `Chat-Group-ID:` and is added to the message ID as `Gr..@` /// - for INCOMING messages, the ID is taken from the Chat-Group-ID-header or from the Message-ID in the In-Reply-To: or References:-Header /// - the group-id should be a string with the characters [a-zA-Z0-9\-_] pub(crate) fn create_id() -> String { @@ -361,7 +361,7 @@ pub(crate) fn get_abs_path(context: &Context, path: impl AsRef) -> PathBuf pub(crate) async fn get_filebytes(context: &Context, path: impl AsRef) -> u64 { let path_abs = get_abs_path(context, &path); match fs::metadata(&path_abs).await { - Ok(meta) => meta.len() as u64, + Ok(meta) => meta.len(), Err(_err) => 0, } } @@ -494,7 +494,7 @@ pub fn open_file_std>( let p: PathBuf = path.as_ref().into(); let path_abs = get_abs_path(context, p); - match std::fs::File::open(&path_abs) { + match std::fs::File::open(path_abs) { Ok(bytes) => Ok(bytes), Err(err) => { warn!( From 90c478e58db372b4e566ac140424c4c28a637748 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 15 Dec 2022 19:40:29 +0000 Subject: [PATCH 008/132] Do not send ephemeral timer updates to unpromoted chats --- CHANGELOG.md | 1 + src/chat.rs | 15 ++++++++++++- src/ephemeral.rs | 57 ++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fad7f1347..a67f888a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - If a classical-email-user sends an email to a group and adds new recipients, add the new recipients as group members #3781 - Remove `pytest-async` plugin #3846 +- Only send the message about ephemeral timer change if the chat is promoted #3847 ### API-Changes diff --git a/src/chat.rs b/src/chat.rs index 58928cf39..6bd3db115 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -806,6 +806,19 @@ impl ChatId { .unwrap_or_default()) } + /// Returns true if the chat is not promoted. + pub(crate) async fn is_unpromoted(self, context: &Context) -> Result { + let param = self.get_param(context).await?; + let unpromoted = param.get_bool(Param::Unpromoted).unwrap_or_default(); + Ok(unpromoted) + } + + /// Returns true if the chat is promoted. + pub(crate) async fn is_promoted(self, context: &Context) -> Result { + let promoted = !self.is_unpromoted(context).await?; + Ok(promoted) + } + // Returns true if chat is a saved messages chat. pub async fn is_self_talk(self, context: &Context) -> Result { Ok(self.get_param(context).await?.exists(Param::Selftalk)) @@ -1277,7 +1290,7 @@ impl Chat { } pub fn is_unpromoted(&self) -> bool { - self.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 + self.param.get_bool(Param::Unpromoted).unwrap_or_default() } pub fn is_promoted(&self) -> bool { diff --git a/src/ephemeral.rs b/src/ephemeral.rs index aff6d7e32..1118ef077 100644 --- a/src/ephemeral.rs +++ b/src/ephemeral.rs @@ -205,14 +205,17 @@ impl ChatId { return Ok(()); } self.inner_set_ephemeral_timer(context, timer).await?; - let mut msg = Message::new(Viewtype::Text); - msg.text = Some(stock_ephemeral_timer_changed(context, timer, ContactId::SELF).await); - msg.param.set_cmd(SystemMessage::EphemeralTimerChanged); - if let Err(err) = send_msg(context, self, &mut msg).await { - error!( - context, - "Failed to send a message about ephemeral message timer change: {:?}", err - ); + + if self.is_promoted(context).await? { + let mut msg = Message::new(Viewtype::Text); + msg.text = Some(stock_ephemeral_timer_changed(context, timer, ContactId::SELF).await); + msg.param.set_cmd(SystemMessage::EphemeralTimerChanged); + if let Err(err) = send_msg(context, self, &mut msg).await { + error!( + context, + "Failed to send a message about ephemeral message timer change: {:?}", err + ); + } } Ok(()) } @@ -630,7 +633,7 @@ mod tests { use crate::test_utils::TestContext; use crate::tools::MAX_SECONDS_TO_LEND_FROM_FUTURE; use crate::{ - chat::{self, Chat, ChatItem}, + chat::{self, create_group_chat, send_text_msg, Chat, ChatItem, ProtectionStatus}, tools::IsNoneOrEmpty, }; @@ -795,6 +798,42 @@ mod tests { Ok(()) } + /// Test that enabling ephemeral timer in unpromoted group does not send a message. + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_ephemeral_unpromoted() -> Result<()> { + let alice = TestContext::new_alice().await; + + let chat_id = + create_group_chat(&alice, ProtectionStatus::Unprotected, "Group name").await?; + + // Group is unpromoted, the timer can be changed without sending a message. + assert!(chat_id.is_unpromoted(&alice).await?); + chat_id + .set_ephemeral_timer(&alice, Timer::Enabled { duration: 60 }) + .await?; + let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await; + assert!(sent.is_none()); + assert_eq!( + chat_id.get_ephemeral_timer(&alice).await?, + Timer::Enabled { duration: 60 } + ); + + // Promote the group. + send_text_msg(&alice, chat_id, "hi!".to_string()).await?; + assert!(chat_id.is_promoted(&alice).await?); + let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await; + assert!(sent.is_some()); + + chat_id + .set_ephemeral_timer(&alice.ctx, Timer::Disabled) + .await?; + let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await; + assert!(sent.is_some()); + assert_eq!(chat_id.get_ephemeral_timer(&alice).await?, Timer::Disabled); + + Ok(()) + } + /// Test that timer is enabled even if the message explicitly enabling the timer is lost. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_ephemeral_enable_lost() -> Result<()> { From 03c273e30f8cbcb5430d02ce49f50d27b883d90f Mon Sep 17 00:00:00 2001 From: iequidoo Date: Sat, 17 Dec 2022 18:13:36 -0300 Subject: [PATCH 009/132] Don't send GroupNameChanged message if the group name doesn't change in terms of improve_single_line_input() (#3650) --- CHANGELOG.md | 2 ++ src/chat.rs | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a67f888a6..d5b707b9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ - Treat attached PGP keys as peer keys with mutual encryption preference #3832 - fix migration of old databases #3842 - Fix cargo clippy and doc errors after Rust update to 1.66 #3850 +- Don't send GroupNameChanged message if the group name doesn't change in terms of + improve_single_line_input() #3852 ## 1.103.0 diff --git a/src/chat.rs b/src/chat.rs index 6bd3db115..22818dda0 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -3052,7 +3052,11 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) - paramsv![new_name.to_string(), chat_id], ) .await?; - if chat.is_promoted() && !chat.is_mailing_list() && chat.typ != Chattype::Broadcast { + if chat.is_promoted() + && !chat.is_mailing_list() + && chat.typ != Chattype::Broadcast + && improve_single_line_input(&chat.name) != new_name + { msg.viewtype = Viewtype::Text; msg.text = Some( stock_str::msg_grp_name(context, &chat.name, &new_name, ContactId::SELF).await, From 04f68fddd948657c817eca898b4b3c13cb9c3f21 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Mon, 19 Dec 2022 12:41:19 +0100 Subject: [PATCH 010/132] Go back to Rust 1.61 as 1.65 makes the iOS build fail. For Android, it would actually be enough to go back to 1.64, but let's try what's needed for iOS. --- .github/workflows/ci.yml | 4 ++-- rust-toolchain | 2 +- scripts/coredeps/install-rust.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 173b342ea..43e765a77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,10 +71,10 @@ jobs: include: # Currently used Rust version, same as in `rust-toolchain` file. - os: ubuntu-latest - rust: 1.65.0 + rust: 1.61.0 python: 3.9 - os: windows-latest - rust: 1.65.0 + rust: 1.61.0 python: false # Python bindings compilation on Windows is not supported. # Minimum Supported Rust Version = 1.61.0 diff --git a/rust-toolchain b/rust-toolchain index 902c74186..91951fd8a 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -1.65.0 +1.61.0 diff --git a/scripts/coredeps/install-rust.sh b/scripts/coredeps/install-rust.sh index dcbf30b77..b2063da8c 100755 --- a/scripts/coredeps/install-rust.sh +++ b/scripts/coredeps/install-rust.sh @@ -7,7 +7,7 @@ set -euo pipefail # # Avoid using rustup here as it depends on reading /proc/self/exe and # has problems running under QEMU. -RUST_VERSION=1.65.0 +RUST_VERSION=1.61.0 ARCH="$(uname -m)" test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu From a3fe105256dd7e6e98c7fc59aa566785373b80a5 Mon Sep 17 00:00:00 2001 From: iequidoo Date: Thu, 15 Dec 2022 13:27:06 -0300 Subject: [PATCH 011/132] Prefer encryption for the peer if the message is encrypted or signed with the known key (#3844) Note that if the message is encrypted, we don't check whether it's signed with an attached key currently, otherwise a massive refactoring of the code is needed because for encrypted messages a signature is checked and discarded first now. --- .gitattributes | 2 +- CHANGELOG.md | 1 + src/decrypt.rs | 8 +- src/mimeparser.rs | 251 ++++++++++------ src/peerstate.rs | 1 - src/receive_imf.rs | 6 + .../thunderbird_signed_unencrypted.eml | 56 ++++ ...thunderbird_with_autocrypt_unencrypted.eml | 284 +++++++++--------- 8 files changed, 372 insertions(+), 237 deletions(-) create mode 100644 test-data/message/thunderbird_signed_unencrypted.eml diff --git a/.gitattributes b/.gitattributes index 26f7e3d9b..1359bb5fc 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,7 +4,7 @@ # This directory contains email messages verbatim, and changing CRLF to # LF will corrupt them. -test-data/* text=false +test-data/** text=false # binary files should be detected by git, however, to be sure, you can add them here explicitly *.png binary diff --git a/CHANGELOG.md b/CHANGELOG.md index d5b707b9d..3c54f2b5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - Fix cargo clippy and doc errors after Rust update to 1.66 #3850 - Don't send GroupNameChanged message if the group name doesn't change in terms of improve_single_line_input() #3852 +- Prefer encryption for the peer if the message is encrypted or signed with the known key #3849 ## 1.103.0 diff --git a/src/decrypt.rs b/src/decrypt.rs index 457af6851..c01be46ce 100644 --- a/src/decrypt.rs +++ b/src/decrypt.rs @@ -31,7 +31,7 @@ pub async fn try_decrypt( decryption_info: &DecryptionInfo, ) -> Result, HashSet)>> { // Possibly perform decryption - let public_keyring_for_validate = keyring_from_peerstate(&decryption_info.peerstate); + let public_keyring_for_validate = keyring_from_peerstate(decryption_info.peerstate.as_ref()); let encrypted_data_part = match get_autocrypt_mime(mail) .or_else(|| get_mixed_up_mime(mail)) @@ -251,7 +251,7 @@ fn has_decrypted_pgp_armor(input: &[u8]) -> bool { /// /// Returns `None` if the part is not a Multipart/Signed part, otherwise retruns the set of key /// fingerprints for which there is a valid signature. -fn validate_detached_signature( +pub(crate) fn validate_detached_signature( mail: &ParsedMail<'_>, public_keyring_for_validate: &Keyring, ) -> Result, HashSet)>> { @@ -272,9 +272,9 @@ fn validate_detached_signature( } } -fn keyring_from_peerstate(peerstate: &Option) -> Keyring { +pub(crate) fn keyring_from_peerstate(peerstate: Option<&Peerstate>) -> Keyring { let mut public_keyring_for_validate: Keyring = Keyring::new(); - if let Some(ref peerstate) = *peerstate { + if let Some(peerstate) = peerstate { if let Some(key) = &peerstate.public_key { public_keyring_for_validate.add(key.clone()); } else if let Some(key) = &peerstate.gossip_key { diff --git a/src/mimeparser.rs b/src/mimeparser.rs index e85c95246..c96129e33 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -18,7 +18,10 @@ use crate::blob::BlobObject; use crate::constants::{DC_DESIRED_TEXT_LINES, DC_DESIRED_TEXT_LINE_LEN}; use crate::contact::{addr_cmp, addr_normalize, ContactId}; use crate::context::Context; -use crate::decrypt::{prepare_decryption, try_decrypt, DecryptionInfo}; +use crate::decrypt::{ + keyring_from_peerstate, prepare_decryption, try_decrypt, validate_detached_signature, + DecryptionInfo, +}; use crate::dehtml::dehtml; use crate::events::EventType; use crate::format_flowed::unformat_flowed; @@ -64,7 +67,8 @@ pub struct MimeMessage { /// If a message is not encrypted or the signature is not valid, /// this set is empty. pub signatures: HashSet, - + /// Whether the message is encrypted in a domestic (not Autocrypt) sense + pub encrypted: bool, /// The set of mail recipient addresses for which gossip headers were applied, regardless of /// whether they modified any peerstates. pub gossiped_addr: HashSet, @@ -232,91 +236,95 @@ impl MimeMessage { hop_info += &decryption_info.dkim_results.to_string(); // `signatures` is non-empty exactly if the message was encrypted and correctly signed. - let (mail, signatures, warn_empty_signature) = - match try_decrypt(context, &mail, &decryption_info).await { - Ok(Some((raw, signatures))) => { - // Encrypted, but maybe unsigned message. Only if - // `signatures` set is non-empty, it is a valid - // autocrypt message. + let (mail, signatures, encrypted) = match try_decrypt(context, &mail, &decryption_info) + .await + { + Ok(Some((raw, signatures))) => { + // Encrypted, but maybe unsigned message. Only if + // `signatures` set is non-empty, it is a valid + // autocrypt message. - mail_raw = raw; - let decrypted_mail = mailparse::parse_mail(&mail_raw)?; - if std::env::var(crate::DCC_MIME_DEBUG).is_ok() { - info!(context, "decrypted message mime-body:"); - println!("{}", String::from_utf8_lossy(&mail_raw)); - } + mail_raw = raw; + let decrypted_mail = mailparse::parse_mail(&mail_raw)?; + if std::env::var(crate::DCC_MIME_DEBUG).is_ok() { + info!(context, "decrypted message mime-body:"); + println!("{}", String::from_utf8_lossy(&mail_raw)); + } - // Handle any gossip headers if the mail was encrypted. See section - // "3.6 Key Gossip" of - // but only if the mail was correctly signed: - if !signatures.is_empty() { - let gossip_headers = - decrypted_mail.headers.get_all_values("Autocrypt-Gossip"); - gossiped_addr = - update_gossip_peerstates(context, message_time, &mail, gossip_headers) - .await?; - } - - // let known protected headers from the decrypted - // part override the unencrypted top-level - - // Signature was checked for original From, so we - // do not allow overriding it. - let mut signed_from = None; - - // We do not want to allow unencrypted subject in encrypted emails because the user might falsely think that the subject is safe. - // See . - headers.remove("subject"); - - MimeMessage::merge_headers( + // Handle any gossip headers if the mail was encrypted. See section + // "3.6 Key Gossip" of + // but only if the mail was correctly signed: + if !signatures.is_empty() { + let gossip_headers = decrypted_mail.headers.get_all_values("Autocrypt-Gossip"); + gossiped_addr = update_gossip_peerstates( context, - &mut headers, - &mut recipients, - &mut signed_from, - &mut list_post, - &mut chat_disposition_notification_to, - &decrypted_mail.headers, - ); - if let Some(signed_from) = signed_from { - if addr_cmp(&signed_from.addr, &from.addr) { - from_is_signed = true; - } else { - // There is a From: header in the encrypted & - // signed part, but it doesn't match the outer one. - // This _might_ be because the sender's mail server - // replaced the sending address, e.g. in a mailing list. - // Or it's because someone is doing some replay attack - // - OTOH, I can't come up with an attack scenario - // where this would be useful. - warn!( - context, - "From header in signed part does't match the outer one" - ); - } - } + message_time, + &from.addr, + &mail, + gossip_headers, + ) + .await?; + } - (Ok(decrypted_mail), signatures, true) - } - Ok(None) => { - // Message was not encrypted. - // If it is not a read receipt, degrade encryption. - if let Some(peerstate) = &mut decryption_info.peerstate { - if message_time > peerstate.last_seen_autocrypt - && mail.ctype.mimetype != "multipart/report" - // Disallowing keychanges is disabled for now: - // && decryption_info.dkim_results.allow_keychange - { - peerstate.degrade_encryption(message_time); - peerstate.save_to_db(&context.sql).await?; - } + // let known protected headers from the decrypted + // part override the unencrypted top-level + + // Signature was checked for original From, so we + // do not allow overriding it. + let mut signed_from = None; + + // We do not want to allow unencrypted subject in encrypted emails because the user might falsely think that the subject is safe. + // See . + headers.remove("subject"); + + MimeMessage::merge_headers( + context, + &mut headers, + &mut recipients, + &mut signed_from, + &mut list_post, + &mut chat_disposition_notification_to, + &decrypted_mail.headers, + ); + if let Some(signed_from) = signed_from { + if addr_cmp(&signed_from.addr, &from.addr) { + from_is_signed = true; + } else { + // There is a From: header in the encrypted & + // signed part, but it doesn't match the outer one. + // This _might_ be because the sender's mail server + // replaced the sending address, e.g. in a mailing list. + // Or it's because someone is doing some replay attack + // - OTOH, I can't come up with an attack scenario + // where this would be useful. + warn!( + context, + "From header in signed part does't match the outer one", + ); } - (Ok(mail), HashSet::new(), false) } - Err(err) => { - warn!(context, "decryption failed: {}", err); - (Err(err), HashSet::new(), true) + + (Ok(decrypted_mail), signatures, true) + } + Ok(None) => { + // Message was not encrypted. + // If it is not a read receipt, degrade encryption. + if let Some(peerstate) = &mut decryption_info.peerstate { + if message_time > peerstate.last_seen_autocrypt + && mail.ctype.mimetype != "multipart/report" + // Disallowing keychanges is disabled for now: + // && decryption_info.dkim_results.allow_keychange + { + peerstate.degrade_encryption(message_time); + } } - }; + (Ok(mail), HashSet::new(), false) + } + Err(err) => { + warn!(context, "decryption failed: {}", err); + (Err(err), HashSet::new(), true) + } + }; let mut parser = MimeMessage { parts: Vec::new(), @@ -331,6 +339,7 @@ impl MimeMessage { // only non-empty if it was a valid autocrypt message signatures, + encrypted, gossiped_addr, is_forwarded: false, mdn_reports: Vec::new(), @@ -385,7 +394,7 @@ impl MimeMessage { // part.error = Some("Seems like DKIM failed, this either is an attack or (more likely) a bug in Authentication-Results checking. Please tell us about this at https://support.delta.chat.".to_string()); // } // } - if warn_empty_signature && parser.signatures.is_empty() { + if encrypted && parser.signatures.is_empty() { for part in parser.parts.iter_mut() { part.error = Some("No valid signature".to_string()); } @@ -400,6 +409,13 @@ impl MimeMessage { peerstate .handle_fingerprint_change(context, message_time) .await?; + // When peerstate is set to Mutual, it's saved immediately to not lose that fact in case + // of an error. Otherwise we don't save peerstate until get here to reduce the number of + // calls to save_to_db() and not to degrade encryption if a mail wasn't parsed + // successfully. + if peerstate.prefer_encrypt != EncryptPreference::Mutual { + peerstate.save_to_db(&context.sql).await?; + } } Ok(parser) @@ -852,6 +868,26 @@ impl MimeMessage { .parse_mime_recursive(context, first, is_related) .await?; } + if let Some(peerstate) = &mut self.decryption_info.peerstate { + let keyring = keyring_from_peerstate(Some(peerstate)); + match validate_detached_signature(mail, &keyring) { + Ok(Some((_, fprints))) => { + if fprints.is_empty() { + warn!(context, "signed message is not signed with a known key"); + } else if peerstate.prefer_encrypt != EncryptPreference::Mutual { + info!( + context, + "message is signed with the known key, setting \ + prefer-encrypt=mutual for '{}'", + peerstate.addr, + ); + Self::upgrade_to_mutual_encryption(context, peerstate).await?; + } + } + Ok(None) => warn!(context, "not a 'multipart/signed' part??"), + Err(err) => warn!(context, "signed message validation failed: {}", err), + } + } } (mime::MULTIPART, "report") => { /* RFC 6522: the first part is for humans, the second for machines */ @@ -929,6 +965,17 @@ impl MimeMessage { Ok(any_part_added) } + async fn upgrade_to_mutual_encryption( + context: &Context, + peerstate: &mut Peerstate, + ) -> Result<()> { + if peerstate.public_key.is_none() { + peerstate.public_key = peerstate.gossip_key.take(); + } + peerstate.prefer_encrypt = EncryptPreference::Mutual; + peerstate.save_to_db(&context.sql).await + } + /// Returns true if any part was added, false otherwise. async fn add_single_part_if_known( &mut self, @@ -1106,7 +1153,13 @@ impl MimeMessage { if peerstate.prefer_encrypt != EncryptPreference::Mutual && mime_type.type_() == mime::APPLICATION && mime_type.subtype().as_str() == "pgp-keys" - && Self::try_set_peer_key_from_file_part(context, peerstate, decoded_data).await? + && Self::try_set_peer_key_from_file_part( + context, + peerstate, + decoded_data, + self.encrypted, + ) + .await? { return Ok(()); } @@ -1196,6 +1249,7 @@ impl MimeMessage { context: &Context, peerstate: &mut Peerstate, decoded_data: &[u8], + mail_is_encrypted: bool, ) -> Result { let key = match str::from_utf8(decoded_data) { Err(err) => { @@ -1240,13 +1294,23 @@ impl MimeMessage { return Ok(false); } } - info!( - context, - "will use attached PGP key for peer '{}' with mutual encryption", peerstate.addr, - ); peerstate.public_key = Some(key); - peerstate.prefer_encrypt = EncryptPreference::Mutual; - peerstate.save_to_db(&context.sql).await?; + if mail_is_encrypted { + info!( + context, + "using attached PGP key for peer '{}' with prefer-encrypt=mutual as the mail is \ + encrypted", + peerstate.addr, + ); + peerstate.prefer_encrypt = EncryptPreference::Mutual; + peerstate.save_to_db(&context.sql).await?; + } else { + info!( + context, + "using attached PGP key for peer '{}'", peerstate.addr, + ); + peerstate.prefer_encrypt = EncryptPreference::NoPreference; + } Ok(true) } @@ -1624,6 +1688,7 @@ impl MimeMessage { async fn update_gossip_peerstates( context: &Context, message_time: i64, + from: &str, mail: &mailparse::ParsedMail<'_>, gossip_headers: Vec, ) -> Result> { @@ -1641,7 +1706,7 @@ async fn update_gossip_peerstates( if !get_recipients(&mail.headers) .iter() - .any(|info| info.addr == header.addr.to_lowercase()) + .any(|info| addr_cmp(&info.addr, &header.addr)) { warn!( context, @@ -1649,6 +1714,14 @@ async fn update_gossip_peerstates( ); continue; } + if addr_cmp(from, &header.addr) { + // Non-standard, but anyway we can't update the cached peerstate here. + warn!( + context, + "Ignoring gossiped \"{}\" as it equals the From address", &header.addr, + ); + continue; + } let peerstate; if let Some(mut p) = Peerstate::from_addr(context, &header.addr).await? { diff --git a/src/peerstate.rs b/src/peerstate.rs index 931891929..ab6d527a3 100644 --- a/src/peerstate.rs +++ b/src/peerstate.rs @@ -806,7 +806,6 @@ mod tests { verified_key_fingerprint: None, fingerprint_changed: false, }; - assert_eq!(peerstate.prefer_encrypt, EncryptPreference::NoPreference); peerstate.apply_header(&header, 100); assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual); diff --git a/src/receive_imf.rs b/src/receive_imf.rs index b477d8686..a1711018e 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -5359,7 +5359,13 @@ Reply from different address let raw = include_bytes!("../test-data/message/thunderbird_with_autocrypt_unencrypted.eml"); receive_imf(&t, raw, false).await?; + let peerstate = Peerstate::from_addr(&t, "alice@example.org") + .await? + .unwrap(); + assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual); + let raw = include_bytes!("../test-data/message/thunderbird_signed_unencrypted.eml"); + receive_imf(&t, raw, false).await?; let peerstate = Peerstate::from_addr(&t, "alice@example.org") .await? .unwrap(); diff --git a/test-data/message/thunderbird_signed_unencrypted.eml b/test-data/message/thunderbird_signed_unencrypted.eml new file mode 100644 index 000000000..ec9646050 --- /dev/null +++ b/test-data/message/thunderbird_signed_unencrypted.eml @@ -0,0 +1,56 @@ +From - Thu, 15 Dec 2022 14:45:17 GMT +X-Mozilla-Status: 0801 +X-Mozilla-Status2: 00000000 +Message-ID: +Date: Thu, 15 Dec 2022 11:45:16 -0300 +MIME-Version: 1.0 +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 + Thunderbird/102.5.1 +Content-Language: en-US +To: bob@example.net +From: Alice +Subject: test message 15:53 +X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0; + attachmentreminder=0; deliveryformat=0 +X-Identity-Key: id3 +Fcc: imap://alice%40example.org@in.example.org/Sent +Content-Type: multipart/signed; micalg=pgp-sha256; + protocol="application/pgp-signature"; + boundary="------------iX39J1p7DOgblwacjo0e7jX7" + +This is an OpenPGP/MIME signed message (RFC 4880 and 3156) +--------------iX39J1p7DOgblwacjo0e7jX7 +Content-Type: multipart/mixed; boundary="------------WD4DG7TcI4p4lbzyM4toRaDw"; + protected-headers="v1" +From: Alice +To: bob@example.net +Message-ID: +Subject: test message 15:53 + +--------------WD4DG7TcI4p4lbzyM4toRaDw +Content-Type: text/plain; charset=UTF-8; format=flowed +Content-Transfer-Encoding: base64 + +DQo= + +--------------WD4DG7TcI4p4lbzyM4toRaDw-- + +--------------iX39J1p7DOgblwacjo0e7jX7 +Content-Type: application/pgp-signature; name="OpenPGP_signature.asc" +Content-Description: OpenPGP digital signature +Content-Disposition: attachment; filename="OpenPGP_signature" + +-----BEGIN PGP SIGNATURE----- + +wsD5BAABCAAjFiEEFKs/ZfwnS721+naMJfAHJFnkeuIFAmObMvwFAwAAAAAACgkQJfAHJFnkeuLM +TgwAnAADX93HE5vXmuBcAbRN2HKIwMzBtRtUF4FNPKchffUvvhSNpHkW2jW7A4hOHNgVSDQdqIUn ++62NgkaKrT1bZqozOZNHXMECHtKBwXWTkIAVqcBdvscCztVIgGby56OPnzZ5y09BsRaqqE5AhDgN +wGCLa6ipu5FYSF6+KzdO0GIPMY5aGRgVhtl4N01v4S3+r/Yu60MkN87nd15Eaqsrs60P9RmKJTt4 +hDie35kKvHnPzLNs8+xLfqPuO/P7ZbPQgkgCwMAMsMDRUYOv+k5c/bL3PKiOENuDpQ7dkKJ2OzSn +nTcg8qhDf17vWe26C/QBhFiGEsrHNBQ1KW5by+cqjIUBJgXElFnPl35S5L3fn6JHZLcz6q+wQuJu +vGT1mJuP//jLFkMHSexukFIVXzn41rWPLd05rBqMgwRcOHMIyzE9zaO1aa8MF2TirPaZ5lH9rx/y +9DCU/d2sqbbYt8TGqj4hM3pqg5K22eq4KT1W7y8+28I5QfjZumLLrHBdYTnR +=6JTB +-----END PGP SIGNATURE----- + +--------------iX39J1p7DOgblwacjo0e7jX7-- diff --git a/test-data/message/thunderbird_with_autocrypt_unencrypted.eml b/test-data/message/thunderbird_with_autocrypt_unencrypted.eml index a8654024a..201044751 100644 --- a/test-data/message/thunderbird_with_autocrypt_unencrypted.eml +++ b/test-data/message/thunderbird_with_autocrypt_unencrypted.eml @@ -1,142 +1,142 @@ -From - Fri, 09 Dec 2022 13:16:11 GMT -X-Mozilla-Status: 0801 -X-Mozilla-Status2: 00000000 -Message-ID: <0c8e3ffc-99ae-eb68-15b5-15c4d85a5c12@example.org> -Date: Fri, 9 Dec 2022 10:16:11 -0300 -MIME-Version: 1.0 -User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 - Thunderbird/102.5.1 -Content-Language: en-US -To: bob@example.net -From: Alice -Subject: test message 10:15 -Autocrypt: addr=alice@example.org; keydata= - xsDNBGOTM3UBDADZ819boOPXK/ZPO1EepYUBve2psYO3rZkPu3uhyn7qpI8c0U5IbR+mAXPH - FkKfvSwTtGiPpXaP6/vx0OjTs1aR7We9MrP+1EckbsyQnnDmDGsGxxyn3+a3ar0FcgOBi/kS - j0fPB1tX92/z3MWtOSXYtYOlMotRdIxt/L8CYQSBe8wWpoOKQPNmtvnEuDlJwSlrhRPx6PDm - BgoKv1qi5UOrAoyUPbdnINnSgj14KBNMgiuJQz6+AwVaYitVJ37N6lrCfhWRPZAVDRW5ajLx - W+DuuYUW675xzi2bLlb4jGeFePvS9Rhw2CpkG608cFVFrUCBH91mfb0UnmxIDMcc6JSn0Uqf - PESC+0wK9xokzi07/FZtXyf925oiMpA7ZQ7aSNW6J7kk618xNQRivLhEV1+QofynAzfwAB+C - vqY+VjNZbGKGW7aba84Nx9Wa7g8rbZ5ZvsQmrn38fpWu+2GcUvnGOxn8lYEljnfCthSigjCg - q3T90aSUwDQfedJej9nzM98AEQEAAc0ZQWxpY2UgPGFsaWNlQGV4YW1wbGUub3JnPsLBDQQT - AQgANxYhBL7Y7n4kdUxZWposQeDVBmYZR/R8BQJjkzN2BQm7+B4AAhsDBAsJCAcFFQgJCgsF - FgIDAQAACgkQ4NUGZhlH9HzsWwv+JucjIbwsHfRWDB81R9d0WIQGvYM8sjUETQWmlwEcts/y - yHVLNnyvxn9EUboo9tLg3VmukPYNLyuVJ6WlWRuskZHXy3TdW+1TcAIzO97vReBOXOunDmoT - PoA9IRUFVVwC3ejyFj9timcKVKX6WUyNY7l0x1Voy5gHqswnlVQ0SXsdBQqDwMJqUuRmWE1z - rk15EdF2OvWADlZ0j9TeGHFYcr6lLXZ92sOQbjsm4vmwPGFC5oiolKmoZXNfNa9Ef3HPv9Q0 - XF576hfu7CyIhWXXxCNGzssuTA42Kdxhcpppi+HtzEr1F3jApDG2T5bfMnIN9udu6UgNTdQm - /Qyuamn2vo11fXsdA41Kajrnj2Vtcf6qd4qv4HSgeyGxZw3btjbmwuVAao0x49jXYZhpx00r - iddTfjBhhE1MCPNHK9ypmodWMiF99dZNhAHB434agfkNWHl8z3QwxDLjWhkzNdnHeO1Xg2zq - 3/mKi2mNyb2iGImDp4GAxOQVLYGwXPRe0NeqzsDNBGOTM3YBDADFQ11NReZAL2vdu5avkfs7 - iw7MNI2DANGvouIcQOP0gqSkF0UY/bMmvWXmDV6iTaxe2/+r/t51zZZRnr1KYF/XayoQmxLu - MAKWAUJvltzcYlJwSphCCbh2OpxHBZqrbhHKGZIkj1Is3uVBSFt6gkr9lYDFk+ehhBBNoE50 - nSamJXNpur2A4aZYmIwKWNeU+skzYu4VDUKXet69fmK4bZlF1ydYturcSQtE6fLb8ob7b/52 - C2FJxRNFJQ7el8bozPKX0ZitKCSh9HXKw4TvD+nD8v4tDAmzno9Z66T4o8WYRA5mCYWVpD+W - Qadcikcqx5G7RIiKgRxvcGAx9kMUjMptjErc+1rKcNw7QdpFu6uiSj1602jBM/JvQRvUVa85 - vkQn0u07PjIzH+ZQeKsijdmDaeOZWjE1/XkOVi3btzoOaQQRh14spC+ztl8hV6/9+bDIWXEK - iiQQUi1Kvw7TfaRQprmD1IUyfb69LpwD8MTnBoDyA/PxY1DurQPJMN5yAvsAEQEAAcLA/AQY - AQgAJhYhBL7Y7n4kdUxZWposQeDVBmYZR/R8BQJjkzN3BQm7+B4AAhsMAAoJEODVBmYZR/R8 - 90sL/0+cJmENgLGI+Ji5rMlZe63hDk4w1p+7THf4vmX/Pg27hUTznTeRLs3dhGVYrSPvxgl7 - L4KlTwe1euSBgfWqCpNjh0g5Hvz3X5uSoLerEsGa7PoGTvpnTuWoRzYJLYRkWtuwfQ3SvpeQ - OglT7vgvsSoC1h6MOnWJgTo8yYyYP92Wq7fv867bSpWjjykHcK5DIjEM71+6IJTn5pnhkG2d - dibfHyDZoBj0P8VrJFEkkCzkycANtmhUBDr/vFhYKWy76ZZNgGHg71iFGwXK/kz5dKA6mIUN - AaeyyarAzoaJh0y3UkAPW/evwD/PP9M4y2mP6TDPeFYBZI7o5gCD6q+t1zCMc1M4V+hOJXfs - ISJPE3J/Rq53QnPOmsz9sdyfOxxfePV64gtv3xHBFUafucFiipeHgx4eXmdMNRnzlGeHlhDn - dpFGkJJeA8TCJqfP0DFY/CCW4mT0FvaVcFtJ/CXvmD6qORTlbJg9XZ2FNCA7x0+WJ2mjn/m1 - rhEBN10sGyg93A== -X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0; - attachmentreminder=0; deliveryformat=0 -X-Identity-Key: id3 -Fcc: imap://alice%40example.org@in.example.org/Sent -Content-Type: multipart/signed; micalg=pgp-sha256; - protocol="application/pgp-signature"; - boundary="------------FFBOG29BVxcOkoFV1hnc0RaY" - -This is an OpenPGP/MIME signed message (RFC 4880 and 3156) ---------------FFBOG29BVxcOkoFV1hnc0RaY -Content-Type: multipart/mixed; boundary="------------4cwiD0i5NnTXNSfPNpFwrv6V"; - protected-headers="v1" -From: Alice -To: bob@example.net -Message-ID: <0c8e3ffc-99ae-eb68-15b5-15c4d85a5c12@example.org> -Subject: test message 10:15 - ---------------4cwiD0i5NnTXNSfPNpFwrv6V -Content-Type: multipart/mixed; boundary="------------fbNEFvfS22YOKnkTd1oAl0ak" - ---------------fbNEFvfS22YOKnkTd1oAl0ak -Content-Type: text/plain; charset=UTF-8; format=flowed -Content-Transfer-Encoding: base64 - -MTIzDQoNCg== ---------------fbNEFvfS22YOKnkTd1oAl0ak -Content-Type: application/pgp-keys; name="OpenPGP_0xE0D506661947F47C.asc" -Content-Disposition: attachment; filename="OpenPGP_0xE0D506661947F47C.asc" -Content-Description: OpenPGP public key -Content-Transfer-Encoding: quoted-printable - ------BEGIN PGP PUBLIC KEY BLOCK----- - -xsDNBGOTM3UBDADZ819boOPXK/ZPO1EepYUBve2psYO3rZkPu3uhyn7qpI8c0U5I -bR+mAXPHFkKfvSwTtGiPpXaP6/vx0OjTs1aR7We9MrP+1EckbsyQnnDmDGsGxxyn -3+a3ar0FcgOBi/kSj0fPB1tX92/z3MWtOSXYtYOlMotRdIxt/L8CYQSBe8wWpoOK -QPNmtvnEuDlJwSlrhRPx6PDmBgoKv1qi5UOrAoyUPbdnINnSgj14KBNMgiuJQz6+ -AwVaYitVJ37N6lrCfhWRPZAVDRW5ajLxW+DuuYUW675xzi2bLlb4jGeFePvS9Rhw -2CpkG608cFVFrUCBH91mfb0UnmxIDMcc6JSn0UqfPESC+0wK9xokzi07/FZtXyf9 -25oiMpA7ZQ7aSNW6J7kk618xNQRivLhEV1+QofynAzfwAB+CvqY+VjNZbGKGW7ab -a84Nx9Wa7g8rbZ5ZvsQmrn38fpWu+2GcUvnGOxn8lYEljnfCthSigjCgq3T90aSU -wDQfedJej9nzM98AEQEAAc0ZQWxpY2UgPGFsaWNlQGV4YW1wbGUub3JnPsLBDQQT -AQgANxYhBL7Y7n4kdUxZWposQeDVBmYZR/R8BQJjkzN2BQm7+B4AAhsDBAsJCAcF -FQgJCgsFFgIDAQAACgkQ4NUGZhlH9HzsWwv+JucjIbwsHfRWDB81R9d0WIQGvYM8 -sjUETQWmlwEcts/yyHVLNnyvxn9EUboo9tLg3VmukPYNLyuVJ6WlWRuskZHXy3Td -W+1TcAIzO97vReBOXOunDmoTPoA9IRUFVVwC3ejyFj9timcKVKX6WUyNY7l0x1Vo -y5gHqswnlVQ0SXsdBQqDwMJqUuRmWE1zrk15EdF2OvWADlZ0j9TeGHFYcr6lLXZ9 -2sOQbjsm4vmwPGFC5oiolKmoZXNfNa9Ef3HPv9Q0XF576hfu7CyIhWXXxCNGzssu -TA42Kdxhcpppi+HtzEr1F3jApDG2T5bfMnIN9udu6UgNTdQm/Qyuamn2vo11fXsd -A41Kajrnj2Vtcf6qd4qv4HSgeyGxZw3btjbmwuVAao0x49jXYZhpx00riddTfjBh -hE1MCPNHK9ypmodWMiF99dZNhAHB434agfkNWHl8z3QwxDLjWhkzNdnHeO1Xg2zq -3/mKi2mNyb2iGImDp4GAxOQVLYGwXPRe0NeqzsDNBGOTM3YBDADFQ11NReZAL2vd -u5avkfs7iw7MNI2DANGvouIcQOP0gqSkF0UY/bMmvWXmDV6iTaxe2/+r/t51zZZR -nr1KYF/XayoQmxLuMAKWAUJvltzcYlJwSphCCbh2OpxHBZqrbhHKGZIkj1Is3uVB -SFt6gkr9lYDFk+ehhBBNoE50nSamJXNpur2A4aZYmIwKWNeU+skzYu4VDUKXet69 -fmK4bZlF1ydYturcSQtE6fLb8ob7b/52C2FJxRNFJQ7el8bozPKX0ZitKCSh9HXK -w4TvD+nD8v4tDAmzno9Z66T4o8WYRA5mCYWVpD+WQadcikcqx5G7RIiKgRxvcGAx -9kMUjMptjErc+1rKcNw7QdpFu6uiSj1602jBM/JvQRvUVa85vkQn0u07PjIzH+ZQ -eKsijdmDaeOZWjE1/XkOVi3btzoOaQQRh14spC+ztl8hV6/9+bDIWXEKiiQQUi1K -vw7TfaRQprmD1IUyfb69LpwD8MTnBoDyA/PxY1DurQPJMN5yAvsAEQEAAcLA/AQY -AQgAJhYhBL7Y7n4kdUxZWposQeDVBmYZR/R8BQJjkzN3BQm7+B4AAhsMAAoJEODV -BmYZR/R890sL/0+cJmENgLGI+Ji5rMlZe63hDk4w1p+7THf4vmX/Pg27hUTznTeR -Ls3dhGVYrSPvxgl7L4KlTwe1euSBgfWqCpNjh0g5Hvz3X5uSoLerEsGa7PoGTvpn -TuWoRzYJLYRkWtuwfQ3SvpeQOglT7vgvsSoC1h6MOnWJgTo8yYyYP92Wq7fv867b -SpWjjykHcK5DIjEM71+6IJTn5pnhkG2ddibfHyDZoBj0P8VrJFEkkCzkycANtmhU -BDr/vFhYKWy76ZZNgGHg71iFGwXK/kz5dKA6mIUNAaeyyarAzoaJh0y3UkAPW/ev -wD/PP9M4y2mP6TDPeFYBZI7o5gCD6q+t1zCMc1M4V+hOJXfsISJPE3J/Rq53QnPO -msz9sdyfOxxfePV64gtv3xHBFUafucFiipeHgx4eXmdMNRnzlGeHlhDndpFGkJJe -A8TCJqfP0DFY/CCW4mT0FvaVcFtJ/CXvmD6qORTlbJg9XZ2FNCA7x0+WJ2mjn/m1 -rhEBN10sGyg93A=3D=3D -=3DDPMe ------END PGP PUBLIC KEY BLOCK----- - ---------------fbNEFvfS22YOKnkTd1oAl0ak-- - ---------------4cwiD0i5NnTXNSfPNpFwrv6V-- - ---------------FFBOG29BVxcOkoFV1hnc0RaY -Content-Type: application/pgp-signature; name="OpenPGP_signature.asc" -Content-Description: OpenPGP digital signature -Content-Disposition: attachment; filename="OpenPGP_signature" - ------BEGIN PGP SIGNATURE----- - -wsD5BAABCAAjFiEEvtjufiR1TFlamixB4NUGZhlH9HwFAmOTNRsFAwAAAAAACgkQ4NUGZhlH9Hzw -Iwv/dNC7LDvRGmZ71IaivkUkSTbpGgg0gnCNOuf+B8OxUBQlWPkBmLxyrXbkxsTghFogDsVQeZQQ -DJ182KMgeC//rUN5DPJNrh95YZnav0nUpzW1mkFZjK+PdhbfdXKoXhJIqcw/7lpy/povRYZ20Igg -tIHLa1NlqPPhSx/o2dsEqWeAtXF4e8T/jQSA5+ZQtVrdcTCNQG6zbqlHZuJ7bF1bwuHPgLgDhJ5k -+T2ny80ZtkfLXJl5tQdblAomhBPfEOj+AeLCKsrJFO3WFZOvsuoKMPZpwW1wEh7+QYLABX/lRvqx -IxjH1Tc26vttlOrVH13FKGSeWJELun+b2dP1LPiBQ7DOsrrFNs3fp56Nb7Y+exH5ld0jz0kJZTUD -yPqZpJXTsWkFPE7x1tbH/7goiH8f9DbQrvmqQ2fnCjzf3UJR3ZhG/13YAUEdLVkVzwMItEd6yisg -MP8mlbwm4aDeCiGXO/xhOoBVl6bn1HVSxo7mb0chHVyD1NOfd7qsxem0L/A2 -=MOQB ------END PGP SIGNATURE----- - ---------------FFBOG29BVxcOkoFV1hnc0RaY-- +From - Wed, 14 Dec 2022 18:53:03 GMT +X-Mozilla-Status: 0801 +X-Mozilla-Status2: 00000000 +Message-ID: <87d75c7e-0f52-1335-e437-af605c09f954@example.org> +Date: Wed, 14 Dec 2022 15:53:03 -0300 +MIME-Version: 1.0 +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 + Thunderbird/102.5.1 +Content-Language: en-US +To: bob@example.net +From: Alice +Subject: test message 15:52 +Autocrypt: addr=alice@example.org; keydata= + xsDNBGOaGzQBDADCFtBNMHRDJQRkd2tNm7CJm1Yo3Y5r3qP6v0FSwP1BIHbiIf0E/jFiKZWj + 1uL68J2mGUuUu+Qi4ovf1l9/QQYzg/DCaLZxlbc0LKu2LXcpUL5DPu37mdw+DKs0YvNIlc+A + RjyFUwd3rsZN3k58inf1mYzKuKU6NpbdXULbOEYwnVEwzQsrtS2JgJ+tLSYUvNJeMJXm/cDL + XKJSApAyvVVdxxteG8uWcDqWV/HcXuopXLILf3yJF0De11/7G62dHNHuhmtgRLsTN4Q372Q9 + KNdYEFLHaN91jEzyD/+aHNskATxtcGhppI8OQsU3NzNgHyd8Smzx5oTyZ/6NdhYoh0pKB8yf + VAyA69t5fctQRb4+bTwL+sS9KDobQOvcyOMUSccDfUhsWMghwsMCwU4Sz9hIY6dCAfroDAiL + vYUfdNJstAqvLf04mZtMmkI7Q2BYLETEgu4KQzQHRQekmOE/3EaSiojNa4ZTVURMdJ9U+I3E + q8e6TbOY7Xa4V8krAt/F2wMAEQEAAc0ZQWxpY2UgPGFsaWNlQGV4YW1wbGUub3JnPsLBDQQT + AQgANxYhBBSrP2X8J0u9tfp2jCXwByRZ5HriBQJjmhs1BQm7+B4AAhsDBAsJCAcFFQgJCgsF + FgIDAQAACgkQJfAHJFnkeuKQ2wwAgDgiCI6bz9PjqE1GoDcy/xQdy+nnYq5pOuHGUndZ7jYK + cOqM8LDEaG7GgrFsbs9vGhTA1fyqncM41pB7SmwQ7zBVaMdtHoulEG4RPGVboDaY9tuMOL3/ + GVxFbovVHyU5Lr1euryNh/0JvMITY0UHaEY1k1M7izYUMyFu8I1ODZ9Iws2trUyU3Omw/sTJ + x15zzCsK8Aq+r3JmB+Q33SFSgWr/YWH0dQVIQ0I5iLN2q14oucmLBaKc9EXdRLiu8S8lLSQl + nfISJ17GBLmH1YxmPPZ3CRHC6iEKCLR6G9wzhsTPNdK7dRCYR5wTI27RVPLBcSnCKAeTopAJ + YskyNndtv0iaNRT7YLOfhrsBAofSjuLegR04CNiqBHtYQ3LO3WKhJ7riRcQ/Ksv0wYkmj1gJ + 8myMwA+ybfYrpNqO4devnCvE3Eo5gzeYbvYU2Z17n9y6HAOG9/Tm/daiGEP2ni6iwV0kqLzw + eC48R1D75T66PxX/jQooujrTph8+K3ckV/q+zsDNBGOaGzUBDADV+DGgKxvCpfVFuPGrSdRU + 06dxowdKOKavO6WGMvN3g/+CFrIsjUFy4S0Soo5ARnLh23i49ZSjacXFpgtZUNV3iGOSOcSE + LldLtZk5BV9w/ATqqgu4/LVdNA9rm+o197bIeSQCRTnY/QV6FdKYxVd4NBVH9abZ7t8Tm4qC + urZj56MjPCg3fqT+Q6sjxH+nKBrs8s8iCJkYhGBgU3q5W+wrtZ56kI9mxJec62KHpyLZ0rTE + xEAeVbChUJOo11vUtJfTrDhI6lhqyr72o/A6bY1OV7WzkxtiBRl35eewQ+RDLJ4yxaNj/XTS + UxOz60xNggEfDVtfgfjBZrBbiHXqf8iKVV1ZPGm0ycvXZGYFw2zXLI2PwevhQCm+t4Ywty1h + 8l019MYmGadpQgbuA4ZippuzOSzSGMQ+S4uYEzeeymR9ksxVSXn90HEzqC7LdHCcd2IO6rfu + g2fuRf258Adfuoh3s8YUlWyXjEHLXKo9SRgGMfGs7qgCOL/ReAwFPtKACvEAEQEAAcLA/AQY + AQgAJhYhBBSrP2X8J0u9tfp2jCXwByRZ5HriBQJjmhs2BQm7+B4AAhsMAAoJECXwByRZ5Hri + EOkMALtq4DVYX8RfoPdU0Dt6y+yDj1NALv5GefvHbgfuaVT8PaOP0gxCjWrnUDvvJEwP1W3j + UXYqDwKP42hiGWsnXk2hbgXbplArgP3H987x7c8bu1wIAmkJ9eLjEM++rbOD4vWbYXRwaDiH + LetFJ5tGHDAIfL48NYpz2o3XZ3/O7WdTZphsAcvgPxTC+zU7WkbUl2SQlj0/qwsoD+qe9RYT + XhVXR7q7sjcGB4TpeqzRT7YKVLoVNq+bQw2lUX4W561gAYbZvVo/XByfDCoxmkxwuMlSmajj + Wy7b9TuT38t1HArv4m/LyVuBHiikX0/MUNBeSSIiKDvTL6NdHTjnZM6ptZvdvW3+ou6ET0pK + MGDpk/1NVuMnIHJESRg/SSFV6sElgq38k9wAT2oUqLcYvYI07nHmnuciaGygkCcGt+l2PvAa + j4mkQQvMU0cNRDBybk5aKi820oGIJjT7e+5RnD2mYZQdOAbQhDVCHvrfS1I60bsHT1MHqyAa + /qMLjKwBpKEd/w== +X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0; + attachmentreminder=0; deliveryformat=0 +X-Identity-Key: id3 +Fcc: imap://alice%40example.org@in.example.org/Sent +Content-Type: multipart/signed; micalg=pgp-sha256; + protocol="application/pgp-signature"; + boundary="------------x6XEHrf0vHmVgEo6f9bMGGUy" + +This is an OpenPGP/MIME signed message (RFC 4880 and 3156) +--------------x6XEHrf0vHmVgEo6f9bMGGUy +Content-Type: multipart/mixed; boundary="------------pePWGfS6inyAJsaJRFnx5r9s"; + protected-headers="v1" +From: Alice +To: bob@example.net +Message-ID: <87d75c7e-0f52-1335-e437-af605c09f954@example.org> +Subject: test message 15:52 + +--------------pePWGfS6inyAJsaJRFnx5r9s +Content-Type: multipart/mixed; boundary="------------bG3L0s709hFHGhT5ybFZLKLf" + +--------------bG3L0s709hFHGhT5ybFZLKLf +Content-Type: text/plain; charset=UTF-8; format=flowed +Content-Transfer-Encoding: base64 + +DQo= +--------------bG3L0s709hFHGhT5ybFZLKLf +Content-Type: application/pgp-keys; name="OpenPGP_0x25F0072459E47AE2.asc" +Content-Disposition: attachment; filename="OpenPGP_0x25F0072459E47AE2.asc" +Content-Description: OpenPGP public key +Content-Transfer-Encoding: quoted-printable + +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xsDNBGOaGzQBDADCFtBNMHRDJQRkd2tNm7CJm1Yo3Y5r3qP6v0FSwP1BIHbiIf0E +/jFiKZWj1uL68J2mGUuUu+Qi4ovf1l9/QQYzg/DCaLZxlbc0LKu2LXcpUL5DPu37 +mdw+DKs0YvNIlc+ARjyFUwd3rsZN3k58inf1mYzKuKU6NpbdXULbOEYwnVEwzQsr +tS2JgJ+tLSYUvNJeMJXm/cDLXKJSApAyvVVdxxteG8uWcDqWV/HcXuopXLILf3yJ +F0De11/7G62dHNHuhmtgRLsTN4Q372Q9KNdYEFLHaN91jEzyD/+aHNskATxtcGhp +pI8OQsU3NzNgHyd8Smzx5oTyZ/6NdhYoh0pKB8yfVAyA69t5fctQRb4+bTwL+sS9 +KDobQOvcyOMUSccDfUhsWMghwsMCwU4Sz9hIY6dCAfroDAiLvYUfdNJstAqvLf04 +mZtMmkI7Q2BYLETEgu4KQzQHRQekmOE/3EaSiojNa4ZTVURMdJ9U+I3Eq8e6TbOY +7Xa4V8krAt/F2wMAEQEAAc0ZQWxpY2UgPGFsaWNlQGV4YW1wbGUub3JnPsLBDQQT +AQgANxYhBBSrP2X8J0u9tfp2jCXwByRZ5HriBQJjmhs1BQm7+B4AAhsDBAsJCAcF +FQgJCgsFFgIDAQAACgkQJfAHJFnkeuKQ2wwAgDgiCI6bz9PjqE1GoDcy/xQdy+nn +Yq5pOuHGUndZ7jYKcOqM8LDEaG7GgrFsbs9vGhTA1fyqncM41pB7SmwQ7zBVaMdt +HoulEG4RPGVboDaY9tuMOL3/GVxFbovVHyU5Lr1euryNh/0JvMITY0UHaEY1k1M7 +izYUMyFu8I1ODZ9Iws2trUyU3Omw/sTJx15zzCsK8Aq+r3JmB+Q33SFSgWr/YWH0 +dQVIQ0I5iLN2q14oucmLBaKc9EXdRLiu8S8lLSQlnfISJ17GBLmH1YxmPPZ3CRHC +6iEKCLR6G9wzhsTPNdK7dRCYR5wTI27RVPLBcSnCKAeTopAJYskyNndtv0iaNRT7 +YLOfhrsBAofSjuLegR04CNiqBHtYQ3LO3WKhJ7riRcQ/Ksv0wYkmj1gJ8myMwA+y +bfYrpNqO4devnCvE3Eo5gzeYbvYU2Z17n9y6HAOG9/Tm/daiGEP2ni6iwV0kqLzw +eC48R1D75T66PxX/jQooujrTph8+K3ckV/q+zsDNBGOaGzUBDADV+DGgKxvCpfVF +uPGrSdRU06dxowdKOKavO6WGMvN3g/+CFrIsjUFy4S0Soo5ARnLh23i49ZSjacXF +pgtZUNV3iGOSOcSELldLtZk5BV9w/ATqqgu4/LVdNA9rm+o197bIeSQCRTnY/QV6 +FdKYxVd4NBVH9abZ7t8Tm4qCurZj56MjPCg3fqT+Q6sjxH+nKBrs8s8iCJkYhGBg +U3q5W+wrtZ56kI9mxJec62KHpyLZ0rTExEAeVbChUJOo11vUtJfTrDhI6lhqyr72 +o/A6bY1OV7WzkxtiBRl35eewQ+RDLJ4yxaNj/XTSUxOz60xNggEfDVtfgfjBZrBb +iHXqf8iKVV1ZPGm0ycvXZGYFw2zXLI2PwevhQCm+t4Ywty1h8l019MYmGadpQgbu +A4ZippuzOSzSGMQ+S4uYEzeeymR9ksxVSXn90HEzqC7LdHCcd2IO6rfug2fuRf25 +8Adfuoh3s8YUlWyXjEHLXKo9SRgGMfGs7qgCOL/ReAwFPtKACvEAEQEAAcLA/AQY +AQgAJhYhBBSrP2X8J0u9tfp2jCXwByRZ5HriBQJjmhs2BQm7+B4AAhsMAAoJECXw +ByRZ5HriEOkMALtq4DVYX8RfoPdU0Dt6y+yDj1NALv5GefvHbgfuaVT8PaOP0gxC +jWrnUDvvJEwP1W3jUXYqDwKP42hiGWsnXk2hbgXbplArgP3H987x7c8bu1wIAmkJ +9eLjEM++rbOD4vWbYXRwaDiHLetFJ5tGHDAIfL48NYpz2o3XZ3/O7WdTZphsAcvg +PxTC+zU7WkbUl2SQlj0/qwsoD+qe9RYTXhVXR7q7sjcGB4TpeqzRT7YKVLoVNq+b +Qw2lUX4W561gAYbZvVo/XByfDCoxmkxwuMlSmajjWy7b9TuT38t1HArv4m/LyVuB +HiikX0/MUNBeSSIiKDvTL6NdHTjnZM6ptZvdvW3+ou6ET0pKMGDpk/1NVuMnIHJE +SRg/SSFV6sElgq38k9wAT2oUqLcYvYI07nHmnuciaGygkCcGt+l2PvAaj4mkQQvM +U0cNRDBybk5aKi820oGIJjT7e+5RnD2mYZQdOAbQhDVCHvrfS1I60bsHT1MHqyAa +/qMLjKwBpKEd/w=3D=3D +=3DE1VA +-----END PGP PUBLIC KEY BLOCK----- + +--------------bG3L0s709hFHGhT5ybFZLKLf-- + +--------------pePWGfS6inyAJsaJRFnx5r9s-- + +--------------x6XEHrf0vHmVgEo6f9bMGGUy +Content-Type: application/pgp-signature; name="OpenPGP_signature.asc" +Content-Description: OpenPGP digital signature +Content-Disposition: attachment; filename="OpenPGP_signature" + +-----BEGIN PGP SIGNATURE----- + +wsD5BAABCAAjFiEEFKs/ZfwnS721+naMJfAHJFnkeuIFAmOaG48FAwAAAAAACgkQJfAHJFnkeuJx +1Av/SkGIP18ql7cImI4/t49MvZdIWNuqyKuHZr+7hCPDq0i3muKuy04e8AsGvhHRS8/aSSFkkCgf +OM5JYwHjOVj7DLTGSfbGM9GGpbu4fP6wa+rCm/WHgRr2H/T4ggy6jNv4rBOMcSNXhpO+J/28Zjoi +47Dl1eH6B8HyiwqHSPixRqWAf0d8dIp5S7Wf4asb+cFA+rM/7UlZqidJP5ihtHA3A6C1SNRnMBk/ +g3ABR45srubkgXu5QN4PsUalE0N4I00aCgR6WiPggJE2Zf1kslj5M7az3Az+dp0apRilxrlP4J4Y +KxEzim2X8tJbqvq8G7295NcNDH3YCx3sOT8utXM5NL9how+4c2iylD2m7Oczz3bv0TYDU/4ksWmO +zwiMm+47+45UNLWjQ2sGTpui6nXFC7ZuGxzKjUrbpfvkFDIeFgO1XVw812YDoMHKMaEzr8fDhoKD +LD14JUpwC47XsDlB4ZAMizcCYiK3MvTk1w/5I5ijzJmMiMIpPNxzYxIm82F3 +=wlaI +-----END PGP SIGNATURE----- + +--------------x6XEHrf0vHmVgEo6f9bMGGUy-- From d722b6ba19e70a4aa2fc6854f112a396aec48f7f Mon Sep 17 00:00:00 2001 From: Hocuri Date: Mon, 19 Dec 2022 17:15:17 +0100 Subject: [PATCH 012/132] Only go to 1.64 for now --- .github/workflows/ci.yml | 4 ++-- rust-toolchain | 2 +- scripts/coredeps/install-rust.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43e765a77..5b1d28faf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,10 +71,10 @@ jobs: include: # Currently used Rust version, same as in `rust-toolchain` file. - os: ubuntu-latest - rust: 1.61.0 + rust: 1.64.0 python: 3.9 - os: windows-latest - rust: 1.61.0 + rust: 1.64.0 python: false # Python bindings compilation on Windows is not supported. # Minimum Supported Rust Version = 1.61.0 diff --git a/rust-toolchain b/rust-toolchain index 91951fd8a..940573042 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -1.61.0 +1.64.0 diff --git a/scripts/coredeps/install-rust.sh b/scripts/coredeps/install-rust.sh index b2063da8c..b4a08f3f1 100755 --- a/scripts/coredeps/install-rust.sh +++ b/scripts/coredeps/install-rust.sh @@ -7,7 +7,7 @@ set -euo pipefail # # Avoid using rustup here as it depends on reading /proc/self/exe and # has problems running under QEMU. -RUST_VERSION=1.61.0 +RUST_VERSION=1.64.0 ARCH="$(uname -m)" test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu From 5922fb38da885c005d746b6ae09247ba2e96e2c6 Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 12 Dec 2022 18:54:30 +0000 Subject: [PATCH 013/132] Store relative accounts path in accounts.toml This makes it possible to move accounts dir, especially useful for bots. --- CHANGELOG.md | 1 + .../src/deltachat_rpc_client/rpc.py | 4 +- src/accounts.rs | 49 ++++++++++++------- 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c54f2b5c..7759d5990 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ add the new recipients as group members #3781 - Remove `pytest-async` plugin #3846 - Only send the message about ephemeral timer change if the chat is promoted #3847 +- Use relative paths in `accounts.toml` #3838 ### API-Changes diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py index 78474aa8b..8a407308d 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py @@ -14,9 +14,7 @@ class Rpc: if accounts_dir: kwargs["env"] = { **kwargs.get("env", os.environ), - "DC_ACCOUNTS_PATH": os.path.abspath( - os.path.expanduser(str(accounts_dir)) - ), + "DC_ACCOUNTS_PATH": str(accounts_dir), } self._kwargs = kwargs diff --git a/src/accounts.rs b/src/accounts.rs index 511f9f5b5..2380de04e 100644 --- a/src/accounts.rs +++ b/src/accounts.rs @@ -64,7 +64,7 @@ impl Accounts { let events = Events::new(); let stockstrings = StockStrings::new(); let accounts = config - .load_accounts(&events, &stockstrings) + .load_accounts(&events, &stockstrings, &dir) .await .context("failed to load accounts")?; @@ -107,10 +107,11 @@ impl Accounts { /// /// Returns account ID. pub async fn add_account(&mut self) -> Result { - let account_config = self.config.new_account(&self.dir).await?; + let account_config = self.config.new_account().await?; + let dbfile = account_config.dbfile(&self.dir); let ctx = Context::new( - &account_config.dbfile(), + &dbfile, account_config.id, self.events.clone(), self.stockstrings.clone(), @@ -123,10 +124,10 @@ impl Accounts { /// Adds a new closed account. pub async fn add_closed_account(&mut self) -> Result { - let account_config = self.config.new_account(&self.dir).await?; + let account_config = self.config.new_account().await?; let ctx = Context::new_closed( - &account_config.dbfile(), + &account_config.dbfile(&self.dir), account_config.id, self.events.clone(), self.stockstrings.clone(), @@ -147,6 +148,8 @@ impl Accounts { drop(ctx); if let Some(cfg) = self.config.get_account(id) { + let account_path = self.dir.join(cfg.dir); + // Spend up to 1 minute trying to remove the files. // Files may remain locked up to 30 seconds due to r2d2 bug: // https://github.com/sfackler/r2d2/issues/99 @@ -154,7 +157,7 @@ impl Accounts { loop { counter += 1; - if let Err(err) = fs::remove_dir_all(&cfg.dir) + if let Err(err) = fs::remove_dir_all(&account_path) .await .context("failed to remove account data") { @@ -187,16 +190,16 @@ impl Accounts { // create new account let account_config = self .config - .new_account(&self.dir) + .new_account() .await .context("failed to create new account")?; - let new_dbfile = account_config.dbfile(); + let new_dbfile = account_config.dbfile(&self.dir); let new_blobdir = Context::derive_blobdir(&new_dbfile); let new_walfile = Context::derive_walfile(&new_dbfile); let res = { - fs::create_dir_all(&account_config.dir) + fs::create_dir_all(self.dir.join(&account_config.dir)) .await .context("failed to create dir")?; fs::rename(&dbfile, &new_dbfile) @@ -358,8 +361,17 @@ impl Config { /// Read a configuration from the given file into memory. pub async fn from_file(file: PathBuf) -> Result { + let dir = file.parent().context("can't get config file directory")?; let bytes = fs::read(&file).await.context("failed to read file")?; - let inner: InnerConfig = toml::from_slice(&bytes).context("failed to parse config")?; + let mut inner: InnerConfig = toml::from_slice(&bytes).context("failed to parse config")?; + + // Previous versions of the core stored absolute paths in account config. + // Convert them to relative paths. + for account in &mut inner.accounts { + if let Ok(new_dir) = account.dir.strip_prefix(dir) { + account.dir = new_dir.to_path_buf(); + } + } Ok(Config { file, inner }) } @@ -372,12 +384,13 @@ impl Config { &self, events: &Events, stockstrings: &StockStrings, + dir: &Path, ) -> Result> { let mut accounts = BTreeMap::new(); for account_config in &self.inner.accounts { let ctx = Context::new( - &account_config.dbfile(), + &account_config.dbfile(dir), account_config.id, events.clone(), stockstrings.clone(), @@ -386,7 +399,7 @@ impl Config { .with_context(|| { format!( "failed to create context from file {:?}", - account_config.dbfile() + account_config.dbfile(dir) ) })?; @@ -396,12 +409,12 @@ impl Config { Ok(accounts) } - /// Create a new account in the given root directory. - async fn new_account(&mut self, dir: &Path) -> Result { + /// Creates a new account in the account manager directory. + async fn new_account(&mut self) -> Result { let id = { let id = self.inner.next_id; let uuid = Uuid::new_v4(); - let target_dir = dir.join(uuid.to_string()); + let target_dir = PathBuf::from(uuid.to_string()); self.inner.accounts.push(AccountConfig { id, @@ -473,14 +486,16 @@ struct AccountConfig { /// Unique id. pub id: u32, /// Root directory for all data for this account. + /// + /// The path is relative to the account manager directory. pub dir: std::path::PathBuf, pub uuid: Uuid, } impl AccountConfig { /// Get the canoncial dbfile name for this configuration. - pub fn dbfile(&self) -> std::path::PathBuf { - self.dir.join(DB_NAME) + pub fn dbfile(&self, accounts_dir: &Path) -> std::path::PathBuf { + accounts_dir.join(&self.dir).join(DB_NAME) } } From 3130fdc4f043274a970b6df99b68ebc2692e44ac Mon Sep 17 00:00:00 2001 From: bjoern Date: Tue, 20 Dec 2022 17:57:16 +0100 Subject: [PATCH 014/132] release 1.104.0 (#3857) --- CHANGELOG.md | 11 ++++++++++- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- deltachat-ffi/Cargo.toml | 2 +- deltachat-jsonrpc/Cargo.toml | 2 +- deltachat-jsonrpc/typescript/package.json | 2 +- deltachat-rpc-server/Cargo.toml | 2 +- package.json | 2 +- 8 files changed, 20 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7759d5990..575e7d3e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## Unreleased +### Changes + +### API-Changes + +### Fixes + + +## 1.104.0 + ### Changes - Don't use deprecated `chrono` functions #3798 - Document accounts manager #3837 @@ -19,7 +28,7 @@ - fix migration of old databases #3842 - Fix cargo clippy and doc errors after Rust update to 1.66 #3850 - Don't send GroupNameChanged message if the group name doesn't change in terms of - improve_single_line_input() #3852 + `improve_single_line_input()` #3852 - Prefer encryption for the peer if the message is encrypted or signed with the known key #3849 diff --git a/Cargo.lock b/Cargo.lock index 1fb73cb46..cbc7a9693 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -867,7 +867,7 @@ checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" [[package]] name = "deltachat" -version = "1.103.0" +version = "1.104.0" dependencies = [ "ansi_term", "anyhow", @@ -940,7 +940,7 @@ dependencies = [ [[package]] name = "deltachat-jsonrpc" -version = "1.103.0" +version = "1.104.0" dependencies = [ "anyhow", "async-channel", @@ -962,7 +962,7 @@ dependencies = [ [[package]] name = "deltachat-rpc-server" -version = "1.103.0" +version = "1.104.0" dependencies = [ "anyhow", "deltachat-jsonrpc", @@ -985,7 +985,7 @@ dependencies = [ [[package]] name = "deltachat_ffi" -version = "1.103.0" +version = "1.104.0" dependencies = [ "anyhow", "deltachat", diff --git a/Cargo.toml b/Cargo.toml index 5841fc8b9..88282eb59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat" -version = "1.103.0" +version = "1.104.0" authors = ["Delta Chat Developers (ML) "] edition = "2021" license = "MPL-2.0" diff --git a/deltachat-ffi/Cargo.toml b/deltachat-ffi/Cargo.toml index 1e967cca4..8153d954f 100644 --- a/deltachat-ffi/Cargo.toml +++ b/deltachat-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat_ffi" -version = "1.103.0" +version = "1.104.0" description = "Deltachat FFI" authors = ["Delta Chat Developers (ML) "] edition = "2018" diff --git a/deltachat-jsonrpc/Cargo.toml b/deltachat-jsonrpc/Cargo.toml index cef348b8f..e2aeddf4e 100644 --- a/deltachat-jsonrpc/Cargo.toml +++ b/deltachat-jsonrpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-jsonrpc" -version = "1.103.0" +version = "1.104.0" description = "DeltaChat JSON-RPC API" authors = ["Delta Chat Developers (ML) "] edition = "2021" diff --git a/deltachat-jsonrpc/typescript/package.json b/deltachat-jsonrpc/typescript/package.json index 1ddb8941d..9937dbabb 100644 --- a/deltachat-jsonrpc/typescript/package.json +++ b/deltachat-jsonrpc/typescript/package.json @@ -48,5 +48,5 @@ }, "type": "module", "types": "dist/deltachat.d.ts", - "version": "1.103.0" + "version": "1.104.0" } \ No newline at end of file diff --git a/deltachat-rpc-server/Cargo.toml b/deltachat-rpc-server/Cargo.toml index bdb6a7168..bd2ba9b34 100644 --- a/deltachat-rpc-server/Cargo.toml +++ b/deltachat-rpc-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-rpc-server" -version = "1.103.0" +version = "1.104.0" description = "DeltaChat JSON-RPC server" authors = ["Delta Chat Developers (ML) "] edition = "2021" diff --git a/package.json b/package.json index fe638ee94..3122efe66 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.103.0" + "version": "1.104.0" } \ No newline at end of file From 1f7ad78f4066d567dfe671b9729cdf99bf4d1e02 Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 19 Dec 2022 12:32:20 +0000 Subject: [PATCH 015/132] Move receive_imf tests into a separate file --- src/receive_imf.rs | 3230 +------------------------------------- src/receive_imf/tests.rs | 3220 +++++++++++++++++++++++++++++++++++++ 2 files changed, 3221 insertions(+), 3229 deletions(-) create mode 100644 src/receive_imf/tests.rs diff --git a/src/receive_imf.rs b/src/receive_imf.rs index a1711018e..d6a10b496 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -2289,3232 +2289,4 @@ async fn add_or_lookup_contact_by_addr( } #[cfg(test)] -mod tests { - use tokio::fs; - - use super::*; - - use crate::aheader::EncryptPreference; - use crate::chat::get_chat_contacts; - use crate::chat::{get_chat_msgs, ChatItem, ChatVisibility}; - use crate::chatlist::Chatlist; - use crate::constants::DC_GCL_NO_SPECIALS; - use crate::imap::prefetch_should_download; - use crate::message::Message; - use crate::test_utils::{get_chat_msg, TestContext, TestContextManager}; - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_grpid_simple() { - let context = TestContext::new().await; - let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: hello@example.org\n\ - Subject: outer-subject\n\ - In-Reply-To: \n\ - References: \n\ - \n\ - hello\x00"; - let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]) - .await - .unwrap(); - assert_eq!(extract_grpid(&mimeparser, HeaderDef::InReplyTo), None); - let grpid = Some("HcxyMARjyJy"); - assert_eq!(extract_grpid(&mimeparser, HeaderDef::References), grpid); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_bad_from() { - let context = TestContext::new().await; - let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: hello\n\ - Subject: outer-subject\n\ - In-Reply-To: \n\ - References: \n\ - \n\ - hello\x00"; - let mimeparser = MimeMessage::from_bytes_with_partial(&context.ctx, &raw[..], None).await; - assert!(mimeparser.is_err()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_grpid_from_multiple() { - let context = TestContext::new().await; - let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: hello@example.org\n\ - Subject: outer-subject\n\ - In-Reply-To: \n\ - References: , \n\ - \n\ - hello\x00"; - let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]) - .await - .unwrap(); - let grpid = Some("HcxyMARjyJy"); - assert_eq!(extract_grpid(&mimeparser, HeaderDef::InReplyTo), grpid); - assert_eq!(extract_grpid(&mimeparser, HeaderDef::References), grpid); - } - - static MSGRMSG: &[u8] = - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: Bob \n\ - To: alice@example.org\n\ - Chat-Version: 1.0\n\ - Subject: Chat: hello\n\ - Message-ID: \n\ - Date: Sun, 22 Mar 2020 22:37:55 +0000\n\ - \n\ - hello\n"; - - static ONETOONE_NOREPLY_MAIL: &[u8] = - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: Bob \n\ - To: alice@example.org\n\ - Subject: Chat: hello\n\ - Message-ID: <2222@example.com>\n\ - Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ - \n\ - hello\n"; - - static GRP_MAIL: &[u8] = - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: bob@example.com\n\ - To: alice@example.org, claire@example.com\n\ - Subject: group with Alice, Bob and Claire\n\ - Message-ID: <3333@example.com>\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n"; - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_adhoc_group_show_chats_only() { - let t = TestContext::new_alice().await; - assert_eq!(t.get_config_int(Config::ShowEmails).await.unwrap(), 0); - - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 0); - - receive_imf(&t, MSGRMSG, false).await.unwrap(); - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); - - receive_imf(&t, ONETOONE_NOREPLY_MAIL, false).await.unwrap(); - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); - - receive_imf(&t, GRP_MAIL, false).await.unwrap(); - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_adhoc_group_show_accepted_contact_unknown() { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("1")).await.unwrap(); - receive_imf(&t, GRP_MAIL, false).await.unwrap(); - - // adhoc-group with unknown contacts with show_emails=accepted is ignored for unknown contacts - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 0); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_adhoc_group_show_accepted_contact_known() { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("1")).await.unwrap(); - Contact::create(&t, "Bob", "bob@example.com").await.unwrap(); - receive_imf(&t, GRP_MAIL, false).await.unwrap(); - - // adhoc-group with known contacts with show_emails=accepted is still ignored for known contacts - // (and existent chat is required) - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 0); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_adhoc_group_show_accepted_contact_accepted() { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("1")).await.unwrap(); - - // accept Bob by accepting a delta-message from Bob - receive_imf(&t, MSGRMSG, false).await.unwrap(); - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); - let chat_id = chats.get_chat_id(0).unwrap(); - assert!(!chat_id.is_special()); - let chat = chat::Chat::load_from_db(&t, chat_id).await.unwrap(); - assert!(chat.is_contact_request()); - chat_id.accept(&t).await.unwrap(); - let chat = chat::Chat::load_from_db(&t, chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Single); - assert_eq!(chat.name, "Bob"); - assert_eq!(chat::get_chat_contacts(&t, chat_id).await.unwrap().len(), 1); - assert_eq!(chat::get_chat_msgs(&t, chat_id, 0).await.unwrap().len(), 1); - - // receive a non-delta-message from Bob, shows up because of the show_emails setting - receive_imf(&t, ONETOONE_NOREPLY_MAIL, false).await.unwrap(); - - assert_eq!(chat::get_chat_msgs(&t, chat_id, 0).await.unwrap().len(), 2); - - // let Bob create an adhoc-group by a non-delta-message, shows up because of the show_emails setting - receive_imf(&t, GRP_MAIL, false).await.unwrap(); - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 2); - let chat_id = chats.get_chat_id(0).unwrap(); - let chat = chat::Chat::load_from_db(&t, chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Group); - assert_eq!(chat.name, "group with Alice, Bob and Claire"); - assert_eq!(chat::get_chat_contacts(&t, chat_id).await.unwrap().len(), 3); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_adhoc_group_show_all() { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); - receive_imf(&t, GRP_MAIL, false).await.unwrap(); - - // adhoc-group with unknown contacts with show_emails=all will show up in a single chat - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); - let chat_id = chats.get_chat_id(0).unwrap(); - let chat = chat::Chat::load_from_db(&t, chat_id).await.unwrap(); - assert!(chat.is_contact_request()); - chat_id.accept(&t).await.unwrap(); - let chat = chat::Chat::load_from_db(&t, chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Group); - assert_eq!(chat.name, "group with Alice, Bob and Claire"); - assert_eq!(chat::get_chat_contacts(&t, chat_id).await.unwrap().len(), 3); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_read_receipt_and_unarchive() -> Result<()> { - // create alice's account - let t = TestContext::new_alice().await; - - let bob_id = Contact::create(&t, "bob", "bob@example.com").await?; - let one2one_id = ChatId::create_for_contact(&t, bob_id).await?; - one2one_id - .set_visibility(&t, ChatVisibility::Archived) - .await - .unwrap(); - let one2one = Chat::load_from_db(&t, one2one_id).await?; - assert!(one2one.get_visibility() == ChatVisibility::Archived); - - // create a group with bob, archive group - let group_id = chat::create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; - chat::add_contact_to_chat(&t, group_id, bob_id).await?; - assert_eq!(chat::get_chat_msgs(&t, group_id, 0).await.unwrap().len(), 0); - group_id - .set_visibility(&t, ChatVisibility::Archived) - .await?; - let group = Chat::load_from_db(&t, group_id).await?; - assert!(group.get_visibility() == ChatVisibility::Archived); - - // everything archived, chatlist should be empty - assert_eq!( - Chatlist::try_load(&t, DC_GCL_NO_SPECIALS, None, None) - .await? - .len(), - 0 - ); - - // send a message to group with bob - receive_imf( - &t, - format!( - "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: alice@example.org\n\ - To: bob@example.com\n\ - Subject: foo\n\ - Message-ID: \n\ - Chat-Version: 1.0\n\ - Chat-Group-ID: {}\n\ - Chat-Group-Name: foo\n\ - Chat-Disposition-Notification-To: alice@example.org\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", - group.grpid, group.grpid - ) - .as_bytes(), - false, - ) - .await?; - let msg = get_chat_msg(&t, group_id, 0, 1).await; - assert_eq!(msg.is_dc_message, MessengerMessage::Yes); - assert_eq!(msg.text.unwrap(), "hello"); - assert_eq!(msg.state, MessageState::OutDelivered); - let group = Chat::load_from_db(&t, group_id).await?; - assert!(group.get_visibility() == ChatVisibility::Normal); - - // bob sends a read receipt to the group - receive_imf( - &t, - format!( - "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: bob@example.com\n\ - To: alice@example.org\n\ - Subject: message opened\n\ - Date: Sun, 22 Mar 2020 23:37:57 +0000\n\ - Chat-Version: 1.0\n\ - Message-ID: \n\ - Content-Type: multipart/report; report-type=disposition-notification; boundary=\"SNIPP\"\n\ - \n\ - \n\ - --SNIPP\n\ - Content-Type: text/plain; charset=utf-8\n\ - \n\ - Read receipts do not guarantee sth. was read.\n\ - \n\ - \n\ - --SNIPP\n\ - Content-Type: message/disposition-notification\n\ - \n\ - Reporting-UA: Delta Chat 1.28.0\n\ - Original-Recipient: rfc822;bob@example.com\n\ - Final-Recipient: rfc822;bob@example.com\n\ - Original-Message-ID: \n\ - Disposition: manual-action/MDN-sent-automatically; displayed\n\ - \n\ - \n\ - --SNIPP--", - group.grpid - ) - .as_bytes(), - false, - ) - .await?; - assert_eq!(chat::get_chat_msgs(&t, group_id, 0).await?.len(), 1); - let msg = message::Message::load_from_db(&t, msg.id).await?; - assert_eq!(msg.state, MessageState::OutMdnRcvd); - - // check, the read-receipt has not unarchived the one2one - assert_eq!( - Chatlist::try_load(&t, DC_GCL_NO_SPECIALS, None, None) - .await? - .len(), - 1 - ); - let one2one = Chat::load_from_db(&t, one2one_id).await?; - assert!(one2one.get_visibility() == ChatVisibility::Archived); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_no_from() { - // if there is no from given, from_id stays 0 which is just fine. These messages - // are very rare, however, we have to add them to the database - // to avoid a re-download from the server. - - let t = TestContext::new_alice().await; - let context = &t; - - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert!(chats.get_msg_id(0).is_err()); - - receive_imf( - context, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - To: bob@example.com\n\ - Subject: foo\n\ - Message-ID: <3924@example.com>\n\ - Chat-Version: 1.0\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", - false, - ) - .await - .unwrap(); - - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - // Check that the message is not shown to the user: - assert!(chats.is_empty()); - - // Check that the message was added to the db: - assert!(message::rfc724_mid_exists(context, "3924@example.com") - .await - .unwrap() - .is_some()); - } - - /// If there is no Message-Id header, we generate a random id. - /// But there is no point in adding a trash entry in the database - /// if the email is malformed (e.g. because `From` is missing) - /// with this random id we just generated. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_no_message_id_header() { - let t = TestContext::new_alice().await; - - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert!(chats.get_msg_id(0).is_err()); - - let received = receive_imf( - &t, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - To: bob@example.com\n\ - Subject: foo\n\ - Chat-Version: 1.0\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", - false, - ) - .await - .unwrap(); - dbg!(&received); - assert!(received.is_none()); - - assert!(!t - .sql - .exists( - "SELECT COUNT(*) FROM msgs WHERE chat_id=?;", - paramsv![DC_CHAT_ID_TRASH], - ) - .await - .unwrap()); - - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - // Check that the message is not shown to the user: - assert!(chats.is_empty()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_escaped_from() { - let t = TestContext::new_alice().await; - let contact_id = Contact::create(&t, "foobar", "foobar@example.com") - .await - .unwrap(); - let chat_id = ChatId::create_for_contact(&t, contact_id).await.unwrap(); - receive_imf( - &t, - b"From: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= \n\ - To: alice@example.org\n\ - Subject: foo\n\ - Message-ID: \n\ - Chat-Version: 1.0\n\ - Chat-Disposition-Notification-To: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= \n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", - false, - ).await.unwrap(); - assert_eq!( - Contact::load_from_db(&t, contact_id) - .await - .unwrap() - .get_authname(), - "Имя, Фамилия", - ); - let msg = get_chat_msg(&t, chat_id, 0, 1).await; - assert_eq!(msg.is_dc_message, MessengerMessage::Yes); - assert_eq!(msg.text.unwrap(), "hello"); - assert_eq!(msg.param.get_int(Param::WantsMdn).unwrap(), 1); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_escaped_recipients() { - let t = TestContext::new_alice().await; - Contact::create(&t, "foobar", "foobar@example.com") - .await - .unwrap(); - - let carl_contact_id = - Contact::add_or_lookup(&t, "Carl", "carl@host.tld", Origin::IncomingUnknownFrom) - .await - .unwrap() - .0; - - receive_imf( - &t, - b"From: Foobar \n\ - To: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= alice@example.org\n\ - Cc: =?utf-8?q?=3Ch2=3E?= \n\ - Subject: foo\n\ - Message-ID: \n\ - Chat-Version: 1.0\n\ - Chat-Disposition-Notification-To: \n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", - false, - ) - .await - .unwrap(); - let contact = Contact::load_from_db(&t, carl_contact_id).await.unwrap(); - assert_eq!(contact.get_name(), ""); - assert_eq!(contact.get_display_name(), "h2"); - - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - let msg = Message::load_from_db(&t, chats.get_msg_id(0).unwrap().unwrap()) - .await - .unwrap(); - assert_eq!(msg.is_dc_message, MessengerMessage::Yes); - assert_eq!(msg.text.unwrap(), "hello"); - assert_eq!(msg.param.get_int(Param::WantsMdn).unwrap(), 1); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_cc_to_contact() { - let t = TestContext::new_alice().await; - Contact::create(&t, "foobar", "foobar@example.com") - .await - .unwrap(); - - let carl_contact_id = - Contact::add_or_lookup(&t, "garabage", "carl@host.tld", Origin::IncomingUnknownFrom) - .await - .unwrap() - .0; - - receive_imf( - &t, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: Foobar \n\ - To: alice@example.org\n\ - Cc: Carl \n\ - Subject: foo\n\ - Message-ID: \n\ - Chat-Version: 1.0\n\ - Chat-Disposition-Notification-To: \n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", - false, - ) - .await - .unwrap(); - let contact = Contact::load_from_db(&t, carl_contact_id).await.unwrap(); - assert_eq!(contact.get_name(), ""); - assert_eq!(contact.get_display_name(), "Carl"); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_ndn_tiscali() { - test_parse_ndn( - "alice@tiscali.it", - "shenauithz@testrun.org", - "Mr.un2NYERi1RM.lbQ5F9q-QyJ@tiscali.it", - include_bytes!("../test-data/message/tiscali_ndn.eml"), - Some("Delivery status notification – This is an automatically generated Delivery Status Notification. \n\nDelivery to the following recipients was aborted after 2 second(s):\n\n * shenauithz@testrun.org"), - ) - .await; - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_ndn_testrun() { - test_parse_ndn( - "alice@testrun.org", - "hcksocnsofoejx@five.chat", - "Mr.A7pTA5IgrUA.q4bP41vAJOp@testrun.org", - include_bytes!("../test-data/message/testrun_ndn.eml"), - Some("Undelivered Mail Returned to Sender – This is the mail system at host hq5.merlinux.eu.\n\nI\'m sorry to have to inform you that your message could not\nbe delivered to one or more recipients. It\'s attached below.\n\nFor further assistance, please send mail to postmaster.\n\nIf you do so, please include this problem report. You can\ndelete your own text from the attached returned message.\n\n The mail system\n\n: host mail.five.chat[195.62.125.103] said: 550 5.1.1\n : Recipient address rejected: User unknown in\n virtual mailbox table (in reply to RCPT TO command)"), - ) - .await; - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_ndn_yahoo() { - test_parse_ndn( - "alice@yahoo.com", - "haeclirth.sinoenrat@yahoo.com", - "1680295672.3657931.1591783872936@mail.yahoo.com", - include_bytes!("../test-data/message/yahoo_ndn.eml"), - Some("Failure Notice – Sorry, we were unable to deliver your message to the following address.\n\n:\n554: delivery error: dd Not a valid recipient - atlas117.free.mail.ne1.yahoo.com [...]"), - ) - .await; - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_ndn_gmail() { - test_parse_ndn( - "alice@gmail.com", - "assidhfaaspocwaeofi@gmail.com", - "CABXKi8zruXJc_6e4Dr087H5wE7sLp+u250o0N2q5DdjF_r-8wg@mail.gmail.com", - include_bytes!("../test-data/message/gmail_ndn.eml"), - Some("Delivery Status Notification (Failure) – ** Die Adresse wurde nicht gefunden **\n\nIhre Nachricht wurde nicht an assidhfaaspocwaeofi@gmail.com zugestellt, weil die Adresse nicht gefunden wurde oder keine E-Mails empfangen kann.\n\nHier erfahren Sie mehr: https://support.google.com/mail/?p=NoSuchUser\n\nAntwort:\n\n550 5.1.1 The email account that you tried to reach does not exist. Please try double-checking the recipient\'s email address for typos or unnecessary spaces. Learn more at https://support.google.com/mail/?p=NoSuchUser i18sor6261697wrs.38 - gsmtp"), - ) - .await; - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_ndn_gmx() { - test_parse_ndn( - "alice@gmx.com", - "snaerituhaeirns@gmail.com", - "9c9c2a32-056b-3592-c372-d7e8f0bd4bc2@gmx.de", - include_bytes!("../test-data/message/gmx_ndn.eml"), - Some("Mail delivery failed: returning message to sender – This message was created automatically by mail delivery software.\n\nA message that you sent could not be delivered to one or more of\nits recipients. This is a permanent error. The following address(es)\nfailed:\n\nsnaerituhaeirns@gmail.com:\nSMTP error from remote server for RCPT TO command, host: gmail-smtp-in.l.google.com (66.102.1.27) reason: 550-5.1.1 The email account that you tried to reach does not exist. Please\n try\n550-5.1.1 double-checking the recipient\'s email address for typos or\n550-5.1.1 unnecessary spaces. Learn more at\n550 5.1.1 https://support.google.com/mail/?p=NoSuchUser f6si2517766wmc.21\n9 - gsmtp [...]"), - ) - .await; - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_ndn_posteo() { - test_parse_ndn( - "alice@posteo.org", - "hanerthaertidiuea@gmx.de", - "04422840-f884-3e37-5778-8192fe22d8e1@posteo.de", - include_bytes!("../test-data/message/posteo_ndn.eml"), - Some("Undelivered Mail Returned to Sender – This is the mail system at host mout01.posteo.de.\n\nI\'m sorry to have to inform you that your message could not\nbe delivered to one or more recipients. It\'s attached below.\n\nFor further assistance, please send mail to postmaster.\n\nIf you do so, please include this problem report. You can\ndelete your own text from the attached returned message.\n\n The mail system\n\n: host mx01.emig.gmx.net[212.227.17.5] said: 550\n Requested action not taken: mailbox unavailable (in reply to RCPT TO\n command)"), - ) - .await; - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_ndn_testrun_2() { - test_parse_ndn( - "alice@example.org", - "bob@example.org", - "Mr.5xqflwt0YFv.IXDFfHauvWx@testrun.org", - include_bytes!("../test-data/message/testrun_ndn_2.eml"), - Some("Undelivered Mail Returned to Sender – This is the mail system at host hq5.merlinux.eu.\n\nI'm sorry to have to inform you that your message could not\nbe delivered to one or more recipients. It's attached below.\n\nFor further assistance, please send mail to postmaster.\n\nIf you do so, please include this problem report. You can\ndelete your own text from the attached returned message.\n\n The mail system\n\n: Host or domain name not found. Name service error for\n name=echedelyr.tk type=AAAA: Host not found"), - ) - .await; - } - - /// Tests that text part is not squashed into OpenPGP attachment. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_ndn_with_attachment() { - test_parse_ndn( - "alice@example.org", - "bob@example.net", - "Mr.I6Da6dXcTel.TroC5J3uSDH@example.org", - include_bytes!("../test-data/message/ndn_with_attachment.eml"), - Some("Undelivered Mail Returned to Sender – This is the mail system at host relay01.example.org.\n\nI'm sorry to have to inform you that your message could not\nbe delivered to one or more recipients. It's attached below.\n\nFor further assistance, please send mail to postmaster.\n\nIf you do so, please include this problem report. You can\ndelete your own text from the attached returned message.\n\n The mail system\n\n: host mx2.example.net[80.241.60.215] said: 552 5.2.2\n : Recipient address rejected: Mailbox quota exceeded (in\n reply to RCPT TO command)\n\n: host mx1.example.net[80.241.60.212] said: 552 5.2.2\n : Recipient address rejected: Mailbox quota\n exceeded (in reply to RCPT TO command)") - ) - .await; - } - - /// Test that DSN is not treated as NDN if Action: is not "failed" - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_dsn_relayed() { - test_parse_ndn( - "anon_1@posteo.de", - "anon_2@gmx.at", - "8b7b1a9d0c8cc588c7bcac47f5687634@posteo.de", - include_bytes!("../test-data/message/dsn_relayed.eml"), - None, - ) - .await; - } - - // ndn = Non Delivery Notification - async fn test_parse_ndn( - self_addr: &str, - foreign_addr: &str, - rfc724_mid_outgoing: &str, - raw_ndn: &[u8], - error_msg: Option<&str>, - ) { - let t = TestContext::new().await; - t.configure_addr(self_addr).await; - - receive_imf( - &t, - format!( - "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: {}\n\ - To: {}\n\ - Subject: foo\n\ - Message-ID: <{}>\n\ - Chat-Version: 1.0\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", - self_addr, foreign_addr, rfc724_mid_outgoing - ) - .as_bytes(), - false, - ) - .await - .unwrap(); - - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - let msg_id = chats.get_msg_id(0).unwrap().unwrap(); - - // Check that the ndn would be downloaded: - let headers = mailparse::parse_mail(raw_ndn).unwrap().headers; - assert!(prefetch_should_download( - &t, - &headers, - "some-other-message-id", - std::iter::empty(), - ShowEmails::Off, - ) - .await - .unwrap()); - - receive_imf(&t, raw_ndn, false).await.unwrap(); - let msg = Message::load_from_db(&t, msg_id).await.unwrap(); - - assert_eq!( - msg.state, - if error_msg.is_some() { - MessageState::OutFailed - } else { - MessageState::OutDelivered - } - ); - - assert_eq!(msg.error(), error_msg.map(|error| error.to_string())); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_ndn_group_msg() -> Result<()> { - let t = TestContext::new().await; - t.configure_addr("alice@gmail.com").await; - - receive_imf( - &t, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: alice@gmail.com\n\ - To: bob@example.com, assidhfaaspocwaeofi@gmail.com\n\ - Subject: foo\n\ - Message-ID: \n\ - Chat-Version: 1.0\n\ - Chat-Group-ID: abcde\n\ - Chat-Group-Name: foo\n\ - Chat-Disposition-Notification-To: alice@example.org\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", - false, - ) - .await?; - - let chats = Chatlist::try_load(&t, 0, None, None).await?; - let msg_id = chats.get_msg_id(0)?.unwrap(); - - let raw = include_bytes!("../test-data/message/gmail_ndn_group.eml"); - receive_imf(&t, raw, false).await?; - - let msg = Message::load_from_db(&t, msg_id).await?; - - assert_eq!(msg.state, MessageState::OutFailed); - - let msgs = chat::get_chat_msgs(&t, msg.chat_id, 0).await?; - let msg_id = if let ChatItem::Message { msg_id } = msgs.last().unwrap() { - msg_id - } else { - panic!("Wrong item type"); - }; - let last_msg = Message::load_from_db(&t, *msg_id).await?; - - assert_eq!( - last_msg.text, - Some(stock_str::failed_sending_to(&t, "assidhfaaspocwaeofi@gmail.com").await,) - ); - assert_eq!(last_msg.from_id, ContactId::INFO); - Ok(()) - } - - async fn load_imf_email(context: &Context, imf_raw: &[u8]) -> Message { - context - .set_config(Config::ShowEmails, Some("2")) - .await - .unwrap(); - receive_imf(context, imf_raw, false).await.unwrap(); - let chats = Chatlist::try_load(context, 0, None, None).await.unwrap(); - let msg_id = chats.get_msg_id(0).unwrap().unwrap(); - Message::load_from_db(context, msg_id).await.unwrap() - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_html_only_mail() { - let t = TestContext::new_alice().await; - let msg = load_imf_email(&t, include_bytes!("../test-data/message/wrong-html.eml")).await; - assert_eq!(msg.text.unwrap(), " Guten Abend, \n\n Lots of text \n\n text with Umlaut ä... \n\n MfG [...]"); - } - - static GH_MAILINGLIST: &[u8] = - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: Max Mustermann \n\ - To: deltachat/deltachat-core-rust \n\ - Subject: Let's put some [brackets here that] have nothing to do with the topic\n\ - Message-ID: <3333@example.org>\n\ - List-ID: deltachat/deltachat-core-rust \n\ - List-Post: \n\ - Precedence: list\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n"; - - static GH_MAILINGLIST2: &str = - "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: Github \n\ - To: deltachat/deltachat-core-rust \n\ - Subject: [deltachat/deltachat-core-rust] PR run failed\n\ - Message-ID: <3334@example.org>\n\ - List-ID: deltachat/deltachat-core-rust \n\ - List-Post: \n\ - Precedence: list\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello back\n"; - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_github_mailing_list() -> Result<()> { - let t = TestContext::new_alice().await; - t.ctx.set_config(Config::ShowEmails, Some("2")).await?; - - receive_imf(&t.ctx, GH_MAILINGLIST, false).await?; - - let chats = Chatlist::try_load(&t.ctx, 0, None, None).await?; - assert_eq!(chats.len(), 1); - - let chat_id = chats.get_chat_id(0).unwrap(); - chat_id.accept(&t).await.unwrap(); - let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await?; - - assert!(chat.is_mailing_list()); - assert!(chat.can_send(&t.ctx).await?); - assert_eq!( - chat.get_mailinglist_addr(), - Some("reply+elernshsetushoyseshetihseusaferuhsedtisneu@reply.github.com") - ); - assert_eq!(chat.name, "deltachat/deltachat-core-rust"); - assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).await?.len(), 1); - - receive_imf(&t.ctx, GH_MAILINGLIST2.as_bytes(), false).await?; - - let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await?; - assert!(!chat.can_send(&t.ctx).await?); - assert_eq!(chat.get_mailinglist_addr(), None); - - let chats = Chatlist::try_load(&t.ctx, 0, None, None).await?; - assert_eq!(chats.len(), 1); - let contacts = Contact::get_all(&t.ctx, 0, None).await?; - assert_eq!(contacts.len(), 0); // mailing list recipients and senders do not count as "known contacts" - - let msg1 = get_chat_msg(&t, chat_id, 0, 2).await; - let contact1 = Contact::load_from_db(&t.ctx, msg1.from_id).await?; - assert_eq!(contact1.get_addr(), "notifications@github.com"); - assert_eq!(contact1.get_display_name(), "notifications@github.com"); // Make sure this is not "Max Mustermann" or somethinng - - let msg2 = get_chat_msg(&t, chat_id, 1, 2).await; - let contact2 = Contact::load_from_db(&t.ctx, msg2.from_id).await?; - assert_eq!(contact2.get_addr(), "notifications@github.com"); - - assert_eq!(msg1.get_override_sender_name().unwrap(), "Max Mustermann"); - assert_eq!(msg2.get_override_sender_name().unwrap(), "Github"); - Ok(()) - } - - static DC_MAILINGLIST: &[u8] = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: Bob \n\ - To: delta@codespeak.net\n\ - Subject: Re: [delta-dev] What's up?\n\ - Message-ID: <38942@posteo.org>\n\ - List-ID: \"discussions about and around https://delta.chat developments\" \n\ - List-Post: \n\ - Precedence: list\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - body\n"; - - static DC_MAILINGLIST2: &[u8] = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: Charlie \n\ - To: delta@codespeak.net\n\ - Subject: Re: [delta-dev] DC is nice!\n\ - Message-ID: <38943@posteo.org>\n\ - List-ID: \"discussions about and around https://delta.chat developments\" \n\ - List-Post: \n\ - Precedence: list\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - body 4\n"; - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_classic_mailing_list() -> Result<()> { - let t = TestContext::new_alice().await; - t.ctx - .set_config(Config::ShowEmails, Some("2")) - .await - .unwrap(); - receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap(); - let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); - let chat_id = chats.get_chat_id(0).unwrap(); - chat_id.accept(&t).await.unwrap(); - let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap(); - assert_eq!(chat.name, "delta-dev"); - assert!(chat.can_send(&t).await?); - assert_eq!(chat.get_mailinglist_addr(), Some("delta@codespeak.net")); - - let msg = get_chat_msg(&t, chat_id, 0, 1).await; - let contact1 = Contact::load_from_db(&t.ctx, msg.from_id).await.unwrap(); - assert_eq!(contact1.get_addr(), "bob@posteo.org"); - - let sent = t.send_text(chat.id, "Hello mailinglist!").await; - let mime = sent.payload(); - - println!("Sent mime message is:\n\n{}\n\n", mime); - assert!( - mime.contains("Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no\r\n") - ); - assert!(mime.contains("Subject: =?utf-8?q?Re=3A_=5Bdelta-dev=5D_What=27s_up=3F?=\r\n")); - assert!(mime.contains("MIME-Version: 1.0\r\n")); - assert!(mime.contains("In-Reply-To: <38942@posteo.org>\r\n")); - assert!(mime.contains("Chat-Version: 1.0\r\n")); - assert!(mime.contains("To: \r\n")); - assert!(mime.contains("From: \r\n")); - assert!(mime.contains( - "\r\n\ -\r\n\ -Hello mailinglist!\r\n" - )); - - receive_imf(&t.ctx, DC_MAILINGLIST2, false).await?; - - let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await?; - assert!(chat.can_send(&t.ctx).await?); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_other_device_writes_to_mailinglist() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await?; - receive_imf(&t, DC_MAILINGLIST, false).await.unwrap(); - let first_msg = t.get_last_msg().await; - let first_chat = Chat::load_from_db(&t, first_msg.chat_id).await?; - assert_eq!( - first_chat.param.get(Param::ListPost).unwrap(), - "delta@codespeak.net" - ); - - let list_post_contact_id = - Contact::lookup_id_by_addr(&t, "delta@codespeak.net", Origin::Unknown) - .await? - .unwrap(); - let list_post_contact = Contact::load_from_db(&t, list_post_contact_id).await?; - assert_eq!( - list_post_contact.param.get(Param::ListId).unwrap(), - "delta.codespeak.net" - ); - assert_eq!( - chat::get_chat_id_by_grpid(&t, "delta.codespeak.net") - .await? - .unwrap(), - (first_chat.id, false, Blocked::Request) - ); - - receive_imf( - &t, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: Alice \n\ - To: delta@codespeak.net\n\ - Subject: [delta-dev] Subject\n\ - Message-ID: <0476@example.org>\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - body 4\n", - false, - ) - .await - .unwrap(); - - let second_msg = t.get_last_msg().await; - - assert_eq!(first_msg.chat_id, second_msg.chat_id); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_block_mailing_list() { - let t = TestContext::new_alice().await; - t.ctx - .set_config(Config::ShowEmails, Some("2")) - .await - .unwrap(); - - receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap(); - t.evtracker.wait_next_incoming_message().await; - let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); - let chat_id = chats.get_chat_id(0).unwrap(); - let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap(); - assert!(chat.is_contact_request()); - - // Block the contact request. - chat_id.block(&t).await.unwrap(); - - let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 0); // Test that the message disappeared - - receive_imf(&t.ctx, DC_MAILINGLIST2, false).await.unwrap(); - - // Check that no notification is displayed for blocked mailing list message. - while let Ok(event) = t.evtracker.try_recv() { - assert!(!matches!(event.typ, EventType::IncomingMsg { .. })); - } - - // Test that the mailing list stays disappeared - let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 0); // Test that the message is not shown - - // Both messages are in the same blocked chat. - let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0).await.unwrap(); - assert_eq!(msgs.len(), 2); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mailing_list_decide_block_then_unblock() { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); - - receive_imf(&t, DC_MAILINGLIST, false).await.unwrap(); - let blocked = Contact::get_all_blocked(&t).await.unwrap(); - assert_eq!(blocked.len(), 0); - - // Block the contact request, this should add one blocked contact. - let msg = t.get_last_msg().await; - msg.chat_id.block(&t).await.unwrap(); - - let blocked = Contact::get_all_blocked(&t).await.unwrap(); - assert_eq!(blocked.len(), 1); - let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 0); // Test that the message is not shown - - // Unblock contact and check if the next message arrives in a chat - Contact::unblock(&t, *blocked.first().unwrap()) - .await - .unwrap(); - let blocked = Contact::get_all_blocked(&t).await.unwrap(); - assert_eq!(blocked.len(), 0); - - receive_imf(&t.ctx, DC_MAILINGLIST2, false).await.unwrap(); - let msg = t.get_last_msg().await; - let msgs = chat::get_chat_msgs(&t, msg.chat_id, 0).await.unwrap(); - assert_eq!(msgs.len(), 2); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mailing_list_decide_not_now() { - let t = TestContext::new_alice().await; - t.ctx - .set_config(Config::ShowEmails, Some("2")) - .await - .unwrap(); - - receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap(); - - let msg = t.get_last_msg().await; - let chat_id = msg.get_chat_id(); - - // Open the chat and go back - chat::marknoticed_chat(&t.ctx, chat_id).await.unwrap(); - - let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); // Test that chat is still in the chatlist - let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0).await.unwrap(); - assert_eq!(msgs.len(), 1); // ...and contains 1 message - - receive_imf(&t.ctx, DC_MAILINGLIST2, false).await.unwrap(); - - let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); // Test that the new mailing list message got into the same chat - let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0).await.unwrap(); - assert_eq!(msgs.len(), 2); - let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap(); - assert!(chat.is_contact_request()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mailing_list_decide_accept() { - let t = TestContext::new_alice().await; - t.ctx - .set_config(Config::ShowEmails, Some("2")) - .await - .unwrap(); - - receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap(); - - let msg = t.get_last_msg().await; - let chat_id = msg.get_chat_id(); - chat_id.accept(&t).await.unwrap(); - - let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); // Test that the message is shown - assert!(!chat_id.is_special()); - - receive_imf(&t.ctx, DC_MAILINGLIST2, false).await.unwrap(); - - let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0).await.unwrap(); - assert_eq!(msgs.len(), 2); - let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await.unwrap(); - assert!(chat.can_send(&t.ctx).await.unwrap()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mailing_list_multiple_names_in_subject() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await?; - receive_imf( - &t, - b"From: Foo Bar \n\ - To: deltachat/deltachat-core-rust \n\ - Subject: [ola list] [foo][bar] just a subject\n\ - Message-ID: <3333@example.org>\n\ - List-ID: \"looong description of 'ola list', with foo, bar\" \n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", - false, - ) - .await - .unwrap(); - let msg = t.get_last_msg().await; - let chat_id = msg.get_chat_id(); - let chat = Chat::load_from_db(&t, chat_id).await?; - assert_eq!(chat.name, "ola list [foo][bar]"); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_majordomo_mailing_list() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); - - // test mailing lists not having a `ListId:`-header - receive_imf( - &t, - b"From: Foo Bar \n\ - To: deltachat/deltachat-core-rust \n\ - Subject: [ola] just a subject\n\ - Message-ID: <3333@example.org>\n\ - Sender: My list \n\ - Precedence: list\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", - false, - ) - .await - .unwrap(); - let msg = t.get_last_msg().await; - let chat_id = msg.get_chat_id(); - let chat = Chat::load_from_db(&t, chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Mailinglist); - assert_eq!(chat.grpid, "mylist@bar.org"); - assert_eq!(chat.name, "ola"); - assert_eq!(chat::get_chat_msgs(&t, chat.id, 0).await.unwrap().len(), 1); - assert!(!chat.can_send(&t).await?); - assert_eq!(chat.get_mailinglist_addr(), None); - - // receive another message with no sender name but the same address, - // make sure this lands in the same chat - receive_imf( - &t, - b"From: Nu Bar \n\ - To: deltachat/deltachat-core-rust \n\ - Subject: [ola] Re: just a subject\n\ - Message-ID: <4444@example.org>\n\ - Sender: mylist@bar.org\n\ - Precedence: list\n\ - Date: Sun, 22 Mar 2020 23:37:57 +0000\n\ - \n\ - hello\n", - false, - ) - .await - .unwrap(); - assert_eq!(chat::get_chat_msgs(&t, chat.id, 0).await.unwrap().len(), 2); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mailchimp_mailing_list() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); - - receive_imf( - &t, - b"To: alice \n\ - Subject: =?utf-8?Q?How=20early=20megacities=20emerged=20from=20Cambodia=E2=80=99s=20jungles?=\n\ - From: =?utf-8?Q?Atlas=20Obscura?= \n\ - List-ID: 399fc0402f1b154b67965632emc list <399fc0402f1b154b67965632e.100761.list-id.mcsv.net>\n\ - Message-ID: <555@example.org>\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", - false, - ) - .await - .unwrap(); - let msg = t.get_last_msg().await; - let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Mailinglist); - assert_eq!(chat.blocked, Blocked::Request); - assert_eq!( - chat.grpid, - "399fc0402f1b154b67965632e.100761.list-id.mcsv.net" - ); - assert_eq!(chat.name, "Atlas Obscura"); - assert!(!chat.can_send(&t).await?); - assert_eq!(chat.get_mailinglist_addr(), None); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_dhl_mailing_list() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); - - receive_imf( - &t, - include_bytes!("../test-data/message/mailinglist_dhl.eml"), - false, - ) - .await - .unwrap(); - let msg = t.get_last_msg().await; - assert_eq!( - msg.text, - Some("Ihr Paket ist in der Packstation 123 – bla bla".to_string()) - ); - assert!(msg.has_html()); - let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Mailinglist); - assert_eq!(chat.blocked, Blocked::Request); - assert_eq!(chat.grpid, "1234ABCD-123LMNO.mailing.dhl.de"); - assert_eq!(chat.name, "DHL Paket"); - assert!(!chat.can_send(&t).await?); - assert_eq!(chat.get_mailinglist_addr(), None); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_dpd_mailing_list() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); - - receive_imf( - &t, - include_bytes!("../test-data/message/mailinglist_dpd.eml"), - false, - ) - .await - .unwrap(); - let msg = t.get_last_msg().await; - assert_eq!( - msg.text, - Some("Bald ist Ihr DPD Paket da – bla bla".to_string()) - ); - assert!(msg.has_html()); - let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Mailinglist); - assert_eq!(chat.blocked, Blocked::Request); - assert_eq!(chat.grpid, "dpdde.mxmail.service.dpd.de"); - assert_eq!(chat.name, "DPD"); - assert!(!chat.can_send(&t).await?); - assert_eq!(chat.get_mailinglist_addr(), None); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_xt_local_mailing_list() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await?; - - receive_imf( - &t, - include_bytes!("../test-data/message/mailinglist_xt_local_microsoft.eml"), - false, - ) - .await?; - let chat = Chat::load_from_db(&t, t.get_last_msg().await.chat_id).await?; - assert_eq!(chat.typ, Chattype::Mailinglist); - assert_eq!(chat.grpid, "96540.xt.local"); - assert_eq!(chat.name, "Microsoft Store"); - assert!(!chat.can_send(&t).await?); - assert_eq!(chat.get_mailinglist_addr(), None); - - receive_imf( - &t, - include_bytes!("../test-data/message/mailinglist_xt_local_spiegel.eml"), - false, - ) - .await?; - let chat = Chat::load_from_db(&t, t.get_last_msg().await.chat_id).await?; - assert_eq!(chat.typ, Chattype::Mailinglist); - assert_eq!(chat.grpid, "121231234.xt.local"); - assert_eq!(chat.name, "DER SPIEGEL Kundenservice"); - assert!(!chat.can_send(&t).await?); - assert_eq!(chat.get_mailinglist_addr(), None); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_xing_mailing_list() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await?; - - receive_imf( - &t, - include_bytes!("../test-data/message/mailinglist_xing.eml"), - false, - ) - .await?; - let msg = t.get_last_msg().await; - assert_eq!(msg.subject, "Kennst Du Dr. Mabuse?"); - let chat = Chat::load_from_db(&t, msg.chat_id).await?; - assert_eq!(chat.typ, Chattype::Mailinglist); - assert_eq!(chat.grpid, "51231231231231231231231232869f58.xing.com"); - assert_eq!(chat.name, "xing.com"); - assert!(!chat.can_send(&t).await?); - assert_eq!(chat.get_mailinglist_addr(), None); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_ttline_mailing_list() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await?; - - receive_imf( - &t, - include_bytes!("../test-data/message/mailinglist_ttline.eml"), - false, - ) - .await?; - let msg = t.get_last_msg().await; - assert_eq!(msg.subject, "Unsere Sommerangebote an Bord ⚓"); - let chat = Chat::load_from_db(&t, msg.chat_id).await?; - assert_eq!(chat.typ, Chattype::Mailinglist); - assert_eq!(chat.grpid, "39123123-1BBQXPY.t.ttline.com"); - assert_eq!(chat.name, "TT-Line - Die Schwedenfähren"); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mailing_list_with_mimepart_footer() { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); - - // the mailing list message contains two top-level texts. - // the second text is a footer that is added by some mailing list software - // if the user-edited text contains html. - // this footer should not become a text-message in delta chat - // (otherwise every second mail might be the same footer) - receive_imf( - &t, - include_bytes!("../test-data/message/mailinglist_with_mimepart_footer.eml"), - false, - ) - .await - .unwrap(); - let msg = t.get_last_msg().await; - assert_eq!( - msg.text, - Some("[Intern] important stuff – Hi mr ... [text part]".to_string()) - ); - assert!(msg.has_html()); - let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); - assert_eq!(get_chat_msgs(&t, msg.chat_id, 0).await.unwrap().len(), 1); - assert_eq!(chat.typ, Chattype::Mailinglist); - assert_eq!(chat.blocked, Blocked::Request); - assert_eq!(chat.grpid, "intern.lists.abc.de"); - assert_eq!(chat.name, "Intern"); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mailing_list_with_mimepart_footer_signed() { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); - - receive_imf( - &t, - include_bytes!("../test-data/message/mailinglist_with_mimepart_footer_signed.eml"), - false, - ) - .await - .unwrap(); - let msg = t.get_last_msg().await; - assert_eq!(get_chat_msgs(&t, msg.chat_id, 0).await.unwrap().len(), 1); - let text = msg.text.clone().unwrap(); - assert!(text.contains("content text")); - assert!(!text.contains("footer text")); - assert!(msg.has_html()); - let html = msg.get_id().get_html(&t).await.unwrap().unwrap(); - assert!(html.contains("content text")); - assert!(!html.contains("footer text")); - } - - /// Test that the changes from apply_mailinglist_changes() are also applied - /// if the message is assigned to the chat by In-Reply-To - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_apply_mailinglist_changes_assigned_by_reply() { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); - - receive_imf(&t, GH_MAILINGLIST, false).await.unwrap(); - - let chat_id = t.get_last_msg().await.chat_id; - chat_id.accept(&t).await.unwrap(); - let chat = Chat::load_from_db(&t, chat_id).await.unwrap(); - assert!(chat.can_send(&t).await.unwrap()); - - let imf_raw = format!("In-Reply-To: 3333@example.org\n{}", GH_MAILINGLIST2); - receive_imf(&t, imf_raw.as_bytes(), false).await.unwrap(); - - assert_eq!( - t.get_last_msg().await.in_reply_to.unwrap(), - "3333@example.org" - ); - // `Assigning message to Chat#... as it's a reply to 3333@example.org` - t.evtracker - .get_info_contains("as it's a reply to 3333@example.org") - .await; - - let chat = Chat::load_from_db(&t, chat_id).await.unwrap(); - assert!(!chat.can_send(&t).await.unwrap()); - - let contact_id = Contact::lookup_id_by_addr( - &t, - "reply+EGELITBABIHXSITUZIEPAKYONASITEPUANERGRUSHE@reply.github.com", - Origin::Hidden, - ) - .await - .unwrap() - .unwrap(); - let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); - assert_eq!( - contact.param.get(Param::ListId).unwrap(), - "deltachat-core-rust.deltachat.github.com" - ) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mailing_list_chat_message() { - let t = TestContext::new_alice().await; - - receive_imf( - &t, - include_bytes!("../test-data/message/mailinglist_chat_message.eml"), - false, - ) - .await - .unwrap(); - let msg = t.get_last_msg().await; - assert_eq!(msg.text, Some("hello, this is a test 👋\n\n_______________________________________________\nTest1 mailing list -- test1@example.net\nTo unsubscribe send an email to test1-leave@example.net".to_string())); - assert!(!msg.has_html()); - let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Mailinglist); - assert_eq!(chat.blocked, Blocked::Request); - assert_eq!(chat.grpid, "test1.example.net"); - assert_eq!(chat.name, "Test1"); - } - - /// Tests that bots automatically accept mailing lists. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mailing_list_bot() { - let t = TestContext::new_alice().await; - t.set_config(Config::Bot, Some("1")).await.unwrap(); - - receive_imf( - &t, - include_bytes!("../test-data/message/mailinglist_chat_message.eml"), - false, - ) - .await - .unwrap(); - let msg = t.get_last_msg().await; - let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); - assert_eq!(chat.blocked, Blocked::Not); - - // Bot should see the message as fresh and process it. - assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_dont_show_tokens_in_contacts_list() { - check_dont_show_in_contacts_list( - "reply+OGHVYCLVBEGATYBICAXBIRQATABUOTUCERABERAHNO@reply.github.com", - ) - .await; - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_dont_show_noreply_in_contacts_list() { - check_dont_show_in_contacts_list("noreply@github.com").await; - } - - async fn check_dont_show_in_contacts_list(addr: &str) { - let t = TestContext::new_alice().await; - t.ctx - .set_config(Config::ShowEmails, Some("2")) - .await - .unwrap(); - receive_imf( - &t, - format!( - "Subject: Re: [deltachat/deltachat-core-rust] DC is the best repo on GitHub! -To: {} -References: - -From: alice@example.org -Message-ID: -Date: Tue, 16 Jun 2020 12:04:20 +0200 -MIME-Version: 1.0 -Content-Type: text/plain; charset=utf-8 -Content-Transfer-Encoding: 7bit - -YEAAAAAA!. -", - addr - ) - .as_bytes(), - false, - ) - .await - .unwrap(); - let contacts = Contact::get_all(&t, 0, None as Option<&str>).await.unwrap(); - assert!(contacts.is_empty()); // The contact should not have been added to the db - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_pdf_filename_simple() { - let t = TestContext::new_alice().await; - let msg = load_imf_email( - &t, - include_bytes!("../test-data/message/pdf_filename_simple.eml"), - ) - .await; - assert_eq!(msg.viewtype, Viewtype::File); - assert_eq!(msg.text.unwrap(), "mail body"); - assert_eq!(msg.param.get(Param::File).unwrap(), "$BLOBDIR/simple.pdf"); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_pdf_filename_continuation() { - // test filenames split across multiple header lines, see rfc 2231 - let t = TestContext::new_alice().await; - let msg = load_imf_email( - &t, - include_bytes!("../test-data/message/pdf_filename_continuation.eml"), - ) - .await; - assert_eq!(msg.viewtype, Viewtype::File); - assert_eq!(msg.text.unwrap(), "mail body"); - assert_eq!( - msg.param.get(Param::File).unwrap(), - "$BLOBDIR/test pdf äöüß.pdf" - ); - } - - /// HTML-images may come with many embedded images, eg. tiny icons, corners for formatting, - /// twitter/facebook/whatever logos and so on. - /// that may easily be 50 and more images, one would not have these images in a chat. - /// - /// fortunately, if we remove them, they are accessible by get_msg_html() now. - /// - /// unfortunately, these images are not that easy to detect as they may also be on purpose, - /// or mua may use multipart/related not correctly - - /// so this test is in competition with parse_thunderbird_html_embedded_image() - /// that wants the image to be kept in the chat. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_many_images() { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); - - receive_imf( - &t, - include_bytes!("../test-data/message/many_images_amazon_via_apple_mail.eml"), - false, - ) - .await - .unwrap(); - let msg = t.get_last_msg().await; - assert_eq!(msg.viewtype, Viewtype::Image); - assert!(msg.has_html()); - let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); - assert_eq!(get_chat_msgs(&t, chat.id, 0).await.unwrap().len(), 1); - } - - /// Test that classical MUA messages are assigned to group chats based on the `In-Reply-To` - /// header. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_in_reply_to() { - let t = TestContext::new().await; - t.configure_addr("bob@example.com").await; - - // Receive message from Alice about group "foo". - receive_imf( - &t, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: alice@example.org\n\ - To: bob@example.com, charlie@example.net\n\ - Subject: foo\n\ - Message-ID: \n\ - Chat-Version: 1.0\n\ - Chat-Group-ID: foo\n\ - Chat-Group-Name: foo\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello foo\n", - false, - ) - .await - .unwrap(); - - // Receive reply from Charlie without group ID but with In-Reply-To header. - receive_imf( - &t, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: charlie@example.net\n\ - To: alice@example.org, bob@example.com\n\ - Subject: Re: foo\n\ - Message-ID: \n\ - In-Reply-To: \n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - reply foo\n", - false, - ) - .await - .unwrap(); - - let msg = t.get_last_msg().await; - assert_eq!(msg.get_text().unwrap(), "reply foo"); - - // Load the first message from the same chat. - let msgs = chat::get_chat_msgs(&t, msg.chat_id, 0).await.unwrap(); - let msg_id = if let ChatItem::Message { msg_id } = msgs.first().unwrap() { - msg_id - } else { - panic!("Wrong item type"); - }; - - let reply_msg = Message::load_from_db(&t, *msg_id).await.unwrap(); - assert_eq!(reply_msg.get_text().unwrap(), "hello foo"); - - // Check that reply got into the same chat as the original message. - assert_eq!(msg.chat_id, reply_msg.chat_id); - - // Make sure we looked at real chat ID and do not just - // test that both messages got into the same virtual chat. - assert!(!msg.chat_id.is_special()); - } - - /// Test that classical MUA messages are assigned to group chats - /// based on the `In-Reply-To` header for two-member groups. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_in_reply_to_two_member_group() { - let t = TestContext::new().await; - t.configure_addr("bob@example.com").await; - - // Receive message from Alice about group "foo". - receive_imf( - &t, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: alice@example.org\n\ - To: bob@example.com\n\ - Subject: foo\n\ - Message-ID: \n\ - Chat-Version: 1.0\n\ - Chat-Group-ID: foo\n\ - Chat-Group-Name: foo\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello foo\n", - false, - ) - .await - .unwrap(); - - // Receive a classic MUA reply from Alice. - // It is assigned to the group chat. - receive_imf( - &t, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: alice@example.org\n\ - To: bob@example.com\n\ - Subject: Re: foo\n\ - Message-ID: \n\ - In-Reply-To: \n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - classic reply\n", - false, - ) - .await - .unwrap(); - - // Ensure message is assigned to group chat. - let msg = t.get_last_msg().await; - let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Group); - assert_eq!(msg.get_text().unwrap(), "classic reply"); - - // Receive a Delta Chat reply from Alice. - // It is assigned to group chat, because it has a group ID. - receive_imf( - &t, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: alice@example.org\n\ - To: bob@example.com\n\ - Subject: Re: foo\n\ - Message-ID: \n\ - In-Reply-To: \n\ - Chat-Version: 1.0\n\ - Chat-Group-ID: foo\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - chat reply\n", - false, - ) - .await - .unwrap(); - - // Ensure message is assigned to group chat. - let msg = t.get_last_msg().await; - let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Group); - assert_eq!(msg.get_text().unwrap(), "chat reply"); - - // Receive a private Delta Chat reply from Alice. - // It is assigned to 1:1 chat, because it has no group ID, - // which means it was created using "reply privately" feature. - // Normally it contains a quote, but it should not matter. - receive_imf( - &t, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: alice@example.org\n\ - To: bob@example.com\n\ - Subject: Re: foo\n\ - Message-ID: \n\ - In-Reply-To: \n\ - Chat-Version: 1.0\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - private reply\n", - false, - ) - .await - .unwrap(); - - // Ensure message is assigned to a 1:1 chat. - let msg = t.get_last_msg().await; - let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Single); - assert_eq!(msg.get_text().unwrap(), "private reply"); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_save_mime_headers_off() -> anyhow::Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let chat_alice = alice.create_chat(&bob).await; - chat::send_text_msg(&alice, chat_alice.id, "hi!".to_string()).await?; - - let msg = bob.recv_msg(&alice.pop_sent_msg().await).await; - assert_eq!(msg.get_text(), Some("hi!".to_string())); - assert!(!msg.get_showpadlock()); - let mime = message::get_mime_headers(&bob, msg.id).await?; - assert!(mime.is_empty()); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_save_mime_headers_on() -> anyhow::Result<()> { - let alice = TestContext::new_alice().await; - alice.set_config_bool(Config::SaveMimeHeaders, true).await?; - let bob = TestContext::new_bob().await; - bob.set_config_bool(Config::SaveMimeHeaders, true).await?; - - // alice sends a message to bob, bob sees full mime - let chat_alice = alice.create_chat(&bob).await; - chat::send_text_msg(&alice, chat_alice.id, "hi!".to_string()).await?; - - let msg = bob.recv_msg(&alice.pop_sent_msg().await).await; - assert_eq!(msg.get_text(), Some("hi!".to_string())); - assert!(!msg.get_showpadlock()); - let mime = message::get_mime_headers(&bob, msg.id).await?; - let mime_str = String::from_utf8_lossy(&mime); - assert!(mime_str.contains("Message-ID:")); - assert!(mime_str.contains("From:")); - - // another one, from bob to alice, that gets encrypted - let chat_bob = bob.create_chat(&alice).await; - chat::send_text_msg(&bob, chat_bob.id, "ho!".to_string()).await?; - let msg = alice.recv_msg(&bob.pop_sent_msg().await).await; - assert_eq!(msg.get_text(), Some("ho!".to_string())); - assert!(msg.get_showpadlock()); - let mime = message::get_mime_headers(&alice, msg.id).await?; - let mime_str = String::from_utf8_lossy(&mime); - assert!(mime_str.contains("Message-ID:")); - assert!(mime_str.contains("From:")); - Ok(()) - } - - async fn create_test_alias( - chat_request: bool, - group_request: bool, - ) -> (TestContext, TestContext) { - // Claire, a customer, sends a support request - // to the alias address from a classic MUA. - // The alias expands to the supporters Alice and Bob. - // Check that Alice receives the message in a group chat. - let claire_request = if group_request { - format!( - "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - To: support@example.org, ceo@example.org\n\ - From: claire@example.org\n\ - Subject: i have a question\n\ - Message-ID: \n\ - {}\ - Date: Sun, 14 Mar 2021 17:04:36 +0100\n\ - Content-Type: text/plain\n\ - \n\ - hi support! what is the current version?", - if chat_request { - "Chat-Group-ID: 8ud29aridt29arid\n\ - Chat-Group-Name: =?utf-8?q?i_have_a_question?=\n" - } else { - "" - } - ) - } else { - format!( - "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - To: support@example.org\n\ - From: claire@example.org\n\ - Subject: i have a question\n\ - Message-ID: \n\ - {}\ - Date: Sun, 14 Mar 2021 17:04:36 +0100\n\ - Content-Type: text/plain\n\ - \n\ - hi support! what is the current version?", - if chat_request { - "Chat-Version: 1.0\n" - } else { - "" - } - ) - }; - - let alice = TestContext::new_alice().await; - alice - .set_config(Config::ShowEmails, Some("2")) - .await - .unwrap(); - receive_imf(&alice, claire_request.as_bytes(), false) - .await - .unwrap(); - - let msg = alice.get_last_msg().await; - assert_eq!(msg.get_subject(), "i have a question"); - assert!(msg.get_text().unwrap().contains("hi support!")); - let chat = Chat::load_from_db(&alice, msg.chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Group); - assert_eq!(get_chat_msgs(&alice, chat.id, 0).await.unwrap().len(), 1); - if group_request { - assert_eq!(get_chat_contacts(&alice, chat.id).await.unwrap().len(), 4); - } else { - assert_eq!(get_chat_contacts(&alice, chat.id).await.unwrap().len(), 3); - } - assert_eq!(msg.get_override_sender_name(), None); - - let claire = TestContext::new().await; - claire.configure_addr("claire@example.org").await; - claire - .set_config(Config::ShowEmails, Some("2")) - .await - .unwrap(); - receive_imf(&claire, claire_request.as_bytes(), false) - .await - .unwrap(); - - let msg_id = rfc724_mid_exists(&claire, "non-dc-1@example.org") - .await - .unwrap() - .unwrap(); - - let msg = Message::load_from_db(&claire, msg_id).await.unwrap(); - msg.chat_id.accept(&claire).await.unwrap(); - assert_eq!(msg.get_subject(), "i have a question"); - assert!(msg.get_text().unwrap().contains("hi support!")); - let chat = Chat::load_from_db(&claire, msg.chat_id).await.unwrap(); - if group_request { - assert_eq!(chat.typ, Chattype::Group); - } else { - assert_eq!(chat.typ, Chattype::Single); - } - assert_eq!(get_chat_msgs(&claire, chat.id, 0).await.unwrap().len(), 1); - assert_eq!(msg.get_override_sender_name(), None); - - (claire, alice) - } - - async fn check_alias_reply(reply: &[u8], chat_request: bool, group_request: bool) { - let (claire, alice) = create_test_alias(chat_request, group_request).await; - - // Check that Alice gets the message in the same chat. - let request = alice.get_last_msg().await; - receive_imf(&alice, reply, false).await.unwrap(); - let answer = alice.get_last_msg().await; - assert_eq!(answer.get_subject(), "Re: i have a question"); - assert!(answer.get_text().unwrap().contains("the version is 1.0")); - assert_eq!(answer.chat_id, request.chat_id); - let chat_contacts = get_chat_contacts(&alice, answer.chat_id) - .await - .unwrap() - .len(); - if group_request { - // Claire, Support, CEO and Alice (Bob is not added) - assert_eq!(chat_contacts, 4); - } else { - // Claire, Support and Alice - assert_eq!(chat_contacts, 3); - } - assert_eq!( - answer.get_override_sender_name().unwrap(), - "bob@example.net" - ); // Bob is not part of the group, so override-sender-name should be set - - // Check that Claire also gets the message in the same chat. - let request = claire.get_last_msg().await; - receive_imf(&claire, reply, false).await.unwrap(); - let answer = claire.get_last_msg().await; - assert_eq!(answer.get_subject(), "Re: i have a question"); - assert!(answer.get_text().unwrap().contains("the version is 1.0")); - assert_eq!(answer.chat_id, request.chat_id); - assert_eq!( - answer.get_override_sender_name().unwrap(), - "bob@example.net" - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_alias_support_answer_from_nondc() { - // Bob, the other supporter, answers with a classic MUA. - let bob_answer = b"To: support@example.org, claire@example.org\n\ - From: bob@example.net\n\ - Subject: =?utf-8?q?Re=3A_i_have_a_question?=\n\ - References: \n\ - In-Reply-To: \n\ - Message-ID: \n\ - Date: Sun, 14 Mar 2021 16:04:57 +0000\n\ - Content-Type: text/plain\n\ - \n\ - hi claire, the version is 1.0, cheers bob"; - - check_alias_reply(bob_answer, true, true).await; - check_alias_reply(bob_answer, false, true).await; - check_alias_reply(bob_answer, true, false).await; - check_alias_reply(bob_answer, false, false).await; - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_alias_answer_from_dc() { - // Bob, the other supporter, answers with Delta Chat. - let bob_answer = b"To: support@example.org, claire@example.org\n\ - From: bob@example.net\n\ - Subject: =?utf-8?q?Re=3A_i_have_a_question?=\n\ - References: \n\ - In-Reply-To: \n\ - Message-ID: \n\ - Date: Sun, 14 Mar 2021 16:04:57 +0000\n\ - Chat-Version: 1.0\n\ - Chat-Group-ID: af9e810c9b592927\n\ - Chat-Group-Name: =?utf-8?q?i_have_a_question?=\n\ - Chat-Disposition-Notification-To: bob@example.net\n\ - Content-Type: text/plain\n\ - \n\ - hi claire, the version is 1.0, cheers bob"; - - check_alias_reply(bob_answer, true, true).await; - check_alias_reply(bob_answer, false, true).await; - check_alias_reply(bob_answer, true, false).await; - check_alias_reply(bob_answer, false, false).await; - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_dont_assign_to_trash_by_parent() { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); - println!("\n========= Receive a message =========="); - receive_imf( - &t, - b"From: Nu Bar \n\ - To: alice@example.org, bob@example.org\n\ - Subject: Hi\n\ - Message-ID: <4444@example.org>\n\ - \n\ - hello\n", - false, - ) - .await - .unwrap(); - let chat_id = t.get_last_msg().await.chat_id; - chat_id.accept(&t).await.unwrap(); - let msg = get_chat_msg(&t, chat_id, 0, 1).await; // Make sure that the message is actually in the chat - assert!(!msg.chat_id.is_special()); - assert_eq!(msg.text.unwrap(), "Hi – hello"); - - println!("\n========= Delete the message =========="); - msg.id.trash(&t).await.unwrap(); - - let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0).await.unwrap(); - assert_eq!(msgs.len(), 0); - - println!("\n========= Receive a message that is a reply to the deleted message =========="); - receive_imf( - &t, - b"From: Nu Bar \n\ - To: alice@example.org, bob@example.org\n\ - Subject: Re: Hi\n\ - Message-ID: <5555@example.org>\n\ - In-Reply-To: <4444@example.org\n\ - \n\ - Reply\n", - false, - ) - .await - .unwrap(); - let msg = t.get_last_msg().await; - assert!(!msg.chat_id.is_special()); // Esp. check that the chat_id is not TRASH - assert_eq!(msg.text.unwrap(), "Reply"); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_dont_show_all_outgoing_msgs_in_self_chat() { - // Regression test for : - // Some servers add a `Bcc: ` header, which caused all outgoing messages to - // be shown in the self-chat. - let t = TestContext::new_alice().await; - - receive_imf( - &t, - b"Bcc: alice@example.org -Received: from [127.0.0.1] -Subject: s -Chat-Version: 1.0 -Message-ID: -To: -From: - -Message content", - false, - ) - .await - .unwrap(); - - let msg = t.get_last_msg().await; - assert_ne!(msg.chat_id, t.get_self_chat().await.id); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_outgoing_classic_mail_creates_chat() { - let alice = TestContext::new_alice().await; - - // Alice enables classic emails. - alice - .set_config(Config::ShowEmails, Some("2")) - .await - .unwrap(); - - // Alice downloads outgoing classic email. - receive_imf( - &alice, - b"Received: from [127.0.0.1] -Subject: Subj -Message-ID: -To: -From: - -Message content", - false, - ) - .await - .unwrap(); - - // Outgoing email should create a chat. - let msg = alice.get_last_msg().await; - assert_eq!(msg.get_text().unwrap(), "Subj – Message content"); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_duplicate_message() -> Result<()> { - // Test that duplicate messages are ignored based on the Message-ID - let alice = TestContext::new_alice().await; - - let bob_contact_id = Contact::add_or_lookup( - &alice, - "Bob", - "bob@example.org", - Origin::IncomingUnknownFrom, - ) - .await? - .0; - - let first_message = b"Received: from [127.0.0.1] -Subject: First message -Message-ID: -To: Alice -From: Bob1 -Chat-Version: 1.0 - -Message content - --- -First signature"; - - let second_message = b"Received: from [127.0.0.1] -Subject: Second message -Message-ID: -To: Alice -From: Bob2 -Chat-Version: 1.0 - -Message content - --- -Second signature"; - - receive_imf(&alice, first_message, false).await?; - let contact = Contact::load_from_db(&alice, bob_contact_id).await?; - assert_eq!(contact.get_status(), "First signature"); - assert_eq!(contact.get_display_name(), "Bob1"); - - receive_imf(&alice, second_message, false).await?; - let contact = Contact::load_from_db(&alice, bob_contact_id).await?; - assert_eq!(contact.get_status(), "Second signature"); - assert_eq!(contact.get_display_name(), "Bob2"); - - // Duplicate message, should be ignored - receive_imf(&alice, first_message, false).await?; - - // No change because last message is duplicate of the first. - let contact = Contact::load_from_db(&alice, bob_contact_id).await?; - assert_eq!(contact.get_status(), "Second signature"); - assert_eq!(contact.get_display_name(), "Bob2"); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_ignore_footer_status_from_mailinglist() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await?; - let bob_id = Contact::add_or_lookup(&t, "", "bob@example.net", Origin::IncomingUnknownCc) - .await? - .0; - let bob = Contact::load_from_db(&t, bob_id).await?; - assert_eq!(bob.get_status(), ""); - assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 0); - - receive_imf( - &t, - b"From: Bob -To: Alice -Message-ID: <1@example.org> -Subject: first message - -body 1 - --- -Original signature", - false, - ) - .await?; - let one2one_chat_id = t.get_last_msg().await.chat_id; - let bob = Contact::load_from_db(&t, bob_id).await?; - assert_eq!(bob.get_status(), "Original signature"); - - receive_imf( - &t, - b"From: Bob -Sender: ml@example.net -To: Alice -Message-ID: <2@example.net> -Precedence: list -Subject: second message - -body 2 - --- -The modified signature --- -Tap here to unsubscribe ...", - false, - ) - .await?; - let ml_chat_id = t.get_last_msg().await.chat_id; - let bob = Contact::load_from_db(&t, bob_id).await?; - assert_eq!(bob.get_status(), "Original signature"); - - receive_imf( - &t, - b"From: Bob -To: Alice -Message-ID: <3@example.org> -Subject: third message - -body 3 - --- -Original signature updated", - false, - ) - .await?; - let bob = Contact::load_from_db(&t, bob_id).await?; - assert_eq!(bob.get_status(), "Original signature updated"); - assert_eq!(get_chat_msgs(&t, one2one_chat_id, 0).await?.len(), 2); - assert_eq!(get_chat_msgs(&t, ml_chat_id, 0).await?.len(), 1); - assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 2); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_chat_assignment_private_classical_reply() { - for outgoing_is_classical in &[true, false] { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); - - receive_imf( - &t, - format!( - r#"Received: from mout.gmx.net (mout.gmx.net [212.227.17.22]) -Subject: =?utf-8?q?single_reply-to?= -{} -Date: Fri, 28 May 2021 10:15:05 +0000 -To: Bob , -From: Alice -Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no -Content-Transfer-Encoding: quoted-printable - -Hello, I've just created the group "single reply-to" for us."#, - if *outgoing_is_classical { - r"Message-ID: abcd@gmx.de" - } else { - r"Chat-Group-ID: eJ_llQIXf0K -Chat-Group-Name: =?utf-8?q?single_reply-to?= -References: -Chat-Version: 1.0 -Message-ID: " - } - ) - .as_bytes(), - false, - ) - .await - .unwrap(); - - let group_msg = t.get_last_msg().await; - assert_eq!( - group_msg.text.unwrap(), - if *outgoing_is_classical { - "single reply-to – Hello, I\'ve just created the group \"single reply-to\" for us." - } else { - "Hello, I've just created the group \"single reply-to\" for us." - } - ); - let group_chat = Chat::load_from_db(&t, group_msg.chat_id).await.unwrap(); - assert_eq!(group_chat.typ, Chattype::Group); - assert_eq!(group_chat.name, "single reply-to"); - - receive_imf( - &t, - format!( - r#"Subject: Re: single reply-to -To: "Alice" -References: <{0}> - <{0}> -From: Bob -Message-ID: <028674eb-77f9-4ad1-1c30-e93e18b891c8@testrun.org> -Date: Fri, 28 May 2021 12:17:03 +0200 -User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 - Thunderbird/78.10.2 -MIME-Version: 1.0 -In-Reply-To: <{0}> - -Private reply"#, - if *outgoing_is_classical { - "abcd@gmx.de" - } else { - "Gr.eJ_llQIXf0K.buxmrnMmG0Y@gmx.de" - } - ) - .as_bytes(), - false, - ) - .await - .unwrap(); - - let private_msg = t.get_last_msg().await; - assert_eq!(private_msg.text.unwrap(), "Private reply"); - let private_chat = Chat::load_from_db(&t, private_msg.chat_id).await.unwrap(); - assert_eq!(private_chat.typ, Chattype::Single); - assert_ne!(private_msg.chat_id, group_msg.chat_id); - } - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_chat_assignment_private_chat_reply() { - for (outgoing_is_classical, outgoing_has_multiple_recipients) in - &[(true, true), (false, true), (false, false)] - { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); - - receive_imf( - &t, - format!( - r#"Received: from mout.gmx.net (mout.gmx.net [212.227.17.22]) -Subject: =?utf-8?q?single_reply-to?= -{} -Date: Fri, 28 May 2021 10:15:05 +0000 -To: Bob {} -From: Alice -Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no -Content-Transfer-Encoding: quoted-printable - -Hello, I've just created the group "single reply-to" for us."#, - if *outgoing_is_classical { - r"Message-ID: abcd@gmx.de" - } else { - r"Chat-Group-ID: eJ_llQIXf0K -Chat-Group-Name: =?utf-8?q?single_reply-to?= -References: -Chat-Version: 1.0 -Message-ID: " - }, - if *outgoing_has_multiple_recipients { - ", " - } else { - "" - } - ) - .as_bytes(), - false, - ) - .await - .unwrap(); - let group_msg = t.get_last_msg().await; - assert_eq!( - group_msg.text.unwrap(), - if *outgoing_is_classical { - "single reply-to – Hello, I\'ve just created the group \"single reply-to\" for us." - } else { - "Hello, I've just created the group \"single reply-to\" for us." - } - ); - let group_chat = Chat::load_from_db(&t, group_msg.chat_id).await.unwrap(); - assert_eq!(group_chat.typ, Chattype::Group); - assert_eq!(group_chat.name, "single reply-to"); - - receive_imf( - &t, - format!( - r#"Subject: =?utf-8?q?Re=3A_single_reply-to?= -MIME-Version: 1.0 -In-Reply-To: <{0}> -Date: Sat, 03 Jul 2021 20:00:26 +0000 -Chat-Version: 1.0 -Message-ID: -To: -From: -Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no -Content-Transfer-Encoding: quoted-printable - -> Hello, I've just created the group "single reply-to" for us. - -Private reply - -=2D- -Sent with my Delta Chat Messenger: https://delta.chat - -"#, - if *outgoing_is_classical { - "abcd@gmx.de" - } else { - "Gr.iy1KCE2y65_.mH2TM52miv9@testrun.org" - } - ) - .as_bytes(), - false, - ) - .await - .unwrap(); - - let private_msg = t.get_last_msg().await; - assert_eq!(private_msg.text.unwrap(), "Private reply"); - let private_chat = Chat::load_from_db(&t, private_msg.chat_id).await.unwrap(); - assert_eq!(private_chat.typ, Chattype::Single); - assert_ne!(private_msg.chat_id, group_msg.chat_id); - } - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_chat_assignment_nonprivate_classical_reply() { - for outgoing_is_classical in &[true, false] { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); - - receive_imf( - &t, - format!( - r#"Received: from mout.gmx.net (mout.gmx.net [212.227.17.22]) -Subject: =?utf-8?q?single_reply-to?= -{} -To: Bob , -From: Alice -Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no -Content-Transfer-Encoding: quoted-printable - -Hello, I've just created the group "single reply-to" for us."#, - if *outgoing_is_classical { - r"Message-ID: abcd@gmx.de" - } else { - r"Chat-Group-ID: eJ_llQIXf0K -Chat-Group-Name: =?utf-8?q?single_reply-to?= -References: -Chat-Version: 1.0 -Message-ID: " - } - ) - .as_bytes(), - false, - ) - .await - .unwrap(); - - let group_msg = t.get_last_msg().await; - assert_eq!( - group_msg.text.unwrap(), - if *outgoing_is_classical { - "single reply-to – Hello, I\'ve just created the group \"single reply-to\" for us." - } else { - "Hello, I've just created the group \"single reply-to\" for us." - } - ); - let group_chat = Chat::load_from_db(&t, group_msg.chat_id).await.unwrap(); - assert_eq!(group_chat.typ, Chattype::Group); - assert_eq!(group_chat.name, "single reply-to"); - - // =============== Receive another outgoing message and check that it is put into the same chat =============== - receive_imf( - &t, - format!( - r#"Received: from mout.gmx.net (mout.gmx.net [212.227.17.22]) -Subject: Out subj -To: "Bob" , "Claire" -From: Alice -Message-ID: -MIME-Version: 1.0 -In-Reply-To: <{0}> - -Outgoing reply to all"#, - if *outgoing_is_classical { - "abcd@gmx.de" - } else { - "Gr.eJ_llQIXf0K.buxmrnMmG0Y@gmx.de" - } - ) - .as_bytes(), - false, - ) - .await - .unwrap(); - - let reply = t.get_last_msg().await; - assert_eq!(reply.text.unwrap(), "Out subj – Outgoing reply to all"); - let reply_chat = Chat::load_from_db(&t, reply.chat_id).await.unwrap(); - assert_eq!(reply_chat.typ, Chattype::Group); - assert_eq!(reply.chat_id, group_msg.chat_id); - - // =============== Receive an incoming message and check that it is put into the same chat =============== - receive_imf( - &t, - br#"Received: from mout.gmx.net (mout.gmx.net [212.227.17.22]) -Subject: In subj -To: "Bob" , "Claire" -From: alice -Message-ID: -MIME-Version: 1.0 -In-Reply-To: - -Reply to all"#, - false, - ) - .await - .unwrap(); - - let reply = t.get_last_msg().await; - assert_eq!(reply.text.unwrap(), "In subj – Reply to all"); - let reply_chat = Chat::load_from_db(&t, reply.chat_id).await.unwrap(); - assert_eq!(reply_chat.typ, Chattype::Group); - assert_eq!(reply.chat_id, group_msg.chat_id); - } - } - - /// Tests that replies to similar ad hoc groups are correctly assigned to chats. - /// - /// The difficutly here is that ad hoc groups don't have unique group IDs, because both - /// messages have the same recipient lists and only differ in the subject and message contents. - /// The messages can be properly assigned to chats only using the In-Reply-To or References - /// headers. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_chat_assignment_adhoc() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - alice.set_config(Config::ShowEmails, Some("2")).await?; - bob.set_config(Config::ShowEmails, Some("2")).await?; - - let first_thread_mime = br#"Subject: First thread -Message-ID: first@example.org -To: Alice , Bob -From: Claire -Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no - -First thread."#; - let second_thread_mime = br#"Subject: Second thread -Message-ID: second@example.org -To: Alice , Bob -From: Claire -Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no - -Second thread."#; - - // Alice receives two classic emails from Claire. - receive_imf(&alice, first_thread_mime, false).await?; - let alice_first_msg = alice.get_last_msg().await; - receive_imf(&alice, second_thread_mime, false).await?; - let alice_second_msg = alice.get_last_msg().await; - - // Bob receives the same two emails. - receive_imf(&bob, first_thread_mime, false).await?; - let bob_first_msg = bob.get_last_msg().await; - receive_imf(&bob, second_thread_mime, false).await?; - let bob_second_msg = bob.get_last_msg().await; - - // Messages go to separate chats both for Alice and Bob. - assert!(alice_first_msg.chat_id != alice_second_msg.chat_id); - assert!(bob_first_msg.chat_id != bob_second_msg.chat_id); - - // Alice replies to both chats. Bob receives two messages and assigns them to corresponding - // chats. - alice_first_msg.chat_id.accept(&alice).await?; - let alice_first_reply = alice - .send_text(alice_first_msg.chat_id, "First reply") - .await; - let bob_first_reply = bob.recv_msg(&alice_first_reply).await; - assert_eq!(bob_first_reply.chat_id, bob_first_msg.chat_id); - - alice_second_msg.chat_id.accept(&alice).await?; - let alice_second_reply = alice - .send_text(alice_second_msg.chat_id, "Second reply") - .await; - let bob_second_reply = bob.recv_msg(&alice_second_reply).await; - assert_eq!(bob_second_reply.chat_id, bob_second_msg.chat_id); - - // Alice adds Fiona to both ad hoc groups. - let fiona = TestContext::new_fiona().await; - let (alice_fiona_contact_id, _) = Contact::add_or_lookup( - &alice, - "Fiona", - "fiona@example.net", - Origin::IncomingUnknownTo, - ) - .await?; - - chat::add_contact_to_chat(&alice, alice_first_msg.chat_id, alice_fiona_contact_id).await?; - let alice_first_invite = alice.pop_sent_msg().await; - let fiona_first_invite = fiona.recv_msg(&alice_first_invite).await; - - chat::add_contact_to_chat(&alice, alice_second_msg.chat_id, alice_fiona_contact_id).await?; - let alice_second_invite = alice.pop_sent_msg().await; - let fiona_second_invite = fiona.recv_msg(&alice_second_invite).await; - - // Fiona was added to two separate chats and should see two separate chats, even though they - // don't have different group IDs to distinguish them. - assert!(fiona_first_invite.chat_id != fiona_second_invite.chat_id); - - Ok(()) - } - - /// Test that read receipts don't create chats. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_read_receipts_dont_create_chats() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let alice_chat = alice.create_chat(&bob).await; - - // Alice sends a message to Bob. - assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0); - bob.recv_msg(&alice.send_text(alice_chat.id, "Message").await) - .await; - let received_msg = bob.get_last_msg().await; - - // Alice deletes the chat. - alice_chat.id.delete(&alice).await?; - let chats = Chatlist::try_load(&alice, 0, None, None).await?; - assert_eq!(chats.len(), 0); - - // Bob sends a read receipt. - let mdn_mimefactory = - crate::mimefactory::MimeFactory::from_mdn(&bob, &received_msg, vec![]).await?; - let rendered_mdn = mdn_mimefactory.render(&bob).await?; - let mdn_body = rendered_mdn.message; - - // Alice receives the read receipt. - receive_imf(&alice, mdn_body.as_bytes(), false).await?; - - // Chat should not pop up in the chatlist. - let chats = Chatlist::try_load(&alice, 0, None, None).await?; - assert_eq!(chats.len(), 0); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_gmx_forwarded_msg() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await?; - - receive_imf( - &t, - include_bytes!("../test-data/message/gmx-forward.eml"), - false, - ) - .await?; - - let msg = t.get_last_msg().await; - assert!(msg.has_html()); - assert_eq!(msg.id.get_html(&t).await?.unwrap().replace("\r\n", "\n"), "
 
\n\n
 \n
 \n
\n
Gesendet: Donnerstag, 12. August 2021 um 15:52 Uhr
\nVon: "Claire" <claire@example.org>
\nAn: alice@example.org
\nBetreff: subject
\n\n
bodytext
\n
\n
\n
\n\n"); - - Ok(()) - } - - /// Tests that user is notified about new incoming contact requests. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_incoming_contact_request() -> Result<()> { - let t = TestContext::new_alice().await; - - receive_imf(&t, MSGRMSG, false).await?; - let msg = t.get_last_msg().await; - let chat = chat::Chat::load_from_db(&t, msg.chat_id).await?; - assert!(chat.is_contact_request()); - - loop { - let event = t - .evtracker - .get_matching(|evt| matches!(evt, EventType::IncomingMsg { .. })) - .await; - match event { - EventType::IncomingMsg { chat_id, msg_id } => { - assert_eq!(msg.chat_id, chat_id); - assert_eq!(msg.id, msg_id); - return Ok(()); - } - _ => unreachable!(), - } - } - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_parent_message() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await?; - - let mime = br#"Subject: First -Message-ID: first@example.net -To: Alice -From: Bob -Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no - -First."#; - receive_imf(&t, mime, false).await?; - let first = t.get_last_msg().await; - let mime = br#"Subject: Second -Message-ID: second@example.net -To: Alice -From: Bob -Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no - -First."#; - receive_imf(&t, mime, false).await?; - let second = t.get_last_msg().await; - let mime = br#"Subject: Third -Message-ID: third@example.net -To: Alice -From: Bob -Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no - -First."#; - receive_imf(&t, mime, false).await?; - let third = t.get_last_msg().await; - - let mime = br#"Subject: Message with references. -Message-ID: second@example.net -To: Alice -From: Bob -In-Reply-To: -References: -Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no - -Message with references."#; - let mime_parser = MimeMessage::from_bytes(&t, &mime[..]).await?; - - let parent = get_parent_message(&t, &mime_parser).await?.unwrap(); - assert_eq!(parent.id, first.id); - - message::delete_msgs(&t, &[first.id]).await?; - let parent = get_parent_message(&t, &mime_parser).await?.unwrap(); - assert_eq!(parent.id, second.id); - - message::delete_msgs(&t, &[second.id]).await?; - let parent = get_parent_message(&t, &mime_parser).await?.unwrap(); - assert_eq!(parent.id, third.id); - - message::delete_msgs(&t, &[third.id]).await?; - let parent = get_parent_message(&t, &mime_parser).await?; - assert!(parent.is_none()); - - Ok(()) - } - - /// Test a message with RFC 1847 encapsulation as created by Thunderbird. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_rfc1847_encapsulation() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - alice.configure_addr("alice@example.org").await; - - // Alice sends an Autocrypt message to Bob so Bob gets Alice's key. - let chat_alice = alice.create_chat(&bob).await; - let first_msg = alice - .send_text(chat_alice.id, "Sending Alice key to Bob.") - .await; - bob.recv_msg(&first_msg).await; - message::delete_msgs(&bob, &[bob.get_last_msg().await.id]).await?; - - bob.set_config(Config::ShowEmails, Some("2")).await?; - - // Alice sends a message to Bob using Thunderbird. - let raw = include_bytes!("../test-data/message/rfc1847_encapsulation.eml"); - receive_imf(&bob, raw, false).await?; - - let msg = bob.get_last_msg().await; - assert!(msg.get_showpadlock()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_invalid_to_address() -> Result<()> { - let alice = TestContext::new_alice().await; - - let mime = include_bytes!("../test-data/message/invalid_email_to.eml"); - - // receive_imf should not fail on this mail with invalid To: field - receive_imf(&alice, mime, false).await?; - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_reply_from_different_addr() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await?; - - // Alice creates a 2-person-group with Bob - receive_imf( - &t, - br#"Subject: =?utf-8?q?Januar_13-19?= -Chat-Group-ID: qetqsutor7a -Chat-Group-Name: =?utf-8?q?Januar_13-19?= -MIME-Version: 1.0 -References: -Date: Mon, 20 Dec 2021 12:15:01 +0000 -Chat-Version: 1.0 -Message-ID: -To: -From: -Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no - -Hi, I created a group"#, - false, - ) - .await?; - let msg_out = t.get_last_msg().await; - assert_eq!(msg_out.from_id, ContactId::SELF); - assert_eq!(msg_out.text.unwrap(), "Hi, I created a group"); - assert_eq!(msg_out.in_reply_to, None); - - // Bob replies from a different address - receive_imf( - &t, - b"Content-Type: text/plain; charset=utf-8 -Content-Transfer-Encoding: quoted-printable -From: -Mime-Version: 1.0 (1.0) -Subject: Re: Januar 13-19 -Date: Mon, 20 Dec 2021 13:54:55 +0100 -Message-Id: -References: -In-Reply-To: -To: holger - -Reply from different address -", - false, - ) - .await?; - let msg_in = t.get_last_msg().await; - assert_eq!(msg_in.to_id, ContactId::SELF); - assert_eq!(msg_in.text.unwrap(), "Reply from different address"); - assert_eq!( - msg_in.in_reply_to.unwrap(), - "Gr.qetqsutor7a.Aresxresy-4@deltachat.de" - ); - assert_eq!( - msg_in.param.get(Param::OverrideSenderDisplayname), - Some("bob-alias@example.com") - ); - - assert_eq!(msg_in.chat_id, msg_out.chat_id); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_long_filenames() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - - for filename_sent in &[ - "foo.bar very long file name test baz.tar.gz", - "foobarabababababababbababababverylongfilenametestbaz.tar.gz", - "fooo...tar.gz", - "foo. .tar.gz", - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.tar.gz", - "a.tar.gz", - "a.a..a.a.a.a.tar.gz", - ] { - let attachment = alice.blobdir.join(filename_sent); - let content = format!("File content of {}", filename_sent); - tokio::fs::write(&attachment, content.as_bytes()).await?; - - let mut msg_alice = Message::new(Viewtype::File); - msg_alice.set_file(attachment.to_str().unwrap(), None); - let alice_chat = alice.create_chat(&bob).await; - let sent = alice.send_msg(alice_chat.id, &mut msg_alice).await; - println!("{}", sent.payload()); - - let msg_bob = bob.recv_msg(&sent).await; - - async fn check_message(msg: &Message, t: &TestContext, content: &str) { - assert_eq!(msg.get_viewtype(), Viewtype::File); - let resulting_filename = msg.get_filename().unwrap(); - let path = msg.get_file(t).unwrap(); - assert!( - resulting_filename.ends_with(".tar.gz"), - "{:?} doesn't end with .tar.gz, path: {:?}", - resulting_filename, - path - ); - assert!( - path.to_str().unwrap().ends_with(".tar.gz"), - "path {:?} doesn't end with .tar.gz", - path - ); - assert_eq!(fs::read_to_string(path).await.unwrap(), content); - } - check_message(&msg_alice, &alice, &content).await; - check_message(&msg_bob, &bob, &content).await; - } - - Ok(()) - } - - /// Tests that contact request is accepted automatically on outgoing message. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_accept_outgoing() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice1 = tcm.alice().await; - let alice2 = tcm.alice().await; - let bob1 = tcm.bob().await; - let bob2 = tcm.bob().await; - - let bob1_chat = bob1.create_chat(&alice1).await; - let sent = bob1.send_text(bob1_chat.id, "Hello!").await; - - alice1.recv_msg(&sent).await; - alice2.recv_msg(&sent).await; - let alice1_msg = bob2.recv_msg(&sent).await; - assert_eq!(alice1_msg.text.unwrap(), "Hello!"); - let alice1_chat = chat::Chat::load_from_db(&alice1, alice1_msg.chat_id).await?; - assert!(alice1_chat.is_contact_request()); - - let alice2_msg = alice2.get_last_msg().await; - assert_eq!(alice2_msg.text.unwrap(), "Hello!"); - let alice2_chat = chat::Chat::load_from_db(&alice2, alice2_msg.chat_id).await?; - assert!(alice2_chat.is_contact_request()); - - let bob1_msg = bob1.get_last_msg().await; - assert_eq!(bob1_msg.text.unwrap(), "Hello!"); - let bob1_chat = chat::Chat::load_from_db(&bob1, bob1_msg.chat_id).await?; - assert!(!bob1_chat.is_contact_request()); - - let bob2_msg = bob2.get_last_msg().await; - assert_eq!(bob2_msg.text.unwrap(), "Hello!"); - let bob2_chat = chat::Chat::load_from_db(&bob2, bob2_msg.chat_id).await?; - assert!(!bob2_chat.is_contact_request()); - - // Alice sends reply. - alice1_msg.chat_id.accept(&alice1).await.unwrap(); - let sent = alice1.send_text(alice1_chat.id, "Hi!").await; - alice2.recv_msg(&sent).await; - - // Second device automatically accepts the contact request. - let alice2_chat = chat::Chat::load_from_db(&alice2, alice2_msg.chat_id).await?; - assert!(!alice2_chat.is_contact_request()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_outgoing_private_reply_multidevice() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice1 = tcm.alice().await; - let alice2 = tcm.alice().await; - let bob = tcm.bob().await; - - // =============== Bob creates a group =============== - let group_id = - chat::create_group_chat(&bob, ProtectionStatus::Unprotected, "Group").await?; - chat::add_to_chat_contacts_table( - &bob, - group_id, - &[ - bob.add_or_lookup_contact(&alice1).await.id, - Contact::create(&bob, "", "charlie@example.org").await?, - ], - ) - .await?; - - // =============== Bob sends the first message to the group =============== - let sent = bob.send_text(group_id, "Hello all!").await; - alice1.recv_msg(&sent).await; - alice2.recv_msg(&sent).await; - - // =============== Alice answers privately with device 1 =============== - let received = alice1.get_last_msg().await; - let alice1_bob_contact = alice1.add_or_lookup_contact(&bob).await; - assert_eq!(received.from_id, alice1_bob_contact.id); - assert_eq!(received.to_id, ContactId::SELF); - assert!(!received.hidden); - assert_eq!(received.text, Some("Hello all!".to_string())); - assert_eq!(received.in_reply_to, None); - assert_eq!(received.chat_blocked, Blocked::Request); - - let received_group = Chat::load_from_db(&alice1, received.chat_id).await?; - assert_eq!(received_group.typ, Chattype::Group); - assert_eq!(received_group.name, "Group"); - assert_eq!(received_group.can_send(&alice1).await?, false); // Can't send because it's Blocked::Request - - let mut msg_out = Message::new(Viewtype::Text); - msg_out.set_text(Some("Private reply".to_string())); - - assert_eq!(received_group.blocked, Blocked::Request); - msg_out.set_quote(&alice1, Some(&received)).await?; - let alice1_bob_chat = alice1.create_chat(&bob).await; - let sent2 = alice1.send_msg(alice1_bob_chat.id, &mut msg_out).await; - alice2.recv_msg(&sent2).await; - - // =============== Alice's second device receives the message =============== - let received = alice2.get_last_msg().await; - - // That's a regression test for https://github.com/deltachat/deltachat-core-rust/issues/2949: - assert_eq!(received.chat_id, alice2.get_chat(&bob).await.unwrap().id); - - let alice2_bob_contact = alice2.add_or_lookup_contact(&bob).await; - assert_eq!(received.from_id, ContactId::SELF); - assert_eq!(received.to_id, alice2_bob_contact.id); - assert!(!received.hidden); - assert_eq!(received.text, Some("Private reply".to_string())); - assert_eq!( - received.parent(&alice2).await?.unwrap().text, - Some("Hello all!".to_string()) - ); - assert_eq!(received.chat_blocked, Blocked::Not); - - let received_chat = Chat::load_from_db(&alice2, received.chat_id).await?; - assert_eq!(received_chat.typ, Chattype::Single); - assert_eq!(received_chat.name, "bob@example.net"); - assert_eq!(received_chat.can_send(&alice2).await?, true); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_auto_accept_for_bots() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config(Config::Bot, Some("1")).await.unwrap(); - receive_imf(&t, MSGRMSG, false).await?; - let msg = t.get_last_msg().await; - let chat = chat::Chat::load_from_db(&t, msg.chat_id).await?; - assert!(!chat.is_contact_request()); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_no_private_reply_to_blocked_account() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - - // =============== Bob creates a group =============== - let group_id = - chat::create_group_chat(&bob, ProtectionStatus::Unprotected, "Group").await?; - chat::add_to_chat_contacts_table( - &bob, - group_id, - &[bob.add_or_lookup_contact(&alice).await.id], - ) - .await?; - - // =============== Bob sends the first message to the group =============== - let sent = bob.send_text(group_id, "Hello all!").await; - alice.recv_msg(&sent).await; - - let chats = Chatlist::try_load(&bob, 0, None, None).await?; - assert_eq!(chats.len(), 1); - - // =============== Bob blocks Alice ================ - Contact::block(&bob, bob.add_or_lookup_contact(&alice).await.id).await?; - - // =============== Alice replies private to Bob ============== - let received = alice.get_last_msg().await; - assert_eq!(received.text, Some("Hello all!".to_string())); - - let received_group = Chat::load_from_db(&alice, received.chat_id).await?; - assert_eq!(received_group.typ, Chattype::Group); - - let mut msg_out = Message::new(Viewtype::Text); - msg_out.set_text(Some("Private reply".to_string())); - msg_out.set_quote(&alice, Some(&received)).await?; - - let alice_bob_chat = alice.create_chat(&bob).await; - let sent2 = alice.send_msg(alice_bob_chat.id, &mut msg_out).await; - bob.recv_msg(&sent2).await; - - // ========= check that no contact request was created ============ - let chats = Chatlist::try_load(&bob, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); - let chat_id = chats.get_chat_id(0).unwrap(); - let chat = Chat::load_from_db(&bob, chat_id).await.unwrap(); - - // since only chat is a group, no new open chat has been created - assert_eq!(chat.typ, Chattype::Group); - let received = bob.get_last_msg().await; - assert_eq!(received.text, Some("Hello all!".to_string())); - - // =============== Bob unblocks Alice ================ - // test if the blocked chat is restored correctly - Contact::unblock(&bob, bob.add_or_lookup_contact(&alice).await.id).await?; - let chats = Chatlist::try_load(&bob, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 2); - let chat_id = chats.get_chat_id(0).unwrap(); - let chat = Chat::load_from_db(&bob, chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Single); - let received = bob.get_last_msg().await; - assert_eq!(received.text, Some("Private reply".to_string())); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_thunderbird_autocrypt() -> Result<()> { - let t = TestContext::new_bob().await; - t.set_config(Config::ShowEmails, Some("2")).await?; - - let raw = include_bytes!("../test-data/message/thunderbird_with_autocrypt.eml"); - receive_imf(&t, raw, false).await?; - - let peerstate = Peerstate::from_addr(&t, "alice@example.org") - .await? - .unwrap(); - assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_thunderbird_autocrypt_unencrypted() -> Result<()> { - let t = TestContext::new_bob().await; - t.set_config(Config::ShowEmails, Some("2")).await?; - - let raw = include_bytes!("../test-data/message/thunderbird_with_autocrypt_unencrypted.eml"); - receive_imf(&t, raw, false).await?; - let peerstate = Peerstate::from_addr(&t, "alice@example.org") - .await? - .unwrap(); - assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual); - - let raw = include_bytes!("../test-data/message/thunderbird_signed_unencrypted.eml"); - receive_imf(&t, raw, false).await?; - let peerstate = Peerstate::from_addr(&t, "alice@example.org") - .await? - .unwrap(); - assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mua_user_adds_member() -> Result<()> { - let t = TestContext::new_alice().await; - - receive_imf( - &t, - b"From: alice@example.org\n\ - To: bob@example.com\n\ - Subject: foo\n\ - Message-ID: \n\ - Chat-Version: 1.0\n\ - Chat-Group-ID: gggroupiddd\n\ - Chat-Group-Name: foo\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", - false, - ) - .await? - .unwrap(); - - receive_imf( - &t, - b"From: bob@example.com\n\ - To: alice@example.org, fiona@example.net\n\ - Subject: foo\n\ - Message-ID: \n\ - In-Reply-To: Gr.gggroupiddd.12345678901@example.com\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", - false, - ) - .await? - .unwrap(); - - let (chat_id, _, _) = chat::get_chat_id_by_grpid(&t, "gggroupiddd") - .await? - .unwrap(); - let mut actual_chat_contacts = chat::get_chat_contacts(&t, chat_id).await?; - actual_chat_contacts.sort(); - let mut expected_chat_contacts = vec![ - Contact::create(&t, "", "bob@example.com").await?, - Contact::create(&t, "", "fiona@example.net").await?, - ContactId::SELF, - ]; - expected_chat_contacts.sort(); - assert_eq!(actual_chat_contacts, expected_chat_contacts); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mua_user_adds_recipient_to_single_chat() -> Result<()> { - let alice = TestContext::new_alice().await; - - // Alice sends a 1:1 message to Bob, creating a 1:1 chat. - let msg = receive_imf( - &alice, - b"Subject: =?utf-8?q?Message_from_alice=40example=2Eorg?=\r\n\ - From: alice@example.org\r\n\ - To: \r\n\ - Date: Mon, 12 Dec 2022 14:30:39 +0000\r\n\ - Message-ID: \r\n\ - Chat-Version: 1.0\r\n\ - \r\n\ - tst\r\n", - false, - ) - .await? - .unwrap(); - let single_chat = Chat::load_from_db(&alice, msg.chat_id).await?; - assert_eq!(single_chat.typ, Chattype::Single); - - // Bob uses a classical MUA to answer in the 1:1 chat. - let msg2 = receive_imf( - &alice, - b"Subject: Re: Message from alice\r\n\ - From: \r\n\ - To: \r\n\ - Date: Mon, 12 Dec 2022 14:31:39 +0000\r\n\ - Message-ID: \r\n\ - In-Reply-To: \r\n\ - \r\n\ - Hi back!\r\n", - false, - ) - .await? - .unwrap(); - assert_eq!(msg2.chat_id, single_chat.id); - - // Bob uses a classical MUA to answer again, this time adding a recipient. - // This message should go to a newly created ad-hoc group. - let msg3 = receive_imf( - &alice, - b"Subject: Re: Message from alice\r\n\ - From: \r\n\ - To: , \r\n\ - Date: Mon, 12 Dec 2022 14:32:39 +0000\r\n\ - Message-ID: \r\n\ - In-Reply-To: \r\n\ - \r\n\ - Hi back!\r\n", - false, - ) - .await? - .unwrap(); - assert_ne!(msg3.chat_id, single_chat.id); - let group_chat = Chat::load_from_db(&alice, msg3.chat_id).await?; - assert_eq!(group_chat.typ, Chattype::Group); - assert_eq!( - chat::get_chat_contacts(&alice, group_chat.id).await?.len(), - 3 - ); - - // Bob uses a classical MUA to answer once more, adding another recipient. - // This new recipient should also be added to the group. - let msg4 = receive_imf( - &alice, - b"Subject: Re: Message from alice\r\n\ - From: \r\n\ - To: , , \r\n\ - Date: Mon, 12 Dec 2022 14:33:39 +0000\r\n\ - Message-ID: <69573857-542f-0fx3-55da-1289be5e0efe@example.net>\r\n\ - In-Reply-To: \r\n\ - \r\n\ - Hi back!\r\n", - false, - ) - .await? - .unwrap(); - assert_eq!(msg4.chat_id, group_chat.id); - assert_eq!( - chat::get_chat_contacts(&alice, group_chat.id).await?.len(), - 4 - ); - let fiona = Contact::lookup_id_by_addr(&alice, "fiona@example.net", Origin::IncomingTo) - .await? - .unwrap(); - assert!(chat::is_contact_in_chat(&alice, group_chat.id, fiona).await?); - - Ok(()) - } -} +mod tests; diff --git a/src/receive_imf/tests.rs b/src/receive_imf/tests.rs new file mode 100644 index 000000000..242e9c76b --- /dev/null +++ b/src/receive_imf/tests.rs @@ -0,0 +1,3220 @@ +use tokio::fs; + +use super::*; + +use crate::aheader::EncryptPreference; +use crate::chat::get_chat_contacts; +use crate::chat::{get_chat_msgs, ChatItem, ChatVisibility}; +use crate::chatlist::Chatlist; +use crate::constants::DC_GCL_NO_SPECIALS; +use crate::imap::prefetch_should_download; +use crate::message::Message; +use crate::test_utils::{get_chat_msg, TestContext, TestContextManager}; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_grpid_simple() { + let context = TestContext::new().await; + let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: hello@example.org\n\ + Subject: outer-subject\n\ + In-Reply-To: \n\ + References: \n\ + \n\ + hello\x00"; + let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]) + .await + .unwrap(); + assert_eq!(extract_grpid(&mimeparser, HeaderDef::InReplyTo), None); + let grpid = Some("HcxyMARjyJy"); + assert_eq!(extract_grpid(&mimeparser, HeaderDef::References), grpid); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_bad_from() { + let context = TestContext::new().await; + let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: hello\n\ + Subject: outer-subject\n\ + In-Reply-To: \n\ + References: \n\ + \n\ + hello\x00"; + let mimeparser = MimeMessage::from_bytes_with_partial(&context.ctx, &raw[..], None).await; + assert!(mimeparser.is_err()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_grpid_from_multiple() { + let context = TestContext::new().await; + let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: hello@example.org\n\ + Subject: outer-subject\n\ + In-Reply-To: \n\ + References: , \n\ + \n\ + hello\x00"; + let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]) + .await + .unwrap(); + let grpid = Some("HcxyMARjyJy"); + assert_eq!(extract_grpid(&mimeparser, HeaderDef::InReplyTo), grpid); + assert_eq!(extract_grpid(&mimeparser, HeaderDef::References), grpid); +} + +static MSGRMSG: &[u8] = + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: Bob \n\ + To: alice@example.org\n\ + Chat-Version: 1.0\n\ + Subject: Chat: hello\n\ + Message-ID: \n\ + Date: Sun, 22 Mar 2020 22:37:55 +0000\n\ + \n\ + hello\n"; + +static ONETOONE_NOREPLY_MAIL: &[u8] = + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: Bob \n\ + To: alice@example.org\n\ + Subject: Chat: hello\n\ + Message-ID: <2222@example.com>\n\ + Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ + \n\ + hello\n"; + +static GRP_MAIL: &[u8] = + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: bob@example.com\n\ + To: alice@example.org, claire@example.com\n\ + Subject: group with Alice, Bob and Claire\n\ + Message-ID: <3333@example.com>\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n"; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_adhoc_group_show_chats_only() { + let t = TestContext::new_alice().await; + assert_eq!(t.get_config_int(Config::ShowEmails).await.unwrap(), 0); + + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 0); + + receive_imf(&t, MSGRMSG, false).await.unwrap(); + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); + + receive_imf(&t, ONETOONE_NOREPLY_MAIL, false).await.unwrap(); + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); + + receive_imf(&t, GRP_MAIL, false).await.unwrap(); + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_adhoc_group_show_accepted_contact_unknown() { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("1")).await.unwrap(); + receive_imf(&t, GRP_MAIL, false).await.unwrap(); + + // adhoc-group with unknown contacts with show_emails=accepted is ignored for unknown contacts + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 0); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_adhoc_group_show_accepted_contact_known() { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("1")).await.unwrap(); + Contact::create(&t, "Bob", "bob@example.com").await.unwrap(); + receive_imf(&t, GRP_MAIL, false).await.unwrap(); + + // adhoc-group with known contacts with show_emails=accepted is still ignored for known contacts + // (and existent chat is required) + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 0); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_adhoc_group_show_accepted_contact_accepted() { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("1")).await.unwrap(); + + // accept Bob by accepting a delta-message from Bob + receive_imf(&t, MSGRMSG, false).await.unwrap(); + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); + let chat_id = chats.get_chat_id(0).unwrap(); + assert!(!chat_id.is_special()); + let chat = chat::Chat::load_from_db(&t, chat_id).await.unwrap(); + assert!(chat.is_contact_request()); + chat_id.accept(&t).await.unwrap(); + let chat = chat::Chat::load_from_db(&t, chat_id).await.unwrap(); + assert_eq!(chat.typ, Chattype::Single); + assert_eq!(chat.name, "Bob"); + assert_eq!(chat::get_chat_contacts(&t, chat_id).await.unwrap().len(), 1); + assert_eq!(chat::get_chat_msgs(&t, chat_id, 0).await.unwrap().len(), 1); + + // receive a non-delta-message from Bob, shows up because of the show_emails setting + receive_imf(&t, ONETOONE_NOREPLY_MAIL, false).await.unwrap(); + + assert_eq!(chat::get_chat_msgs(&t, chat_id, 0).await.unwrap().len(), 2); + + // let Bob create an adhoc-group by a non-delta-message, shows up because of the show_emails setting + receive_imf(&t, GRP_MAIL, false).await.unwrap(); + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 2); + let chat_id = chats.get_chat_id(0).unwrap(); + let chat = chat::Chat::load_from_db(&t, chat_id).await.unwrap(); + assert_eq!(chat.typ, Chattype::Group); + assert_eq!(chat.name, "group with Alice, Bob and Claire"); + assert_eq!(chat::get_chat_contacts(&t, chat_id).await.unwrap().len(), 3); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_adhoc_group_show_all() { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); + receive_imf(&t, GRP_MAIL, false).await.unwrap(); + + // adhoc-group with unknown contacts with show_emails=all will show up in a single chat + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); + let chat_id = chats.get_chat_id(0).unwrap(); + let chat = chat::Chat::load_from_db(&t, chat_id).await.unwrap(); + assert!(chat.is_contact_request()); + chat_id.accept(&t).await.unwrap(); + let chat = chat::Chat::load_from_db(&t, chat_id).await.unwrap(); + assert_eq!(chat.typ, Chattype::Group); + assert_eq!(chat.name, "group with Alice, Bob and Claire"); + assert_eq!(chat::get_chat_contacts(&t, chat_id).await.unwrap().len(), 3); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_read_receipt_and_unarchive() -> Result<()> { + // create alice's account + let t = TestContext::new_alice().await; + + let bob_id = Contact::create(&t, "bob", "bob@example.com").await?; + let one2one_id = ChatId::create_for_contact(&t, bob_id).await?; + one2one_id + .set_visibility(&t, ChatVisibility::Archived) + .await + .unwrap(); + let one2one = Chat::load_from_db(&t, one2one_id).await?; + assert!(one2one.get_visibility() == ChatVisibility::Archived); + + // create a group with bob, archive group + let group_id = chat::create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + chat::add_contact_to_chat(&t, group_id, bob_id).await?; + assert_eq!(chat::get_chat_msgs(&t, group_id, 0).await.unwrap().len(), 0); + group_id + .set_visibility(&t, ChatVisibility::Archived) + .await?; + let group = Chat::load_from_db(&t, group_id).await?; + assert!(group.get_visibility() == ChatVisibility::Archived); + + // everything archived, chatlist should be empty + assert_eq!( + Chatlist::try_load(&t, DC_GCL_NO_SPECIALS, None, None) + .await? + .len(), + 0 + ); + + // send a message to group with bob + receive_imf( + &t, + format!( + "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: alice@example.org\n\ + To: bob@example.com\n\ + Subject: foo\n\ + Message-ID: \n\ + Chat-Version: 1.0\n\ + Chat-Group-ID: {}\n\ + Chat-Group-Name: foo\n\ + Chat-Disposition-Notification-To: alice@example.org\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", + group.grpid, group.grpid + ) + .as_bytes(), + false, + ) + .await?; + let msg = get_chat_msg(&t, group_id, 0, 1).await; + assert_eq!(msg.is_dc_message, MessengerMessage::Yes); + assert_eq!(msg.text.unwrap(), "hello"); + assert_eq!(msg.state, MessageState::OutDelivered); + let group = Chat::load_from_db(&t, group_id).await?; + assert!(group.get_visibility() == ChatVisibility::Normal); + + // bob sends a read receipt to the group + receive_imf( + &t, + format!( + "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: bob@example.com\n\ + To: alice@example.org\n\ + Subject: message opened\n\ + Date: Sun, 22 Mar 2020 23:37:57 +0000\n\ + Chat-Version: 1.0\n\ + Message-ID: \n\ + Content-Type: multipart/report; report-type=disposition-notification; boundary=\"SNIPP\"\n\ + \n\ + \n\ + --SNIPP\n\ + Content-Type: text/plain; charset=utf-8\n\ + \n\ + Read receipts do not guarantee sth. was read.\n\ + \n\ + \n\ + --SNIPP\n\ + Content-Type: message/disposition-notification\n\ + \n\ + Reporting-UA: Delta Chat 1.28.0\n\ + Original-Recipient: rfc822;bob@example.com\n\ + Final-Recipient: rfc822;bob@example.com\n\ + Original-Message-ID: \n\ + Disposition: manual-action/MDN-sent-automatically; displayed\n\ + \n\ + \n\ + --SNIPP--", + group.grpid + ) + .as_bytes(), + false, + ) + .await?; + assert_eq!(chat::get_chat_msgs(&t, group_id, 0).await?.len(), 1); + let msg = message::Message::load_from_db(&t, msg.id).await?; + assert_eq!(msg.state, MessageState::OutMdnRcvd); + + // check, the read-receipt has not unarchived the one2one + assert_eq!( + Chatlist::try_load(&t, DC_GCL_NO_SPECIALS, None, None) + .await? + .len(), + 1 + ); + let one2one = Chat::load_from_db(&t, one2one_id).await?; + assert!(one2one.get_visibility() == ChatVisibility::Archived); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_no_from() { + // if there is no from given, from_id stays 0 which is just fine. These messages + // are very rare, however, we have to add them to the database + // to avoid a re-download from the server. + + let t = TestContext::new_alice().await; + let context = &t; + + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert!(chats.get_msg_id(0).is_err()); + + receive_imf( + context, + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + To: bob@example.com\n\ + Subject: foo\n\ + Message-ID: <3924@example.com>\n\ + Chat-Version: 1.0\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", + false, + ) + .await + .unwrap(); + + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + // Check that the message is not shown to the user: + assert!(chats.is_empty()); + + // Check that the message was added to the db: + assert!(message::rfc724_mid_exists(context, "3924@example.com") + .await + .unwrap() + .is_some()); +} + +/// If there is no Message-Id header, we generate a random id. +/// But there is no point in adding a trash entry in the database +/// if the email is malformed (e.g. because `From` is missing) +/// with this random id we just generated. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_no_message_id_header() { + let t = TestContext::new_alice().await; + + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert!(chats.get_msg_id(0).is_err()); + + let received = receive_imf( + &t, + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + To: bob@example.com\n\ + Subject: foo\n\ + Chat-Version: 1.0\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", + false, + ) + .await + .unwrap(); + dbg!(&received); + assert!(received.is_none()); + + assert!(!t + .sql + .exists( + "SELECT COUNT(*) FROM msgs WHERE chat_id=?;", + paramsv![DC_CHAT_ID_TRASH], + ) + .await + .unwrap()); + + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + // Check that the message is not shown to the user: + assert!(chats.is_empty()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_escaped_from() { + let t = TestContext::new_alice().await; + let contact_id = Contact::create(&t, "foobar", "foobar@example.com") + .await + .unwrap(); + let chat_id = ChatId::create_for_contact(&t, contact_id).await.unwrap(); + receive_imf( + &t, + b"From: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= \n\ + To: alice@example.org\n\ + Subject: foo\n\ + Message-ID: \n\ + Chat-Version: 1.0\n\ + Chat-Disposition-Notification-To: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= \n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", + false, + ).await.unwrap(); + assert_eq!( + Contact::load_from_db(&t, contact_id) + .await + .unwrap() + .get_authname(), + "Имя, Фамилия", + ); + let msg = get_chat_msg(&t, chat_id, 0, 1).await; + assert_eq!(msg.is_dc_message, MessengerMessage::Yes); + assert_eq!(msg.text.unwrap(), "hello"); + assert_eq!(msg.param.get_int(Param::WantsMdn).unwrap(), 1); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_escaped_recipients() { + let t = TestContext::new_alice().await; + Contact::create(&t, "foobar", "foobar@example.com") + .await + .unwrap(); + + let carl_contact_id = + Contact::add_or_lookup(&t, "Carl", "carl@host.tld", Origin::IncomingUnknownFrom) + .await + .unwrap() + .0; + + receive_imf( + &t, + b"From: Foobar \n\ + To: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= alice@example.org\n\ + Cc: =?utf-8?q?=3Ch2=3E?= \n\ + Subject: foo\n\ + Message-ID: \n\ + Chat-Version: 1.0\n\ + Chat-Disposition-Notification-To: \n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", + false, + ) + .await + .unwrap(); + let contact = Contact::load_from_db(&t, carl_contact_id).await.unwrap(); + assert_eq!(contact.get_name(), ""); + assert_eq!(contact.get_display_name(), "h2"); + + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + let msg = Message::load_from_db(&t, chats.get_msg_id(0).unwrap().unwrap()) + .await + .unwrap(); + assert_eq!(msg.is_dc_message, MessengerMessage::Yes); + assert_eq!(msg.text.unwrap(), "hello"); + assert_eq!(msg.param.get_int(Param::WantsMdn).unwrap(), 1); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_cc_to_contact() { + let t = TestContext::new_alice().await; + Contact::create(&t, "foobar", "foobar@example.com") + .await + .unwrap(); + + let carl_contact_id = + Contact::add_or_lookup(&t, "garabage", "carl@host.tld", Origin::IncomingUnknownFrom) + .await + .unwrap() + .0; + + receive_imf( + &t, + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: Foobar \n\ + To: alice@example.org\n\ + Cc: Carl \n\ + Subject: foo\n\ + Message-ID: \n\ + Chat-Version: 1.0\n\ + Chat-Disposition-Notification-To: \n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", + false, + ) + .await + .unwrap(); + let contact = Contact::load_from_db(&t, carl_contact_id).await.unwrap(); + assert_eq!(contact.get_name(), ""); + assert_eq!(contact.get_display_name(), "Carl"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_ndn_tiscali() { + test_parse_ndn( + "alice@tiscali.it", + "shenauithz@testrun.org", + "Mr.un2NYERi1RM.lbQ5F9q-QyJ@tiscali.it", + include_bytes!("../../test-data/message/tiscali_ndn.eml"), + Some("Delivery status notification – This is an automatically generated Delivery Status Notification. \n\nDelivery to the following recipients was aborted after 2 second(s):\n\n * shenauithz@testrun.org"), + ) + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_ndn_testrun() { + test_parse_ndn( + "alice@testrun.org", + "hcksocnsofoejx@five.chat", + "Mr.A7pTA5IgrUA.q4bP41vAJOp@testrun.org", + include_bytes!("../../test-data/message/testrun_ndn.eml"), + Some("Undelivered Mail Returned to Sender – This is the mail system at host hq5.merlinux.eu.\n\nI\'m sorry to have to inform you that your message could not\nbe delivered to one or more recipients. It\'s attached below.\n\nFor further assistance, please send mail to postmaster.\n\nIf you do so, please include this problem report. You can\ndelete your own text from the attached returned message.\n\n The mail system\n\n: host mail.five.chat[195.62.125.103] said: 550 5.1.1\n : Recipient address rejected: User unknown in\n virtual mailbox table (in reply to RCPT TO command)"), + ) + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_ndn_yahoo() { + test_parse_ndn( + "alice@yahoo.com", + "haeclirth.sinoenrat@yahoo.com", + "1680295672.3657931.1591783872936@mail.yahoo.com", + include_bytes!("../../test-data/message/yahoo_ndn.eml"), + Some("Failure Notice – Sorry, we were unable to deliver your message to the following address.\n\n:\n554: delivery error: dd Not a valid recipient - atlas117.free.mail.ne1.yahoo.com [...]"), + ) + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_ndn_gmail() { + test_parse_ndn( + "alice@gmail.com", + "assidhfaaspocwaeofi@gmail.com", + "CABXKi8zruXJc_6e4Dr087H5wE7sLp+u250o0N2q5DdjF_r-8wg@mail.gmail.com", + include_bytes!("../../test-data/message/gmail_ndn.eml"), + Some("Delivery Status Notification (Failure) – ** Die Adresse wurde nicht gefunden **\n\nIhre Nachricht wurde nicht an assidhfaaspocwaeofi@gmail.com zugestellt, weil die Adresse nicht gefunden wurde oder keine E-Mails empfangen kann.\n\nHier erfahren Sie mehr: https://support.google.com/mail/?p=NoSuchUser\n\nAntwort:\n\n550 5.1.1 The email account that you tried to reach does not exist. Please try double-checking the recipient\'s email address for typos or unnecessary spaces. Learn more at https://support.google.com/mail/?p=NoSuchUser i18sor6261697wrs.38 - gsmtp"), + ) + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_ndn_gmx() { + test_parse_ndn( + "alice@gmx.com", + "snaerituhaeirns@gmail.com", + "9c9c2a32-056b-3592-c372-d7e8f0bd4bc2@gmx.de", + include_bytes!("../../test-data/message/gmx_ndn.eml"), + Some("Mail delivery failed: returning message to sender – This message was created automatically by mail delivery software.\n\nA message that you sent could not be delivered to one or more of\nits recipients. This is a permanent error. The following address(es)\nfailed:\n\nsnaerituhaeirns@gmail.com:\nSMTP error from remote server for RCPT TO command, host: gmail-smtp-in.l.google.com (66.102.1.27) reason: 550-5.1.1 The email account that you tried to reach does not exist. Please\n try\n550-5.1.1 double-checking the recipient\'s email address for typos or\n550-5.1.1 unnecessary spaces. Learn more at\n550 5.1.1 https://support.google.com/mail/?p=NoSuchUser f6si2517766wmc.21\n9 - gsmtp [...]"), + ) + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_ndn_posteo() { + test_parse_ndn( + "alice@posteo.org", + "hanerthaertidiuea@gmx.de", + "04422840-f884-3e37-5778-8192fe22d8e1@posteo.de", + include_bytes!("../../test-data/message/posteo_ndn.eml"), + Some("Undelivered Mail Returned to Sender – This is the mail system at host mout01.posteo.de.\n\nI\'m sorry to have to inform you that your message could not\nbe delivered to one or more recipients. It\'s attached below.\n\nFor further assistance, please send mail to postmaster.\n\nIf you do so, please include this problem report. You can\ndelete your own text from the attached returned message.\n\n The mail system\n\n: host mx01.emig.gmx.net[212.227.17.5] said: 550\n Requested action not taken: mailbox unavailable (in reply to RCPT TO\n command)"), + ) + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_ndn_testrun_2() { + test_parse_ndn( + "alice@example.org", + "bob@example.org", + "Mr.5xqflwt0YFv.IXDFfHauvWx@testrun.org", + include_bytes!("../../test-data/message/testrun_ndn_2.eml"), + Some("Undelivered Mail Returned to Sender – This is the mail system at host hq5.merlinux.eu.\n\nI'm sorry to have to inform you that your message could not\nbe delivered to one or more recipients. It's attached below.\n\nFor further assistance, please send mail to postmaster.\n\nIf you do so, please include this problem report. You can\ndelete your own text from the attached returned message.\n\n The mail system\n\n: Host or domain name not found. Name service error for\n name=echedelyr.tk type=AAAA: Host not found"), + ) + .await; +} + +/// Tests that text part is not squashed into OpenPGP attachment. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_ndn_with_attachment() { + test_parse_ndn( + "alice@example.org", + "bob@example.net", + "Mr.I6Da6dXcTel.TroC5J3uSDH@example.org", + include_bytes!("../../test-data/message/ndn_with_attachment.eml"), + Some("Undelivered Mail Returned to Sender – This is the mail system at host relay01.example.org.\n\nI'm sorry to have to inform you that your message could not\nbe delivered to one or more recipients. It's attached below.\n\nFor further assistance, please send mail to postmaster.\n\nIf you do so, please include this problem report. You can\ndelete your own text from the attached returned message.\n\n The mail system\n\n: host mx2.example.net[80.241.60.215] said: 552 5.2.2\n : Recipient address rejected: Mailbox quota exceeded (in\n reply to RCPT TO command)\n\n: host mx1.example.net[80.241.60.212] said: 552 5.2.2\n : Recipient address rejected: Mailbox quota\n exceeded (in reply to RCPT TO command)") + ) + .await; +} + +/// Test that DSN is not treated as NDN if Action: is not "failed" +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_dsn_relayed() { + test_parse_ndn( + "anon_1@posteo.de", + "anon_2@gmx.at", + "8b7b1a9d0c8cc588c7bcac47f5687634@posteo.de", + include_bytes!("../../test-data/message/dsn_relayed.eml"), + None, + ) + .await; +} + +// ndn = Non Delivery Notification +async fn test_parse_ndn( + self_addr: &str, + foreign_addr: &str, + rfc724_mid_outgoing: &str, + raw_ndn: &[u8], + error_msg: Option<&str>, +) { + let t = TestContext::new().await; + t.configure_addr(self_addr).await; + + receive_imf( + &t, + format!( + "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: {}\n\ + To: {}\n\ + Subject: foo\n\ + Message-ID: <{}>\n\ + Chat-Version: 1.0\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", + self_addr, foreign_addr, rfc724_mid_outgoing + ) + .as_bytes(), + false, + ) + .await + .unwrap(); + + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + let msg_id = chats.get_msg_id(0).unwrap().unwrap(); + + // Check that the ndn would be downloaded: + let headers = mailparse::parse_mail(raw_ndn).unwrap().headers; + assert!(prefetch_should_download( + &t, + &headers, + "some-other-message-id", + std::iter::empty(), + ShowEmails::Off, + ) + .await + .unwrap()); + + receive_imf(&t, raw_ndn, false).await.unwrap(); + let msg = Message::load_from_db(&t, msg_id).await.unwrap(); + + assert_eq!( + msg.state, + if error_msg.is_some() { + MessageState::OutFailed + } else { + MessageState::OutDelivered + } + ); + + assert_eq!(msg.error(), error_msg.map(|error| error.to_string())); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_ndn_group_msg() -> Result<()> { + let t = TestContext::new().await; + t.configure_addr("alice@gmail.com").await; + + receive_imf( + &t, + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: alice@gmail.com\n\ + To: bob@example.com, assidhfaaspocwaeofi@gmail.com\n\ + Subject: foo\n\ + Message-ID: \n\ + Chat-Version: 1.0\n\ + Chat-Group-ID: abcde\n\ + Chat-Group-Name: foo\n\ + Chat-Disposition-Notification-To: alice@example.org\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", + false, + ) + .await?; + + let chats = Chatlist::try_load(&t, 0, None, None).await?; + let msg_id = chats.get_msg_id(0)?.unwrap(); + + let raw = include_bytes!("../../test-data/message/gmail_ndn_group.eml"); + receive_imf(&t, raw, false).await?; + + let msg = Message::load_from_db(&t, msg_id).await?; + + assert_eq!(msg.state, MessageState::OutFailed); + + let msgs = chat::get_chat_msgs(&t, msg.chat_id, 0).await?; + let msg_id = if let ChatItem::Message { msg_id } = msgs.last().unwrap() { + msg_id + } else { + panic!("Wrong item type"); + }; + let last_msg = Message::load_from_db(&t, *msg_id).await?; + + assert_eq!( + last_msg.text, + Some(stock_str::failed_sending_to(&t, "assidhfaaspocwaeofi@gmail.com").await,) + ); + assert_eq!(last_msg.from_id, ContactId::INFO); + Ok(()) +} + +async fn load_imf_email(context: &Context, imf_raw: &[u8]) -> Message { + context + .set_config(Config::ShowEmails, Some("2")) + .await + .unwrap(); + receive_imf(context, imf_raw, false).await.unwrap(); + let chats = Chatlist::try_load(context, 0, None, None).await.unwrap(); + let msg_id = chats.get_msg_id(0).unwrap().unwrap(); + Message::load_from_db(context, msg_id).await.unwrap() +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_html_only_mail() { + let t = TestContext::new_alice().await; + let msg = load_imf_email(&t, include_bytes!("../../test-data/message/wrong-html.eml")).await; + assert_eq!(msg.text.unwrap(), " Guten Abend, \n\n Lots of text \n\n text with Umlaut ä... \n\n MfG [...]"); +} + +static GH_MAILINGLIST: &[u8] = + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: Max Mustermann \n\ + To: deltachat/deltachat-core-rust \n\ + Subject: Let's put some [brackets here that] have nothing to do with the topic\n\ + Message-ID: <3333@example.org>\n\ + List-ID: deltachat/deltachat-core-rust \n\ + List-Post: \n\ + Precedence: list\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n"; + +static GH_MAILINGLIST2: &str = + "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: Github \n\ + To: deltachat/deltachat-core-rust \n\ + Subject: [deltachat/deltachat-core-rust] PR run failed\n\ + Message-ID: <3334@example.org>\n\ + List-ID: deltachat/deltachat-core-rust \n\ + List-Post: \n\ + Precedence: list\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello back\n"; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_github_mailing_list() -> Result<()> { + let t = TestContext::new_alice().await; + t.ctx.set_config(Config::ShowEmails, Some("2")).await?; + + receive_imf(&t.ctx, GH_MAILINGLIST, false).await?; + + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await?; + assert_eq!(chats.len(), 1); + + let chat_id = chats.get_chat_id(0).unwrap(); + chat_id.accept(&t).await.unwrap(); + let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await?; + + assert!(chat.is_mailing_list()); + assert!(chat.can_send(&t.ctx).await?); + assert_eq!( + chat.get_mailinglist_addr(), + Some("reply+elernshsetushoyseshetihseusaferuhsedtisneu@reply.github.com") + ); + assert_eq!(chat.name, "deltachat/deltachat-core-rust"); + assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).await?.len(), 1); + + receive_imf(&t.ctx, GH_MAILINGLIST2.as_bytes(), false).await?; + + let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await?; + assert!(!chat.can_send(&t.ctx).await?); + assert_eq!(chat.get_mailinglist_addr(), None); + + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await?; + assert_eq!(chats.len(), 1); + let contacts = Contact::get_all(&t.ctx, 0, None).await?; + assert_eq!(contacts.len(), 0); // mailing list recipients and senders do not count as "known contacts" + + let msg1 = get_chat_msg(&t, chat_id, 0, 2).await; + let contact1 = Contact::load_from_db(&t.ctx, msg1.from_id).await?; + assert_eq!(contact1.get_addr(), "notifications@github.com"); + assert_eq!(contact1.get_display_name(), "notifications@github.com"); // Make sure this is not "Max Mustermann" or somethinng + + let msg2 = get_chat_msg(&t, chat_id, 1, 2).await; + let contact2 = Contact::load_from_db(&t.ctx, msg2.from_id).await?; + assert_eq!(contact2.get_addr(), "notifications@github.com"); + + assert_eq!(msg1.get_override_sender_name().unwrap(), "Max Mustermann"); + assert_eq!(msg2.get_override_sender_name().unwrap(), "Github"); + Ok(()) +} + +static DC_MAILINGLIST: &[u8] = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: Bob \n\ + To: delta@codespeak.net\n\ + Subject: Re: [delta-dev] What's up?\n\ + Message-ID: <38942@posteo.org>\n\ + List-ID: \"discussions about and around https://delta.chat developments\" \n\ + List-Post: \n\ + Precedence: list\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + body\n"; + +static DC_MAILINGLIST2: &[u8] = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: Charlie \n\ + To: delta@codespeak.net\n\ + Subject: Re: [delta-dev] DC is nice!\n\ + Message-ID: <38943@posteo.org>\n\ + List-ID: \"discussions about and around https://delta.chat developments\" \n\ + List-Post: \n\ + Precedence: list\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + body 4\n"; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_classic_mailing_list() -> Result<()> { + let t = TestContext::new_alice().await; + t.ctx + .set_config(Config::ShowEmails, Some("2")) + .await + .unwrap(); + receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap(); + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); + let chat_id = chats.get_chat_id(0).unwrap(); + chat_id.accept(&t).await.unwrap(); + let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap(); + assert_eq!(chat.name, "delta-dev"); + assert!(chat.can_send(&t).await?); + assert_eq!(chat.get_mailinglist_addr(), Some("delta@codespeak.net")); + + let msg = get_chat_msg(&t, chat_id, 0, 1).await; + let contact1 = Contact::load_from_db(&t.ctx, msg.from_id).await.unwrap(); + assert_eq!(contact1.get_addr(), "bob@posteo.org"); + + let sent = t.send_text(chat.id, "Hello mailinglist!").await; + let mime = sent.payload(); + + println!("Sent mime message is:\n\n{}\n\n", mime); + assert!(mime.contains("Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no\r\n")); + assert!(mime.contains("Subject: =?utf-8?q?Re=3A_=5Bdelta-dev=5D_What=27s_up=3F?=\r\n")); + assert!(mime.contains("MIME-Version: 1.0\r\n")); + assert!(mime.contains("In-Reply-To: <38942@posteo.org>\r\n")); + assert!(mime.contains("Chat-Version: 1.0\r\n")); + assert!(mime.contains("To: \r\n")); + assert!(mime.contains("From: \r\n")); + assert!(mime.contains( + "\r\n\ +\r\n\ +Hello mailinglist!\r\n" + )); + + receive_imf(&t.ctx, DC_MAILINGLIST2, false).await?; + + let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await?; + assert!(chat.can_send(&t.ctx).await?); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_other_device_writes_to_mailinglist() -> Result<()> { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await?; + receive_imf(&t, DC_MAILINGLIST, false).await.unwrap(); + let first_msg = t.get_last_msg().await; + let first_chat = Chat::load_from_db(&t, first_msg.chat_id).await?; + assert_eq!( + first_chat.param.get(Param::ListPost).unwrap(), + "delta@codespeak.net" + ); + + let list_post_contact_id = + Contact::lookup_id_by_addr(&t, "delta@codespeak.net", Origin::Unknown) + .await? + .unwrap(); + let list_post_contact = Contact::load_from_db(&t, list_post_contact_id).await?; + assert_eq!( + list_post_contact.param.get(Param::ListId).unwrap(), + "delta.codespeak.net" + ); + assert_eq!( + chat::get_chat_id_by_grpid(&t, "delta.codespeak.net") + .await? + .unwrap(), + (first_chat.id, false, Blocked::Request) + ); + + receive_imf( + &t, + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: Alice \n\ + To: delta@codespeak.net\n\ + Subject: [delta-dev] Subject\n\ + Message-ID: <0476@example.org>\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + body 4\n", + false, + ) + .await + .unwrap(); + + let second_msg = t.get_last_msg().await; + + assert_eq!(first_msg.chat_id, second_msg.chat_id); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_block_mailing_list() { + let t = TestContext::new_alice().await; + t.ctx + .set_config(Config::ShowEmails, Some("2")) + .await + .unwrap(); + + receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap(); + t.evtracker.wait_next_incoming_message().await; + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); + let chat_id = chats.get_chat_id(0).unwrap(); + let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap(); + assert!(chat.is_contact_request()); + + // Block the contact request. + chat_id.block(&t).await.unwrap(); + + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 0); // Test that the message disappeared + + receive_imf(&t.ctx, DC_MAILINGLIST2, false).await.unwrap(); + + // Check that no notification is displayed for blocked mailing list message. + while let Ok(event) = t.evtracker.try_recv() { + assert!(!matches!(event.typ, EventType::IncomingMsg { .. })); + } + + // Test that the mailing list stays disappeared + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 0); // Test that the message is not shown + + // Both messages are in the same blocked chat. + let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0).await.unwrap(); + assert_eq!(msgs.len(), 2); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mailing_list_decide_block_then_unblock() { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); + + receive_imf(&t, DC_MAILINGLIST, false).await.unwrap(); + let blocked = Contact::get_all_blocked(&t).await.unwrap(); + assert_eq!(blocked.len(), 0); + + // Block the contact request, this should add one blocked contact. + let msg = t.get_last_msg().await; + msg.chat_id.block(&t).await.unwrap(); + + let blocked = Contact::get_all_blocked(&t).await.unwrap(); + assert_eq!(blocked.len(), 1); + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 0); // Test that the message is not shown + + // Unblock contact and check if the next message arrives in a chat + Contact::unblock(&t, *blocked.first().unwrap()) + .await + .unwrap(); + let blocked = Contact::get_all_blocked(&t).await.unwrap(); + assert_eq!(blocked.len(), 0); + + receive_imf(&t.ctx, DC_MAILINGLIST2, false).await.unwrap(); + let msg = t.get_last_msg().await; + let msgs = chat::get_chat_msgs(&t, msg.chat_id, 0).await.unwrap(); + assert_eq!(msgs.len(), 2); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mailing_list_decide_not_now() { + let t = TestContext::new_alice().await; + t.ctx + .set_config(Config::ShowEmails, Some("2")) + .await + .unwrap(); + + receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap(); + + let msg = t.get_last_msg().await; + let chat_id = msg.get_chat_id(); + + // Open the chat and go back + chat::marknoticed_chat(&t.ctx, chat_id).await.unwrap(); + + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); // Test that chat is still in the chatlist + let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0).await.unwrap(); + assert_eq!(msgs.len(), 1); // ...and contains 1 message + + receive_imf(&t.ctx, DC_MAILINGLIST2, false).await.unwrap(); + + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); // Test that the new mailing list message got into the same chat + let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0).await.unwrap(); + assert_eq!(msgs.len(), 2); + let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap(); + assert!(chat.is_contact_request()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mailing_list_decide_accept() { + let t = TestContext::new_alice().await; + t.ctx + .set_config(Config::ShowEmails, Some("2")) + .await + .unwrap(); + + receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap(); + + let msg = t.get_last_msg().await; + let chat_id = msg.get_chat_id(); + chat_id.accept(&t).await.unwrap(); + + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); // Test that the message is shown + assert!(!chat_id.is_special()); + + receive_imf(&t.ctx, DC_MAILINGLIST2, false).await.unwrap(); + + let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0).await.unwrap(); + assert_eq!(msgs.len(), 2); + let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await.unwrap(); + assert!(chat.can_send(&t.ctx).await.unwrap()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mailing_list_multiple_names_in_subject() -> Result<()> { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await?; + receive_imf( + &t, + b"From: Foo Bar \n\ + To: deltachat/deltachat-core-rust \n\ + Subject: [ola list] [foo][bar] just a subject\n\ + Message-ID: <3333@example.org>\n\ + List-ID: \"looong description of 'ola list', with foo, bar\" \n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", + false, + ) + .await + .unwrap(); + let msg = t.get_last_msg().await; + let chat_id = msg.get_chat_id(); + let chat = Chat::load_from_db(&t, chat_id).await?; + assert_eq!(chat.name, "ola list [foo][bar]"); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_majordomo_mailing_list() -> Result<()> { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); + + // test mailing lists not having a `ListId:`-header + receive_imf( + &t, + b"From: Foo Bar \n\ + To: deltachat/deltachat-core-rust \n\ + Subject: [ola] just a subject\n\ + Message-ID: <3333@example.org>\n\ + Sender: My list \n\ + Precedence: list\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", + false, + ) + .await + .unwrap(); + let msg = t.get_last_msg().await; + let chat_id = msg.get_chat_id(); + let chat = Chat::load_from_db(&t, chat_id).await.unwrap(); + assert_eq!(chat.typ, Chattype::Mailinglist); + assert_eq!(chat.grpid, "mylist@bar.org"); + assert_eq!(chat.name, "ola"); + assert_eq!(chat::get_chat_msgs(&t, chat.id, 0).await.unwrap().len(), 1); + assert!(!chat.can_send(&t).await?); + assert_eq!(chat.get_mailinglist_addr(), None); + + // receive another message with no sender name but the same address, + // make sure this lands in the same chat + receive_imf( + &t, + b"From: Nu Bar \n\ + To: deltachat/deltachat-core-rust \n\ + Subject: [ola] Re: just a subject\n\ + Message-ID: <4444@example.org>\n\ + Sender: mylist@bar.org\n\ + Precedence: list\n\ + Date: Sun, 22 Mar 2020 23:37:57 +0000\n\ + \n\ + hello\n", + false, + ) + .await + .unwrap(); + assert_eq!(chat::get_chat_msgs(&t, chat.id, 0).await.unwrap().len(), 2); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mailchimp_mailing_list() -> Result<()> { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); + + receive_imf( + &t, + b"To: alice \n\ + Subject: =?utf-8?Q?How=20early=20megacities=20emerged=20from=20Cambodia=E2=80=99s=20jungles?=\n\ + From: =?utf-8?Q?Atlas=20Obscura?= \n\ + List-ID: 399fc0402f1b154b67965632emc list <399fc0402f1b154b67965632e.100761.list-id.mcsv.net>\n\ + Message-ID: <555@example.org>\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", + false, + ) + .await + .unwrap(); + let msg = t.get_last_msg().await; + let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); + assert_eq!(chat.typ, Chattype::Mailinglist); + assert_eq!(chat.blocked, Blocked::Request); + assert_eq!( + chat.grpid, + "399fc0402f1b154b67965632e.100761.list-id.mcsv.net" + ); + assert_eq!(chat.name, "Atlas Obscura"); + assert!(!chat.can_send(&t).await?); + assert_eq!(chat.get_mailinglist_addr(), None); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_dhl_mailing_list() -> Result<()> { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); + + receive_imf( + &t, + include_bytes!("../../test-data/message/mailinglist_dhl.eml"), + false, + ) + .await + .unwrap(); + let msg = t.get_last_msg().await; + assert_eq!( + msg.text, + Some("Ihr Paket ist in der Packstation 123 – bla bla".to_string()) + ); + assert!(msg.has_html()); + let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); + assert_eq!(chat.typ, Chattype::Mailinglist); + assert_eq!(chat.blocked, Blocked::Request); + assert_eq!(chat.grpid, "1234ABCD-123LMNO.mailing.dhl.de"); + assert_eq!(chat.name, "DHL Paket"); + assert!(!chat.can_send(&t).await?); + assert_eq!(chat.get_mailinglist_addr(), None); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_dpd_mailing_list() -> Result<()> { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); + + receive_imf( + &t, + include_bytes!("../../test-data/message/mailinglist_dpd.eml"), + false, + ) + .await + .unwrap(); + let msg = t.get_last_msg().await; + assert_eq!( + msg.text, + Some("Bald ist Ihr DPD Paket da – bla bla".to_string()) + ); + assert!(msg.has_html()); + let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); + assert_eq!(chat.typ, Chattype::Mailinglist); + assert_eq!(chat.blocked, Blocked::Request); + assert_eq!(chat.grpid, "dpdde.mxmail.service.dpd.de"); + assert_eq!(chat.name, "DPD"); + assert!(!chat.can_send(&t).await?); + assert_eq!(chat.get_mailinglist_addr(), None); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_xt_local_mailing_list() -> Result<()> { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await?; + + receive_imf( + &t, + include_bytes!("../../test-data/message/mailinglist_xt_local_microsoft.eml"), + false, + ) + .await?; + let chat = Chat::load_from_db(&t, t.get_last_msg().await.chat_id).await?; + assert_eq!(chat.typ, Chattype::Mailinglist); + assert_eq!(chat.grpid, "96540.xt.local"); + assert_eq!(chat.name, "Microsoft Store"); + assert!(!chat.can_send(&t).await?); + assert_eq!(chat.get_mailinglist_addr(), None); + + receive_imf( + &t, + include_bytes!("../../test-data/message/mailinglist_xt_local_spiegel.eml"), + false, + ) + .await?; + let chat = Chat::load_from_db(&t, t.get_last_msg().await.chat_id).await?; + assert_eq!(chat.typ, Chattype::Mailinglist); + assert_eq!(chat.grpid, "121231234.xt.local"); + assert_eq!(chat.name, "DER SPIEGEL Kundenservice"); + assert!(!chat.can_send(&t).await?); + assert_eq!(chat.get_mailinglist_addr(), None); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_xing_mailing_list() -> Result<()> { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await?; + + receive_imf( + &t, + include_bytes!("../../test-data/message/mailinglist_xing.eml"), + false, + ) + .await?; + let msg = t.get_last_msg().await; + assert_eq!(msg.subject, "Kennst Du Dr. Mabuse?"); + let chat = Chat::load_from_db(&t, msg.chat_id).await?; + assert_eq!(chat.typ, Chattype::Mailinglist); + assert_eq!(chat.grpid, "51231231231231231231231232869f58.xing.com"); + assert_eq!(chat.name, "xing.com"); + assert!(!chat.can_send(&t).await?); + assert_eq!(chat.get_mailinglist_addr(), None); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_ttline_mailing_list() -> Result<()> { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await?; + + receive_imf( + &t, + include_bytes!("../../test-data/message/mailinglist_ttline.eml"), + false, + ) + .await?; + let msg = t.get_last_msg().await; + assert_eq!(msg.subject, "Unsere Sommerangebote an Bord ⚓"); + let chat = Chat::load_from_db(&t, msg.chat_id).await?; + assert_eq!(chat.typ, Chattype::Mailinglist); + assert_eq!(chat.grpid, "39123123-1BBQXPY.t.ttline.com"); + assert_eq!(chat.name, "TT-Line - Die Schwedenfähren"); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mailing_list_with_mimepart_footer() { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); + + // the mailing list message contains two top-level texts. + // the second text is a footer that is added by some mailing list software + // if the user-edited text contains html. + // this footer should not become a text-message in delta chat + // (otherwise every second mail might be the same footer) + receive_imf( + &t, + include_bytes!("../../test-data/message/mailinglist_with_mimepart_footer.eml"), + false, + ) + .await + .unwrap(); + let msg = t.get_last_msg().await; + assert_eq!( + msg.text, + Some("[Intern] important stuff – Hi mr ... [text part]".to_string()) + ); + assert!(msg.has_html()); + let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); + assert_eq!(get_chat_msgs(&t, msg.chat_id, 0).await.unwrap().len(), 1); + assert_eq!(chat.typ, Chattype::Mailinglist); + assert_eq!(chat.blocked, Blocked::Request); + assert_eq!(chat.grpid, "intern.lists.abc.de"); + assert_eq!(chat.name, "Intern"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mailing_list_with_mimepart_footer_signed() { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); + + receive_imf( + &t, + include_bytes!("../../test-data/message/mailinglist_with_mimepart_footer_signed.eml"), + false, + ) + .await + .unwrap(); + let msg = t.get_last_msg().await; + assert_eq!(get_chat_msgs(&t, msg.chat_id, 0).await.unwrap().len(), 1); + let text = msg.text.clone().unwrap(); + assert!(text.contains("content text")); + assert!(!text.contains("footer text")); + assert!(msg.has_html()); + let html = msg.get_id().get_html(&t).await.unwrap().unwrap(); + assert!(html.contains("content text")); + assert!(!html.contains("footer text")); +} + +/// Test that the changes from apply_mailinglist_changes() are also applied +/// if the message is assigned to the chat by In-Reply-To +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_apply_mailinglist_changes_assigned_by_reply() { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); + + receive_imf(&t, GH_MAILINGLIST, false).await.unwrap(); + + let chat_id = t.get_last_msg().await.chat_id; + chat_id.accept(&t).await.unwrap(); + let chat = Chat::load_from_db(&t, chat_id).await.unwrap(); + assert!(chat.can_send(&t).await.unwrap()); + + let imf_raw = format!("In-Reply-To: 3333@example.org\n{}", GH_MAILINGLIST2); + receive_imf(&t, imf_raw.as_bytes(), false).await.unwrap(); + + assert_eq!( + t.get_last_msg().await.in_reply_to.unwrap(), + "3333@example.org" + ); + // `Assigning message to Chat#... as it's a reply to 3333@example.org` + t.evtracker + .get_info_contains("as it's a reply to 3333@example.org") + .await; + + let chat = Chat::load_from_db(&t, chat_id).await.unwrap(); + assert!(!chat.can_send(&t).await.unwrap()); + + let contact_id = Contact::lookup_id_by_addr( + &t, + "reply+EGELITBABIHXSITUZIEPAKYONASITEPUANERGRUSHE@reply.github.com", + Origin::Hidden, + ) + .await + .unwrap() + .unwrap(); + let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); + assert_eq!( + contact.param.get(Param::ListId).unwrap(), + "deltachat-core-rust.deltachat.github.com" + ) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mailing_list_chat_message() { + let t = TestContext::new_alice().await; + + receive_imf( + &t, + include_bytes!("../../test-data/message/mailinglist_chat_message.eml"), + false, + ) + .await + .unwrap(); + let msg = t.get_last_msg().await; + assert_eq!(msg.text, Some("hello, this is a test 👋\n\n_______________________________________________\nTest1 mailing list -- test1@example.net\nTo unsubscribe send an email to test1-leave@example.net".to_string())); + assert!(!msg.has_html()); + let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); + assert_eq!(chat.typ, Chattype::Mailinglist); + assert_eq!(chat.blocked, Blocked::Request); + assert_eq!(chat.grpid, "test1.example.net"); + assert_eq!(chat.name, "Test1"); +} + +/// Tests that bots automatically accept mailing lists. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mailing_list_bot() { + let t = TestContext::new_alice().await; + t.set_config(Config::Bot, Some("1")).await.unwrap(); + + receive_imf( + &t, + include_bytes!("../../test-data/message/mailinglist_chat_message.eml"), + false, + ) + .await + .unwrap(); + let msg = t.get_last_msg().await; + let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); + assert_eq!(chat.blocked, Blocked::Not); + + // Bot should see the message as fresh and process it. + assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_dont_show_tokens_in_contacts_list() { + check_dont_show_in_contacts_list( + "reply+OGHVYCLVBEGATYBICAXBIRQATABUOTUCERABERAHNO@reply.github.com", + ) + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_dont_show_noreply_in_contacts_list() { + check_dont_show_in_contacts_list("noreply@github.com").await; +} + +async fn check_dont_show_in_contacts_list(addr: &str) { + let t = TestContext::new_alice().await; + t.ctx + .set_config(Config::ShowEmails, Some("2")) + .await + .unwrap(); + receive_imf( + &t, + format!( + "Subject: Re: [deltachat/deltachat-core-rust] DC is the best repo on GitHub! +To: {} +References: + +From: alice@example.org +Message-ID: +Date: Tue, 16 Jun 2020 12:04:20 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: 7bit + +YEAAAAAA!. +", + addr + ) + .as_bytes(), + false, + ) + .await + .unwrap(); + let contacts = Contact::get_all(&t, 0, None as Option<&str>).await.unwrap(); + assert!(contacts.is_empty()); // The contact should not have been added to the db +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_pdf_filename_simple() { + let t = TestContext::new_alice().await; + let msg = load_imf_email( + &t, + include_bytes!("../../test-data/message/pdf_filename_simple.eml"), + ) + .await; + assert_eq!(msg.viewtype, Viewtype::File); + assert_eq!(msg.text.unwrap(), "mail body"); + assert_eq!(msg.param.get(Param::File).unwrap(), "$BLOBDIR/simple.pdf"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_pdf_filename_continuation() { + // test filenames split across multiple header lines, see rfc 2231 + let t = TestContext::new_alice().await; + let msg = load_imf_email( + &t, + include_bytes!("../../test-data/message/pdf_filename_continuation.eml"), + ) + .await; + assert_eq!(msg.viewtype, Viewtype::File); + assert_eq!(msg.text.unwrap(), "mail body"); + assert_eq!( + msg.param.get(Param::File).unwrap(), + "$BLOBDIR/test pdf äöüß.pdf" + ); +} + +/// HTML-images may come with many embedded images, eg. tiny icons, corners for formatting, +/// twitter/facebook/whatever logos and so on. +/// that may easily be 50 and more images, one would not have these images in a chat. +/// +/// fortunately, if we remove them, they are accessible by get_msg_html() now. +/// +/// unfortunately, these images are not that easy to detect as they may also be on purpose, +/// or mua may use multipart/related not correctly - +/// so this test is in competition with parse_thunderbird_html_embedded_image() +/// that wants the image to be kept in the chat. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_many_images() { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); + + receive_imf( + &t, + include_bytes!("../../test-data/message/many_images_amazon_via_apple_mail.eml"), + false, + ) + .await + .unwrap(); + let msg = t.get_last_msg().await; + assert_eq!(msg.viewtype, Viewtype::Image); + assert!(msg.has_html()); + let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); + assert_eq!(get_chat_msgs(&t, chat.id, 0).await.unwrap().len(), 1); +} + +/// Test that classical MUA messages are assigned to group chats based on the `In-Reply-To` +/// header. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_in_reply_to() { + let t = TestContext::new().await; + t.configure_addr("bob@example.com").await; + + // Receive message from Alice about group "foo". + receive_imf( + &t, + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: alice@example.org\n\ + To: bob@example.com, charlie@example.net\n\ + Subject: foo\n\ + Message-ID: \n\ + Chat-Version: 1.0\n\ + Chat-Group-ID: foo\n\ + Chat-Group-Name: foo\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello foo\n", + false, + ) + .await + .unwrap(); + + // Receive reply from Charlie without group ID but with In-Reply-To header. + receive_imf( + &t, + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: charlie@example.net\n\ + To: alice@example.org, bob@example.com\n\ + Subject: Re: foo\n\ + Message-ID: \n\ + In-Reply-To: \n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + reply foo\n", + false, + ) + .await + .unwrap(); + + let msg = t.get_last_msg().await; + assert_eq!(msg.get_text().unwrap(), "reply foo"); + + // Load the first message from the same chat. + let msgs = chat::get_chat_msgs(&t, msg.chat_id, 0).await.unwrap(); + let msg_id = if let ChatItem::Message { msg_id } = msgs.first().unwrap() { + msg_id + } else { + panic!("Wrong item type"); + }; + + let reply_msg = Message::load_from_db(&t, *msg_id).await.unwrap(); + assert_eq!(reply_msg.get_text().unwrap(), "hello foo"); + + // Check that reply got into the same chat as the original message. + assert_eq!(msg.chat_id, reply_msg.chat_id); + + // Make sure we looked at real chat ID and do not just + // test that both messages got into the same virtual chat. + assert!(!msg.chat_id.is_special()); +} + +/// Test that classical MUA messages are assigned to group chats +/// based on the `In-Reply-To` header for two-member groups. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_in_reply_to_two_member_group() { + let t = TestContext::new().await; + t.configure_addr("bob@example.com").await; + + // Receive message from Alice about group "foo". + receive_imf( + &t, + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: alice@example.org\n\ + To: bob@example.com\n\ + Subject: foo\n\ + Message-ID: \n\ + Chat-Version: 1.0\n\ + Chat-Group-ID: foo\n\ + Chat-Group-Name: foo\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello foo\n", + false, + ) + .await + .unwrap(); + + // Receive a classic MUA reply from Alice. + // It is assigned to the group chat. + receive_imf( + &t, + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: alice@example.org\n\ + To: bob@example.com\n\ + Subject: Re: foo\n\ + Message-ID: \n\ + In-Reply-To: \n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + classic reply\n", + false, + ) + .await + .unwrap(); + + // Ensure message is assigned to group chat. + let msg = t.get_last_msg().await; + let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); + assert_eq!(chat.typ, Chattype::Group); + assert_eq!(msg.get_text().unwrap(), "classic reply"); + + // Receive a Delta Chat reply from Alice. + // It is assigned to group chat, because it has a group ID. + receive_imf( + &t, + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: alice@example.org\n\ + To: bob@example.com\n\ + Subject: Re: foo\n\ + Message-ID: \n\ + In-Reply-To: \n\ + Chat-Version: 1.0\n\ + Chat-Group-ID: foo\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + chat reply\n", + false, + ) + .await + .unwrap(); + + // Ensure message is assigned to group chat. + let msg = t.get_last_msg().await; + let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); + assert_eq!(chat.typ, Chattype::Group); + assert_eq!(msg.get_text().unwrap(), "chat reply"); + + // Receive a private Delta Chat reply from Alice. + // It is assigned to 1:1 chat, because it has no group ID, + // which means it was created using "reply privately" feature. + // Normally it contains a quote, but it should not matter. + receive_imf( + &t, + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: alice@example.org\n\ + To: bob@example.com\n\ + Subject: Re: foo\n\ + Message-ID: \n\ + In-Reply-To: \n\ + Chat-Version: 1.0\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + private reply\n", + false, + ) + .await + .unwrap(); + + // Ensure message is assigned to a 1:1 chat. + let msg = t.get_last_msg().await; + let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); + assert_eq!(chat.typ, Chattype::Single); + assert_eq!(msg.get_text().unwrap(), "private reply"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_save_mime_headers_off() -> anyhow::Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let chat_alice = alice.create_chat(&bob).await; + chat::send_text_msg(&alice, chat_alice.id, "hi!".to_string()).await?; + + let msg = bob.recv_msg(&alice.pop_sent_msg().await).await; + assert_eq!(msg.get_text(), Some("hi!".to_string())); + assert!(!msg.get_showpadlock()); + let mime = message::get_mime_headers(&bob, msg.id).await?; + assert!(mime.is_empty()); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_save_mime_headers_on() -> anyhow::Result<()> { + let alice = TestContext::new_alice().await; + alice.set_config_bool(Config::SaveMimeHeaders, true).await?; + let bob = TestContext::new_bob().await; + bob.set_config_bool(Config::SaveMimeHeaders, true).await?; + + // alice sends a message to bob, bob sees full mime + let chat_alice = alice.create_chat(&bob).await; + chat::send_text_msg(&alice, chat_alice.id, "hi!".to_string()).await?; + + let msg = bob.recv_msg(&alice.pop_sent_msg().await).await; + assert_eq!(msg.get_text(), Some("hi!".to_string())); + assert!(!msg.get_showpadlock()); + let mime = message::get_mime_headers(&bob, msg.id).await?; + let mime_str = String::from_utf8_lossy(&mime); + assert!(mime_str.contains("Message-ID:")); + assert!(mime_str.contains("From:")); + + // another one, from bob to alice, that gets encrypted + let chat_bob = bob.create_chat(&alice).await; + chat::send_text_msg(&bob, chat_bob.id, "ho!".to_string()).await?; + let msg = alice.recv_msg(&bob.pop_sent_msg().await).await; + assert_eq!(msg.get_text(), Some("ho!".to_string())); + assert!(msg.get_showpadlock()); + let mime = message::get_mime_headers(&alice, msg.id).await?; + let mime_str = String::from_utf8_lossy(&mime); + assert!(mime_str.contains("Message-ID:")); + assert!(mime_str.contains("From:")); + Ok(()) +} + +async fn create_test_alias(chat_request: bool, group_request: bool) -> (TestContext, TestContext) { + // Claire, a customer, sends a support request + // to the alias address from a classic MUA. + // The alias expands to the supporters Alice and Bob. + // Check that Alice receives the message in a group chat. + let claire_request = if group_request { + format!( + "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + To: support@example.org, ceo@example.org\n\ + From: claire@example.org\n\ + Subject: i have a question\n\ + Message-ID: \n\ + {}\ + Date: Sun, 14 Mar 2021 17:04:36 +0100\n\ + Content-Type: text/plain\n\ + \n\ + hi support! what is the current version?", + if chat_request { + "Chat-Group-ID: 8ud29aridt29arid\n\ + Chat-Group-Name: =?utf-8?q?i_have_a_question?=\n" + } else { + "" + } + ) + } else { + format!( + "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + To: support@example.org\n\ + From: claire@example.org\n\ + Subject: i have a question\n\ + Message-ID: \n\ + {}\ + Date: Sun, 14 Mar 2021 17:04:36 +0100\n\ + Content-Type: text/plain\n\ + \n\ + hi support! what is the current version?", + if chat_request { + "Chat-Version: 1.0\n" + } else { + "" + } + ) + }; + + let alice = TestContext::new_alice().await; + alice + .set_config(Config::ShowEmails, Some("2")) + .await + .unwrap(); + receive_imf(&alice, claire_request.as_bytes(), false) + .await + .unwrap(); + + let msg = alice.get_last_msg().await; + assert_eq!(msg.get_subject(), "i have a question"); + assert!(msg.get_text().unwrap().contains("hi support!")); + let chat = Chat::load_from_db(&alice, msg.chat_id).await.unwrap(); + assert_eq!(chat.typ, Chattype::Group); + assert_eq!(get_chat_msgs(&alice, chat.id, 0).await.unwrap().len(), 1); + if group_request { + assert_eq!(get_chat_contacts(&alice, chat.id).await.unwrap().len(), 4); + } else { + assert_eq!(get_chat_contacts(&alice, chat.id).await.unwrap().len(), 3); + } + assert_eq!(msg.get_override_sender_name(), None); + + let claire = TestContext::new().await; + claire.configure_addr("claire@example.org").await; + claire + .set_config(Config::ShowEmails, Some("2")) + .await + .unwrap(); + receive_imf(&claire, claire_request.as_bytes(), false) + .await + .unwrap(); + + let msg_id = rfc724_mid_exists(&claire, "non-dc-1@example.org") + .await + .unwrap() + .unwrap(); + + let msg = Message::load_from_db(&claire, msg_id).await.unwrap(); + msg.chat_id.accept(&claire).await.unwrap(); + assert_eq!(msg.get_subject(), "i have a question"); + assert!(msg.get_text().unwrap().contains("hi support!")); + let chat = Chat::load_from_db(&claire, msg.chat_id).await.unwrap(); + if group_request { + assert_eq!(chat.typ, Chattype::Group); + } else { + assert_eq!(chat.typ, Chattype::Single); + } + assert_eq!(get_chat_msgs(&claire, chat.id, 0).await.unwrap().len(), 1); + assert_eq!(msg.get_override_sender_name(), None); + + (claire, alice) +} + +async fn check_alias_reply(reply: &[u8], chat_request: bool, group_request: bool) { + let (claire, alice) = create_test_alias(chat_request, group_request).await; + + // Check that Alice gets the message in the same chat. + let request = alice.get_last_msg().await; + receive_imf(&alice, reply, false).await.unwrap(); + let answer = alice.get_last_msg().await; + assert_eq!(answer.get_subject(), "Re: i have a question"); + assert!(answer.get_text().unwrap().contains("the version is 1.0")); + assert_eq!(answer.chat_id, request.chat_id); + let chat_contacts = get_chat_contacts(&alice, answer.chat_id) + .await + .unwrap() + .len(); + if group_request { + // Claire, Support, CEO and Alice (Bob is not added) + assert_eq!(chat_contacts, 4); + } else { + // Claire, Support and Alice + assert_eq!(chat_contacts, 3); + } + assert_eq!( + answer.get_override_sender_name().unwrap(), + "bob@example.net" + ); // Bob is not part of the group, so override-sender-name should be set + + // Check that Claire also gets the message in the same chat. + let request = claire.get_last_msg().await; + receive_imf(&claire, reply, false).await.unwrap(); + let answer = claire.get_last_msg().await; + assert_eq!(answer.get_subject(), "Re: i have a question"); + assert!(answer.get_text().unwrap().contains("the version is 1.0")); + assert_eq!(answer.chat_id, request.chat_id); + assert_eq!( + answer.get_override_sender_name().unwrap(), + "bob@example.net" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_alias_support_answer_from_nondc() { + // Bob, the other supporter, answers with a classic MUA. + let bob_answer = b"To: support@example.org, claire@example.org\n\ + From: bob@example.net\n\ + Subject: =?utf-8?q?Re=3A_i_have_a_question?=\n\ + References: \n\ + In-Reply-To: \n\ + Message-ID: \n\ + Date: Sun, 14 Mar 2021 16:04:57 +0000\n\ + Content-Type: text/plain\n\ + \n\ + hi claire, the version is 1.0, cheers bob"; + + check_alias_reply(bob_answer, true, true).await; + check_alias_reply(bob_answer, false, true).await; + check_alias_reply(bob_answer, true, false).await; + check_alias_reply(bob_answer, false, false).await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_alias_answer_from_dc() { + // Bob, the other supporter, answers with Delta Chat. + let bob_answer = b"To: support@example.org, claire@example.org\n\ + From: bob@example.net\n\ + Subject: =?utf-8?q?Re=3A_i_have_a_question?=\n\ + References: \n\ + In-Reply-To: \n\ + Message-ID: \n\ + Date: Sun, 14 Mar 2021 16:04:57 +0000\n\ + Chat-Version: 1.0\n\ + Chat-Group-ID: af9e810c9b592927\n\ + Chat-Group-Name: =?utf-8?q?i_have_a_question?=\n\ + Chat-Disposition-Notification-To: bob@example.net\n\ + Content-Type: text/plain\n\ + \n\ + hi claire, the version is 1.0, cheers bob"; + + check_alias_reply(bob_answer, true, true).await; + check_alias_reply(bob_answer, false, true).await; + check_alias_reply(bob_answer, true, false).await; + check_alias_reply(bob_answer, false, false).await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_dont_assign_to_trash_by_parent() { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); + println!("\n========= Receive a message =========="); + receive_imf( + &t, + b"From: Nu Bar \n\ + To: alice@example.org, bob@example.org\n\ + Subject: Hi\n\ + Message-ID: <4444@example.org>\n\ + \n\ + hello\n", + false, + ) + .await + .unwrap(); + let chat_id = t.get_last_msg().await.chat_id; + chat_id.accept(&t).await.unwrap(); + let msg = get_chat_msg(&t, chat_id, 0, 1).await; // Make sure that the message is actually in the chat + assert!(!msg.chat_id.is_special()); + assert_eq!(msg.text.unwrap(), "Hi – hello"); + + println!("\n========= Delete the message =========="); + msg.id.trash(&t).await.unwrap(); + + let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0).await.unwrap(); + assert_eq!(msgs.len(), 0); + + println!("\n========= Receive a message that is a reply to the deleted message =========="); + receive_imf( + &t, + b"From: Nu Bar \n\ + To: alice@example.org, bob@example.org\n\ + Subject: Re: Hi\n\ + Message-ID: <5555@example.org>\n\ + In-Reply-To: <4444@example.org\n\ + \n\ + Reply\n", + false, + ) + .await + .unwrap(); + let msg = t.get_last_msg().await; + assert!(!msg.chat_id.is_special()); // Esp. check that the chat_id is not TRASH + assert_eq!(msg.text.unwrap(), "Reply"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_dont_show_all_outgoing_msgs_in_self_chat() { + // Regression test for : + // Some servers add a `Bcc: ` header, which caused all outgoing messages to + // be shown in the self-chat. + let t = TestContext::new_alice().await; + + receive_imf( + &t, + b"Bcc: alice@example.org +Received: from [127.0.0.1] +Subject: s +Chat-Version: 1.0 +Message-ID: +To: +From: + +Message content", + false, + ) + .await + .unwrap(); + + let msg = t.get_last_msg().await; + assert_ne!(msg.chat_id, t.get_self_chat().await.id); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_outgoing_classic_mail_creates_chat() { + let alice = TestContext::new_alice().await; + + // Alice enables classic emails. + alice + .set_config(Config::ShowEmails, Some("2")) + .await + .unwrap(); + + // Alice downloads outgoing classic email. + receive_imf( + &alice, + b"Received: from [127.0.0.1] +Subject: Subj +Message-ID: +To: +From: + +Message content", + false, + ) + .await + .unwrap(); + + // Outgoing email should create a chat. + let msg = alice.get_last_msg().await; + assert_eq!(msg.get_text().unwrap(), "Subj – Message content"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_duplicate_message() -> Result<()> { + // Test that duplicate messages are ignored based on the Message-ID + let alice = TestContext::new_alice().await; + + let bob_contact_id = Contact::add_or_lookup( + &alice, + "Bob", + "bob@example.org", + Origin::IncomingUnknownFrom, + ) + .await? + .0; + + let first_message = b"Received: from [127.0.0.1] +Subject: First message +Message-ID: +To: Alice +From: Bob1 +Chat-Version: 1.0 + +Message content + +-- +First signature"; + + let second_message = b"Received: from [127.0.0.1] +Subject: Second message +Message-ID: +To: Alice +From: Bob2 +Chat-Version: 1.0 + +Message content + +-- +Second signature"; + + receive_imf(&alice, first_message, false).await?; + let contact = Contact::load_from_db(&alice, bob_contact_id).await?; + assert_eq!(contact.get_status(), "First signature"); + assert_eq!(contact.get_display_name(), "Bob1"); + + receive_imf(&alice, second_message, false).await?; + let contact = Contact::load_from_db(&alice, bob_contact_id).await?; + assert_eq!(contact.get_status(), "Second signature"); + assert_eq!(contact.get_display_name(), "Bob2"); + + // Duplicate message, should be ignored + receive_imf(&alice, first_message, false).await?; + + // No change because last message is duplicate of the first. + let contact = Contact::load_from_db(&alice, bob_contact_id).await?; + assert_eq!(contact.get_status(), "Second signature"); + assert_eq!(contact.get_display_name(), "Bob2"); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_ignore_footer_status_from_mailinglist() -> Result<()> { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await?; + let bob_id = Contact::add_or_lookup(&t, "", "bob@example.net", Origin::IncomingUnknownCc) + .await? + .0; + let bob = Contact::load_from_db(&t, bob_id).await?; + assert_eq!(bob.get_status(), ""); + assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 0); + + receive_imf( + &t, + b"From: Bob +To: Alice +Message-ID: <1@example.org> +Subject: first message + +body 1 + +-- +Original signature", + false, + ) + .await?; + let one2one_chat_id = t.get_last_msg().await.chat_id; + let bob = Contact::load_from_db(&t, bob_id).await?; + assert_eq!(bob.get_status(), "Original signature"); + + receive_imf( + &t, + b"From: Bob +Sender: ml@example.net +To: Alice +Message-ID: <2@example.net> +Precedence: list +Subject: second message + +body 2 + +-- +The modified signature +-- +Tap here to unsubscribe ...", + false, + ) + .await?; + let ml_chat_id = t.get_last_msg().await.chat_id; + let bob = Contact::load_from_db(&t, bob_id).await?; + assert_eq!(bob.get_status(), "Original signature"); + + receive_imf( + &t, + b"From: Bob +To: Alice +Message-ID: <3@example.org> +Subject: third message + +body 3 + +-- +Original signature updated", + false, + ) + .await?; + let bob = Contact::load_from_db(&t, bob_id).await?; + assert_eq!(bob.get_status(), "Original signature updated"); + assert_eq!(get_chat_msgs(&t, one2one_chat_id, 0).await?.len(), 2); + assert_eq!(get_chat_msgs(&t, ml_chat_id, 0).await?.len(), 1); + assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 2); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_chat_assignment_private_classical_reply() { + for outgoing_is_classical in &[true, false] { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); + + receive_imf( + &t, + format!( + r#"Received: from mout.gmx.net (mout.gmx.net [212.227.17.22]) +Subject: =?utf-8?q?single_reply-to?= +{} +Date: Fri, 28 May 2021 10:15:05 +0000 +To: Bob , +From: Alice +Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no +Content-Transfer-Encoding: quoted-printable + +Hello, I've just created the group "single reply-to" for us."#, + if *outgoing_is_classical { + r"Message-ID: abcd@gmx.de" + } else { + r"Chat-Group-ID: eJ_llQIXf0K +Chat-Group-Name: =?utf-8?q?single_reply-to?= +References: +Chat-Version: 1.0 +Message-ID: " + } + ) + .as_bytes(), + false, + ) + .await + .unwrap(); + + let group_msg = t.get_last_msg().await; + assert_eq!( + group_msg.text.unwrap(), + if *outgoing_is_classical { + "single reply-to – Hello, I\'ve just created the group \"single reply-to\" for us." + } else { + "Hello, I've just created the group \"single reply-to\" for us." + } + ); + let group_chat = Chat::load_from_db(&t, group_msg.chat_id).await.unwrap(); + assert_eq!(group_chat.typ, Chattype::Group); + assert_eq!(group_chat.name, "single reply-to"); + + receive_imf( + &t, + format!( + r#"Subject: Re: single reply-to +To: "Alice" +References: <{0}> + <{0}> +From: Bob +Message-ID: <028674eb-77f9-4ad1-1c30-e93e18b891c8@testrun.org> +Date: Fri, 28 May 2021 12:17:03 +0200 +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 + Thunderbird/78.10.2 +MIME-Version: 1.0 +In-Reply-To: <{0}> + +Private reply"#, + if *outgoing_is_classical { + "abcd@gmx.de" + } else { + "Gr.eJ_llQIXf0K.buxmrnMmG0Y@gmx.de" + } + ) + .as_bytes(), + false, + ) + .await + .unwrap(); + + let private_msg = t.get_last_msg().await; + assert_eq!(private_msg.text.unwrap(), "Private reply"); + let private_chat = Chat::load_from_db(&t, private_msg.chat_id).await.unwrap(); + assert_eq!(private_chat.typ, Chattype::Single); + assert_ne!(private_msg.chat_id, group_msg.chat_id); + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_chat_assignment_private_chat_reply() { + for (outgoing_is_classical, outgoing_has_multiple_recipients) in + &[(true, true), (false, true), (false, false)] + { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); + + receive_imf( + &t, + format!( + r#"Received: from mout.gmx.net (mout.gmx.net [212.227.17.22]) +Subject: =?utf-8?q?single_reply-to?= +{} +Date: Fri, 28 May 2021 10:15:05 +0000 +To: Bob {} +From: Alice +Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no +Content-Transfer-Encoding: quoted-printable + +Hello, I've just created the group "single reply-to" for us."#, + if *outgoing_is_classical { + r"Message-ID: abcd@gmx.de" + } else { + r"Chat-Group-ID: eJ_llQIXf0K +Chat-Group-Name: =?utf-8?q?single_reply-to?= +References: +Chat-Version: 1.0 +Message-ID: " + }, + if *outgoing_has_multiple_recipients { + ", " + } else { + "" + } + ) + .as_bytes(), + false, + ) + .await + .unwrap(); + let group_msg = t.get_last_msg().await; + assert_eq!( + group_msg.text.unwrap(), + if *outgoing_is_classical { + "single reply-to – Hello, I\'ve just created the group \"single reply-to\" for us." + } else { + "Hello, I've just created the group \"single reply-to\" for us." + } + ); + let group_chat = Chat::load_from_db(&t, group_msg.chat_id).await.unwrap(); + assert_eq!(group_chat.typ, Chattype::Group); + assert_eq!(group_chat.name, "single reply-to"); + + receive_imf( + &t, + format!( + r#"Subject: =?utf-8?q?Re=3A_single_reply-to?= +MIME-Version: 1.0 +In-Reply-To: <{0}> +Date: Sat, 03 Jul 2021 20:00:26 +0000 +Chat-Version: 1.0 +Message-ID: +To: +From: +Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no +Content-Transfer-Encoding: quoted-printable + +> Hello, I've just created the group "single reply-to" for us. + +Private reply + +=2D- +Sent with my Delta Chat Messenger: https://delta.chat + +"#, + if *outgoing_is_classical { + "abcd@gmx.de" + } else { + "Gr.iy1KCE2y65_.mH2TM52miv9@testrun.org" + } + ) + .as_bytes(), + false, + ) + .await + .unwrap(); + + let private_msg = t.get_last_msg().await; + assert_eq!(private_msg.text.unwrap(), "Private reply"); + let private_chat = Chat::load_from_db(&t, private_msg.chat_id).await.unwrap(); + assert_eq!(private_chat.typ, Chattype::Single); + assert_ne!(private_msg.chat_id, group_msg.chat_id); + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_chat_assignment_nonprivate_classical_reply() { + for outgoing_is_classical in &[true, false] { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); + + receive_imf( + &t, + format!( + r#"Received: from mout.gmx.net (mout.gmx.net [212.227.17.22]) +Subject: =?utf-8?q?single_reply-to?= +{} +To: Bob , +From: Alice +Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no +Content-Transfer-Encoding: quoted-printable + +Hello, I've just created the group "single reply-to" for us."#, + if *outgoing_is_classical { + r"Message-ID: abcd@gmx.de" + } else { + r"Chat-Group-ID: eJ_llQIXf0K +Chat-Group-Name: =?utf-8?q?single_reply-to?= +References: +Chat-Version: 1.0 +Message-ID: " + } + ) + .as_bytes(), + false, + ) + .await + .unwrap(); + + let group_msg = t.get_last_msg().await; + assert_eq!( + group_msg.text.unwrap(), + if *outgoing_is_classical { + "single reply-to – Hello, I\'ve just created the group \"single reply-to\" for us." + } else { + "Hello, I've just created the group \"single reply-to\" for us." + } + ); + let group_chat = Chat::load_from_db(&t, group_msg.chat_id).await.unwrap(); + assert_eq!(group_chat.typ, Chattype::Group); + assert_eq!(group_chat.name, "single reply-to"); + + // =============== Receive another outgoing message and check that it is put into the same chat =============== + receive_imf( + &t, + format!( + r#"Received: from mout.gmx.net (mout.gmx.net [212.227.17.22]) +Subject: Out subj +To: "Bob" , "Claire" +From: Alice +Message-ID: +MIME-Version: 1.0 +In-Reply-To: <{0}> + +Outgoing reply to all"#, + if *outgoing_is_classical { + "abcd@gmx.de" + } else { + "Gr.eJ_llQIXf0K.buxmrnMmG0Y@gmx.de" + } + ) + .as_bytes(), + false, + ) + .await + .unwrap(); + + let reply = t.get_last_msg().await; + assert_eq!(reply.text.unwrap(), "Out subj – Outgoing reply to all"); + let reply_chat = Chat::load_from_db(&t, reply.chat_id).await.unwrap(); + assert_eq!(reply_chat.typ, Chattype::Group); + assert_eq!(reply.chat_id, group_msg.chat_id); + + // =============== Receive an incoming message and check that it is put into the same chat =============== + receive_imf( + &t, + br#"Received: from mout.gmx.net (mout.gmx.net [212.227.17.22]) +Subject: In subj +To: "Bob" , "Claire" +From: alice +Message-ID: +MIME-Version: 1.0 +In-Reply-To: + +Reply to all"#, + false, + ) + .await + .unwrap(); + + let reply = t.get_last_msg().await; + assert_eq!(reply.text.unwrap(), "In subj – Reply to all"); + let reply_chat = Chat::load_from_db(&t, reply.chat_id).await.unwrap(); + assert_eq!(reply_chat.typ, Chattype::Group); + assert_eq!(reply.chat_id, group_msg.chat_id); + } +} + +/// Tests that replies to similar ad hoc groups are correctly assigned to chats. +/// +/// The difficutly here is that ad hoc groups don't have unique group IDs, because both +/// messages have the same recipient lists and only differ in the subject and message contents. +/// The messages can be properly assigned to chats only using the In-Reply-To or References +/// headers. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_chat_assignment_adhoc() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + alice.set_config(Config::ShowEmails, Some("2")).await?; + bob.set_config(Config::ShowEmails, Some("2")).await?; + + let first_thread_mime = br#"Subject: First thread +Message-ID: first@example.org +To: Alice , Bob +From: Claire +Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no + +First thread."#; + let second_thread_mime = br#"Subject: Second thread +Message-ID: second@example.org +To: Alice , Bob +From: Claire +Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no + +Second thread."#; + + // Alice receives two classic emails from Claire. + receive_imf(&alice, first_thread_mime, false).await?; + let alice_first_msg = alice.get_last_msg().await; + receive_imf(&alice, second_thread_mime, false).await?; + let alice_second_msg = alice.get_last_msg().await; + + // Bob receives the same two emails. + receive_imf(&bob, first_thread_mime, false).await?; + let bob_first_msg = bob.get_last_msg().await; + receive_imf(&bob, second_thread_mime, false).await?; + let bob_second_msg = bob.get_last_msg().await; + + // Messages go to separate chats both for Alice and Bob. + assert!(alice_first_msg.chat_id != alice_second_msg.chat_id); + assert!(bob_first_msg.chat_id != bob_second_msg.chat_id); + + // Alice replies to both chats. Bob receives two messages and assigns them to corresponding + // chats. + alice_first_msg.chat_id.accept(&alice).await?; + let alice_first_reply = alice + .send_text(alice_first_msg.chat_id, "First reply") + .await; + let bob_first_reply = bob.recv_msg(&alice_first_reply).await; + assert_eq!(bob_first_reply.chat_id, bob_first_msg.chat_id); + + alice_second_msg.chat_id.accept(&alice).await?; + let alice_second_reply = alice + .send_text(alice_second_msg.chat_id, "Second reply") + .await; + let bob_second_reply = bob.recv_msg(&alice_second_reply).await; + assert_eq!(bob_second_reply.chat_id, bob_second_msg.chat_id); + + // Alice adds Fiona to both ad hoc groups. + let fiona = TestContext::new_fiona().await; + let (alice_fiona_contact_id, _) = Contact::add_or_lookup( + &alice, + "Fiona", + "fiona@example.net", + Origin::IncomingUnknownTo, + ) + .await?; + + chat::add_contact_to_chat(&alice, alice_first_msg.chat_id, alice_fiona_contact_id).await?; + let alice_first_invite = alice.pop_sent_msg().await; + let fiona_first_invite = fiona.recv_msg(&alice_first_invite).await; + + chat::add_contact_to_chat(&alice, alice_second_msg.chat_id, alice_fiona_contact_id).await?; + let alice_second_invite = alice.pop_sent_msg().await; + let fiona_second_invite = fiona.recv_msg(&alice_second_invite).await; + + // Fiona was added to two separate chats and should see two separate chats, even though they + // don't have different group IDs to distinguish them. + assert!(fiona_first_invite.chat_id != fiona_second_invite.chat_id); + + Ok(()) +} + +/// Test that read receipts don't create chats. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_read_receipts_dont_create_chats() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let alice_chat = alice.create_chat(&bob).await; + + // Alice sends a message to Bob. + assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0); + bob.recv_msg(&alice.send_text(alice_chat.id, "Message").await) + .await; + let received_msg = bob.get_last_msg().await; + + // Alice deletes the chat. + alice_chat.id.delete(&alice).await?; + let chats = Chatlist::try_load(&alice, 0, None, None).await?; + assert_eq!(chats.len(), 0); + + // Bob sends a read receipt. + let mdn_mimefactory = + crate::mimefactory::MimeFactory::from_mdn(&bob, &received_msg, vec![]).await?; + let rendered_mdn = mdn_mimefactory.render(&bob).await?; + let mdn_body = rendered_mdn.message; + + // Alice receives the read receipt. + receive_imf(&alice, mdn_body.as_bytes(), false).await?; + + // Chat should not pop up in the chatlist. + let chats = Chatlist::try_load(&alice, 0, None, None).await?; + assert_eq!(chats.len(), 0); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_gmx_forwarded_msg() -> Result<()> { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await?; + + receive_imf( + &t, + include_bytes!("../../test-data/message/gmx-forward.eml"), + false, + ) + .await?; + + let msg = t.get_last_msg().await; + assert!(msg.has_html()); + assert_eq!(msg.id.get_html(&t).await?.unwrap().replace("\r\n", "\n"), "
 
\n\n
 \n
 \n
\n
Gesendet: Donnerstag, 12. August 2021 um 15:52 Uhr
\nVon: "Claire" <claire@example.org>
\nAn: alice@example.org
\nBetreff: subject
\n\n
bodytext
\n
\n
\n
\n\n"); + + Ok(()) +} + +/// Tests that user is notified about new incoming contact requests. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_incoming_contact_request() -> Result<()> { + let t = TestContext::new_alice().await; + + receive_imf(&t, MSGRMSG, false).await?; + let msg = t.get_last_msg().await; + let chat = chat::Chat::load_from_db(&t, msg.chat_id).await?; + assert!(chat.is_contact_request()); + + loop { + let event = t + .evtracker + .get_matching(|evt| matches!(evt, EventType::IncomingMsg { .. })) + .await; + match event { + EventType::IncomingMsg { chat_id, msg_id } => { + assert_eq!(msg.chat_id, chat_id); + assert_eq!(msg.id, msg_id); + return Ok(()); + } + _ => unreachable!(), + } + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_parent_message() -> Result<()> { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await?; + + let mime = br#"Subject: First +Message-ID: first@example.net +To: Alice +From: Bob +Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no + +First."#; + receive_imf(&t, mime, false).await?; + let first = t.get_last_msg().await; + let mime = br#"Subject: Second +Message-ID: second@example.net +To: Alice +From: Bob +Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no + +First."#; + receive_imf(&t, mime, false).await?; + let second = t.get_last_msg().await; + let mime = br#"Subject: Third +Message-ID: third@example.net +To: Alice +From: Bob +Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no + +First."#; + receive_imf(&t, mime, false).await?; + let third = t.get_last_msg().await; + + let mime = br#"Subject: Message with references. +Message-ID: second@example.net +To: Alice +From: Bob +In-Reply-To: +References: +Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no + +Message with references."#; + let mime_parser = MimeMessage::from_bytes(&t, &mime[..]).await?; + + let parent = get_parent_message(&t, &mime_parser).await?.unwrap(); + assert_eq!(parent.id, first.id); + + message::delete_msgs(&t, &[first.id]).await?; + let parent = get_parent_message(&t, &mime_parser).await?.unwrap(); + assert_eq!(parent.id, second.id); + + message::delete_msgs(&t, &[second.id]).await?; + let parent = get_parent_message(&t, &mime_parser).await?.unwrap(); + assert_eq!(parent.id, third.id); + + message::delete_msgs(&t, &[third.id]).await?; + let parent = get_parent_message(&t, &mime_parser).await?; + assert!(parent.is_none()); + + Ok(()) +} + +/// Test a message with RFC 1847 encapsulation as created by Thunderbird. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_rfc1847_encapsulation() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + alice.configure_addr("alice@example.org").await; + + // Alice sends an Autocrypt message to Bob so Bob gets Alice's key. + let chat_alice = alice.create_chat(&bob).await; + let first_msg = alice + .send_text(chat_alice.id, "Sending Alice key to Bob.") + .await; + bob.recv_msg(&first_msg).await; + message::delete_msgs(&bob, &[bob.get_last_msg().await.id]).await?; + + bob.set_config(Config::ShowEmails, Some("2")).await?; + + // Alice sends a message to Bob using Thunderbird. + let raw = include_bytes!("../../test-data/message/rfc1847_encapsulation.eml"); + receive_imf(&bob, raw, false).await?; + + let msg = bob.get_last_msg().await; + assert!(msg.get_showpadlock()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_invalid_to_address() -> Result<()> { + let alice = TestContext::new_alice().await; + + let mime = include_bytes!("../../test-data/message/invalid_email_to.eml"); + + // receive_imf should not fail on this mail with invalid To: field + receive_imf(&alice, mime, false).await?; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_reply_from_different_addr() -> Result<()> { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await?; + + // Alice creates a 2-person-group with Bob + receive_imf( + &t, + br#"Subject: =?utf-8?q?Januar_13-19?= +Chat-Group-ID: qetqsutor7a +Chat-Group-Name: =?utf-8?q?Januar_13-19?= +MIME-Version: 1.0 +References: +Date: Mon, 20 Dec 2021 12:15:01 +0000 +Chat-Version: 1.0 +Message-ID: +To: +From: +Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no + +Hi, I created a group"#, + false, + ) + .await?; + let msg_out = t.get_last_msg().await; + assert_eq!(msg_out.from_id, ContactId::SELF); + assert_eq!(msg_out.text.unwrap(), "Hi, I created a group"); + assert_eq!(msg_out.in_reply_to, None); + + // Bob replies from a different address + receive_imf( + &t, + b"Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: quoted-printable +From: +Mime-Version: 1.0 (1.0) +Subject: Re: Januar 13-19 +Date: Mon, 20 Dec 2021 13:54:55 +0100 +Message-Id: +References: +In-Reply-To: +To: holger + +Reply from different address +", + false, + ) + .await?; + let msg_in = t.get_last_msg().await; + assert_eq!(msg_in.to_id, ContactId::SELF); + assert_eq!(msg_in.text.unwrap(), "Reply from different address"); + assert_eq!( + msg_in.in_reply_to.unwrap(), + "Gr.qetqsutor7a.Aresxresy-4@deltachat.de" + ); + assert_eq!( + msg_in.param.get(Param::OverrideSenderDisplayname), + Some("bob-alias@example.com") + ); + + assert_eq!(msg_in.chat_id, msg_out.chat_id); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_long_filenames() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + + for filename_sent in &[ + "foo.bar very long file name test baz.tar.gz", + "foobarabababababababbababababverylongfilenametestbaz.tar.gz", + "fooo...tar.gz", + "foo. .tar.gz", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.tar.gz", + "a.tar.gz", + "a.a..a.a.a.a.tar.gz", + ] { + let attachment = alice.blobdir.join(filename_sent); + let content = format!("File content of {}", filename_sent); + tokio::fs::write(&attachment, content.as_bytes()).await?; + + let mut msg_alice = Message::new(Viewtype::File); + msg_alice.set_file(attachment.to_str().unwrap(), None); + let alice_chat = alice.create_chat(&bob).await; + let sent = alice.send_msg(alice_chat.id, &mut msg_alice).await; + println!("{}", sent.payload()); + + let msg_bob = bob.recv_msg(&sent).await; + + async fn check_message(msg: &Message, t: &TestContext, content: &str) { + assert_eq!(msg.get_viewtype(), Viewtype::File); + let resulting_filename = msg.get_filename().unwrap(); + let path = msg.get_file(t).unwrap(); + assert!( + resulting_filename.ends_with(".tar.gz"), + "{:?} doesn't end with .tar.gz, path: {:?}", + resulting_filename, + path + ); + assert!( + path.to_str().unwrap().ends_with(".tar.gz"), + "path {:?} doesn't end with .tar.gz", + path + ); + assert_eq!(fs::read_to_string(path).await.unwrap(), content); + } + check_message(&msg_alice, &alice, &content).await; + check_message(&msg_bob, &bob, &content).await; + } + + Ok(()) +} + +/// Tests that contact request is accepted automatically on outgoing message. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_accept_outgoing() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice1 = tcm.alice().await; + let alice2 = tcm.alice().await; + let bob1 = tcm.bob().await; + let bob2 = tcm.bob().await; + + let bob1_chat = bob1.create_chat(&alice1).await; + let sent = bob1.send_text(bob1_chat.id, "Hello!").await; + + alice1.recv_msg(&sent).await; + alice2.recv_msg(&sent).await; + let alice1_msg = bob2.recv_msg(&sent).await; + assert_eq!(alice1_msg.text.unwrap(), "Hello!"); + let alice1_chat = chat::Chat::load_from_db(&alice1, alice1_msg.chat_id).await?; + assert!(alice1_chat.is_contact_request()); + + let alice2_msg = alice2.get_last_msg().await; + assert_eq!(alice2_msg.text.unwrap(), "Hello!"); + let alice2_chat = chat::Chat::load_from_db(&alice2, alice2_msg.chat_id).await?; + assert!(alice2_chat.is_contact_request()); + + let bob1_msg = bob1.get_last_msg().await; + assert_eq!(bob1_msg.text.unwrap(), "Hello!"); + let bob1_chat = chat::Chat::load_from_db(&bob1, bob1_msg.chat_id).await?; + assert!(!bob1_chat.is_contact_request()); + + let bob2_msg = bob2.get_last_msg().await; + assert_eq!(bob2_msg.text.unwrap(), "Hello!"); + let bob2_chat = chat::Chat::load_from_db(&bob2, bob2_msg.chat_id).await?; + assert!(!bob2_chat.is_contact_request()); + + // Alice sends reply. + alice1_msg.chat_id.accept(&alice1).await.unwrap(); + let sent = alice1.send_text(alice1_chat.id, "Hi!").await; + alice2.recv_msg(&sent).await; + + // Second device automatically accepts the contact request. + let alice2_chat = chat::Chat::load_from_db(&alice2, alice2_msg.chat_id).await?; + assert!(!alice2_chat.is_contact_request()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_outgoing_private_reply_multidevice() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice1 = tcm.alice().await; + let alice2 = tcm.alice().await; + let bob = tcm.bob().await; + + // =============== Bob creates a group =============== + let group_id = chat::create_group_chat(&bob, ProtectionStatus::Unprotected, "Group").await?; + chat::add_to_chat_contacts_table( + &bob, + group_id, + &[ + bob.add_or_lookup_contact(&alice1).await.id, + Contact::create(&bob, "", "charlie@example.org").await?, + ], + ) + .await?; + + // =============== Bob sends the first message to the group =============== + let sent = bob.send_text(group_id, "Hello all!").await; + alice1.recv_msg(&sent).await; + alice2.recv_msg(&sent).await; + + // =============== Alice answers privately with device 1 =============== + let received = alice1.get_last_msg().await; + let alice1_bob_contact = alice1.add_or_lookup_contact(&bob).await; + assert_eq!(received.from_id, alice1_bob_contact.id); + assert_eq!(received.to_id, ContactId::SELF); + assert!(!received.hidden); + assert_eq!(received.text, Some("Hello all!".to_string())); + assert_eq!(received.in_reply_to, None); + assert_eq!(received.chat_blocked, Blocked::Request); + + let received_group = Chat::load_from_db(&alice1, received.chat_id).await?; + assert_eq!(received_group.typ, Chattype::Group); + assert_eq!(received_group.name, "Group"); + assert_eq!(received_group.can_send(&alice1).await?, false); // Can't send because it's Blocked::Request + + let mut msg_out = Message::new(Viewtype::Text); + msg_out.set_text(Some("Private reply".to_string())); + + assert_eq!(received_group.blocked, Blocked::Request); + msg_out.set_quote(&alice1, Some(&received)).await?; + let alice1_bob_chat = alice1.create_chat(&bob).await; + let sent2 = alice1.send_msg(alice1_bob_chat.id, &mut msg_out).await; + alice2.recv_msg(&sent2).await; + + // =============== Alice's second device receives the message =============== + let received = alice2.get_last_msg().await; + + // That's a regression test for https://github.com/deltachat/deltachat-core-rust/issues/2949: + assert_eq!(received.chat_id, alice2.get_chat(&bob).await.unwrap().id); + + let alice2_bob_contact = alice2.add_or_lookup_contact(&bob).await; + assert_eq!(received.from_id, ContactId::SELF); + assert_eq!(received.to_id, alice2_bob_contact.id); + assert!(!received.hidden); + assert_eq!(received.text, Some("Private reply".to_string())); + assert_eq!( + received.parent(&alice2).await?.unwrap().text, + Some("Hello all!".to_string()) + ); + assert_eq!(received.chat_blocked, Blocked::Not); + + let received_chat = Chat::load_from_db(&alice2, received.chat_id).await?; + assert_eq!(received_chat.typ, Chattype::Single); + assert_eq!(received_chat.name, "bob@example.net"); + assert_eq!(received_chat.can_send(&alice2).await?, true); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_auto_accept_for_bots() -> Result<()> { + let t = TestContext::new_alice().await; + t.set_config(Config::Bot, Some("1")).await.unwrap(); + receive_imf(&t, MSGRMSG, false).await?; + let msg = t.get_last_msg().await; + let chat = chat::Chat::load_from_db(&t, msg.chat_id).await?; + assert!(!chat.is_contact_request()); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_no_private_reply_to_blocked_account() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + + // =============== Bob creates a group =============== + let group_id = chat::create_group_chat(&bob, ProtectionStatus::Unprotected, "Group").await?; + chat::add_to_chat_contacts_table( + &bob, + group_id, + &[bob.add_or_lookup_contact(&alice).await.id], + ) + .await?; + + // =============== Bob sends the first message to the group =============== + let sent = bob.send_text(group_id, "Hello all!").await; + alice.recv_msg(&sent).await; + + let chats = Chatlist::try_load(&bob, 0, None, None).await?; + assert_eq!(chats.len(), 1); + + // =============== Bob blocks Alice ================ + Contact::block(&bob, bob.add_or_lookup_contact(&alice).await.id).await?; + + // =============== Alice replies private to Bob ============== + let received = alice.get_last_msg().await; + assert_eq!(received.text, Some("Hello all!".to_string())); + + let received_group = Chat::load_from_db(&alice, received.chat_id).await?; + assert_eq!(received_group.typ, Chattype::Group); + + let mut msg_out = Message::new(Viewtype::Text); + msg_out.set_text(Some("Private reply".to_string())); + msg_out.set_quote(&alice, Some(&received)).await?; + + let alice_bob_chat = alice.create_chat(&bob).await; + let sent2 = alice.send_msg(alice_bob_chat.id, &mut msg_out).await; + bob.recv_msg(&sent2).await; + + // ========= check that no contact request was created ============ + let chats = Chatlist::try_load(&bob, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); + let chat_id = chats.get_chat_id(0).unwrap(); + let chat = Chat::load_from_db(&bob, chat_id).await.unwrap(); + + // since only chat is a group, no new open chat has been created + assert_eq!(chat.typ, Chattype::Group); + let received = bob.get_last_msg().await; + assert_eq!(received.text, Some("Hello all!".to_string())); + + // =============== Bob unblocks Alice ================ + // test if the blocked chat is restored correctly + Contact::unblock(&bob, bob.add_or_lookup_contact(&alice).await.id).await?; + let chats = Chatlist::try_load(&bob, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 2); + let chat_id = chats.get_chat_id(0).unwrap(); + let chat = Chat::load_from_db(&bob, chat_id).await.unwrap(); + assert_eq!(chat.typ, Chattype::Single); + let received = bob.get_last_msg().await; + assert_eq!(received.text, Some("Private reply".to_string())); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_thunderbird_autocrypt() -> Result<()> { + let t = TestContext::new_bob().await; + t.set_config(Config::ShowEmails, Some("2")).await?; + + let raw = include_bytes!("../../test-data/message/thunderbird_with_autocrypt.eml"); + receive_imf(&t, raw, false).await?; + + let peerstate = Peerstate::from_addr(&t, "alice@example.org") + .await? + .unwrap(); + assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_thunderbird_autocrypt_unencrypted() -> Result<()> { + let t = TestContext::new_bob().await; + t.set_config(Config::ShowEmails, Some("2")).await?; + + let raw = include_bytes!("../../test-data/message/thunderbird_with_autocrypt_unencrypted.eml"); + receive_imf(&t, raw, false).await?; + let peerstate = Peerstate::from_addr(&t, "alice@example.org") + .await? + .unwrap(); + assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual); + + let raw = include_bytes!("../../test-data/message/thunderbird_signed_unencrypted.eml"); + receive_imf(&t, raw, false).await?; + let peerstate = Peerstate::from_addr(&t, "alice@example.org") + .await? + .unwrap(); + assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mua_user_adds_member() -> Result<()> { + let t = TestContext::new_alice().await; + + receive_imf( + &t, + b"From: alice@example.org\n\ + To: bob@example.com\n\ + Subject: foo\n\ + Message-ID: \n\ + Chat-Version: 1.0\n\ + Chat-Group-ID: gggroupiddd\n\ + Chat-Group-Name: foo\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", + false, + ) + .await? + .unwrap(); + + receive_imf( + &t, + b"From: bob@example.com\n\ + To: alice@example.org, fiona@example.net\n\ + Subject: foo\n\ + Message-ID: \n\ + In-Reply-To: Gr.gggroupiddd.12345678901@example.com\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", + false, + ) + .await? + .unwrap(); + + let (chat_id, _, _) = chat::get_chat_id_by_grpid(&t, "gggroupiddd") + .await? + .unwrap(); + let mut actual_chat_contacts = chat::get_chat_contacts(&t, chat_id).await?; + actual_chat_contacts.sort(); + let mut expected_chat_contacts = vec![ + Contact::create(&t, "", "bob@example.com").await?, + Contact::create(&t, "", "fiona@example.net").await?, + ContactId::SELF, + ]; + expected_chat_contacts.sort(); + assert_eq!(actual_chat_contacts, expected_chat_contacts); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mua_user_adds_recipient_to_single_chat() -> Result<()> { + let alice = TestContext::new_alice().await; + + // Alice sends a 1:1 message to Bob, creating a 1:1 chat. + let msg = receive_imf( + &alice, + b"Subject: =?utf-8?q?Message_from_alice=40example=2Eorg?=\r\n\ + From: alice@example.org\r\n\ + To: \r\n\ + Date: Mon, 12 Dec 2022 14:30:39 +0000\r\n\ + Message-ID: \r\n\ + Chat-Version: 1.0\r\n\ + \r\n\ + tst\r\n", + false, + ) + .await? + .unwrap(); + let single_chat = Chat::load_from_db(&alice, msg.chat_id).await?; + assert_eq!(single_chat.typ, Chattype::Single); + + // Bob uses a classical MUA to answer in the 1:1 chat. + let msg2 = receive_imf( + &alice, + b"Subject: Re: Message from alice\r\n\ + From: \r\n\ + To: \r\n\ + Date: Mon, 12 Dec 2022 14:31:39 +0000\r\n\ + Message-ID: \r\n\ + In-Reply-To: \r\n\ + \r\n\ + Hi back!\r\n", + false, + ) + .await? + .unwrap(); + assert_eq!(msg2.chat_id, single_chat.id); + + // Bob uses a classical MUA to answer again, this time adding a recipient. + // This message should go to a newly created ad-hoc group. + let msg3 = receive_imf( + &alice, + b"Subject: Re: Message from alice\r\n\ + From: \r\n\ + To: , \r\n\ + Date: Mon, 12 Dec 2022 14:32:39 +0000\r\n\ + Message-ID: \r\n\ + In-Reply-To: \r\n\ + \r\n\ + Hi back!\r\n", + false, + ) + .await? + .unwrap(); + assert_ne!(msg3.chat_id, single_chat.id); + let group_chat = Chat::load_from_db(&alice, msg3.chat_id).await?; + assert_eq!(group_chat.typ, Chattype::Group); + assert_eq!( + chat::get_chat_contacts(&alice, group_chat.id).await?.len(), + 3 + ); + + // Bob uses a classical MUA to answer once more, adding another recipient. + // This new recipient should also be added to the group. + let msg4 = receive_imf( + &alice, + b"Subject: Re: Message from alice\r\n\ + From: \r\n\ + To: , , \r\n\ + Date: Mon, 12 Dec 2022 14:33:39 +0000\r\n\ + Message-ID: <69573857-542f-0fx3-55da-1289be5e0efe@example.net>\r\n\ + In-Reply-To: \r\n\ + \r\n\ + Hi back!\r\n", + false, + ) + .await? + .unwrap(); + assert_eq!(msg4.chat_id, group_chat.id); + assert_eq!( + chat::get_chat_contacts(&alice, group_chat.id).await?.len(), + 4 + ); + let fiona = Contact::lookup_id_by_addr(&alice, "fiona@example.net", Origin::IncomingTo) + .await? + .unwrap(); + assert!(chat::is_contact_in_chat(&alice, group_chat.id, fiona).await?); + + Ok(()) +} From ba82ce2798cfdb8f94a47c0340a89c92865df5e5 Mon Sep 17 00:00:00 2001 From: iequidoo Date: Tue, 20 Dec 2022 16:00:33 -0300 Subject: [PATCH 016/132] Validate signatures in try_decrypt() even if the message isn't encrypted (#3844) This way we don't need a separate code path for signatures validation for unencrypted messages. Also, now we degrade encryption only if there are no valid signatures, so the code for upgrading encryption back isn't needed. --- CHANGELOG.md | 1 + src/decrypt.rs | 33 ++++--- src/mimeparser.rs | 227 ++++++++++++++++++---------------------------- 3 files changed, 107 insertions(+), 154 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 575e7d3e8..7b55c99f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased ### Changes +- Validate signatures in try_decrypt() even if the message isn't encrypted #3859 ### API-Changes diff --git a/src/decrypt.rs b/src/decrypt.rs index c01be46ce..ae8e16ade 100644 --- a/src/decrypt.rs +++ b/src/decrypt.rs @@ -18,18 +18,17 @@ use crate::peerstate::Peerstate; use crate::pgp; /// Tries to decrypt a message, but only if it is structured as an -/// Autocrypt message. +/// Autocrypt message, otherwise just validates signatures. /// -/// Returns decrypted body and a set of valid signature fingerprints -/// if successful. +/// If successful and the message is encrypted or signed, returns \[decrypted\] body, a set of valid +/// signature fingerprints and whether the message is encrypted. /// -/// If the message is wrongly signed, this will still return the decrypted -/// message but the HashSet will be empty. +/// If the message is wrongly signed, HashSet will be empty. pub async fn try_decrypt( context: &Context, mail: &ParsedMail<'_>, decryption_info: &DecryptionInfo, -) -> Result, HashSet)>> { +) -> Result, HashSet, bool)>> { // Possibly perform decryption let public_keyring_for_validate = keyring_from_peerstate(decryption_info.peerstate.as_ref()); @@ -38,8 +37,10 @@ pub async fn try_decrypt( .or_else(|| get_attachment_mime(mail)) { None => { - // not an autocrypt mime message, abort and ignore - return Ok(None); + return Ok( + validate_detached_signature(mail, &public_keyring_for_validate)? + .map(|(raw, fprints)| (raw, fprints, false)), + ) } Some(res) => res, }; @@ -48,12 +49,13 @@ pub async fn try_decrypt( .await .context("failed to get own keyring")?; - decrypt_part( + Ok(decrypt_part( encrypted_data_part, private_keyring, public_keyring_for_validate, ) - .await + .await? + .map(|(raw, fprints)| (raw, fprints, true))) } pub(crate) async fn prepare_decryption( @@ -251,7 +253,7 @@ fn has_decrypted_pgp_armor(input: &[u8]) -> bool { /// /// Returns `None` if the part is not a Multipart/Signed part, otherwise retruns the set of key /// fingerprints for which there is a valid signature. -pub(crate) fn validate_detached_signature( +fn validate_detached_signature( mail: &ParsedMail<'_>, public_keyring_for_validate: &Keyring, ) -> Result, HashSet)>> { @@ -262,10 +264,11 @@ pub(crate) fn validate_detached_signature( if let [first_part, second_part] = &mail.subparts[..] { // First part is the content, second part is the signature. let content = first_part.raw_bytes; - let signature = second_part.get_body_raw()?; - let ret_valid_signatures = - pgp::pk_validate(content, &signature, public_keyring_for_validate)?; - + let ret_valid_signatures = match second_part.get_body_raw() { + Ok(signature) => pgp::pk_validate(content, &signature, public_keyring_for_validate) + .unwrap_or_default(), + Err(_) => Default::default(), + }; Ok(Some((content.to_vec(), ret_valid_signatures))) } else { Ok(None) diff --git a/src/mimeparser.rs b/src/mimeparser.rs index c96129e33..c4fa590d0 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -18,10 +18,7 @@ use crate::blob::BlobObject; use crate::constants::{DC_DESIRED_TEXT_LINES, DC_DESIRED_TEXT_LINE_LEN}; use crate::contact::{addr_cmp, addr_normalize, ContactId}; use crate::context::Context; -use crate::decrypt::{ - keyring_from_peerstate, prepare_decryption, try_decrypt, validate_detached_signature, - DecryptionInfo, -}; +use crate::decrypt::{prepare_decryption, try_decrypt, DecryptionInfo}; use crate::dehtml::dehtml; use crate::events::EventType; use crate::format_flowed::unformat_flowed; @@ -67,8 +64,6 @@ pub struct MimeMessage { /// If a message is not encrypted or the signature is not valid, /// this set is empty. pub signatures: HashSet, - /// Whether the message is encrypted in a domestic (not Autocrypt) sense - pub encrypted: bool, /// The set of mail recipient addresses for which gossip headers were applied, regardless of /// whether they modified any peerstates. pub gossiped_addr: HashSet, @@ -235,96 +230,99 @@ impl MimeMessage { hop_info += "\n\n"; hop_info += &decryption_info.dkim_results.to_string(); - // `signatures` is non-empty exactly if the message was encrypted and correctly signed. - let (mail, signatures, encrypted) = match try_decrypt(context, &mail, &decryption_info) - .await - { - Ok(Some((raw, signatures))) => { - // Encrypted, but maybe unsigned message. Only if - // `signatures` set is non-empty, it is a valid - // autocrypt message. + let (mail, mut signatures, encrypted) = + match try_decrypt(context, &mail, &decryption_info).await { + Ok(Some((raw, signatures, encrypted))) => { + // Only if `encrypted` and `signatures` set is non-empty, it is a valid + // autocrypt message. - mail_raw = raw; - let decrypted_mail = mailparse::parse_mail(&mail_raw)?; - if std::env::var(crate::DCC_MIME_DEBUG).is_ok() { - info!(context, "decrypted message mime-body:"); - println!("{}", String::from_utf8_lossy(&mail_raw)); - } + mail_raw = raw; + let decrypted_mail = mailparse::parse_mail(&mail_raw)?; + if std::env::var(crate::DCC_MIME_DEBUG).is_ok() { + info!(context, "decrypted message mime-body:"); + println!("{}", String::from_utf8_lossy(&mail_raw)); + } - // Handle any gossip headers if the mail was encrypted. See section - // "3.6 Key Gossip" of - // but only if the mail was correctly signed: - if !signatures.is_empty() { - let gossip_headers = decrypted_mail.headers.get_all_values("Autocrypt-Gossip"); - gossiped_addr = update_gossip_peerstates( - context, - message_time, - &from.addr, - &mail, - gossip_headers, - ) - .await?; - } + if encrypted { + // Handle any gossip headers if the mail was encrypted. See section + // "3.6 Key Gossip" of + // but only if the mail was correctly signed: + if !signatures.is_empty() { + let gossip_headers = + decrypted_mail.headers.get_all_values("Autocrypt-Gossip"); + gossiped_addr = update_gossip_peerstates( + context, + message_time, + &from.addr, + &mail, + gossip_headers, + ) + .await?; + } - // let known protected headers from the decrypted - // part override the unencrypted top-level + // let known protected headers from the decrypted + // part override the unencrypted top-level - // Signature was checked for original From, so we - // do not allow overriding it. - let mut signed_from = None; + // Signature was checked for original From, so we + // do not allow overriding it. + let mut signed_from = None; - // We do not want to allow unencrypted subject in encrypted emails because the user might falsely think that the subject is safe. - // See . - headers.remove("subject"); + // We do not want to allow unencrypted subject in encrypted emails because the + // user might falsely think that the subject is safe. + // See . + headers.remove("subject"); - MimeMessage::merge_headers( - context, - &mut headers, - &mut recipients, - &mut signed_from, - &mut list_post, - &mut chat_disposition_notification_to, - &decrypted_mail.headers, - ); - if let Some(signed_from) = signed_from { - if addr_cmp(&signed_from.addr, &from.addr) { - from_is_signed = true; - } else { - // There is a From: header in the encrypted & - // signed part, but it doesn't match the outer one. - // This _might_ be because the sender's mail server - // replaced the sending address, e.g. in a mailing list. - // Or it's because someone is doing some replay attack - // - OTOH, I can't come up with an attack scenario - // where this would be useful. - warn!( + MimeMessage::merge_headers( context, - "From header in signed part does't match the outer one", + &mut headers, + &mut recipients, + &mut signed_from, + &mut list_post, + &mut chat_disposition_notification_to, + &decrypted_mail.headers, ); + if let Some(signed_from) = signed_from { + if addr_cmp(&signed_from.addr, &from.addr) { + from_is_signed = true; + } else { + // There is a From: header in the encrypted & + // signed part, but it doesn't match the outer one. + // This _might_ be because the sender's mail server + // replaced the sending address, e.g. in a mailing list. + // Or it's because someone is doing some replay attack + // - OTOH, I can't come up with an attack scenario + // where this would be useful. + warn!( + context, + "From header in signed part does't match the outer one", + ); + } + } } + (Ok(decrypted_mail), signatures, encrypted) } + Ok(None) => (Ok(mail), HashSet::new(), false), + Err(err) => { + warn!(context, "decryption failed: {}", err); + (Err(err), HashSet::new(), true) + } + }; - (Ok(decrypted_mail), signatures, true) - } - Ok(None) => { - // Message was not encrypted. - // If it is not a read receipt, degrade encryption. - if let Some(peerstate) = &mut decryption_info.peerstate { - if message_time > peerstate.last_seen_autocrypt - && mail.ctype.mimetype != "multipart/report" - // Disallowing keychanges is disabled for now: - // && decryption_info.dkim_results.allow_keychange - { - peerstate.degrade_encryption(message_time); - } + if signatures.is_empty() { + // If it is not a read receipt, degrade encryption. + if let (Some(peerstate), Ok(mail)) = (&mut decryption_info.peerstate, &mail) { + if message_time > peerstate.last_seen_autocrypt + && mail.ctype.mimetype != "multipart/report" + // Disallowing keychanges is disabled for now: + // && decryption_info.dkim_results.allow_keychange + { + peerstate.degrade_encryption(message_time); } - (Ok(mail), HashSet::new(), false) } - Err(err) => { - warn!(context, "decryption failed: {}", err); - (Err(err), HashSet::new(), true) - } - }; + } + if !encrypted { + signatures.clear(); + } let mut parser = MimeMessage { parts: Vec::new(), @@ -339,7 +337,6 @@ impl MimeMessage { // only non-empty if it was a valid autocrypt message signatures, - encrypted, gossiped_addr, is_forwarded: false, mdn_reports: Vec::new(), @@ -868,26 +865,6 @@ impl MimeMessage { .parse_mime_recursive(context, first, is_related) .await?; } - if let Some(peerstate) = &mut self.decryption_info.peerstate { - let keyring = keyring_from_peerstate(Some(peerstate)); - match validate_detached_signature(mail, &keyring) { - Ok(Some((_, fprints))) => { - if fprints.is_empty() { - warn!(context, "signed message is not signed with a known key"); - } else if peerstate.prefer_encrypt != EncryptPreference::Mutual { - info!( - context, - "message is signed with the known key, setting \ - prefer-encrypt=mutual for '{}'", - peerstate.addr, - ); - Self::upgrade_to_mutual_encryption(context, peerstate).await?; - } - } - Ok(None) => warn!(context, "not a 'multipart/signed' part??"), - Err(err) => warn!(context, "signed message validation failed: {}", err), - } - } } (mime::MULTIPART, "report") => { /* RFC 6522: the first part is for humans, the second for machines */ @@ -965,17 +942,6 @@ impl MimeMessage { Ok(any_part_added) } - async fn upgrade_to_mutual_encryption( - context: &Context, - peerstate: &mut Peerstate, - ) -> Result<()> { - if peerstate.public_key.is_none() { - peerstate.public_key = peerstate.gossip_key.take(); - } - peerstate.prefer_encrypt = EncryptPreference::Mutual; - peerstate.save_to_db(&context.sql).await - } - /// Returns true if any part was added, false otherwise. async fn add_single_part_if_known( &mut self, @@ -1153,13 +1119,7 @@ impl MimeMessage { if peerstate.prefer_encrypt != EncryptPreference::Mutual && mime_type.type_() == mime::APPLICATION && mime_type.subtype().as_str() == "pgp-keys" - && Self::try_set_peer_key_from_file_part( - context, - peerstate, - decoded_data, - self.encrypted, - ) - .await? + && Self::try_set_peer_key_from_file_part(context, peerstate, decoded_data).await? { return Ok(()); } @@ -1249,7 +1209,6 @@ impl MimeMessage { context: &Context, peerstate: &mut Peerstate, decoded_data: &[u8], - mail_is_encrypted: bool, ) -> Result { let key = match str::from_utf8(decoded_data) { Err(err) => { @@ -1295,22 +1254,12 @@ impl MimeMessage { } } peerstate.public_key = Some(key); - if mail_is_encrypted { - info!( - context, - "using attached PGP key for peer '{}' with prefer-encrypt=mutual as the mail is \ - encrypted", - peerstate.addr, - ); - peerstate.prefer_encrypt = EncryptPreference::Mutual; - peerstate.save_to_db(&context.sql).await?; - } else { - info!( - context, - "using attached PGP key for peer '{}'", peerstate.addr, - ); - peerstate.prefer_encrypt = EncryptPreference::NoPreference; - } + info!( + context, + "using attached PGP key for peer '{}' with prefer-encrypt=mutual", peerstate.addr, + ); + peerstate.prefer_encrypt = EncryptPreference::Mutual; + peerstate.save_to_db(&context.sql).await?; Ok(true) } From be63e18ebf2d6ddf7a02a8c39839d7a51b843fe4 Mon Sep 17 00:00:00 2001 From: adbenitez Date: Sat, 10 Dec 2022 19:22:57 -0500 Subject: [PATCH 017/132] improve hook filters --- deltachat-rpc-client/examples/echobot.py | 4 +- .../examples/echobot_advanced.py | 13 +++--- .../src/deltachat_rpc_client/client.py | 46 ++++++++++++++++--- .../src/deltachat_rpc_client/const.py | 2 + .../src/deltachat_rpc_client/events.py | 32 ++++++++++--- deltachat-rpc-client/tests/test_something.py | 15 ++++-- 6 files changed, 87 insertions(+), 25 deletions(-) diff --git a/deltachat-rpc-client/examples/echobot.py b/deltachat-rpc-client/examples/echobot.py index 792dbae86..c77b422d2 100755 --- a/deltachat-rpc-client/examples/echobot.py +++ b/deltachat-rpc-client/examples/echobot.py @@ -17,8 +17,8 @@ async def log_event(event): @hooks.on(events.NewMessage) -async def echo(msg): - await msg.chat.send_text(msg.text) +async def echo(event): + await event.chat.send_text(event.text) if __name__ == "__main__": diff --git a/deltachat-rpc-client/examples/echobot_advanced.py b/deltachat-rpc-client/examples/echobot_advanced.py index 88ddd1303..28b1750b6 100644 --- a/deltachat-rpc-client/examples/echobot_advanced.py +++ b/deltachat-rpc-client/examples/echobot_advanced.py @@ -25,14 +25,15 @@ async def log_error(event): logging.error(event.msg) -@hooks.on(events.NewMessage(r".+", func=lambda msg: not msg.text.startswith("/"))) -async def echo(msg): - await msg.chat.send_text(msg.text) +@hooks.on(events.NewMessage(func=lambda e: not e.command)) +async def echo(event): + if event.text or event.file: + await event.chat.send_message(text=event.text, file=event.file) -@hooks.on(events.NewMessage(r"/help")) -async def help_command(msg): - await msg.chat.send_text("Send me any text message and I will echo it back") +@hooks.on(events.NewMessage(command="/help")) +async def help_command(event): + await event.chat.send_text("Send me any message and I will echo it back") async def main(): diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/client.py b/deltachat-rpc-client/src/deltachat_rpc_client/client.py index 2a749eefe..6abcc8a59 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/client.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/client.py @@ -4,8 +4,8 @@ from typing import Callable, Dict, Iterable, Optional, Set, Tuple, Type, Union from deltachat_rpc_client.account import Account -from .const import EventType -from .events import EventFilter, NewInfoMessage, NewMessage, RawEvent +from .const import COMMAND_PREFIX, EventType +from .events import EventFilter, NewMessage, RawEvent from .utils import AttrDict @@ -79,16 +79,48 @@ class Client: self.logger.exception(ex) def _should_process_messages(self) -> bool: - return any(issubclass(filter_type, NewMessage) for filter_type in self._hooks) + return NewMessage in self._hooks + + async def _parse_command(self, snapshot: AttrDict) -> None: + cmds = [ + hook[1].command + for hook in self._hooks.get(NewMessage, []) + if hook[1].command + ] + parts = snapshot.text.split(maxsplit=1) + payload = parts[1] if len(parts) > 1 else "" + cmd = parts.pop(0) + + if "@" in cmd: + suffix = "@" + (await self.account.self_contact.get_snapshot()).address + if cmd.endswith(suffix): + cmd = cmd[: -len(suffix)] + else: + return + + parts = cmd.split("_") + _payload = payload + while parts: + _cmd = "_".join(parts) + if _cmd in cmds: + break + _payload = (parts.pop() + " " + _payload).rstrip() + + if parts: + cmd = _cmd + payload = _payload + + snapshot["command"] = cmd + snapshot["payload"] = payload async def _process_messages(self) -> None: if self._should_process_messages(): for message in await self.account.get_fresh_messages_in_arrival_order(): snapshot = await message.get_snapshot() - if snapshot.is_info: - await self._on_event(snapshot, NewInfoMessage) - else: - await self._on_event(snapshot, NewMessage) + snapshot["command"], snapshot["payload"] = "", "" + if not snapshot.is_info and snapshot.text.startswith(COMMAND_PREFIX): + await self._parse_command(snapshot) + await self._on_event(snapshot, NewMessage) await snapshot.message.mark_seen() diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/const.py b/deltachat-rpc-client/src/deltachat_rpc_client/const.py index c8bb925fc..b2a4e7f12 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/const.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/const.py @@ -1,5 +1,7 @@ from enum import Enum, IntEnum +COMMAND_PREFIX = "/" + class ContactFlag(IntEnum): VERIFIED_ONLY = 0x01 diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/events.py b/deltachat-rpc-client/src/deltachat_rpc_client/events.py index 606fe9896..7654bdf8b 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/events.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/events.py @@ -91,8 +91,15 @@ class RawEvent(EventFilter): class NewMessage(EventFilter): """Matches whenever a new message arrives. - Warning: registering a handler for this event or any subclass will cause the messages + Warning: registering a handler for this event will cause the messages to be marked as read. Its usage is mainly intended for bots. + + :param pattern: if set, this Pattern will be used to filter the message by its text + content. + :param command: If set, only match messages with the given command (ex. /help). + :param is_info: If set to True only match info/system messages, if set to False + only match messages that are not info/system messages. If omitted + info/system messages as well as normal messages will be matched. """ def __init__( @@ -103,9 +110,15 @@ class NewMessage(EventFilter): Callable[[str], bool], re.Pattern, ] = None, + command: Optional[str] = None, + is_info: Optional[bool] = None, func: Optional[Callable[[AttrDict], bool]] = None, ) -> None: super().__init__(func=func) + self.is_info = is_info + if command is not None and not isinstance(command, str): + raise TypeError("Invalid command") + self.command = command if isinstance(pattern, str): pattern = re.compile(pattern) if isinstance(pattern, re.Pattern): @@ -119,11 +132,20 @@ class NewMessage(EventFilter): return hash((self.pattern, self.func)) def __eq__(self, other) -> bool: - if type(other) is self.__class__: # noqa - return (self.pattern, self.func) == (other.pattern, other.func) + if isinstance(other, NewMessage): + return (self.pattern, self.command, self.is_info, self.func) == ( + other.pattern, + other.command, + other.is_info, + other.func, + ) return False async def filter(self, event: AttrDict) -> bool: + if self.is_info is not None and self.is_info != event.is_info: + return False + if self.command and self.command != event.command: + return False if self.pattern: match = self.pattern(event.text) if inspect.isawaitable(match): @@ -133,10 +155,6 @@ class NewMessage(EventFilter): return await super()._call_func(event) -class NewInfoMessage(NewMessage): - """Matches whenever a new info/system message arrives.""" - - class HookCollection: """ Helper class to collect event hooks that can later be added to a Delta Chat client. diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index 6d8136058..7c265f1ec 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -235,12 +235,21 @@ async def test_bot(acfactory) -> None: res = [] bot.add_hook(callback, events.NewMessage(r"hello")) - snapshot1 = AttrDict(text="hello") - snapshot2 = AttrDict(text="hello, world") - snapshot3 = AttrDict(text="hey!") + bot.add_hook(callback, events.NewMessage(command="/help")) + snapshot1 = AttrDict(text="hello", command=None) + snapshot2 = AttrDict(text="hello, world", command=None) + snapshot3 = AttrDict(text="hey!", command=None) for snapshot in [snapshot1, snapshot2, snapshot3]: await bot._on_event(snapshot, events.NewMessage) assert len(res) == 2 assert snapshot1 in res assert snapshot2 in res assert snapshot3 not in res + + res = [] + bot.remove_hook(callback, events.NewMessage(r"hello")) + snapshot4 = AttrDict(command="/help") + await bot._on_event(snapshot, events.NewMessage) + await bot._on_event(snapshot4, events.NewMessage) + assert len(res) == 1 + assert snapshot4 in res From 2ebd3f54e61b22d2170dbc3e5a4ddc12e58fc3a6 Mon Sep 17 00:00:00 2001 From: adbenitez Date: Sun, 11 Dec 2022 03:31:29 -0500 Subject: [PATCH 018/132] add Client.run_until() --- .../src/deltachat_rpc_client/client.py | 31 +++++++- .../src/deltachat_rpc_client/pytestplugin.py | 46 ++++++++++-- deltachat-rpc-client/tests/test_something.py | 70 +++++++++++-------- 3 files changed, 111 insertions(+), 36 deletions(-) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/client.py b/deltachat-rpc-client/src/deltachat_rpc_client/client.py index 6abcc8a59..f04e0d1e1 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/client.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/client.py @@ -1,6 +1,17 @@ """Event loop implementations offering high level event handling/hooking.""" +import inspect import logging -from typing import Callable, Dict, Iterable, Optional, Set, Tuple, Type, Union +from typing import ( + Callable, + Coroutine, + Dict, + Iterable, + Optional, + Set, + Tuple, + Type, + Union, +) from deltachat_rpc_client.account import Account @@ -56,6 +67,18 @@ class Client: self.logger.debug("Account configured") async def run_forever(self) -> None: + """Process events forever.""" + await self.run_until(lambda _: False) + + async def run_until( + self, func: Callable[[AttrDict], Union[bool, Coroutine]] + ) -> AttrDict: + """Process events until the given callable evaluates to True. + + The callable should accept an AttrDict object representing the + last processed event. The event is returned when the callable + evaluates to True. + """ self.logger.debug("Listening to incoming events...") if await self.is_configured(): await self.account.start_io() @@ -68,6 +91,12 @@ class Client: if event.type == EventType.INCOMING_MSG: await self._process_messages() + stop = func(event) + if inspect.isawaitable(stop): + stop = await stop + if stop: + return event + async def _on_event( self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent ) -> None: diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py index e8cce5f4c..0d8aca5d0 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py @@ -1,13 +1,11 @@ import json import os -from typing import AsyncGenerator, List +from typing import AsyncGenerator, List, Optional import aiohttp import pytest_asyncio -from .account import Account -from .client import Bot -from .deltachat import DeltaChat +from . import Account, AttrDict, Bot, Client, DeltaChat, EventType, Message from .rpc import Rpc @@ -51,6 +49,46 @@ class ACFactory: await account.start_io() return accounts + async def send_message( + self, + to_account: Account, + from_account: Optional[Account] = None, + text: Optional[str] = None, + file: Optional[str] = None, + group: Optional[str] = None, + ) -> Message: + if not from_account: + from_account = (await self.get_online_accounts(1))[0] + to_contact = await from_account.create_contact( + await to_account.get_config("addr") + ) + if group: + to_chat = await from_account.create_group(group) + await to_chat.add_contact(to_contact) + else: + to_chat = await to_contact.create_chat() + return await to_chat.send_message(text=text, file=file) + + async def process_message( + self, + to_client: Client, + from_account: Optional[Account] = None, + text: Optional[str] = None, + file: Optional[str] = None, + group: Optional[str] = None, + ) -> AttrDict: + await self.send_message( + to_account=to_client.account, + from_account=from_account, + text=text, + file=file, + group=group, + ) + + event = await to_client.run_until(lambda e: e.type == EventType.INCOMING_MSG) + msg = await to_client.account.get_message_by_id(event.msg_id) + return await msg.get_snapshot() + @pytest_asyncio.fixture async def rpc(tmp_path) -> AsyncGenerator: diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index 7c265f1ec..1b2b3625e 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -1,6 +1,8 @@ +from unittest.mock import MagicMock + import pytest -from deltachat_rpc_client import AttrDict, EventType, events +from deltachat_rpc_client import EventType, events from deltachat_rpc_client.rpc import JsonRpcError @@ -216,40 +218,46 @@ async def test_message(acfactory) -> None: @pytest.mark.asyncio async def test_bot(acfactory) -> None: - async def callback(e): - res.append(e) + def track(key): + async def wrapper(e): + mock.hook(e[key]) - res = [] + return wrapper + + mock = MagicMock() + user = (await acfactory.get_online_accounts(1))[0] bot = await acfactory.new_configured_bot() + assert await bot.is_configured() assert await bot.account.get_config("bot") == "1" - bot.add_hook(callback, events.RawEvent(EventType.INFO)) - info_event = AttrDict(account=bot.account, type=EventType.INFO, msg="info") - warn_event = AttrDict(account=bot.account, type=EventType.WARNING, msg="warning") - await bot._on_event(info_event) - await bot._on_event(warn_event) - assert info_event in res - assert warn_event not in res - assert len(res) == 1 + hook = track("msg_id"), events.RawEvent(EventType.INCOMING_MSG) + bot.add_hook(*hook) + event = await acfactory.process_message( + from_account=user, to_client=bot, text="Hello!" + ) + mock.hook.assert_called_once_with(event.id) + bot.remove_hook(*hook) - res = [] - bot.add_hook(callback, events.NewMessage(r"hello")) - bot.add_hook(callback, events.NewMessage(command="/help")) - snapshot1 = AttrDict(text="hello", command=None) - snapshot2 = AttrDict(text="hello, world", command=None) - snapshot3 = AttrDict(text="hey!", command=None) - for snapshot in [snapshot1, snapshot2, snapshot3]: - await bot._on_event(snapshot, events.NewMessage) - assert len(res) == 2 - assert snapshot1 in res - assert snapshot2 in res - assert snapshot3 not in res + mock.hook.reset_mock() + hook = track("id"), events.NewMessage(r"hello") + bot.add_hook(*hook) + bot.add_hook(track("id"), events.NewMessage(command="/help")) + event = await acfactory.process_message( + from_account=user, to_client=bot, text="hello" + ) + mock.hook.assert_called_with(event.id) + event = await acfactory.process_message( + from_account=user, to_client=bot, text="hello!" + ) + mock.hook.assert_called_with(event.id) + await acfactory.process_message(from_account=user, to_client=bot, text="hey!") + assert len(mock.hook.mock_calls) == 2 + bot.remove_hook(*hook) - res = [] - bot.remove_hook(callback, events.NewMessage(r"hello")) - snapshot4 = AttrDict(command="/help") - await bot._on_event(snapshot, events.NewMessage) - await bot._on_event(snapshot4, events.NewMessage) - assert len(res) == 1 - assert snapshot4 in res + mock.hook.reset_mock() + await acfactory.process_message(from_account=user, to_client=bot, text="hello") + event = await acfactory.process_message( + from_account=user, to_client=bot, text="/help" + ) + mock.hook.assert_called_once_with(event.id) From adf754ad3232e4f14cd3db0142c8f3fe9f53fa01 Mon Sep 17 00:00:00 2001 From: adbenitez Date: Wed, 21 Dec 2022 13:25:46 -0500 Subject: [PATCH 019/132] add more high-level event filters --- deltachat-rpc-client/examples/echobot.py | 3 +- .../examples/echobot_advanced.py | 25 ++++- .../src/deltachat_rpc_client/__init__.py | 2 +- .../{utils.py => _utils.py} | 63 ++++++++++++ .../src/deltachat_rpc_client/account.py | 2 +- .../src/deltachat_rpc_client/chat.py | 2 +- .../src/deltachat_rpc_client/client.py | 88 ++++++++++++++--- .../src/deltachat_rpc_client/contact.py | 2 +- .../src/deltachat_rpc_client/deltachat.py | 2 +- .../src/deltachat_rpc_client/events.py | 99 ++++++++++++++++++- .../src/deltachat_rpc_client/message.py | 2 +- .../src/deltachat_rpc_client/pytestplugin.py | 4 +- deltachat-rpc-client/tests/test_something.py | 22 ++--- 13 files changed, 272 insertions(+), 44 deletions(-) rename deltachat-rpc-client/src/deltachat_rpc_client/{utils.py => _utils.py} (63%) diff --git a/deltachat-rpc-client/examples/echobot.py b/deltachat-rpc-client/examples/echobot.py index c77b422d2..65d447bf9 100755 --- a/deltachat-rpc-client/examples/echobot.py +++ b/deltachat-rpc-client/examples/echobot.py @@ -18,7 +18,8 @@ async def log_event(event): @hooks.on(events.NewMessage) async def echo(event): - await event.chat.send_text(event.text) + snapshot = event.message_snapshot + await snapshot.chat.send_text(snapshot.text) if __name__ == "__main__": diff --git a/deltachat-rpc-client/examples/echobot_advanced.py b/deltachat-rpc-client/examples/echobot_advanced.py index 28b1750b6..48e3d025b 100644 --- a/deltachat-rpc-client/examples/echobot_advanced.py +++ b/deltachat-rpc-client/examples/echobot_advanced.py @@ -25,15 +25,34 @@ async def log_error(event): logging.error(event.msg) +@hooks.on(events.MemberListChanged) +async def on_memberlist_changed(event): + logging.info( + "member %s was %s", event.member, "added" if event.member_added else "removed" + ) + + +@hooks.on(events.GroupImageChanged) +async def on_group_image_changed(event): + logging.info("group image %s", "deleted" if event.image_deleted else "changed") + + +@hooks.on(events.GroupNameChanged) +async def on_group_name_changed(event): + logging.info("group name changed, old name: %s", event.old_name) + + @hooks.on(events.NewMessage(func=lambda e: not e.command)) async def echo(event): - if event.text or event.file: - await event.chat.send_message(text=event.text, file=event.file) + snapshot = event.message_snapshot + if snapshot.text or snapshot.file: + await snapshot.chat.send_message(text=snapshot.text, file=snapshot.file) @hooks.on(events.NewMessage(command="/help")) async def help_command(event): - await event.chat.send_text("Send me any message and I will echo it back") + snapshot = event.message_snapshot + await snapshot.chat.send_text("Send me any message and I will echo it back") async def main(): diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py b/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py index a62ae5f63..ff15923b5 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py @@ -1,4 +1,5 @@ """Delta Chat asynchronous high-level API""" +from ._utils import AttrDict, run_bot_cli, run_client_cli from .account import Account from .chat import Chat from .client import Bot, Client @@ -7,4 +8,3 @@ from .contact import Contact from .deltachat import DeltaChat from .message import Message from .rpc import Rpc -from .utils import AttrDict, run_bot_cli, run_client_cli diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/utils.py b/deltachat-rpc-client/src/deltachat_rpc_client/_utils.py similarity index 63% rename from deltachat-rpc-client/src/deltachat_rpc_client/utils.py rename to deltachat-rpc-client/src/deltachat_rpc_client/_utils.py index f7782ad8b..562c62de1 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/utils.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/_utils.py @@ -17,6 +17,8 @@ def _camel_to_snake(name: str) -> str: def _to_attrdict(obj): + if isinstance(obj, AttrDict): + return obj if isinstance(obj, dict): return AttrDict(obj) if isinstance(obj, list): @@ -112,3 +114,64 @@ async def _run_cli( client.configure(email=args.email, password=args.password) ) await client.run_forever() + + +def extract_addr(text: str) -> str: + """extract email address from the given text.""" + match = re.match(r".*\((.+@.+)\)", text) + if match: + text = match.group(1) + text = text.rstrip(".") + return text.strip() + + +def parse_system_image_changed(text: str) -> Optional[Tuple[str, bool]]: + """return image changed/deleted info from parsing the given system message text.""" + text = text.lower() + match = re.match(r"group image (changed|deleted) by (.+).", text) + if match: + action, actor = match.groups() + return (extract_addr(actor), action == "deleted") + return None + + +def parse_system_title_changed(text: str) -> Optional[Tuple[str, str]]: + text = text.lower() + match = re.match(r'group name changed from "(.+)" to ".+" by (.+).', text) + if match: + old_title, actor = match.groups() + return (extract_addr(actor), old_title) + return None + + +def parse_system_add_remove(text: str) -> Optional[Tuple[str, str, str]]: + """return add/remove info from parsing the given system message text. + + returns a (action, affected, actor) tuple. + """ + # You removed member a@b. + # You added member a@b. + # Member Me (x@y) removed by a@b. + # Member x@y added by a@b + # Member With space (tmp1@x.org) removed by tmp2@x.org. + # Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).", + # Group left by some one (tmp1@x.org). + # Group left by tmp1@x.org. + text = text.lower() + + match = re.match(r"member (.+) (removed|added) by (.+)", text) + if match: + affected, action, actor = match.groups() + return action, extract_addr(affected), extract_addr(actor) + + match = re.match(r"you (removed|added) member (.+)", text) + if match: + action, affected = match.groups() + return action, extract_addr(affected), "me" + + if text.startswith("group left by "): + addr = extract_addr(text[13:]) + if addr: + return "removed", addr, addr + + return None diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/account.py b/deltachat-rpc-client/src/deltachat_rpc_client/account.py index d75ef67dd..9d9d92e41 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/account.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/account.py @@ -1,11 +1,11 @@ from typing import TYPE_CHECKING, List, Optional, Tuple, Union +from ._utils import AttrDict from .chat import Chat from .const import ChatlistFlag, ContactFlag, SpecialContactId from .contact import Contact from .message import Message from .rpc import Rpc -from .utils import AttrDict if TYPE_CHECKING: from .deltachat import DeltaChat diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py index 9e053b1fc..4a2f4ae77 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py @@ -2,11 +2,11 @@ import calendar from datetime import datetime from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union +from ._utils import AttrDict from .const import ChatVisibility from .contact import Contact from .message import Message from .rpc import Rpc -from .utils import AttrDict if TYPE_CHECKING: from .account import Account diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/client.py b/deltachat-rpc-client/src/deltachat_rpc_client/client.py index f04e0d1e1..4c6deafa1 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/client.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/client.py @@ -15,9 +15,21 @@ from typing import ( from deltachat_rpc_client.account import Account -from .const import COMMAND_PREFIX, EventType -from .events import EventFilter, NewMessage, RawEvent -from .utils import AttrDict +from ._utils import ( + AttrDict, + parse_system_add_remove, + parse_system_image_changed, + parse_system_title_changed, +) +from .const import COMMAND_PREFIX, EventType, SystemMessageType +from .events import ( + EventFilter, + GroupImageChanged, + GroupNameChanged, + MemberListChanged, + NewMessage, + RawEvent, +) class Client: @@ -32,6 +44,7 @@ class Client: self.account = account self.logger = logger or logging self._hooks: Dict[type, Set[tuple]] = {} + self._should_process_messages = 0 self.add_hooks(hooks or []) def add_hooks( @@ -47,12 +60,24 @@ class Client: if isinstance(event, type): event = event() assert isinstance(event, EventFilter) + self._should_process_messages += int( + isinstance( + event, + (NewMessage, MemberListChanged, GroupImageChanged, GroupNameChanged), + ) + ) self._hooks.setdefault(type(event), set()).add((hook, event)) def remove_hook(self, hook: Callable, event: Union[type, EventFilter]) -> None: """Unregister hook from the given event filter.""" if isinstance(event, type): event = event() + self._should_process_messages -= int( + isinstance( + event, + (NewMessage, MemberListChanged, GroupImageChanged, GroupNameChanged), + ) + ) self._hooks.get(type(event), set()).remove((hook, event)) async def is_configured(self) -> bool: @@ -107,16 +132,13 @@ class Client: except Exception as ex: self.logger.exception(ex) - def _should_process_messages(self) -> bool: - return NewMessage in self._hooks - - async def _parse_command(self, snapshot: AttrDict) -> None: + async def _parse_command(self, event: AttrDict) -> None: cmds = [ hook[1].command for hook in self._hooks.get(NewMessage, []) if hook[1].command ] - parts = snapshot.text.split(maxsplit=1) + parts = event.message_snapshot.text.split(maxsplit=1) payload = parts[1] if len(parts) > 1 else "" cmd = parts.pop(0) @@ -139,17 +161,53 @@ class Client: cmd = _cmd payload = _payload - snapshot["command"] = cmd - snapshot["payload"] = payload + event["command"], event["payload"] = cmd, payload + + async def _on_new_msg(self, snapshot: AttrDict) -> None: + event = AttrDict(command="", payload="", message_snapshot=snapshot) + if not snapshot.is_info and snapshot.text.startswith(COMMAND_PREFIX): + await self._parse_command(event) + await self._on_event(event, NewMessage) + + async def _handle_info_msg(self, snapshot: AttrDict) -> None: + event = AttrDict(message_snapshot=snapshot) + + img_changed = parse_system_image_changed(snapshot.text) + if img_changed: + _, event["image_deleted"] = img_changed + await self._on_event(event, GroupImageChanged) + return + + title_changed = parse_system_title_changed(snapshot.text) + if title_changed: + _, event["old_name"] = title_changed + await self._on_event(event, GroupNameChanged) + return + + members_changed = parse_system_add_remove(snapshot.text) + if members_changed: + action, event["member"], _ = members_changed + event["member_added"] = action == "added" + await self._on_event(event, MemberListChanged) + return + + self.logger.warning( + "ignoring unsupported system message id=%s text=%s", + snapshot.id, + snapshot.text, + ) async def _process_messages(self) -> None: - if self._should_process_messages(): + if self._should_process_messages: for message in await self.account.get_fresh_messages_in_arrival_order(): snapshot = await message.get_snapshot() - snapshot["command"], snapshot["payload"] = "", "" - if not snapshot.is_info and snapshot.text.startswith(COMMAND_PREFIX): - await self._parse_command(snapshot) - await self._on_event(snapshot, NewMessage) + await self._on_new_msg(snapshot) + if ( + snapshot.is_info + and snapshot.system_message_type + != SystemMessageType.WEBXDC_INFO_MESSAGE + ): + await self._handle_info_msg(snapshot) await snapshot.message.mark_seen() diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/contact.py b/deltachat-rpc-client/src/deltachat_rpc_client/contact.py index 804ac4af1..7c5267b4f 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/contact.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/contact.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING +from ._utils import AttrDict from .rpc import Rpc -from .utils import AttrDict if TYPE_CHECKING: from .account import Account diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py b/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py index 2c926bac5..16afe458b 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py @@ -1,8 +1,8 @@ from typing import Dict, List +from ._utils import AttrDict from .account import Account from .rpc import Rpc -from .utils import AttrDict class DeltaChat: diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/events.py b/deltachat-rpc-client/src/deltachat_rpc_client/events.py index 7654bdf8b..146c89ea5 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/events.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/events.py @@ -4,8 +4,8 @@ import re from abc import ABC, abstractmethod from typing import Callable, Iterable, Iterator, Optional, Set, Tuple, Union +from ._utils import AttrDict from .const import EventType -from .utils import AttrDict def _tuple_of(obj, type_: type) -> tuple: @@ -97,9 +97,13 @@ class NewMessage(EventFilter): :param pattern: if set, this Pattern will be used to filter the message by its text content. :param command: If set, only match messages with the given command (ex. /help). + Setting this property implies `is_info==False`. :param is_info: If set to True only match info/system messages, if set to False only match messages that are not info/system messages. If omitted info/system messages as well as normal messages will be matched. + :param func: A Callable (async or not) function that should accept the event as input + parameter, and return a bool value indicating whether the event + should be dispatched or not. """ def __init__( @@ -119,6 +123,8 @@ class NewMessage(EventFilter): if command is not None and not isinstance(command, str): raise TypeError("Invalid command") self.command = command + if self.is_info and self.command: + raise AttributeError("Can not use command and is_info at the same time.") if isinstance(pattern, str): pattern = re.compile(pattern) if isinstance(pattern, re.Pattern): @@ -142,12 +148,12 @@ class NewMessage(EventFilter): return False async def filter(self, event: AttrDict) -> bool: - if self.is_info is not None and self.is_info != event.is_info: + if self.is_info is not None and self.is_info != event.message_snapshot.is_info: return False if self.command and self.command != event.command: return False if self.pattern: - match = self.pattern(event.text) + match = self.pattern(event.message_snapshot.text) if inspect.isawaitable(match): match = await match if not match: @@ -155,6 +161,93 @@ class NewMessage(EventFilter): return await super()._call_func(event) +class MemberListChanged(EventFilter): + """Matches when a group member is added or removed. + + Warning: registering a handler for this event will cause the messages + to be marked as read. Its usage is mainly intended for bots. + + :param added: If set to True only match if a member was added, if set to False + only match if a member was removed. If omitted both, member additions + and removals, will be matched. + :param func: A Callable (async or not) function that should accept the event as input + parameter, and return a bool value indicating whether the event + should be dispatched or not. + """ + + def __init__(self, added: Optional[bool] = None, **kwargs): + super().__init__(**kwargs) + self.added = added + + def __hash__(self) -> int: + return hash((self.added, self.func)) + + def __eq__(self, other) -> bool: + if isinstance(other, MemberListChanged): + return (self.added, self.func) == (other.added, other.func) + return False + + async def filter(self, event: AttrDict) -> bool: + if self.added is not None and self.added != event.member_added: + return False + return await self._call_func(event) + + +class GroupImageChanged(EventFilter): + """Matches when the group image is changed. + + Warning: registering a handler for this event will cause the messages + to be marked as read. Its usage is mainly intended for bots. + + :param deleted: If set to True only match if the image was deleted, if set to False + only match if a new image was set. If omitted both, image changes and + removals, will be matched. + :param func: A Callable (async or not) function that should accept the event as input + parameter, and return a bool value indicating whether the event + should be dispatched or not. + """ + + def __init__(self, deleted: Optional[bool] = None, **kwargs): + super().__init__(**kwargs) + self.deleted = deleted + + def __hash__(self) -> int: + return hash((self.deleted, self.func)) + + def __eq__(self, other) -> bool: + if isinstance(other, GroupImageChanged): + return (self.deleted, self.func) == (other.deleted, other.func) + return False + + async def filter(self, event: AttrDict) -> bool: + if self.deleted is not None and self.deleted != event.image_deleted: + return False + return await self._call_func(event) + + +class GroupNameChanged(EventFilter): + """Matches when the group name is changed. + + Warning: registering a handler for this event will cause the messages + to be marked as read. Its usage is mainly intended for bots. + + :param func: A Callable (async or not) function that should accept the event as input + parameter, and return a bool value indicating whether the event + should be dispatched or not. + """ + + def __hash__(self) -> int: + return hash((GroupNameChanged, self.func)) + + def __eq__(self, other) -> bool: + if isinstance(other, GroupNameChanged): + return self.func == other.func + return False + + async def filter(self, event: AttrDict) -> bool: + return await self._call_func(event) + + class HookCollection: """ Helper class to collect event hooks that can later be added to a Delta Chat client. diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/message.py b/deltachat-rpc-client/src/deltachat_rpc_client/message.py index 0001dd308..644984fc1 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/message.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/message.py @@ -1,8 +1,8 @@ from typing import TYPE_CHECKING +from ._utils import AttrDict from .contact import Contact from .rpc import Rpc -from .utils import AttrDict if TYPE_CHECKING: from .account import Account diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py index 0d8aca5d0..f628fdcfa 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py @@ -85,9 +85,7 @@ class ACFactory: group=group, ) - event = await to_client.run_until(lambda e: e.type == EventType.INCOMING_MSG) - msg = await to_client.account.get_message_by_id(event.msg_id) - return await msg.get_snapshot() + return await to_client.run_until(lambda e: e.type == EventType.INCOMING_MSG) @pytest_asyncio.fixture diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index 1b2b3625e..c0465cc05 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -218,12 +218,6 @@ async def test_message(acfactory) -> None: @pytest.mark.asyncio async def test_bot(acfactory) -> None: - def track(key): - async def wrapper(e): - mock.hook(e[key]) - - return wrapper - mock = MagicMock() user = (await acfactory.get_online_accounts(1))[0] bot = await acfactory.new_configured_bot() @@ -231,26 +225,28 @@ async def test_bot(acfactory) -> None: assert await bot.is_configured() assert await bot.account.get_config("bot") == "1" - hook = track("msg_id"), events.RawEvent(EventType.INCOMING_MSG) + hook = lambda e: mock.hook(e.msg_id), events.RawEvent(EventType.INCOMING_MSG) bot.add_hook(*hook) event = await acfactory.process_message( from_account=user, to_client=bot, text="Hello!" ) - mock.hook.assert_called_once_with(event.id) + mock.hook.assert_called_once_with(event.msg_id) bot.remove_hook(*hook) + track = lambda e: mock.hook(e.message_snapshot.id) + mock.hook.reset_mock() - hook = track("id"), events.NewMessage(r"hello") + hook = track, events.NewMessage(r"hello") bot.add_hook(*hook) - bot.add_hook(track("id"), events.NewMessage(command="/help")) + bot.add_hook(track, events.NewMessage(command="/help")) event = await acfactory.process_message( from_account=user, to_client=bot, text="hello" ) - mock.hook.assert_called_with(event.id) + mock.hook.assert_called_with(event.msg_id) event = await acfactory.process_message( from_account=user, to_client=bot, text="hello!" ) - mock.hook.assert_called_with(event.id) + mock.hook.assert_called_with(event.msg_id) await acfactory.process_message(from_account=user, to_client=bot, text="hey!") assert len(mock.hook.mock_calls) == 2 bot.remove_hook(*hook) @@ -260,4 +256,4 @@ async def test_bot(acfactory) -> None: event = await acfactory.process_message( from_account=user, to_client=bot, text="/help" ) - mock.hook.assert_called_once_with(event.id) + mock.hook.assert_called_once_with(event.msg_id) From 736950ab3fb2f0aae957915e132a750ef0f2f510 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 23 Dec 2022 01:18:03 +0000 Subject: [PATCH 020/132] Do not return Result from validate_detached_signature It never returns errors. --- src/decrypt.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/decrypt.rs b/src/decrypt.rs index ae8e16ade..7b055f654 100644 --- a/src/decrypt.rs +++ b/src/decrypt.rs @@ -38,7 +38,7 @@ pub async fn try_decrypt( { None => { return Ok( - validate_detached_signature(mail, &public_keyring_for_validate)? + validate_detached_signature(mail, &public_keyring_for_validate) .map(|(raw, fprints)| (raw, fprints, false)), ) } @@ -221,7 +221,7 @@ async fn decrypt_part( // If decrypted part is a multipart/signed, then there is a detached signature. let decrypted_part = mailparse::parse_mail(&plain)?; if let Some((content, valid_detached_signatures)) = - validate_detached_signature(&decrypted_part, &public_keyring_for_validate)? + validate_detached_signature(&decrypted_part, &public_keyring_for_validate) { return Ok(Some((content, valid_detached_signatures))); } else { @@ -256,9 +256,9 @@ fn has_decrypted_pgp_armor(input: &[u8]) -> bool { fn validate_detached_signature( mail: &ParsedMail<'_>, public_keyring_for_validate: &Keyring, -) -> Result, HashSet)>> { +) -> Option<(Vec, HashSet)> { if mail.ctype.mimetype != "multipart/signed" { - return Ok(None); + return None; } if let [first_part, second_part] = &mail.subparts[..] { @@ -269,9 +269,9 @@ fn validate_detached_signature( .unwrap_or_default(), Err(_) => Default::default(), }; - Ok(Some((content.to_vec(), ret_valid_signatures))) + Some((content.to_vec(), ret_valid_signatures)) } else { - Ok(None) + None } } From 06ead557dcceb072add261d0508762a79227d17a Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 23 Dec 2022 10:19:11 +0000 Subject: [PATCH 021/132] Do not add an error if the message is encrypted but not signed Services like Lacre [1] on Disroot and Inbound Encryption on Posteo [2] offer to encrypt all incoming messages with the provided OpenPGP public key. Resulting messages are encrypted, but not end-to-end encrypted and not signed by the sender, therefore should not have a padlock displayed. However, such encrypted and unsigned message is also not an indication of an error on ongoing attack, so we shoud not report this as a problem to the user. [1] https://lacre.io/ [2] https://posteo.de/en/help/how-do-i-activate-inbound-encryption-with-my-public-pgp-key --- CHANGELOG.md | 1 + src/mimeparser.rs | 5 --- src/receive_imf/tests.rs | 20 +++++++++ .../thunderbird_encrypted_unsigned.eml | 44 +++++++++++++++++++ 4 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 test-data/message/thunderbird_encrypted_unsigned.eml diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b55c99f9..0395f1e56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### API-Changes ### Fixes +- Do not add an error if the message is encrypted but not signed #3860 ## 1.104.0 diff --git a/src/mimeparser.rs b/src/mimeparser.rs index c4fa590d0..8050951df 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -391,11 +391,6 @@ impl MimeMessage { // part.error = Some("Seems like DKIM failed, this either is an attack or (more likely) a bug in Authentication-Results checking. Please tell us about this at https://support.delta.chat.".to_string()); // } // } - if encrypted && parser.signatures.is_empty() { - for part in parser.parts.iter_mut() { - part.error = Some("No valid signature".to_string()); - } - } if parser.is_mime_modified { parser.decoded_data = mail_raw; diff --git a/src/receive_imf/tests.rs b/src/receive_imf/tests.rs index 242e9c76b..c781b3785 100644 --- a/src/receive_imf/tests.rs +++ b/src/receive_imf/tests.rs @@ -3075,6 +3075,26 @@ async fn test_thunderbird_autocrypt_unencrypted() -> Result<()> { Ok(()) } +/// Alice receives an encrypted, but unsigned message. +/// +/// Test that the message is displayed without any errors, +/// but also without a padlock. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_thunderbird_unsigned() -> Result<()> { + let alice = TestContext::new_alice().await; + alice.set_config(Config::ShowEmails, Some("2")).await?; + + // Alice receives an unsigned message from Bob. + let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_unsigned.eml"); + receive_imf(&alice, raw, false).await?; + + let msg = alice.get_last_msg().await; + assert!(!msg.get_showpadlock()); + assert!(msg.error().is_none()); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_mua_user_adds_member() -> Result<()> { let t = TestContext::new_alice().await; diff --git a/test-data/message/thunderbird_encrypted_unsigned.eml b/test-data/message/thunderbird_encrypted_unsigned.eml new file mode 100644 index 000000000..924edaced --- /dev/null +++ b/test-data/message/thunderbird_encrypted_unsigned.eml @@ -0,0 +1,44 @@ +Message-ID: <1e87b947-d7be-1ebd-e374-e22ebaa5e00a@example.net> +Date: Fri, 23 Dec 2022 13:00:00 +0000 +MIME-Version: 1.0 +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 + Thunderbird/102.6.1 +Content-Language: en-US +To: Alice +From: Bob +Subject: ... +Content-Type: multipart/encrypted; + protocol="application/pgp-encrypted"; + boundary="------------LXsQ1GfDj60OyJAdRFaJ9eSx" + +This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156) +--------------LXsQ1GfDj60OyJAdRFaJ9eSx +Content-Type: application/pgp-encrypted +Content-Description: PGP/MIME version identification + +Version: 1 + +--------------LXsQ1GfDj60OyJAdRFaJ9eSx +Content-Type: application/octet-stream; name="encrypted.asc" +Content-Description: OpenPGP encrypted message +Content-Disposition: inline; filename="encrypted.asc" + +-----BEGIN PGP MESSAGE----- + +wV4D5tq63hTeebASAQdAgKVFSZ8p8RyNRongu8xc0qnEl54jbOTjrow1rYHECFUw +44bDFHvaSjFYMFcFmumYZQ+hFcjaoIEv78Wx6GmEGTFzFcs8mBKUGbRDFeLJebbU +0sE8AcETwB3nrhR4WvUYlmqt87EAJfigMYX3Cmod0Pz9cFPdE13NIoOotZXEcxHq +aGefenHJUYF1/FX9iuGRMt/qo41stlSJ6Z575mxUKnHexJVnvoDgFLTwS2dGTNlt +0VBk7NOeaCmbxm8u4uJ3kt2vPG7ViYsSvHMGdYciIkGMsyHpJPzrOHzyoXZuuea1 +t8QTZym3FhHH9gbTRCnfnIX60G1Qu9hSMRAQandA6J2to9IahmlahFP9XoUin3fK +ikeCHoNs9kTFFXarU0q99O6byhQqwSehoMhx4BLEFXvMp49jD9LzHUGJmR3Pdcqb +lBgU9mPYWflfiQ8wj2Awyj94+YR3ovaOuc75LE+JylJ9BLk3axZou2HI6hDCoZJg +XQkF46JMPV4NArOtPxP1N5/Gvo9TGvH7H4LdMhsI1Wc8Lfiks6UdcGXUblAlEfcN +rQfywPIH4n+H8cTqk1v2ON3OsD9sFluggWUHEWLP3Eqtr1RO2YQURv+N+pcqvLyq +kPIP9JQ5rfSMjbRNBmN5RReflomYcq9Dt8iobMWXt2fokiyJueaRwZSst2d/pG6H +oYqzzxM7DXnxaJvZELGwJ2tGlQPL5JtSZL+jgL+Zd7+Z7czuRLLoVqf0Q6tgQPKE +s8cpgxVDW0hp5T3ukNvL03SxK+v+dqBPFLd9FYcxMA== +=Wl0m +-----END PGP MESSAGE----- + +--------------LXsQ1GfDj60OyJAdRFaJ9eSx-- From 76cf1707082216cbb7cfbe07cf72e4b218610707 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 23 Dec 2022 11:54:36 +0000 Subject: [PATCH 022/132] ci: update swatinem/rust-cache action --- .github/workflows/ci.yml | 8 ++++---- .github/workflows/jsonrpc.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b1d28faf..a4d22f425 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: override: true - run: rustup component add rustfmt - name: Cache rust cargo artifacts - uses: swatinem/rust-cache@v1 + uses: swatinem/rust-cache@v2 - run: cargo fmt --all -- --check run_clippy: @@ -38,7 +38,7 @@ jobs: components: clippy override: true - name: Cache rust cargo artifacts - uses: swatinem/rust-cache@v1 + uses: swatinem/rust-cache@v2 - uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} @@ -60,7 +60,7 @@ jobs: components: rust-docs override: true - name: Cache rust cargo artifacts - uses: swatinem/rust-cache@v1 + uses: swatinem/rust-cache@v2 - name: Rustdoc run: cargo doc --document-private-items --no-deps @@ -96,7 +96,7 @@ jobs: override: true - name: Cache rust cargo artifacts - uses: swatinem/rust-cache@v1 + uses: swatinem/rust-cache@v2 - name: check run: cargo check --all --bins --examples --tests --features repl --benches diff --git a/.github/workflows/jsonrpc.yml b/.github/workflows/jsonrpc.yml index c6819110a..e84985229 100644 --- a/.github/workflows/jsonrpc.yml +++ b/.github/workflows/jsonrpc.yml @@ -25,7 +25,7 @@ jobs: toolchain: stable override: true - name: Add Rust cache - uses: Swatinem/rust-cache@v1.3.0 + uses: Swatinem/rust-cache@v2 - name: npm install run: | cd deltachat-jsonrpc/typescript From 6d2b2ac5f99e85f62d4296b691dc933bd41baf1a Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 23 Dec 2022 17:28:14 +0000 Subject: [PATCH 023/132] python: pass DC_RS_DEV and DC_RS_TARGET into auditwheels env Otherwise python binding wheels fail to build with tox 4.0. --- python/tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python/tox.ini b/python/tox.ini index f8304a9b3..b182c41a1 100644 --- a/python/tox.ini +++ b/python/tox.ini @@ -38,6 +38,8 @@ passenv = skipsdist = True deps = auditwheel passenv = + DCC_RS_DEV + DCC_RS_TARGET AUDITWHEEL_ARCH AUDITWHEEL_PLAT AUDITWHEEL_POLICY From 3de53a313f29920934a5de4a4da12df2c2f1777e Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 23 Dec 2022 18:15:38 +0000 Subject: [PATCH 024/132] Make pk_decrypt synchronous --- src/decrypt.rs | 2 +- src/pgp.rs | 20 ++++++-------------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/decrypt.rs b/src/decrypt.rs index 7b055f654..ca07a3a82 100644 --- a/src/decrypt.rs +++ b/src/decrypt.rs @@ -215,7 +215,7 @@ async fn decrypt_part( if has_decrypted_pgp_armor(&data) { let (plain, ret_valid_signatures) = - pgp::pk_decrypt(data, private_keyring, &public_keyring_for_validate).await?; + pgp::pk_decrypt(data, private_keyring, &public_keyring_for_validate)?; // Check for detached signatures. // If decrypted part is a multipart/signed, then there is a detached signature. diff --git a/src/pgp.rs b/src/pgp.rs index 8a91a46b3..486d510d2 100644 --- a/src/pgp.rs +++ b/src/pgp.rs @@ -265,23 +265,20 @@ pub async fn pk_encrypt( /// of all keys from the `public_keys_for_validation` keyring that /// have valid signatures there. #[allow(clippy::implicit_hasher)] -pub async fn pk_decrypt( +pub fn pk_decrypt( ctext: Vec, private_keys_for_decryption: Keyring, public_keys_for_validation: &Keyring, ) -> Result<(Vec, HashSet)> { let mut ret_signature_fingerprints: HashSet = Default::default(); - let msgs = tokio::task::spawn_blocking(move || { - let cursor = Cursor::new(ctext); - let (msg, _) = Message::from_armor_single(cursor)?; + let cursor = Cursor::new(ctext); + let (msg, _) = Message::from_armor_single(cursor)?; - let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption.keys().iter().collect(); + let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption.keys().iter().collect(); - let (decryptor, _) = msg.decrypt(|| "".into(), || "".into(), &skeys[..])?; - decryptor.collect::>>() - }) - .await??; + let (decryptor, _) = msg.decrypt(|| "".into(), || "".into(), &skeys[..])?; + let msgs = decryptor.collect::>>()?; if let Some(msg) = msgs.into_iter().next() { // get_content() will decompress the message if needed, @@ -517,7 +514,6 @@ mod tests { decrypt_keyring, &sig_check_keyring, ) - .await .unwrap(); assert_eq!(plain, CLEARTEXT); assert_eq!(valid_signatures.len(), 1); @@ -532,7 +528,6 @@ mod tests { decrypt_keyring, &sig_check_keyring, ) - .await .unwrap(); assert_eq!(plain, CLEARTEXT); assert_eq!(valid_signatures.len(), 1); @@ -548,7 +543,6 @@ mod tests { keyring, &empty_keyring, ) - .await .unwrap(); assert_eq!(plain, CLEARTEXT); assert_eq!(valid_signatures.len(), 0); @@ -566,7 +560,6 @@ mod tests { decrypt_keyring, &sig_check_keyring, ) - .await .unwrap(); assert_eq!(plain, CLEARTEXT); assert_eq!(valid_signatures.len(), 0); @@ -582,7 +575,6 @@ mod tests { decrypt_keyring, &sig_check_keyring, ) - .await .unwrap(); assert_eq!(plain, CLEARTEXT); assert_eq!(valid_signatures.len(), 0); From ed24eac29c860378910aeeb803c8775c4e630b8e Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 23 Dec 2022 18:17:31 +0000 Subject: [PATCH 025/132] Make decrypt_part synchronous --- src/decrypt.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/decrypt.rs b/src/decrypt.rs index ca07a3a82..8adee03fd 100644 --- a/src/decrypt.rs +++ b/src/decrypt.rs @@ -53,8 +53,7 @@ pub async fn try_decrypt( encrypted_data_part, private_keyring, public_keyring_for_validate, - ) - .await? + )? .map(|(raw, fprints)| (raw, fprints, true))) } @@ -206,7 +205,7 @@ fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail } /// Returns Ok(None) if nothing encrypted was found. -async fn decrypt_part( +fn decrypt_part( mail: &ParsedMail<'_>, private_keyring: Keyring, public_keyring_for_validate: Keyring, From 2cd1da5222ca61d6432720778c337d057c3463a0 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 23 Dec 2022 18:43:24 +0000 Subject: [PATCH 026/132] Pass private keyring around as a reference --- src/decrypt.rs | 4 ++-- src/pgp.rs | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/decrypt.rs b/src/decrypt.rs index 8adee03fd..a4aeffaab 100644 --- a/src/decrypt.rs +++ b/src/decrypt.rs @@ -51,7 +51,7 @@ pub async fn try_decrypt( Ok(decrypt_part( encrypted_data_part, - private_keyring, + &private_keyring, public_keyring_for_validate, )? .map(|(raw, fprints)| (raw, fprints, true))) @@ -207,7 +207,7 @@ fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail /// Returns Ok(None) if nothing encrypted was found. fn decrypt_part( mail: &ParsedMail<'_>, - private_keyring: Keyring, + private_keyring: &Keyring, public_keyring_for_validate: Keyring, ) -> Result, HashSet)>> { let data = mail.get_body_raw()?; diff --git a/src/pgp.rs b/src/pgp.rs index 486d510d2..93762e68a 100644 --- a/src/pgp.rs +++ b/src/pgp.rs @@ -267,7 +267,7 @@ pub async fn pk_encrypt( #[allow(clippy::implicit_hasher)] pub fn pk_decrypt( ctext: Vec, - private_keys_for_decryption: Keyring, + private_keys_for_decryption: &Keyring, public_keys_for_validation: &Keyring, ) -> Result<(Vec, HashSet)> { let mut ret_signature_fingerprints: HashSet = Default::default(); @@ -511,7 +511,7 @@ mod tests { sig_check_keyring.add(KEYS.alice_public.clone()); let (plain, valid_signatures) = pk_decrypt( ctext_signed().await.as_bytes().to_vec(), - decrypt_keyring, + &decrypt_keyring, &sig_check_keyring, ) .unwrap(); @@ -525,7 +525,7 @@ mod tests { sig_check_keyring.add(KEYS.alice_public.clone()); let (plain, valid_signatures) = pk_decrypt( ctext_signed().await.as_bytes().to_vec(), - decrypt_keyring, + &decrypt_keyring, &sig_check_keyring, ) .unwrap(); @@ -540,7 +540,7 @@ mod tests { let empty_keyring = Keyring::new(); let (plain, valid_signatures) = pk_decrypt( ctext_signed().await.as_bytes().to_vec(), - keyring, + &keyring, &empty_keyring, ) .unwrap(); @@ -557,7 +557,7 @@ mod tests { sig_check_keyring.add(KEYS.bob_public.clone()); let (plain, valid_signatures) = pk_decrypt( ctext_signed().await.as_bytes().to_vec(), - decrypt_keyring, + &decrypt_keyring, &sig_check_keyring, ) .unwrap(); @@ -572,7 +572,7 @@ mod tests { let sig_check_keyring = Keyring::new(); let (plain, valid_signatures) = pk_decrypt( ctext_unsigned().await.as_bytes().to_vec(), - decrypt_keyring, + &decrypt_keyring, &sig_check_keyring, ) .unwrap(); From 93054ef87c0d9c5b76283d54530d5845e431c47a Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 Dec 2022 10:40:11 +0000 Subject: [PATCH 027/132] Use new_alice() instead of new() in mimeparser tests This way contexts have a private key and attempts to load it does not result in an error. --- src/mimeparser.rs | 58 ++++++++++++++++++++-------------------- src/receive_imf/tests.rs | 6 ++--- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 8050951df..6c0a71221 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -2020,7 +2020,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_mimeparser_crash() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = include_bytes!("../test-data/message/issue_523.txt"); let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]) .await @@ -2032,7 +2032,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_get_rfc724_mid_exists() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = include_bytes!("../test-data/message/mail_with_message_id.txt"); let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]) .await @@ -2046,7 +2046,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_get_rfc724_mid_not_exists() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = include_bytes!("../test-data/message/issue_523.txt"); let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]) .await @@ -2252,7 +2252,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_get_parent_timestamp() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = b"From: foo@example.org\n\ Content-Type: text/plain\n\ Chat-Version: 1.0\n\ @@ -2285,7 +2285,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_mimeparser_with_context() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = b"From: hello@example.org\n\ Content-Type: multipart/mixed; boundary=\"==break==\";\n\ Subject: outer-subject\n\ @@ -2337,7 +2337,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_mimeparser_with_avatars() { - let t = TestContext::new().await; + let t = TestContext::new_alice().await; let raw = include_bytes!("../test-data/message/mail_attach_txt.eml"); let mimeparser = MimeMessage::from_bytes(&t, &raw[..]).await.unwrap(); @@ -2378,7 +2378,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_mimeparser_with_videochat() { - let t = TestContext::new().await; + let t = TestContext::new_alice().await; let raw = include_bytes!("../test-data/message/videochat_invitation.eml"); let mimeparser = MimeMessage::from_bytes(&t, &raw[..]).await.unwrap(); @@ -2400,7 +2400,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_mimeparser_message_kml() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = b"Chat-Version: 1.0\n\ From: foo \n\ To: bar \n\ @@ -2445,7 +2445,7 @@ Content-Disposition: attachment; filename=\"message.kml\"\n\ #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_parse_mdn() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\ Date: Mon, 10 Jan 2020 00:00:00 +0000\n\ Chat-Version: 1.0\n\ @@ -2495,7 +2495,7 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\ /// multipart MIME messages. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_parse_multiple_mdns() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\ Date: Mon, 10 Jan 2020 00:00:00 +0000\n\ Chat-Version: 1.0\n\ @@ -2571,7 +2571,7 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\ #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_parse_mdn_with_additional_message_ids() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\ Date: Mon, 10 Jan 2020 00:00:00 +0000\n\ Chat-Version: 1.0\n\ @@ -2626,7 +2626,7 @@ Additional-Message-IDs: \n\ #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_parse_inline_attachment() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = br#"Date: Thu, 13 Feb 2020 22:41:20 +0000 (UTC) From: sender@example.com To: receiver@example.com @@ -2666,7 +2666,7 @@ MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg== #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_hide_html_without_content() { - let t = TestContext::new().await; + let t = TestContext::new_alice().await; let raw = br#"Date: Thu, 13 Feb 2020 22:41:20 +0000 (UTC) From: sender@example.com To: receiver@example.com @@ -2715,7 +2715,7 @@ MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg== #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn parse_inline_image() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = br#"Message-ID: From: foo Subject: example @@ -2761,7 +2761,7 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I= #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn parse_thunderbird_html_embedded_image() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = br#"To: Alice From: Bob Subject: Test subject @@ -2834,7 +2834,7 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I= // Outlook specifies filename in the "name" attribute of Content-Type #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn parse_outlook_html_embedded_image() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = br##"From: Anonymous To: Anonymous Subject: Delta Chat is great stuff! @@ -2973,7 +2973,7 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I= #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn parse_format_flowed_quote() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = br##"Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no Subject: Re: swipe-to-reply MIME-Version: 1.0 @@ -3009,7 +3009,7 @@ Reply #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn parse_quote_without_reply() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = br##"Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no Subject: Re: swipe-to-reply MIME-Version: 1.0 @@ -3041,7 +3041,7 @@ From: alice #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn parse_quote_top_posting() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = br##"Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no Subject: Re: top posting MIME-Version: 1.0 @@ -3072,7 +3072,7 @@ On 2020-10-25, Bob wrote: #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_attachment_quote() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = include_bytes!("../test-data/message/quote_attach.eml"); let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]) .await @@ -3090,7 +3090,7 @@ On 2020-10-25, Bob wrote: #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_quote_div() { - let t = TestContext::new().await; + let t = TestContext::new_alice().await; let raw = include_bytes!("../test-data/message/gmx-quote.eml"); let mimeparser = MimeMessage::from_bytes(&t, raw).await.unwrap(); assert_eq!(mimeparser.parts[0].msg, "YIPPEEEEEE\n\nMulti-line"); @@ -3100,7 +3100,7 @@ On 2020-10-25, Bob wrote: #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_allinkl_blockquote() { // all-inkl.com puts quotes into `
`. - let t = TestContext::new().await; + let t = TestContext::new_alice().await; let raw = include_bytes!("../test-data/message/allinkl-quote.eml"); let mimeparser = MimeMessage::from_bytes(&t, raw).await.unwrap(); assert!(mimeparser.parts[0].msg.starts_with("It's 1.0.")); @@ -3145,7 +3145,7 @@ On 2020-10-25, Bob wrote: #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_mime_modified_plain() { - let t = TestContext::new().await; + let t = TestContext::new_alice().await; let raw = include_bytes!("../test-data/message/text_plain_unspecified.eml"); let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await.unwrap(); assert!(!mimeparser.is_mime_modified); @@ -3157,7 +3157,7 @@ On 2020-10-25, Bob wrote: #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_mime_modified_alt_plain_html() { - let t = TestContext::new().await; + let t = TestContext::new_alice().await; let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml"); let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await.unwrap(); assert!(mimeparser.is_mime_modified); @@ -3169,7 +3169,7 @@ On 2020-10-25, Bob wrote: #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_mime_modified_alt_plain() { - let t = TestContext::new().await; + let t = TestContext::new_alice().await; let raw = include_bytes!("../test-data/message/text_alt_plain.eml"); let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await.unwrap(); assert!(!mimeparser.is_mime_modified); @@ -3184,7 +3184,7 @@ On 2020-10-25, Bob wrote: #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_mime_modified_alt_html() { - let t = TestContext::new().await; + let t = TestContext::new_alice().await; let raw = include_bytes!("../test-data/message/text_alt_html.eml"); let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await.unwrap(); assert!(mimeparser.is_mime_modified); @@ -3196,7 +3196,7 @@ On 2020-10-25, Bob wrote: #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_mime_modified_html() { - let t = TestContext::new().await; + let t = TestContext::new_alice().await; let raw = include_bytes!("../test-data/message/text_html.eml"); let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await.unwrap(); assert!(mimeparser.is_mime_modified); @@ -3208,7 +3208,7 @@ On 2020-10-25, Bob wrote: #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_mime_modified_large_plain() { - let t = TestContext::new().await; + let t = TestContext::new_alice().await; static REPEAT_TXT: &str = "this text with 42 chars is just repeated.\n"; static REPEAT_CNT: usize = 2000; // results in a text of 84k, should be more than DC_DESIRED_TEXT_LEN @@ -3229,7 +3229,7 @@ On 2020-10-25, Bob wrote: #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_x_microsoft_original_message_id() { - let t = TestContext::new().await; + let t = TestContext::new_alice().await; let message = MimeMessage::from_bytes(&t, b"Date: Wed, 17 Feb 2021 15:45:15 +0000\n\ Chat-Version: 1.0\n\ Message-ID: \n\ diff --git a/src/receive_imf/tests.rs b/src/receive_imf/tests.rs index c781b3785..a92c8aead 100644 --- a/src/receive_imf/tests.rs +++ b/src/receive_imf/tests.rs @@ -13,7 +13,7 @@ use crate::test_utils::{get_chat_msg, TestContext, TestContextManager}; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_grpid_simple() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ From: hello@example.org\n\ Subject: outer-subject\n\ @@ -31,7 +31,7 @@ async fn test_grpid_simple() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_bad_from() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ From: hello\n\ Subject: outer-subject\n\ @@ -45,7 +45,7 @@ async fn test_bad_from() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_grpid_from_multiple() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ From: hello@example.org\n\ Subject: outer-subject\n\ From e328de5293689649d097ad6b7be9e918de001a1b Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 24 Dec 2022 10:41:22 +0000 Subject: [PATCH 028/132] Make try_decrypt non-async Private keyring is now loaded outside of try_decrypt --- src/decrypt.rs | 10 ++++------ src/mimeparser.rs | 8 ++++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/decrypt.rs b/src/decrypt.rs index a4aeffaab..4d6880be8 100644 --- a/src/decrypt.rs +++ b/src/decrypt.rs @@ -2,7 +2,7 @@ use std::collections::HashSet; -use anyhow::{Context as _, Result}; +use anyhow::Result; use mailparse::ParsedMail; use crate::aheader::Aheader; @@ -24,9 +24,10 @@ use crate::pgp; /// signature fingerprints and whether the message is encrypted. /// /// If the message is wrongly signed, HashSet will be empty. -pub async fn try_decrypt( +pub fn try_decrypt( context: &Context, mail: &ParsedMail<'_>, + private_keyring: &Keyring, decryption_info: &DecryptionInfo, ) -> Result, HashSet, bool)>> { // Possibly perform decryption @@ -45,13 +46,10 @@ pub async fn try_decrypt( Some(res) => res, }; info!(context, "Detected Autocrypt-mime message"); - let private_keyring: Keyring = Keyring::new_self(context) - .await - .context("failed to get own keyring")?; Ok(decrypt_part( encrypted_data_part, - &private_keyring, + private_keyring, public_keyring_for_validate, )? .map(|(raw, fprints)| (raw, fprints, true))) diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 6c0a71221..11d45e0a2 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -23,7 +23,8 @@ use crate::dehtml::dehtml; use crate::events::EventType; use crate::format_flowed::unformat_flowed; use crate::headerdef::{HeaderDef, HeaderDefMap}; -use crate::key::{DcKey, Fingerprint, SignedPublicKey}; +use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey}; +use crate::keyring::Keyring; use crate::message::{self, Viewtype}; use crate::param::{Param, Params}; use crate::peerstate::Peerstate; @@ -220,6 +221,9 @@ impl MimeMessage { headers.remove("chat-verified"); let from = from.context("No from in message")?; + let private_keyring: Keyring = Keyring::new_self(context) + .await + .context("failed to get own keyring")?; let mut decryption_info = prepare_decryption(context, &mail, &from.addr, message_time).await?; @@ -231,7 +235,7 @@ impl MimeMessage { hop_info += &decryption_info.dkim_results.to_string(); let (mail, mut signatures, encrypted) = - match try_decrypt(context, &mail, &decryption_info).await { + match try_decrypt(context, &mail, &private_keyring, &decryption_info) { Ok(Some((raw, signatures, encrypted))) => { // Only if `encrypted` and `signatures` set is non-empty, it is a valid // autocrypt message. From d73c4a92a70d309bfc566f1185340c94505d660d Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 26 Dec 2022 00:25:56 +0000 Subject: [PATCH 029/132] Silence clippy warning about type complexity --- src/decrypt.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/decrypt.rs b/src/decrypt.rs index 4d6880be8..51e4c5a8f 100644 --- a/src/decrypt.rs +++ b/src/decrypt.rs @@ -24,6 +24,7 @@ use crate::pgp; /// signature fingerprints and whether the message is encrypted. /// /// If the message is wrongly signed, HashSet will be empty. +#[allow(clippy::type_complexity)] pub fn try_decrypt( context: &Context, mail: &ParsedMail<'_>, From 8de7014eebd51a540cd94a3af46030e87253fc3c Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 26 Dec 2022 00:01:58 +0000 Subject: [PATCH 030/132] Fix nightly clippy warnings --- src/imex.rs | 2 +- src/key.rs | 2 +- src/lib.rs | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/imex.rs b/src/imex.rs index 11bc0c748..ea4033959 100644 --- a/src/imex.rs +++ b/src/imex.rs @@ -350,7 +350,7 @@ async fn decrypt_setup_file( fn normalize_setup_code(s: &str) -> String { let mut out = String::new(); for c in s.chars() { - if ('0'..='9').contains(&c) { + if c.is_ascii_digit() { out.push(c); if let 4 | 9 | 14 | 19 | 24 | 29 | 34 | 39 = out.len() { out += "-" diff --git a/src/key.rs b/src/key.rs index a67cdab43..e2fbe7c82 100644 --- a/src/key.rs +++ b/src/key.rs @@ -387,7 +387,7 @@ impl std::str::FromStr for Fingerprint { let hex_repr: String = input .to_uppercase() .chars() - .filter(|&c| ('0'..='9').contains(&c) || ('A'..='F').contains(&c)) + .filter(|&c| c.is_ascii_hexdigit()) .collect(); let v: Vec = hex::decode(&hex_repr)?; ensure!(v.len() == 20, "wrong fingerprint length: {}", hex_repr); diff --git a/src/lib.rs b/src/lib.rs index a50483843..7def6a20b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,7 @@ clippy::unused_async )] #![allow( + clippy::uninlined_format_args, clippy::match_bool, clippy::mixed_read_write_in_expression, clippy::bool_assert_comparison, From 942f64f04d5780b763934710c60b37d311e029a2 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 25 Dec 2022 12:23:48 +0000 Subject: [PATCH 031/132] Remove authors field from Cargo metadata See Rust RFC 3052 [1]. This field is no longer required, so there is no need to have a filler value here anymore. [1] --- Cargo.toml | 1 - deltachat-ffi/Cargo.toml | 1 - deltachat-jsonrpc/Cargo.toml | 1 - deltachat-rpc-server/Cargo.toml | 1 - deltachat_derive/Cargo.toml | 1 - 5 files changed, 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 88282eb59..d7d0302df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,6 @@ [package] name = "deltachat" version = "1.104.0" -authors = ["Delta Chat Developers (ML) "] edition = "2021" license = "MPL-2.0" rust-version = "1.61" diff --git a/deltachat-ffi/Cargo.toml b/deltachat-ffi/Cargo.toml index 8153d954f..e7cc70998 100644 --- a/deltachat-ffi/Cargo.toml +++ b/deltachat-ffi/Cargo.toml @@ -2,7 +2,6 @@ name = "deltachat_ffi" version = "1.104.0" description = "Deltachat FFI" -authors = ["Delta Chat Developers (ML) "] edition = "2018" readme = "README.md" license = "MPL-2.0" diff --git a/deltachat-jsonrpc/Cargo.toml b/deltachat-jsonrpc/Cargo.toml index e2aeddf4e..c8d77c5b9 100644 --- a/deltachat-jsonrpc/Cargo.toml +++ b/deltachat-jsonrpc/Cargo.toml @@ -2,7 +2,6 @@ name = "deltachat-jsonrpc" version = "1.104.0" description = "DeltaChat JSON-RPC API" -authors = ["Delta Chat Developers (ML) "] edition = "2021" default-run = "deltachat-jsonrpc-server" license = "MPL-2.0" diff --git a/deltachat-rpc-server/Cargo.toml b/deltachat-rpc-server/Cargo.toml index bd2ba9b34..49c568bd9 100644 --- a/deltachat-rpc-server/Cargo.toml +++ b/deltachat-rpc-server/Cargo.toml @@ -2,7 +2,6 @@ name = "deltachat-rpc-server" version = "1.104.0" description = "DeltaChat JSON-RPC server" -authors = ["Delta Chat Developers (ML) "] edition = "2021" readme = "README.md" license = "MPL-2.0" diff --git a/deltachat_derive/Cargo.toml b/deltachat_derive/Cargo.toml index 616a71515..094796cda 100644 --- a/deltachat_derive/Cargo.toml +++ b/deltachat_derive/Cargo.toml @@ -1,7 +1,6 @@ [package] name = "deltachat_derive" version = "2.0.0" -authors = ["Delta Chat Developers (ML) "] edition = "2018" license = "MPL-2.0" From 6f7bb8a777991516e2255af221eaafd4ef22327b Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 25 Dec 2022 19:55:53 +0000 Subject: [PATCH 032/132] Do not trim leading spaces from message lines --- CHANGELOG.md | 1 + src/format_flowed.rs | 52 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0395f1e56..71fe2921d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixes - Do not add an error if the message is encrypted but not signed #3860 +- Do not strip leading spaces from message lines #3867 ## 1.104.0 diff --git a/src/format_flowed.rs b/src/format_flowed.rs index cb4c5f045..cea31306d 100644 --- a/src/format_flowed.rs +++ b/src/format_flowed.rs @@ -28,6 +28,10 @@ fn format_line_flowed(line: &str, prefix: &str) -> String { for c in line.chars() { if c == ' ' { + if buffer.is_empty() { + // Space stuffing, see RFC 3676 + buffer.push(' '); + } buffer.push(c); after_space = true; } else if c == '>' { @@ -66,16 +70,18 @@ pub(crate) fn format_flowed(text: &str) -> String { result += "\r\n"; } - let line_no_prefix = line.strip_prefix('>'); + let line_no_prefix = line + .strip_prefix('>') + .map(|line| line.strip_prefix(' ').unwrap_or(line)); let is_quote = line_no_prefix.is_some(); - let line = line_no_prefix.unwrap_or(line).trim(); + let line = line_no_prefix.unwrap_or(line).trim_end(); let prefix = if is_quote { "> " } else { "" }; if prefix.len() + line.len() > 78 { result += &format_line_flowed(line, prefix); } else { result += prefix; - if prefix.is_empty() && line.starts_with('>') { + if prefix.is_empty() && (line.starts_with('>') || line.starts_with(' ')) { // Space stuffing, see RFC 3676 result.push(' '); } @@ -142,6 +148,7 @@ pub fn unformat_flowed(text: &str, delsp: bool) -> String { mod tests { use super::*; use crate::test_utils::TestContext; + use anyhow::Result; #[test] fn test_format_flowed() { @@ -170,6 +177,13 @@ mod tests { > To decrypt and use your key, open the message in an Autocrypt-compliant \r\n\ > client and enter the setup code presented on the generating device."; assert_eq!(format_flowed(text), expected); + + // Test space stuffing of spaces. + let text = " Foo bar baz"; + assert_eq!(format_flowed(text), " Foo bar baz"); + + let text = " Foo bar baz"; + assert_eq!(format_flowed(text), " Foo bar baz"); } #[test] @@ -180,6 +194,10 @@ mod tests { "this is a very long message that should be wrapped using format=flowed and \ unwrapped on the receiver"; assert_eq!(unformat_flowed(text, false), expected); + + let text = " Foo bar"; + let expected = " Foo bar"; + assert_eq!(unformat_flowed(text, false), expected); } #[test] @@ -204,7 +222,7 @@ mod tests { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_send_quotes() -> anyhow::Result<()> { + async fn test_send_quotes() -> Result<()> { let alice = TestContext::new_alice().await; let bob = TestContext::new_bob().await; let chat = alice.create_chat(&bob).await; @@ -223,4 +241,30 @@ mod tests { Ok(()) } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_format_flowed_round_trip() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let chat = alice.create_chat(&bob).await; + + let text = " Foo bar"; + let sent = alice.send_text(chat.id, text).await; + let received = bob.recv_msg(&sent).await; + assert_eq!(received.text.as_deref(), Some(text)); + + let text = "Foo bar baz"; + let sent = alice.send_text(chat.id, text).await; + let received = bob.recv_msg(&sent).await; + assert_eq!(received.text.as_deref(), Some(text)); + + let python_program = "\ +def hello(): + return 'Hello, world!'"; + let sent = alice.send_text(chat.id, python_program).await; + let received = bob.recv_msg(&sent).await; + assert_eq!(received.text.as_deref(), Some(python_program)); + + Ok(()) + } } From 7d62df6f1a9aff3812b06f73f7772b822d92b3d9 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 25 Dec 2022 15:08:33 +0000 Subject: [PATCH 033/132] Bump MSRV to 1.63.0 Bumping MSRV from 1.61.0 to 1.63.0, because `arbitrary` crate requires it and fuzzing crates depend on it, at least by default. We still use 1.64.0 as our default rust toolchain. --- .github/workflows/ci.yml | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4d22f425..5b57e5b06 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,13 +77,13 @@ jobs: rust: 1.64.0 python: false # Python bindings compilation on Windows is not supported. - # Minimum Supported Rust Version = 1.61.0 + # Minimum Supported Rust Version = 1.63.0 # # Minimum Supported Python Version = 3.7 # This is the minimum version for which manylinux Python wheels are # built. - os: ubuntu-latest - rust: 1.61.0 + rust: 1.63.0 python: 3.7 runs-on: ${{ matrix.os }} steps: diff --git a/Cargo.toml b/Cargo.toml index d7d0302df..4d09f5492 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "deltachat" version = "1.104.0" edition = "2021" license = "MPL-2.0" -rust-version = "1.61" +rust-version = "1.63" [profile.dev] debug = 0 From 6dc790f4470673835d50ed1ef494e6993a3d2d2d Mon Sep 17 00:00:00 2001 From: iequidoo Date: Fri, 23 Dec 2022 21:30:14 -0300 Subject: [PATCH 034/132] Don't parse the message again after detached signatures validation If we move the detached signatures validation code out of try_decrypt(), we don't need to convert an already parsed signed message part to Vec and then parse it back. Also this simplifies the try_decrypt() semantics and return type. It can't make a good coffee anyway. --- CHANGELOG.md | 1 + src/decrypt.rs | 58 ++++++------------- src/mimeparser.rs | 142 ++++++++++++++++++++++++---------------------- 3 files changed, 92 insertions(+), 109 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71fe2921d..33954c041 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Changes - Validate signatures in try_decrypt() even if the message isn't encrypted #3859 +- Don't parse the message again after detached signatures validation #3862 ### API-Changes diff --git a/src/decrypt.rs b/src/decrypt.rs index 51e4c5a8f..68bcba737 100644 --- a/src/decrypt.rs +++ b/src/decrypt.rs @@ -17,43 +17,32 @@ use crate::log::LogExt; use crate::peerstate::Peerstate; use crate::pgp; -/// Tries to decrypt a message, but only if it is structured as an -/// Autocrypt message, otherwise just validates signatures. +/// Tries to decrypt a message, but only if it is structured as an Autocrypt message. /// -/// If successful and the message is encrypted or signed, returns \[decrypted\] body, a set of valid -/// signature fingerprints and whether the message is encrypted. +/// If successful and the message is encrypted, returns decrypted body and a set of valid +/// signature fingerprints. /// /// If the message is wrongly signed, HashSet will be empty. -#[allow(clippy::type_complexity)] pub fn try_decrypt( context: &Context, mail: &ParsedMail<'_>, private_keyring: &Keyring, - decryption_info: &DecryptionInfo, -) -> Result, HashSet, bool)>> { - // Possibly perform decryption - let public_keyring_for_validate = keyring_from_peerstate(decryption_info.peerstate.as_ref()); - + public_keyring_for_validate: &Keyring, +) -> Result, HashSet)>> { let encrypted_data_part = match get_autocrypt_mime(mail) .or_else(|| get_mixed_up_mime(mail)) .or_else(|| get_attachment_mime(mail)) { - None => { - return Ok( - validate_detached_signature(mail, &public_keyring_for_validate) - .map(|(raw, fprints)| (raw, fprints, false)), - ) - } + None => return Ok(None), Some(res) => res, }; info!(context, "Detected Autocrypt-mime message"); - Ok(decrypt_part( + decrypt_part( encrypted_data_part, private_keyring, public_keyring_for_validate, - )? - .map(|(raw, fprints)| (raw, fprints, true))) + ) } pub(crate) async fn prepare_decryption( @@ -207,27 +196,14 @@ fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail fn decrypt_part( mail: &ParsedMail<'_>, private_keyring: &Keyring, - public_keyring_for_validate: Keyring, + public_keyring_for_validate: &Keyring, ) -> Result, HashSet)>> { let data = mail.get_body_raw()?; if has_decrypted_pgp_armor(&data) { let (plain, ret_valid_signatures) = - pgp::pk_decrypt(data, private_keyring, &public_keyring_for_validate)?; - - // Check for detached signatures. - // If decrypted part is a multipart/signed, then there is a detached signature. - let decrypted_part = mailparse::parse_mail(&plain)?; - if let Some((content, valid_detached_signatures)) = - validate_detached_signature(&decrypted_part, &public_keyring_for_validate) - { - return Ok(Some((content, valid_detached_signatures))); - } else { - // If the message was wrongly or not signed, still return the plain text. - // The caller has to check if the signatures set is empty then. - - return Ok(Some((plain, ret_valid_signatures))); - } + pgp::pk_decrypt(data, private_keyring, public_keyring_for_validate)?; + return Ok(Some((plain, ret_valid_signatures))); } Ok(None) @@ -249,12 +225,14 @@ fn has_decrypted_pgp_armor(input: &[u8]) -> bool { /// Validates signatures of Multipart/Signed message part, as defined in RFC 1847. /// -/// Returns `None` if the part is not a Multipart/Signed part, otherwise retruns the set of key +/// Returns the signed part and the set of key /// fingerprints for which there is a valid signature. -fn validate_detached_signature( - mail: &ParsedMail<'_>, +/// +/// Returns None if the message is not Multipart/Signed or doesn't contain necessary parts. +pub(crate) fn validate_detached_signature<'a, 'b>( + mail: &'a ParsedMail<'b>, public_keyring_for_validate: &Keyring, -) -> Option<(Vec, HashSet)> { +) -> Option<(&'a ParsedMail<'b>, HashSet)> { if mail.ctype.mimetype != "multipart/signed" { return None; } @@ -267,7 +245,7 @@ fn validate_detached_signature( .unwrap_or_default(), Err(_) => Default::default(), }; - Some((content.to_vec(), ret_valid_signatures)) + Some((first_part, ret_valid_signatures)) } else { None } diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 11d45e0a2..7996a01b8 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -18,7 +18,10 @@ use crate::blob::BlobObject; use crate::constants::{DC_DESIRED_TEXT_LINES, DC_DESIRED_TEXT_LINE_LEN}; use crate::contact::{addr_cmp, addr_normalize, ContactId}; use crate::context::Context; -use crate::decrypt::{prepare_decryption, try_decrypt, DecryptionInfo}; +use crate::decrypt::{ + keyring_from_peerstate, prepare_decryption, try_decrypt, validate_detached_signature, + DecryptionInfo, +}; use crate::dehtml::dehtml; use crate::events::EventType; use crate::format_flowed::unformat_flowed; @@ -234,87 +237,88 @@ impl MimeMessage { hop_info += "\n\n"; hop_info += &decryption_info.dkim_results.to_string(); + let public_keyring = keyring_from_peerstate(decryption_info.peerstate.as_ref()); let (mail, mut signatures, encrypted) = - match try_decrypt(context, &mail, &private_keyring, &decryption_info) { - Ok(Some((raw, signatures, encrypted))) => { - // Only if `encrypted` and `signatures` set is non-empty, it is a valid - // autocrypt message. - + match try_decrypt(context, &mail, &private_keyring, &public_keyring) { + Ok(Some((raw, signatures))) => { mail_raw = raw; let decrypted_mail = mailparse::parse_mail(&mail_raw)?; if std::env::var(crate::DCC_MIME_DEBUG).is_ok() { info!(context, "decrypted message mime-body:"); println!("{}", String::from_utf8_lossy(&mail_raw)); } - - if encrypted { - // Handle any gossip headers if the mail was encrypted. See section - // "3.6 Key Gossip" of - // but only if the mail was correctly signed: - if !signatures.is_empty() { - let gossip_headers = - decrypted_mail.headers.get_all_values("Autocrypt-Gossip"); - gossiped_addr = update_gossip_peerstates( - context, - message_time, - &from.addr, - &mail, - gossip_headers, - ) - .await?; - } - - // let known protected headers from the decrypted - // part override the unencrypted top-level - - // Signature was checked for original From, so we - // do not allow overriding it. - let mut signed_from = None; - - // We do not want to allow unencrypted subject in encrypted emails because the - // user might falsely think that the subject is safe. - // See . - headers.remove("subject"); - - MimeMessage::merge_headers( - context, - &mut headers, - &mut recipients, - &mut signed_from, - &mut list_post, - &mut chat_disposition_notification_to, - &decrypted_mail.headers, - ); - if let Some(signed_from) = signed_from { - if addr_cmp(&signed_from.addr, &from.addr) { - from_is_signed = true; - } else { - // There is a From: header in the encrypted & - // signed part, but it doesn't match the outer one. - // This _might_ be because the sender's mail server - // replaced the sending address, e.g. in a mailing list. - // Or it's because someone is doing some replay attack - // - OTOH, I can't come up with an attack scenario - // where this would be useful. - warn!( - context, - "From header in signed part does't match the outer one", - ); - } - } - } - (Ok(decrypted_mail), signatures, encrypted) + (Ok(decrypted_mail), signatures, true) } Ok(None) => (Ok(mail), HashSet::new(), false), Err(err) => { warn!(context, "decryption failed: {}", err); - (Err(err), HashSet::new(), true) + (Err(err), HashSet::new(), false) } }; + let mail = mail.as_ref().map(|mail| { + let (content, signatures_detached) = validate_detached_signature(mail, &public_keyring) + .unwrap_or((mail, Default::default())); + signatures.extend(signatures_detached); + content + }); + if let (Ok(mail), true) = (mail, encrypted) { + // Handle any gossip headers if the mail was encrypted. See section + // "3.6 Key Gossip" of + // but only if the mail was correctly signed: + if !signatures.is_empty() { + let gossip_headers = mail.headers.get_all_values("Autocrypt-Gossip"); + gossiped_addr = update_gossip_peerstates( + context, + message_time, + &from.addr, + &recipients, + gossip_headers, + ) + .await?; + } + // let known protected headers from the decrypted + // part override the unencrypted top-level + + // Signature was checked for original From, so we + // do not allow overriding it. + let mut signed_from = None; + + // We do not want to allow unencrypted subject in encrypted emails because the + // user might falsely think that the subject is safe. + // See . + headers.remove("subject"); + + MimeMessage::merge_headers( + context, + &mut headers, + &mut recipients, + &mut signed_from, + &mut list_post, + &mut chat_disposition_notification_to, + &mail.headers, + ); + if let Some(signed_from) = signed_from { + if addr_cmp(&signed_from.addr, &from.addr) { + from_is_signed = true; + } else { + // There is a From: header in the encrypted & + // signed part, but it doesn't match the outer one. + // This _might_ be because the sender's mail server + // replaced the sending address, e.g. in a mailing list. + // Or it's because someone is doing some replay attack + // - OTOH, I can't come up with an attack scenario + // where this would be useful. + warn!( + context, + "From header in signed part does't match the outer one", + ); + } + } + } if signatures.is_empty() { // If it is not a read receipt, degrade encryption. - if let (Some(peerstate), Ok(mail)) = (&mut decryption_info.peerstate, &mail) { + if let (Some(peerstate), Ok(mail)) = (&mut decryption_info.peerstate, mail) { if message_time > peerstate.last_seen_autocrypt && mail.ctype.mimetype != "multipart/report" // Disallowing keychanges is disabled for now: @@ -366,7 +370,7 @@ impl MimeMessage { } None => match mail { Ok(mail) => { - parser.parse_mime_recursive(context, &mail, false).await?; + parser.parse_mime_recursive(context, mail, false).await?; } Err(err) => { let msg_body = stock_str::cant_decrypt_msg_body(context).await; @@ -1637,7 +1641,7 @@ async fn update_gossip_peerstates( context: &Context, message_time: i64, from: &str, - mail: &mailparse::ParsedMail<'_>, + recipients: &[SingleInfo], gossip_headers: Vec, ) -> Result> { // XXX split the parsing from the modification part @@ -1652,7 +1656,7 @@ async fn update_gossip_peerstates( } }; - if !get_recipients(&mail.headers) + if !recipients .iter() .any(|info| addr_cmp(&info.addr, &header.addr)) { From 89b7ce4c4e97b3e1d369bf7b5ff68d529d5905a5 Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 26 Dec 2022 13:34:38 +0000 Subject: [PATCH 035/132] Move format_flowed to a separate crate This makes it possible to fuzz test the functions without exposing the module interface in the deltachat core interface. Also ensure that format_flowed will not grow a dependency on deltachat core types. --- CHANGELOG.md | 1 + Cargo.lock | 5 ++ Cargo.toml | 4 +- format-flowed/Cargo.toml | 11 ++++ .../src/lib.rs | 51 +------------------ src/lib.rs | 1 - src/message.rs | 47 +++++++++++++++++ src/mimefactory.rs | 2 +- src/mimeparser.rs | 2 +- 9 files changed, 70 insertions(+), 54 deletions(-) create mode 100644 format-flowed/Cargo.toml rename src/format_flowed.rs => format-flowed/src/lib.rs (79%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33954c041..500f46ebc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Changes - Validate signatures in try_decrypt() even if the message isn't encrypted #3859 - Don't parse the message again after detached signatures validation #3862 +- Move format=flowed support to a separate crate #3869 ### API-Changes diff --git a/Cargo.lock b/Cargo.lock index cbc7a9693..198ae37eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -887,6 +887,7 @@ dependencies = [ "encoded-words", "escaper", "fast-socks5", + "format-flowed", "futures", "futures-lite", "hex", @@ -1445,6 +1446,10 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "format-flowed" +version = "1.0.0" + [[package]] name = "futures" version = "0.3.25" diff --git a/Cargo.toml b/Cargo.toml index 4d09f5492..9fbd387e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ panic = 'abort' [dependencies] deltachat_derive = { path = "./deltachat_derive" } +format-flowed = { path = "./format-flowed" } ansi_term = { version = "0.12.1", optional = true } anyhow = "1" @@ -99,7 +100,8 @@ members = [ "deltachat-ffi", "deltachat_derive", "deltachat-jsonrpc", - "deltachat-rpc-server" + "deltachat-rpc-server", + "format-flowed", ] [[example]] diff --git a/format-flowed/Cargo.toml b/format-flowed/Cargo.toml new file mode 100644 index 000000000..480704215 --- /dev/null +++ b/format-flowed/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "format-flowed" +version = "1.0.0" +description = "format=flowed support" +edition = "2021" +license = "MPL-2.0" + +keywords = ["email"] +categories = ["email"] + +[dependencies] diff --git a/src/format_flowed.rs b/format-flowed/src/lib.rs similarity index 79% rename from src/format_flowed.rs rename to format-flowed/src/lib.rs index cea31306d..67d18e8d1 100644 --- a/src/format_flowed.rs +++ b/format-flowed/src/lib.rs @@ -62,7 +62,7 @@ fn format_line_flowed(line: &str, prefix: &str) -> String { /// /// RFC 2646 technique is used to insert soft line breaks, so DelSp /// SHOULD be set to "no" when sending. -pub(crate) fn format_flowed(text: &str) -> String { +pub fn format_flowed(text: &str) -> String { let mut result = String::new(); for line in text.split('\n') { @@ -147,8 +147,6 @@ pub fn unformat_flowed(text: &str, delsp: bool) -> String { #[cfg(test)] mod tests { use super::*; - use crate::test_utils::TestContext; - use anyhow::Result; #[test] fn test_format_flowed() { @@ -220,51 +218,4 @@ mod tests { > unwrapped on the receiver"; assert_eq!(format_flowed_quote(quote), expected); } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_send_quotes() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let chat = alice.create_chat(&bob).await; - - let sent = alice.send_text(chat.id, "> First quote").await; - let received = bob.recv_msg(&sent).await; - assert_eq!(received.text.as_deref(), Some("> First quote")); - assert!(received.quoted_text().is_none()); - assert!(received.quoted_message(&bob).await?.is_none()); - - let sent = alice.send_text(chat.id, "> Second quote").await; - let received = bob.recv_msg(&sent).await; - assert_eq!(received.text.as_deref(), Some("> Second quote")); - assert!(received.quoted_text().is_none()); - assert!(received.quoted_message(&bob).await?.is_none()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_format_flowed_round_trip() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let chat = alice.create_chat(&bob).await; - - let text = " Foo bar"; - let sent = alice.send_text(chat.id, text).await; - let received = bob.recv_msg(&sent).await; - assert_eq!(received.text.as_deref(), Some(text)); - - let text = "Foo bar baz"; - let sent = alice.send_text(chat.id, text).await; - let received = bob.recv_msg(&sent).await; - assert_eq!(received.text.as_deref(), Some(text)); - - let python_program = "\ -def hello(): - return 'Hello, world!'"; - let sent = alice.send_text(chat.id, python_program).await; - let received = bob.recv_msg(&sent).await; - assert_eq!(received.text.as_deref(), Some(python_program)); - - Ok(()) - } } diff --git a/src/lib.rs b/src/lib.rs index 7def6a20b..771783ba6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,7 +74,6 @@ pub mod imex; mod scheduler; #[macro_use] mod job; -mod format_flowed; pub mod key; mod keyring; pub mod location; diff --git a/src/message.rs b/src/message.rs index 2baef9d3f..a1e1b0b97 100644 --- a/src/message.rs +++ b/src/message.rs @@ -2340,4 +2340,51 @@ mod tests { ); assert_eq!(Viewtype::Webxdc, Viewtype::from_i32(80).unwrap()); } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_send_quotes() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let chat = alice.create_chat(&bob).await; + + let sent = alice.send_text(chat.id, "> First quote").await; + let received = bob.recv_msg(&sent).await; + assert_eq!(received.text.as_deref(), Some("> First quote")); + assert!(received.quoted_text().is_none()); + assert!(received.quoted_message(&bob).await?.is_none()); + + let sent = alice.send_text(chat.id, "> Second quote").await; + let received = bob.recv_msg(&sent).await; + assert_eq!(received.text.as_deref(), Some("> Second quote")); + assert!(received.quoted_text().is_none()); + assert!(received.quoted_message(&bob).await?.is_none()); + + Ok(()) + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_format_flowed_round_trip() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let chat = alice.create_chat(&bob).await; + + let text = " Foo bar"; + let sent = alice.send_text(chat.id, text).await; + let received = bob.recv_msg(&sent).await; + assert_eq!(received.text.as_deref(), Some(text)); + + let text = "Foo bar baz"; + let sent = alice.send_text(chat.id, text).await; + let received = bob.recv_msg(&sent).await; + assert_eq!(received.text.as_deref(), Some(text)); + + let python_program = "\ +def hello(): + return 'Hello, world!'"; + let sent = alice.send_text(chat.id, python_program).await; + let received = bob.recv_msg(&sent).await; + assert_eq!(received.text.as_deref(), Some(python_program)); + + Ok(()) + } } diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 5f6f33adb..184acaae0 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -4,6 +4,7 @@ use std::convert::TryInto; use anyhow::{bail, ensure, Context as _, Result}; use chrono::TimeZone; +use format_flowed::{format_flowed, format_flowed_quote}; use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder}; use tokio::fs; @@ -15,7 +16,6 @@ use crate::contact::Contact; use crate::context::{get_version_str, Context}; use crate::e2ee::EncryptHelper; use crate::ephemeral::Timer as EphemeralTimer; -use crate::format_flowed::{format_flowed, format_flowed_quote}; use crate::html::new_html_mimepart; use crate::location; use crate::message::{self, Message, MsgId, Viewtype}; diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 7996a01b8..5d43db545 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -9,6 +9,7 @@ use std::str; use anyhow::{bail, Context as _, Result}; use deltachat_derive::{FromSql, ToSql}; +use format_flowed::unformat_flowed; use lettre_email::mime::{self, Mime}; use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo}; use once_cell::sync::Lazy; @@ -24,7 +25,6 @@ use crate::decrypt::{ }; use crate::dehtml::dehtml; use crate::events::EventType; -use crate::format_flowed::unformat_flowed; use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey}; use crate::keyring::Keyring; From 9734552da5b4c8364d0574c7b8e0c5ce4eb1621c Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 26 Dec 2022 22:24:45 +0000 Subject: [PATCH 036/132] deltachat_rpc_client: make get_{chat,message}_by_id non-async --- .../src/deltachat_rpc_client/account.py | 4 ++-- deltachat-rpc-client/tests/test_something.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/account.py b/deltachat-rpc-client/src/deltachat_rpc_client/account.py index 9d9d92e41..c7d02af21 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/account.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/account.py @@ -212,7 +212,7 @@ class Account: """ return Chat(self, await self._rpc.create_group_chat(self.id, name, protect)) - async def get_chat_by_id(self, chat_id: int) -> Chat: + def get_chat_by_id(self, chat_id: int) -> Chat: """Return the Chat instance with the given ID.""" return Chat(self, chat_id) @@ -237,7 +237,7 @@ class Account: """ return await self._rpc.get_chat_securejoin_qr_code_svg(self.id, None) - async def get_message_by_id(self, msg_id: int) -> Message: + def get_message_by_id(self, msg_id: int) -> Message: """Return the Message instance with the given ID.""" return Message(self, msg_id) diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index c0465cc05..b40a45199 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -57,7 +57,7 @@ async def test_account(acfactory) -> None: msg_id = event.msg_id break - message = await bob.get_message_by_id(msg_id) + message = bob.get_message_by_id(msg_id) snapshot = await message.get_snapshot() assert snapshot.chat_id == chat_id assert snapshot.text == "Hello!" @@ -82,8 +82,8 @@ async def test_account(acfactory) -> None: group = await alice.create_group("test group") await group.add_contact(alice_contact_bob) group_msg = await group.send_message(text="hello") - assert group_msg == await alice.get_message_by_id(group_msg.id) - assert group == await alice.get_chat_by_id(group.id) + assert group_msg == alice.get_message_by_id(group_msg.id) + assert group == alice.get_chat_by_id(group.id) await alice.delete_messages([group_msg]) await alice.set_config("selfstatus", "test") @@ -116,11 +116,11 @@ async def test_chat(acfactory) -> None: chat_id = event.chat_id msg_id = event.msg_id break - message = await bob.get_message_by_id(msg_id) + message = bob.get_message_by_id(msg_id) snapshot = await message.get_snapshot() assert snapshot.chat_id == chat_id assert snapshot.text == "Hello!" - bob_chat_alice = await bob.get_chat_by_id(chat_id) + bob_chat_alice = bob.get_chat_by_id(chat_id) assert alice_chat_bob != bob_chat_alice assert repr(alice_chat_bob) @@ -201,7 +201,7 @@ async def test_message(acfactory) -> None: msg_id = event.msg_id break - message = await bob.get_message_by_id(msg_id) + message = bob.get_message_by_id(msg_id) snapshot = await message.get_snapshot() assert snapshot.chat_id == chat_id assert snapshot.text == "Hello!" From f92b8dcec0e9e50af7f0f19a32bce99df2045e24 Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 26 Dec 2022 22:25:10 +0000 Subject: [PATCH 037/132] deltachat_rpc_client: add webxdc API --- CHANGELOG.md | 1 + .../src/deltachat_rpc_client/message.py | 17 +++++++ deltachat-rpc-client/tests/test_webxdc.py | 50 +++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 deltachat-rpc-client/tests/test_webxdc.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 500f46ebc..787d93d2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Move format=flowed support to a separate crate #3869 ### API-Changes +- jsonrpc: add python API for webxdc updates #3872 ### Fixes - Do not add an error if the message is encrypted but not signed #3860 diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/message.py b/deltachat-rpc-client/src/deltachat_rpc_client/message.py index 644984fc1..a83da8b96 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/message.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/message.py @@ -1,3 +1,4 @@ +import json from typing import TYPE_CHECKING from ._utils import AttrDict @@ -47,3 +48,19 @@ class Message: async def mark_seen(self) -> None: """Mark the message as seen.""" await self._rpc.markseen_msgs(self.account.id, [self.id]) + + async def send_webxdc_status_update(self, update: dict, description: str) -> None: + """Send a webxdc status update. This message must be a webxdc.""" + await self._rpc.send_webxdc_status_update( + self.account.id, self.id, json.dumps(update), description + ) + + async def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list: + return json.loads( + await self._rpc.get_webxdc_status_updates( + self.account.id, self.id, last_known_serial + ) + ) + + async def get_webxdc_info(self) -> dict: + return await self._rpc.get_webxdc_info(self.account.id, self.id) diff --git a/deltachat-rpc-client/tests/test_webxdc.py b/deltachat-rpc-client/tests/test_webxdc.py new file mode 100644 index 000000000..1bc5f02c1 --- /dev/null +++ b/deltachat-rpc-client/tests/test_webxdc.py @@ -0,0 +1,50 @@ +import pytest + +from deltachat_rpc_client import EventType + + +@pytest.mark.asyncio +async def test_webxdc(acfactory) -> None: + alice, bob = await acfactory.get_online_accounts(2) + + bob_addr = await bob.get_config("addr") + alice_contact_bob = await alice.create_contact(bob_addr, "Bob") + alice_chat_bob = await alice_contact_bob.create_chat() + await alice_chat_bob.send_message( + text="Let's play chess!", file="../test-data/webxdc/chess.xdc" + ) + + while True: + event = await bob.wait_for_event() + if event.type == EventType.INCOMING_MSG: + bob_chat_alice = bob.get_chat_by_id(event.chat_id) + message = bob.get_message_by_id(event.msg_id) + break + + webxdc_info = await message.get_webxdc_info() + assert webxdc_info == { + "document": None, + "icon": "icon.png", + "internetAccess": False, + "name": "Chess Board", + "sourceCodeUrl": None, + "summary": None, + } + + status_updates = await message.get_webxdc_status_updates() + assert status_updates == [] + + await bob_chat_alice.accept() + await message.send_webxdc_status_update({"payload": 42}, "") + await message.send_webxdc_status_update({"payload": "Second update"}, "description") + + status_updates = await message.get_webxdc_status_updates() + assert status_updates == [ + {"payload": 42, "serial": 1, "max_serial": 2}, + {"payload": "Second update", "serial": 2, "max_serial": 2}, + ] + + status_updates = await message.get_webxdc_status_updates(1) + assert status_updates == [ + {"payload": "Second update", "serial": 2, "max_serial": 2}, + ] From 4b15f960e1e21b69a6474a28bb4888f6f0e0f611 Mon Sep 17 00:00:00 2001 From: adbenitez Date: Mon, 26 Dec 2022 18:26:14 -0500 Subject: [PATCH 038/132] make get_contact_by_id non-async --- deltachat-rpc-client/src/deltachat_rpc_client/account.py | 2 +- deltachat-rpc-client/tests/test_something.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/account.py b/deltachat-rpc-client/src/deltachat_rpc_client/account.py index c7d02af21..eab67d10d 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/account.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/account.py @@ -108,7 +108,7 @@ class Account: obj = (await obj.get_snapshot()).address return Contact(self, await self._rpc.create_contact(self.id, obj, name)) - async def get_contact_by_id(self, contact_id: int) -> Contact: + def get_contact_by_id(self, contact_id: int) -> Contact: """Return Contact instance for the given contact ID.""" return Contact(self, contact_id) diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index b40a45199..5b2e67af9 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -174,7 +174,7 @@ async def test_contact(acfactory) -> None: bob_addr = await bob.get_config("addr") alice_contact_bob = await alice.create_contact(bob_addr, "Bob") - assert alice_contact_bob == await alice.get_contact_by_id(alice_contact_bob.id) + assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id) assert repr(alice_contact_bob) await alice_contact_bob.block() await alice_contact_bob.unblock() From cf1d6919bfae9ff91ba95c3ed95608672f50a5a2 Mon Sep 17 00:00:00 2001 From: adbenitez Date: Mon, 26 Dec 2022 18:34:55 -0500 Subject: [PATCH 039/132] allow to pass string as update for objects that don't support default json.dump() conversion --- .../src/deltachat_rpc_client/message.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/message.py b/deltachat-rpc-client/src/deltachat_rpc_client/message.py index a83da8b96..86ddbc95e 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/message.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/message.py @@ -1,5 +1,5 @@ import json -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Union from ._utils import AttrDict from .contact import Contact @@ -49,10 +49,14 @@ class Message: """Mark the message as seen.""" await self._rpc.markseen_msgs(self.account.id, [self.id]) - async def send_webxdc_status_update(self, update: dict, description: str) -> None: + async def send_webxdc_status_update( + self, update: Union[dict, str], description: str + ) -> None: """Send a webxdc status update. This message must be a webxdc.""" + if not isinstance(update, str): + update = json.dumps(update) await self._rpc.send_webxdc_status_update( - self.account.id, self.id, json.dumps(update), description + self.account.id, self.id, update, description ) async def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list: From c562d17925a7782f6c7eef2cddd879596e09f202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kl=C3=A4hn?= <39526136+Septias@users.noreply.github.com> Date: Tue, 27 Dec 2022 11:41:15 +0100 Subject: [PATCH 040/132] Add verifier information (#3839) * add verifier information * cleanup Co-authored-by: bjoern * finish name change * simple improvements & new ffi * fixs Co-authored-by: bjoern Co-authored-by: septias --- CHANGELOG.md | 3 +- deltachat-ffi/deltachat.h | 31 +++++++++++++++++++ deltachat-ffi/src/lib.rs | 34 +++++++++++++++++++++ python/src/deltachat/contact.py | 4 +++ python/tests/test_0_complex_or_slow.py | 14 ++++++++- src/chat.rs | 1 + src/contact.rs | 26 +++++++++++++++- src/e2ee.rs | 1 + src/lib.rs | 5 +--- src/location.rs | 6 +--- src/mimeparser.rs | 2 ++ src/peerstate.rs | 41 +++++++++++++++++++++----- src/qr.rs | 1 + src/receive_imf.rs | 3 +- src/securejoin.rs | 31 ++++++++++++++++--- src/securejoin/bobstate.rs | 9 ++++-- src/sql/migrations.rs | 7 +++++ src/test_utils.rs | 1 + src/tools.rs | 6 +--- 19 files changed, 194 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 787d93d2f..8edd9f065 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### API-Changes - jsonrpc: add python API for webxdc updates #3872 +- Add ffi functions to retrieve `verified by` information #3786 ### Fixes - Do not add an error if the message is encrypted but not signed #3860 @@ -26,8 +27,6 @@ - Only send the message about ephemeral timer change if the chat is promoted #3847 - Use relative paths in `accounts.toml` #3838 -### API-Changes - ### Fixes - Set read/write timeouts for IMAP over SOCKS5 #3833 - Treat attached PGP keys as peer keys with mutual encryption preference #3832 diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 95e4cf45f..4270d0003 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -4735,6 +4735,37 @@ int dc_contact_is_blocked (const dc_contact_t* contact); int dc_contact_is_verified (dc_contact_t* contact); + +/** + * Return the address that verified a contact + * + * The UI may use this in addition to a checkmark showing the verification status + * + * @memberof dc_contact_t + * @param contact The contact object. + * @return + * A string containing the verifiers address. If it is the same address as the contact itself, + * we verified the contact ourself. If it is an empty string, we don't have verifier + * information or the contact is not verified. + */ +char* dc_contact_get_verifier_addr (dc_contact_t* contact); + + +/** + * Return the `ContactId` that verified a contact + * + * The UI may use this in addition to a checkmark showing the verification status + * + * @memberof dc_contact_t + * @param contact The contact object. + * @return + * The `ContactId` of the verifiers address. If it is the same address as the contact itself, + * we verified the contact ourself. If it is 0, we don't have verifier information or + * the contact is not verified. + */ +int dc_contact_get_verifier_id (dc_contact_t* contact); + + /** * @class dc_provider_t * diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index a3b772c28..51a5d3353 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -3963,6 +3963,40 @@ pub unsafe extern "C" fn dc_contact_is_verified(contact: *mut dc_contact_t) -> l .unwrap_or_default() as libc::c_int } +#[no_mangle] +pub unsafe extern "C" fn dc_contact_get_verifier_addr( + contact: *mut dc_contact_t, +) -> *mut libc::c_char { + if contact.is_null() { + eprintln!("ignoring careless call to dc_contact_get_verifier_addr()"); + return "".strdup(); + } + let ffi_contact = &*contact; + let ctx = &*ffi_contact.context; + block_on(Contact::get_verifier_addr( + ctx, + &ffi_contact.contact.get_id(), + )) + .log_err(ctx, "failed to get verifier for contact") + .unwrap_or_default() + .strdup() +} + +#[no_mangle] +pub unsafe extern "C" fn dc_contact_get_verifier_id(contact: *mut dc_contact_t) -> libc::c_int { + if contact.is_null() { + eprintln!("ignoring careless call to dc_contact_get_verifier_id()"); + return 0 as libc::c_int; + } + let ffi_contact = &*contact; + let ctx = &*ffi_contact.context; + let contact_id = block_on(Contact::get_verifier_id(ctx, &ffi_contact.contact.get_id())) + .log_err(ctx, "failed to get verifier") + .unwrap_or_default() + .unwrap_or_default(); + + contact_id.to_u32() as libc::c_int +} // dc_lot_t pub type dc_lot_t = lot::Lot; diff --git a/python/src/deltachat/contact.py b/python/src/deltachat/contact.py index 69fa809b6..75c44b8e5 100644 --- a/python/src/deltachat/contact.py +++ b/python/src/deltachat/contact.py @@ -75,6 +75,10 @@ class Contact(object): """Return True if the contact is verified.""" return lib.dc_contact_is_verified(self._dc_contact) + def get_verifier(self, contact): + """Return the address of the contact that verified the contact""" + return from_dc_charpointer(lib.dc_contact_get_verifier_addr(contact._dc_contact)) + def get_profile_image(self) -> Optional[str]: """Get contact profile image. diff --git a/python/tests/test_0_complex_or_slow.py b/python/tests/test_0_complex_or_slow.py index 335d07590..034839ee5 100644 --- a/python/tests/test_0_complex_or_slow.py +++ b/python/tests/test_0_complex_or_slow.py @@ -123,6 +123,7 @@ class TestGroupStressTests: def test_qr_verified_group_and_chatting(acfactory, lp): ac1, ac2, ac3 = acfactory.get_online_accounts(3) + ac1_addr = ac1.get_self_contact().addr lp.sec("ac1: create verified-group QR, ac2 scans and joins") chat1 = ac1.create_group_chat("hello", verified=True) assert chat1.is_protected() @@ -141,12 +142,17 @@ def test_qr_verified_group_and_chatting(acfactory, lp): msg_out = chat1.send_text("hello") assert msg_out.is_encrypted() - lp.sec("ac2: read message and check it's verified chat") + lp.sec("ac2: read message and check that it's a verified chat") msg = ac2._evtracker.wait_next_incoming_message() assert msg.text == "hello" assert msg.chat.is_protected() assert msg.is_encrypted() + lp.sec("ac2: Check that ac2 verified ac1") + # If we verified the contact ourselves then verifier addr == contact addr + ac2_ac1_contact = ac2.get_contacts()[0] + assert ac2.get_self_contact().get_verifier(ac2_ac1_contact) == ac1_addr + lp.sec("ac2: send message and let ac1 read it") chat2.send_text("world") msg = ac1._evtracker.wait_next_incoming_message() @@ -168,6 +174,12 @@ def test_qr_verified_group_and_chatting(acfactory, lp): assert msg.is_system_message() assert not msg.error + lp.sec("ac2: Check that ac1 verified ac3 for ac2") + ac2_ac1_contact = ac2.get_contacts()[0] + assert ac2.get_self_contact().get_verifier(ac2_ac1_contact) == ac1_addr + ac2_ac3_contact = ac2.get_contacts()[1] + assert ac2.get_self_contact().get_verifier(ac2_ac3_contact) == ac1_addr + lp.sec("ac2: send message and let ac3 read it") chat2.send_text("hi") # Skip system message about added member diff --git a/src/chat.rs b/src/chat.rs index 22818dda0..8be73e1ef 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -3562,6 +3562,7 @@ mod tests { use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS}; use crate::contact::Contact; use crate::receive_imf::receive_imf; + use crate::test_utils::TestContext; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] diff --git a/src/contact.rs b/src/contact.rs index 17b74987d..baf74b4ac 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -1138,6 +1138,31 @@ impl Contact { Ok(VerifiedStatus::Unverified) } + /// Return the address that verified the given contact + pub async fn get_verifier_addr( + context: &Context, + contact_id: &ContactId, + ) -> Result> { + let contact = Contact::load_from_db(context, *contact_id).await?; + + Ok(Peerstate::from_addr(context, contact.get_addr()) + .await? + .and_then(|peerstate| peerstate.get_verifier().map(|addr| addr.to_owned()))) + } + + pub async fn get_verifier_id( + context: &Context, + contact_id: &ContactId, + ) -> Result> { + let verifier_addr = Contact::get_verifier_addr(context, contact_id).await?; + if let Some(addr) = verifier_addr { + Ok(Contact::lookup_id_by_addr(context, &addr, Origin::AddressBook).await?) + } else { + Ok(None) + } + } + + /// Return the ContactId that verified the given contact pub async fn get_real_cnt(context: &Context) -> Result { if !context.sql.is_open().await { return Ok(0); @@ -2300,7 +2325,6 @@ bob@example.net: CCCB 5AA9 F6E1 141C 9431 65F1 DB18 B18C BCF7 0487" ); - Ok(()) } diff --git a/src/e2ee.rs b/src/e2ee.rs index 311894a04..62f699fdf 100644 --- a/src/e2ee.rs +++ b/src/e2ee.rs @@ -297,6 +297,7 @@ Sent with my Delta Chat Messenger: https://delta.chat"; verified_key: Some(pub_key.clone()), verified_key_fingerprint: Some(pub_key.fingerprint()), fingerprint_changed: false, + verifier: None, }; vec![(Some(peerstate), addr)] } diff --git a/src/lib.rs b/src/lib.rs index 771783ba6..a672ec7c0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,10 +21,7 @@ clippy::bool_assert_comparison, clippy::manual_split_once, clippy::format_push_string, - clippy::bool_to_int_with_if, - // This lint can be re-enabled once we don't target - // Rust 1.56 anymore: - clippy::collapsible_str_replace + clippy::bool_to_int_with_if )] #[macro_use] diff --git a/src/location.rs b/src/location.rs index c6845898a..e381039ac 100644 --- a/src/location.rs +++ b/src/location.rs @@ -100,11 +100,7 @@ impl Kml { if self.tag.contains(KmlTag::WHEN) || self.tag.contains(KmlTag::COORDINATES) { let val = event.unescape_and_decode(reader).unwrap_or_default(); - let val = val - .replace('\n', "") - .replace('\r', "") - .replace('\t', "") - .replace(' ', ""); + let val = val.replace(['\n', '\r', '\t', ' '], ""); if self.tag.contains(KmlTag::WHEN) && val.len() >= 19 { // YYYY-MM-DDTHH:MM:SSZ diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 5d43db545..843053930 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -1635,6 +1635,8 @@ impl MimeMessage { } /// Parses `Autocrypt-Gossip` headers from the email and applies them to peerstates. +/// Params: +/// from: The address which sent the message currently beeing parsed /// /// Returns the set of mail recipient addresses for which valid gossip headers were found. async fn update_gossip_peerstates( diff --git a/src/peerstate.rs b/src/peerstate.rs index ab6d527a3..2699bf6eb 100644 --- a/src/peerstate.rs +++ b/src/peerstate.rs @@ -48,6 +48,8 @@ pub struct Peerstate { pub verified_key: Option, pub verified_key_fingerprint: Option, pub fingerprint_changed: bool, + /// The address that verified this contact + pub verifier: Option, } impl PartialEq for Peerstate { @@ -103,9 +105,11 @@ impl Peerstate { verified_key: None, verified_key_fingerprint: None, fingerprint_changed: false, + verifier: None, } } + /// Create peerstate from gossip pub fn from_gossip(gossip_header: &Aheader, message_time: i64) -> Self { Peerstate { addr: gossip_header.addr.clone(), @@ -119,7 +123,6 @@ impl Peerstate { // learn encryption preferences of other members immediately and don't send unencrypted // messages to a group where everyone prefers encryption. prefer_encrypt: gossip_header.prefer_encrypt, - public_key: None, public_key_fingerprint: None, gossip_key: Some(gossip_header.public_key.clone()), @@ -128,13 +131,14 @@ impl Peerstate { verified_key: None, verified_key_fingerprint: None, fingerprint_changed: false, + verifier: None, } } pub async fn from_addr(context: &Context, addr: &str) -> Result> { let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \ gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \ - verified_key, verified_key_fingerprint \ + verified_key, verified_key_fingerprint, verifier \ FROM acpeerstates \ WHERE addr=? COLLATE NOCASE LIMIT 1;"; Self::from_stmt(context, query, paramsv![addr]).await @@ -146,7 +150,7 @@ impl Peerstate { ) -> Result> { let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \ gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \ - verified_key, verified_key_fingerprint \ + verified_key, verified_key_fingerprint, verifier \ FROM acpeerstates \ WHERE public_key_fingerprint=? \ OR gossip_key_fingerprint=? \ @@ -162,7 +166,7 @@ impl Peerstate { ) -> Result> { let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \ gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \ - verified_key, verified_key_fingerprint \ + verified_key, verified_key_fingerprint, verifier \ FROM acpeerstates \ WHERE verified_key_fingerprint=? \ OR addr=? COLLATE NOCASE \ @@ -219,6 +223,7 @@ impl Peerstate { .transpose() .unwrap_or_default(), fingerprint_changed: false, + verifier: row.get("verifier")?, }; Ok(res) @@ -358,11 +363,19 @@ impl Peerstate { } } + /// Set this peerstate to verified + /// Make sure to call `self.save_to_db` to save these changes + /// Params: + /// verifier: + /// The address which verifies the given contact + /// If we are verifying the contact, use that contacts address + /// Returns whether the value of the key has changed pub fn set_verified( &mut self, which_key: PeerstateKeyType, fingerprint: &Fingerprint, verified: PeerstateVerifiedStatus, + verifier: String, ) -> bool { if verified == PeerstateVerifiedStatus::BidirectVerified { match which_key { @@ -372,6 +385,7 @@ impl Peerstate { { self.verified_key = self.public_key.clone(); self.verified_key_fingerprint = self.public_key_fingerprint.clone(); + self.verifier = Some(verifier); true } else { false @@ -383,6 +397,7 @@ impl Peerstate { { self.verified_key = self.gossip_key.clone(); self.verified_key_fingerprint = self.gossip_key_fingerprint.clone(); + self.verifier = Some(verifier); true } else { false @@ -407,8 +422,9 @@ impl Peerstate { gossip_key_fingerprint, verified_key, verified_key_fingerprint, - addr) - VALUES (?,?,?,?,?,?,?,?,?,?,?) + addr, + verifier) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT (addr) DO UPDATE SET last_seen = excluded.last_seen, @@ -420,7 +436,8 @@ impl Peerstate { public_key_fingerprint = excluded.public_key_fingerprint, gossip_key_fingerprint = excluded.gossip_key_fingerprint, verified_key = excluded.verified_key, - verified_key_fingerprint = excluded.verified_key_fingerprint", + verified_key_fingerprint = excluded.verified_key_fingerprint, + verifier = excluded.verifier", paramsv![ self.last_seen, self.last_seen_autocrypt, @@ -433,6 +450,7 @@ impl Peerstate { self.verified_key.as_ref().map(|k| k.to_bytes()), self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()), self.addr, + self.verifier, ], ) .await?; @@ -447,6 +465,11 @@ impl Peerstate { } } + /// Returns the address that verified the contact + pub fn get_verifier(&self) -> Option<&str> { + self.verifier.as_deref() + } + /// Add an info message to all the chats with this contact, informing about /// a [`PeerstateChange`]. /// @@ -672,6 +695,7 @@ mod tests { verified_key: Some(pub_key.clone()), verified_key_fingerprint: Some(pub_key.fingerprint()), fingerprint_changed: false, + verifier: None, }; assert!( @@ -711,6 +735,7 @@ mod tests { verified_key: None, verified_key_fingerprint: None, fingerprint_changed: false, + verifier: None, }; assert!( @@ -743,6 +768,7 @@ mod tests { verified_key: None, verified_key_fingerprint: None, fingerprint_changed: false, + verifier: None, }; assert!( @@ -805,6 +831,7 @@ mod tests { verified_key: None, verified_key_fingerprint: None, fingerprint_changed: false, + verifier: None, }; peerstate.apply_header(&header, 100); diff --git a/src/qr.rs b/src/qr.rs index 6a735216e..2813c762d 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -896,6 +896,7 @@ mod tests { verified_key: None, verified_key_fingerprint: None, fingerprint_changed: false, + verifier: None, }; assert!( peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(), diff --git a/src/receive_imf.rs b/src/receive_imf.rs index d6a10b496..41bb20ff9 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -2138,13 +2138,14 @@ async fn check_verified_properties( || peerstate.verified_key_fingerprint != peerstate.public_key_fingerprint && peerstate.verified_key_fingerprint != peerstate.gossip_key_fingerprint { - info!(context, "{} has verified {}.", contact.get_addr(), to_addr,); + info!(context, "{} has verified {}.", contact.get_addr(), to_addr); let fp = peerstate.gossip_key_fingerprint.clone(); if let Some(fp) = fp { peerstate.set_verified( PeerstateKeyType::GossipKey, &fp, PeerstateVerifiedStatus::BidirectVerified, + contact.get_addr().to_owned(), ); peerstate.save_to_db(&context.sql).await?; is_verified = true; diff --git a/src/securejoin.rs b/src/securejoin.rs index 9663a30a8..77d743c17 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -411,7 +411,14 @@ pub(crate) async fn handle_securejoin_handshake( .await?; return Ok(HandshakeMessage::Ignore); } - if mark_peer_as_verified(context, &fingerprint).await.is_err() { + let contact_addr = Contact::load_from_db(context, contact_id) + .await? + .get_addr() + .to_owned(); + if mark_peer_as_verified(context, &fingerprint, contact_addr) + .await + .is_err() + { could_not_establish_secure_connection( context, contact_id, @@ -531,7 +538,7 @@ pub(crate) async fn handle_securejoin_handshake( /// /// - if we see the self-sent-message vg-member-added/vc-contact-confirm, /// we know that we're an inviter-observer. -/// the inviting device has marked a peer as verified on vg-request-with-auth/vc-request-with-auth +/// The inviting device has marked a peer as verified on vg-request-with-auth/vc-request-with-auth /// before sending vg-member-added/vc-contact-confirm - so, if we observe vg-member-added/vc-contact-confirm, /// we can mark the peer as verified as well. /// @@ -586,7 +593,17 @@ pub(crate) async fn observe_securejoin_on_other_device( return Ok(HandshakeMessage::Ignore); } }; - if mark_peer_as_verified(context, &fingerprint).await.is_err() { + if mark_peer_as_verified( + context, + &fingerprint, + Contact::load_from_db(context, contact_id) + .await? + .get_addr() + .to_owned(), + ) + .await + .is_err() + { could_not_establish_secure_connection( context, contact_id, @@ -634,12 +651,17 @@ async fn could_not_establish_secure_connection( Ok(()) } -async fn mark_peer_as_verified(context: &Context, fingerprint: &Fingerprint) -> Result<(), Error> { +async fn mark_peer_as_verified( + context: &Context, + fingerprint: &Fingerprint, + verifier: String, +) -> Result<(), Error> { if let Some(ref mut peerstate) = Peerstate::from_fingerprint(context, fingerprint).await? { if peerstate.set_verified( PeerstateKeyType::PublicKey, fingerprint, PeerstateVerifiedStatus::BidirectVerified, + verifier, ) { peerstate.prefer_encrypt = EncryptPreference::Mutual; peerstate.save_to_db(&context.sql).await.unwrap_or_default(); @@ -931,6 +953,7 @@ mod tests { verified_key: None, verified_key_fingerprint: None, fingerprint_changed: false, + verifier: None, }; peerstate.save_to_db(&bob.ctx.sql).await?; diff --git a/src/securejoin/bobstate.rs b/src/securejoin/bobstate.rs index f5e340622..e1145a006 100644 --- a/src/securejoin/bobstate.rs +++ b/src/securejoin/bobstate.rs @@ -326,7 +326,7 @@ impl BobState { /// /// This deviates from the protocol by also sending a confirmation message in response /// to the *vc-contact-confirm* message. This has no specific value to the protocol and - /// is only done out of symmerty with *vg-member-added* handling. + /// is only done out of symmetry with *vg-member-added* handling. async fn step_contact_confirm( &mut self, context: &Context, @@ -366,7 +366,12 @@ impl BobState { "Contact confirm message not encrypted", ))); } - mark_peer_as_verified(context, self.invite.fingerprint()).await?; + mark_peer_as_verified( + context, + self.invite.fingerprint(), + mime_message.from.addr.to_string(), + ) + .await?; Contact::scaleup_origin_by_id(context, self.invite.contact_id(), Origin::SecurejoinJoined) .await?; context.emit_event(EventType::ContactsChanged(None)); diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 6324f01fb..fde27051c 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -664,6 +664,13 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid); 95 ).await?; } + if dbversion < 96 { + sql.execute_migration( + "ALTER TABLE acpeerstates ADD COLUMN verifier TEXT DEFAULT '';", + 96, + ) + .await?; + } let new_version = sql .get_raw_config_int(VERSION_CFG) diff --git a/src/test_utils.rs b/src/test_utils.rs index 43318edfc..ecf219d5c 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -244,6 +244,7 @@ impl TestContext { /// /// This is a shortcut which automatically calls [`TestContext::configure_alice`] after /// creating the context. + /// alice-email: alice@example.org pub async fn new_alice() -> Self { Self::builder().configure_alice().build().await } diff --git a/src/tools.rs b/src/tools.rs index 187a38ae4..b766dc73e 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -593,11 +593,7 @@ impl rusqlite::types::ToSql for EmailAddress { /// 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: &str) -> String { - input - .replace('\n', " ") - .replace('\r', " ") - .trim() - .to_string() + input.replace(['\n', '\r'], " ").trim().to_string() } pub(crate) trait IsNoneOrEmpty { From 3fcd17e6a5bce0101a65a97160440c4b004209a8 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 27 Dec 2022 12:02:23 +0000 Subject: [PATCH 041/132] Add missing documentation for Summary constructor --- src/summary.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/summary.rs b/src/summary.rs index cbf301ed7..5b4d3f0bc 100644 --- a/src/summary.rs +++ b/src/summary.rs @@ -1,7 +1,5 @@ //! # Message summary for chatlist. -#![allow(missing_docs)] - use crate::chat::Chat; use crate::constants::Chattype; use crate::contact::{Contact, ContactId}; @@ -54,6 +52,8 @@ pub struct Summary { } impl Summary { + /// Constucts chatlist summary + /// from the provided message, chat and message author contact snapshots. pub async fn new( context: &Context, msg: &Message, From 5432e108a1cdeb3d15eade329238f986e71c2e83 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Dec 2022 15:16:12 +0000 Subject: [PATCH 042/132] cargo: bump quick-xml from 0.23.0 to 0.26.0 Bumps [quick-xml](https://github.com/tafia/quick-xml) from 0.23.0 to 0.26.0. - [Release notes](https://github.com/tafia/quick-xml/releases) - [Changelog](https://github.com/tafia/quick-xml/blob/master/Changelog.md) - [Commits](https://github.com/tafia/quick-xml/compare/v0.23.0...v0.26.0) --- updated-dependencies: - dependency-name: quick-xml dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- CHANGELOG.md | 1 + Cargo.lock | 4 ++-- Cargo.toml | 2 +- src/configure/auto_mozilla.rs | 31 +++++++++++++++----------- src/configure/auto_outlook.rs | 24 +++++++++++++------- src/dehtml.rs | 39 +++++++++++++++++++++++++-------- src/location.rs | 41 +++++++++++++++++++++++------------ 7 files changed, 95 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8edd9f065..9db270c67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Validate signatures in try_decrypt() even if the message isn't encrypted #3859 - Don't parse the message again after detached signatures validation #3862 - Move format=flowed support to a separate crate #3869 +- cargo: bump quick-xml from 0.23.0 to 0.26.0 #3722 ### API-Changes - jsonrpc: add python API for webxdc updates #3872 diff --git a/Cargo.lock b/Cargo.lock index 198ae37eb..33f9f9eda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2739,9 +2739,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.23.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9279fbdacaad3baf559d8cabe0acc3d06e30ea14931af31af79578ac0946decc" +checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 9fbd387e3..a76e6d5c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ once_cell = "1.16.0" percent-encoding = "2.2" pgp = { version = "0.9", default-features = false } pretty_env_logger = { version = "0.4", optional = true } -quick-xml = "0.23" +quick-xml = "0.26" r2d2 = "0.8" r2d2_sqlite = "0.20" rand = "0.8" diff --git a/src/configure/auto_mozilla.rs b/src/configure/auto_mozilla.rs index bc7b69712..2f5ca45b2 100644 --- a/src/configure/auto_mozilla.rs +++ b/src/configure/auto_mozilla.rs @@ -62,7 +62,7 @@ fn parse_server( reader: &mut quick_xml::Reader, server_event: &BytesStart, ) -> Result, quick_xml::Error> { - let end_tag = String::from_utf8_lossy(server_event.name()) + let end_tag = String::from_utf8_lossy(server_event.name().as_ref()) .trim() .to_lowercase(); @@ -70,12 +70,17 @@ fn parse_server( .attributes() .find(|attr| { attr.as_ref() - .map(|a| String::from_utf8_lossy(a.key).trim().to_lowercase() == "type") + .map(|a| { + String::from_utf8_lossy(a.key.as_ref()) + .trim() + .to_lowercase() + == "type" + }) .unwrap_or_default() }) .map(|typ| { typ.unwrap() - .unescape_and_decode_value(reader) + .decode_and_unescape_value(reader) .unwrap_or_default() .to_lowercase() }) @@ -89,25 +94,23 @@ fn parse_server( let mut tag_config = MozConfigTag::Undefined; let mut buf = Vec::new(); loop { - match reader.read_event(&mut buf)? { + match reader.read_event_into(&mut buf)? { Event::Start(ref event) => { - tag_config = String::from_utf8_lossy(event.name()) + tag_config = String::from_utf8_lossy(event.name().as_ref()) .parse() .unwrap_or_default(); } Event::End(ref event) => { - let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase(); + let tag = String::from_utf8_lossy(event.name().as_ref()) + .trim() + .to_lowercase(); if tag == end_tag { break; } } Event::Text(ref event) => { - let val = event - .unescape_and_decode(reader) - .unwrap_or_default() - .trim() - .to_owned(); + let val = event.unescape().unwrap_or_default().trim().to_owned(); match tag_config { MozConfigTag::Hostname => hostname = Some(val), @@ -150,9 +153,11 @@ fn parse_xml_reader( let mut buf = Vec::new(); loop { - match reader.read_event(&mut buf)? { + match reader.read_event_into(&mut buf)? { Event::Start(ref event) => { - let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase(); + let tag = String::from_utf8_lossy(event.name().as_ref()) + .trim() + .to_lowercase(); if tag == "incomingserver" { if let Some(incoming_server) = parse_server(reader, event)? { diff --git a/src/configure/auto_outlook.rs b/src/configure/auto_outlook.rs index 0be2f520a..d374f7016 100644 --- a/src/configure/auto_outlook.rs +++ b/src/configure/auto_outlook.rs @@ -59,12 +59,18 @@ fn parse_protocol( let mut current_tag: Option = None; loop { - match reader.read_event(&mut buf)? { + match reader.read_event_into(&mut buf)? { Event::Start(ref event) => { - current_tag = Some(String::from_utf8_lossy(event.name()).trim().to_lowercase()); + current_tag = Some( + String::from_utf8_lossy(event.name().as_ref()) + .trim() + .to_lowercase(), + ); } Event::End(ref event) => { - let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase(); + let tag = String::from_utf8_lossy(event.name().as_ref()) + .trim() + .to_lowercase(); if tag == "protocol" { break; } @@ -73,7 +79,7 @@ fn parse_protocol( } } Event::Text(ref e) => { - let val = e.unescape_and_decode(reader).unwrap_or_default(); + let val = e.unescape().unwrap_or_default(); if let Some(ref tag) = current_tag { match tag.as_str() { @@ -115,9 +121,9 @@ fn parse_redirecturl( reader: &mut quick_xml::Reader, ) -> Result { let mut buf = Vec::new(); - match reader.read_event(&mut buf)? { + match reader.read_event_into(&mut buf)? { Event::Text(ref e) => { - let val = e.unescape_and_decode(reader).unwrap_or_default(); + let val = e.unescape().unwrap_or_default(); Ok(val.trim().to_string()) } _ => Ok("".to_string()), @@ -131,9 +137,11 @@ fn parse_xml_reader( let mut buf = Vec::new(); loop { - match reader.read_event(&mut buf)? { + match reader.read_event_into(&mut buf)? { Event::Start(ref e) => { - let tag = String::from_utf8_lossy(e.name()).trim().to_lowercase(); + let tag = String::from_utf8_lossy(e.name().as_ref()) + .trim() + .to_lowercase(); if tag == "protocol" { if let Some(protocol) = parse_protocol(reader)? { diff --git a/src/dehtml.rs b/src/dehtml.rs index 20075dbab..30c96e366 100644 --- a/src/dehtml.rs +++ b/src/dehtml.rs @@ -88,18 +88,30 @@ fn dehtml_quick_xml(buf: &str) -> String { let mut buf = Vec::new(); loop { - match reader.read_event(&mut buf) { + match reader.read_event_into(&mut buf) { Ok(quick_xml::events::Event::Start(ref e)) => { dehtml_starttag_cb(e, &mut dehtml, &reader) } Ok(quick_xml::events::Event::End(ref e)) => dehtml_endtag_cb(e, &mut dehtml), Ok(quick_xml::events::Event::Text(ref e)) => dehtml_text_cb(e, &mut dehtml), - Ok(quick_xml::events::Event::CData(e)) => dehtml_text_cb(&e.escape(), &mut dehtml), + Ok(quick_xml::events::Event::CData(e)) => match e.escape() { + Ok(e) => dehtml_text_cb(&e, &mut dehtml), + Err(e) => { + eprintln!( + "CDATA escape error at position {}: {:?}", + reader.buffer_position(), + e, + ); + } + }, Ok(quick_xml::events::Event::Empty(ref e)) => { // Handle empty tags as a start tag immediately followed by end tag. // For example, `

` is treated as `

`. dehtml_starttag_cb(e, &mut dehtml, &reader); - dehtml_endtag_cb(&BytesEnd::borrowed(e.name()), &mut dehtml); + dehtml_endtag_cb( + &BytesEnd::new(String::from_utf8_lossy(e.name().as_ref())), + &mut dehtml, + ); } Err(e) => { eprintln!( @@ -121,7 +133,7 @@ fn dehtml_text_cb(event: &BytesText, dehtml: &mut Dehtml) { if dehtml.get_add_text() == AddText::YesPreserveLineEnds || dehtml.get_add_text() == AddText::YesRemoveLineEnds { - let last_added = escaper::decode_html_buf_sloppy(event.escaped()).unwrap_or_default(); + let last_added = escaper::decode_html_buf_sloppy(event as &[_]).unwrap_or_default(); if dehtml.get_add_text() == AddText::YesRemoveLineEnds { dehtml.strbuilder += LINE_RE.replace_all(&last_added, "\r").as_ref(); @@ -135,7 +147,9 @@ fn dehtml_text_cb(event: &BytesText, dehtml: &mut Dehtml) { } fn dehtml_endtag_cb(event: &BytesEnd, dehtml: &mut Dehtml) { - let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase(); + let tag = String::from_utf8_lossy(event.name().as_ref()) + .trim() + .to_lowercase(); match tag.as_str() { "p" | "table" | "td" | "style" | "script" | "title" | "pre" => { @@ -176,7 +190,9 @@ fn dehtml_starttag_cb( dehtml: &mut Dehtml, reader: &quick_xml::Reader, ) { - let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase(); + let tag = String::from_utf8_lossy(event.name().as_ref()) + .trim() + .to_lowercase(); match tag.as_str() { "p" | "table" | "td" => { @@ -206,10 +222,15 @@ fn dehtml_starttag_cb( if let Some(href) = event .html_attributes() .filter_map(|attr| attr.ok()) - .find(|attr| String::from_utf8_lossy(attr.key).trim().to_lowercase() == "href") + .find(|attr| { + String::from_utf8_lossy(attr.key.as_ref()) + .trim() + .to_lowercase() + == "href" + }) { let href = href - .unescape_and_decode_value(reader) + .decode_and_unescape_value(reader) .unwrap_or_default() .to_lowercase(); @@ -258,7 +279,7 @@ fn maybe_push_tag( fn tag_contains_attr(event: &BytesStart, reader: &Reader, name: &str) -> bool { event.attributes().any(|r| { r.map(|a| { - a.unescape_and_decode_value(reader) + a.decode_and_unescape_value(reader) .map(|v| v == name) .unwrap_or(false) }) diff --git a/src/location.rs b/src/location.rs index e381039ac..8467d4aaf 100644 --- a/src/location.rs +++ b/src/location.rs @@ -78,7 +78,7 @@ impl Kml { let mut buf = Vec::new(); loop { - match reader.read_event(&mut buf).with_context(|| { + match reader.read_event_into(&mut buf).with_context(|| { format!( "location parsing error at position {}", reader.buffer_position() @@ -86,7 +86,7 @@ impl Kml { })? { quick_xml::events::Event::Start(ref e) => kml.starttag_cb(e, &reader), quick_xml::events::Event::End(ref e) => kml.endtag_cb(e), - quick_xml::events::Event::Text(ref e) => kml.text_cb(e, &reader), + quick_xml::events::Event::Text(ref e) => kml.text_cb(e), quick_xml::events::Event::Eof => break, _ => (), } @@ -96,9 +96,9 @@ impl Kml { Ok(kml) } - fn text_cb(&mut self, event: &BytesText, reader: &quick_xml::Reader) { + fn text_cb(&mut self, event: &BytesText) { if self.tag.contains(KmlTag::WHEN) || self.tag.contains(KmlTag::COORDINATES) { - let val = event.unescape_and_decode(reader).unwrap_or_default(); + let val = event.unescape().unwrap_or_default(); let val = val.replace(['\n', '\r', '\t', ' '], ""); @@ -127,7 +127,9 @@ impl Kml { } fn endtag_cb(&mut self, event: &BytesEnd) { - let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase(); + let tag = String::from_utf8_lossy(event.name().as_ref()) + .trim() + .to_lowercase(); if tag == "placemark" { if self.tag.contains(KmlTag::PLACEMARK) @@ -147,14 +149,20 @@ impl Kml { event: &BytesStart, reader: &quick_xml::Reader, ) { - let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase(); + let tag = String::from_utf8_lossy(event.name().as_ref()) + .trim() + .to_lowercase(); if tag == "document" { - if let Some(addr) = event - .attributes() - .filter_map(|a| a.ok()) - .find(|attr| String::from_utf8_lossy(attr.key).trim().to_lowercase() == "addr") - { - self.addr = addr.unescape_and_decode_value(reader).ok(); + if let Some(addr) = event.attributes().filter_map(|a| a.ok()).find(|attr| { + String::from_utf8_lossy(attr.key.as_ref()) + .trim() + .to_lowercase() + == "addr" + }) { + self.addr = addr + .decode_and_unescape_value(reader) + .ok() + .map(|a| a.into_owned()); } } else if tag == "placemark" { self.tag = KmlTag::PLACEMARK; @@ -172,12 +180,17 @@ impl Kml { self.tag = KmlTag::PLACEMARK | KmlTag::POINT | KmlTag::COORDINATES; if let Some(acc) = event.attributes().find(|attr| { attr.as_ref() - .map(|a| String::from_utf8_lossy(a.key).trim().to_lowercase() == "accuracy") + .map(|a| { + String::from_utf8_lossy(a.key.as_ref()) + .trim() + .to_lowercase() + == "accuracy" + }) .unwrap_or_default() }) { let v = acc .unwrap() - .unescape_and_decode_value(reader) + .decode_and_unescape_value(reader) .unwrap_or_default(); self.curr.accuracy = v.trim().parse().unwrap_or_default(); From 6e63555bc8aec62e55b39dedf9258d562afe71ac Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 27 Dec 2022 11:58:01 +0000 Subject: [PATCH 043/132] Add missing documentation for the download state --- src/download.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/download.rs b/src/download.rs index 3253e54fe..de127c7ae 100644 --- a/src/download.rs +++ b/src/download.rs @@ -1,7 +1,5 @@ //! # Download large messages manually. -#![allow(missing_docs)] - use anyhow::{anyhow, Result}; use deltachat_derive::{FromSql, ToSql}; use serde::{Deserialize, Serialize}; @@ -33,6 +31,7 @@ const MIN_DOWNLOAD_LIMIT: u32 = 32768; /// `MIN_DELETE_SERVER_AFTER` increases the timeout in this case. pub(crate) const MIN_DELETE_SERVER_AFTER: i64 = 48 * 60 * 60; +/// Download state of the message. #[derive( Debug, Display, @@ -49,9 +48,16 @@ pub(crate) const MIN_DELETE_SERVER_AFTER: i64 = 48 * 60 * 60; )] #[repr(u32)] pub enum DownloadState { + /// Message is fully downloaded. Done = 0, + + /// Message is partially downloaded and can be fully downloaded at request. Available = 10, + + /// Failed to fully download the message. Failure = 20, + + /// Full download of the message is in progress. InProgress = 1000, } From 256ef7c5ece94bac0157567d32dad665e7e924da Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 27 Dec 2022 11:43:03 +0000 Subject: [PATCH 044/132] Add missing documentation for location streaming --- src/location.rs | 43 ++++++++++++++++++++++++++++++++++++++++--- src/mimefactory.rs | 2 ++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/location.rs b/src/location.rs index e381039ac..9e83ebd7b 100644 --- a/src/location.rs +++ b/src/location.rs @@ -1,7 +1,5 @@ //! Location handling. -#![allow(missing_docs)] - use std::convert::TryFrom; use std::time::Duration; @@ -20,32 +18,63 @@ use crate::mimeparser::SystemMessage; use crate::stock_str; use crate::tools::{duration_to_str, time}; -/// Location record +/// Location record. #[derive(Debug, Clone, Default)] pub struct Location { + /// Row ID of the location. pub location_id: u32, + + /// Location latitude. pub latitude: f64, + + /// Location longitude. pub longitude: f64, + + /// Nonstandard `accuracy` attribute of the `coordinates` tag. pub accuracy: f64, + + /// Location timestamp in seconds. pub timestamp: i64, + + /// Contact ID. pub contact_id: ContactId, + + /// Message ID. pub msg_id: u32, + + /// Chat ID. pub chat_id: ChatId, + + /// A marker string, such as an emoji, to be displayed on top of the location. pub marker: Option, + + /// Whether location is independent, i.e. not part of the path. pub independent: u32, } impl Location { + /// Creates a new empty location. pub fn new() -> Self { Default::default() } } +/// KML document. +/// +/// See for the standard and +/// for documentation. #[derive(Debug, Clone, Default)] pub struct Kml { + /// Nonstandard `addr` attribute of the `Document` tag storing the user email address. pub addr: Option, + + /// Placemarks. pub locations: Vec, + + /// Currently parsed XML tag. tag: KmlTag, + + /// Currently parsed placemark. pub curr: Location, } @@ -62,10 +91,12 @@ bitflags! { } impl Kml { + /// Creates a new empty KML document. pub fn new() -> Self { Default::default() } + /// Parses a KML document. pub fn parse(to_parse: &[u8]) -> Result { ensure!(to_parse.len() <= 1024 * 1024, "kml-file is too large"); @@ -259,6 +290,7 @@ pub async fn is_sending_locations_to_chat( Ok(exists) } +/// Sets current location of the user device. pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> bool { if latitude == 0.0 && longitude == 0.0 { return true; @@ -306,6 +338,7 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64 continue_streaming } +/// Searches for locations in the given time range, optionally filtering by chat and contact IDs. pub async fn get_range( context: &Context, chat_id: Option, @@ -396,6 +429,7 @@ pub async fn delete_all(context: &Context) -> Result<()> { Ok(()) } +/// Returns `location.kml` contents. pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)> { let mut last_added_location_id = 0; @@ -481,6 +515,7 @@ fn get_kml_timestamp(utc: i64) -> String { .to_string() } +/// Returns a KML document containing a single location with the given timestamp and coordinates. pub fn get_message_kml(timestamp: i64, latitude: f64, longitude: f64) -> String { format!( "\n\ @@ -498,6 +533,7 @@ pub fn get_message_kml(timestamp: i64, latitude: f64, longitude: f64) -> String ) } +/// Sets the timestamp of the last time location was sent in the chat. pub async fn set_kml_sent_timestamp( context: &Context, chat_id: ChatId, @@ -513,6 +549,7 @@ pub async fn set_kml_sent_timestamp( Ok(()) } +/// Sets the location of the message. pub async fn set_msg_location_id(context: &Context, msg_id: MsgId, location_id: u32) -> Result<()> { context .sql diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 184acaae0..43f3e5886 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -799,6 +799,7 @@ impl<'a> MimeFactory<'a> { }) } + /// Returns MIME part with a `message.kml` attachment. fn get_message_kml_part(&self) -> Option { let latitude = self.msg.param.get_float(Param::SetLatitude)?; let longitude = self.msg.param.get_float(Param::SetLongitude)?; @@ -818,6 +819,7 @@ impl<'a> MimeFactory<'a> { Some(part) } + /// Returns MIME part with a `location.kml` attachment. async fn get_location_kml_part(&mut self, context: &Context) -> Result { let (kml_content, last_added_location_id) = location::get_kml(context, self.msg.chat_id).await?; From 1e351bd05f1dfd93b08b57163489b2621cda837c Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 28 Dec 2022 15:09:20 +0000 Subject: [PATCH 045/132] Add documentation to simplify.rs --- src/simplify.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/simplify.rs b/src/simplify.rs index 36daa333b..1c30bc2ef 100644 --- a/src/simplify.rs +++ b/src/simplify.rs @@ -1,13 +1,13 @@ //! # Simplify incoming plaintext. -// protect lines starting with `--` against being treated as a footer. -// for that, we insert a ZERO WIDTH SPACE (ZWSP, 0x200B); -// this should be invisible on most systems and there is no need to unescape it again -// (which won't be done by non-deltas anyway) -// -// this escapes a bit more than actually needed by delta (eg. also lines as "-- footer"), -// but for non-delta-compatibility, that seems to be better. -// (to be only compatible with delta, only "[\r\n|\n]-- {0,2}[\r\n|\n]" needs to be replaced) +/// Protects lines starting with `--` against being treated as a footer. +/// for that, we insert a ZERO WIDTH SPACE (ZWSP, 0x200B); +/// this should be invisible on most systems and there is no need to unescape it again +/// (which won't be done by non-deltas anyway). +/// +/// This escapes a bit more than actually needed by delta (e.g. also lines as "-- footer"), +/// but for non-delta-compatibility, that seems to be better. +/// (to be only compatible with delta, only "[\r\n|\n]-- {0,2}[\r\n|\n]" needs to be replaced) pub fn escape_message_footer_marks(text: &str) -> String { if let Some(text) = text.strip_prefix("--") { "-\u{200B}-".to_string() + &text.replace("\n--", "\n-\u{200B}-") @@ -74,6 +74,7 @@ pub(crate) fn split_lines(buf: &str) -> Vec<&str> { /// Simplified text and some additional information gained from the input. #[derive(Debug, Default)] pub(crate) struct SimplifiedText { + /// The text itself. pub text: String, /// True if the message is forwarded. From 4a982fe632aff23b935e35fcbcb3178395fc7867 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 27 Dec 2022 09:17:19 +0000 Subject: [PATCH 046/132] Add fuzzing tests --- CHANGELOG.md | 1 + README.md | 23 + format-flowed/src/lib.rs | 6 + fuzz/Cargo.lock | 3441 +++++++++++++++++++++++ fuzz/Cargo.toml | 36 + fuzz/fuzz_targets/fuzz_dateparse.rs | 10 + fuzz/fuzz_targets/fuzz_format_flowed.rs | 25 + fuzz/fuzz_targets/fuzz_mailparse.rs | 7 + fuzz/fuzz_targets/fuzz_simplify.rs | 13 + src/fuzzing.rs | 8 + src/lib.rs | 3 + 11 files changed, 3573 insertions(+) create mode 100644 fuzz/Cargo.lock create mode 100644 fuzz/Cargo.toml create mode 100644 fuzz/fuzz_targets/fuzz_dateparse.rs create mode 100644 fuzz/fuzz_targets/fuzz_format_flowed.rs create mode 100644 fuzz/fuzz_targets/fuzz_mailparse.rs create mode 100644 fuzz/fuzz_targets/fuzz_simplify.rs create mode 100644 src/fuzzing.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9db270c67..706431cfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Don't parse the message again after detached signatures validation #3862 - Move format=flowed support to a separate crate #3869 - cargo: bump quick-xml from 0.23.0 to 0.26.0 #3722 +- Add fuzzing tests #3853 ### API-Changes - jsonrpc: add python API for webxdc updates #3872 diff --git a/README.md b/README.md index 71a723eac..3ea334637 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,29 @@ use the `--ignored` argument to the test binary (not to cargo itself): $ cargo test -- --ignored ``` +### Fuzzing + +Install [`cargo-bolero`](https://github.com/camshaft/bolero) with +```sh +$ cargo install cargo-bolero +``` + +Run fuzzing tests with +```sh +$ cd fuzz +$ cargo bolero test fuzz_mailparse --release=false -s NONE +``` + +Corpus is created at `fuzz/fuzz_targets/corpus`, +you can add initial inputs there. +For `fuzz_mailparse` target corpus can be populated with +`../test-data/message/*.eml`. + +To run with AFL instead of libFuzzer: +```sh +$ cargo bolero test fuzz_format_flowed --release=false -e afl -s NONE +``` + ## Features - `vendored`: When using Openssl for TLS, this bundles a vendored version. diff --git a/format-flowed/src/lib.rs b/format-flowed/src/lib.rs index 67d18e8d1..52b6827e5 100644 --- a/format-flowed/src/lib.rs +++ b/format-flowed/src/lib.rs @@ -182,6 +182,12 @@ mod tests { let text = " Foo bar baz"; assert_eq!(format_flowed(text), " Foo bar baz"); + + let text = + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAA"; + let expected = + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA \r\nAAAAAA"; + assert_eq!(format_flowed(text), expected); } #[test] diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock new file mode 100644 index 000000000..b2f3f5f36 --- /dev/null +++ b/fuzz/Cargo.lock @@ -0,0 +1,3441 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + +[[package]] +name = "addr2line" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aes" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom 0.2.8", + "once_cell", + "version_check 0.9.4", +] + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" + +[[package]] +name = "ascii_utils" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" + +[[package]] +name = "async-channel" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-compression" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942c7cd7ae39e91bde4820d74132e9862e62c2f386c3aa90ccf55949f5bad63a" +dependencies = [ + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-imap" +version = "0.6.0" +source = "git+https://github.com/async-email/async-imap?branch=master#85ff7a3d9d71a3715354fabf2fc1a8d047b5710e" +dependencies = [ + "async-channel", + "async-native-tls", + "base64 0.13.1", + "byte-pool", + "chrono", + "futures", + "imap-proto", + "log", + "nom 7.1.1", + "once_cell", + "ouroboros", + "pin-utils", + "stop-token", + "thiserror", + "tokio", +] + +[[package]] +name = "async-native-tls" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d57d4cec3c647232e1094dc013546c0b33ce785d8aeb251e1f20dfaf8a9a13fe" +dependencies = [ + "native-tls", + "thiserror", + "tokio", + "url", +] + +[[package]] +name = "async-smtp" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6da21e1dd19fbad3e095ad519fb1558ab77fd82e5c4778dca8f9be0464589e1e" +dependencies = [ + "async-native-tls", + "async-trait", + "base64 0.13.1", + "bufstream", + "fast-socks5", + "futures", + "hostname", + "log", + "nom 7.1.1", + "pin-project", + "pin-utils", + "thiserror", + "tokio", +] + +[[package]] +name = "async-trait" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d1d8ab452a3936018a687b20e6f7cf5363d713b732b8884001317b0e48aa3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async_io_utilities" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b20cffc5590f4bf33f05f97a3ea587feba9c50d20325b401daa096b92ff7da0" +dependencies = [ + "tokio", +] + +[[package]] +name = "async_zip" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a36d43bdefc7215b2b3a97edd03b1553b7969ad76551025eedd3b913c645f6e" +dependencies = [ + "async-compression", + "async_io_utilities", + "chrono", + "crc32fast", + "thiserror", + "tokio", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64ct" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf" + +[[package]] +name = "bitfield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a90ec2df9600c28a01c56c4784c9207a96d2451833aeceb8cc97e4c9548bb78" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + +[[package]] +name = "bolero" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3387d308f66ed222bdbb19c6ba06b1517168c4e45dc64051c5f1b4845db2901c" +dependencies = [ + "bolero-afl", + "bolero-engine", + "bolero-generator", + "bolero-honggfuzz", + "bolero-kani", + "bolero-libfuzzer", + "cfg-if", + "rand 0.8.5", +] + +[[package]] +name = "bolero-afl" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973bc6341b6a865dee93f17b78de4a100551014a527798ff1d7265d3bc0f7d89" +dependencies = [ + "bolero-engine", + "cc", +] + +[[package]] +name = "bolero-engine" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c506a476cea9e95f58c264b343ee279c353d93ceaebe98cbfb16e74bfaee2e2" +dependencies = [ + "anyhow", + "backtrace", + "bolero-generator", + "lazy_static", + "pretty-hex", + "rand 0.8.5", +] + +[[package]] +name = "bolero-generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d52eca8714d110e581cf17eeacf0d1a0d409d38a9e9ce07efeda6125f7febb" +dependencies = [ + "bolero-generator-derive", + "either", + "rand_core 0.6.4", +] + +[[package]] +name = "bolero-generator-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3c57c2a0967ad1a09ba4c2bf8f1c6b6db2f71e8c0db4fa280c65a0f6c249c3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bolero-honggfuzz" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7996a3fa8d93652358b9b3b805233807168f49740a8bf91a531cd61e4da65355" +dependencies = [ + "bolero-engine", +] + +[[package]] +name = "bolero-kani" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206879993fffa1cf2c703b1ef93b0febfa76bae85a0a5d4ae0ee6d99a2e3b74e" +dependencies = [ + "bolero-engine", +] + +[[package]] +name = "bolero-libfuzzer" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc5547411b84703d9020914f15a7d709cfb738c72b5e0f5a499fe56b8465c98" +dependencies = [ + "bolero-engine", + "cc", +] + +[[package]] +name = "buf_redux" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" +dependencies = [ + "memchr", + "safemem", +] + +[[package]] +name = "bufstream" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8" + +[[package]] +name = "bumpalo" +version = "3.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" + +[[package]] +name = "byte-pool" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c7230ddbb427b1094d477d821a99f3f54d36333178eeb806e279bcdcecf0ca" +dependencies = [ + "crossbeam-queue", + "stable_deref_trait", +] + +[[package]] +name = "bytemuck" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" + +[[package]] +name = "cast5" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b07d673db1ccf000e90f54b819db9e75a8348d6eb056e9b8ab53231b7a9911" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" + +[[package]] +name = "cfb-mode" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "738b8d467867f80a71351933f70461f5b56f24d5c93e0cf216e59229c968d330" +dependencies = [ + "cipher", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "charset" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e9079d1a12a2cc2bffb5db039c43661836ead4082120d5844f02555aca2d46" +dependencies = [ + "base64 0.13.1", + "encoding_rs", +] + +[[package]] +name = "chrono" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "time", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "cipher" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "concurrent-queue" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd7bef69dc86e3c610e4e7aed41035e2a7ed12e72dd7530f61327a6579a4390b" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cec318a675afcb6a1ea1d4340e2d377e56e47c266f28043ceccbf4412ddfdd3b" + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + +[[package]] +name = "crc24" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd121741cf3eb82c08dd3023eb55bf2665e5f60ec20f89760cf836ae4562e6a0" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "cxx" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add3fc1717409d029b20c5b6903fc0c0b02fa6741d820054f4a2efa5e5816fd" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c87959ba14bc6fbc61df77c3fcfe180fc32b93538c4f1031dd802ccb5f2ff0" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69a3e162fde4e594ed2b07d0f83c6c67b745e7f28ce58c6df5e6b6bef99dfb59" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e7e2adeb6a0d4a282e581096b06e1791532b7d576dcde5ccd9382acf55db8e6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "darling" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0dd3cd20dc6b5a876612a6e5accfe7f3dd883db6d07acfbf14c128f61550dfa" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a784d2ccaf7c98501746bf0be29b2022ba41fd62a2e622af997a03e9f972859f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7618812407e9402654622dd402b0a89dff9ba93badd6540781526117b92aab7e" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" + +[[package]] +name = "deltachat" +version = "1.104.0" +dependencies = [ + "anyhow", + "async-channel", + "async-imap", + "async-native-tls", + "async-smtp", + "async_zip", + "backtrace", + "base64 0.13.1", + "bitflags", + "chrono", + "deltachat_derive", + "email", + "encoded-words", + "escaper", + "fast-socks5", + "format-flowed", + "futures", + "futures-lite", + "hex", + "humansize", + "image", + "kamadak-exif", + "lettre_email", + "libc", + "mailparse", + "native-tls", + "num-derive", + "num-traits", + "num_cpus", + "once_cell", + "percent-encoding", + "pgp", + "qrcodegen", + "quick-xml", + "r2d2", + "r2d2_sqlite", + "rand 0.8.5", + "regex", + "reqwest", + "rusqlite", + "rust-hsluv", + "sanitize-filename", + "serde", + "serde_json", + "sha-1", + "sha2 0.10.6", + "smallvec", + "strum", + "strum_macros", + "tagger", + "textwrap", + "thiserror", + "tokio", + "tokio-io-timeout", + "tokio-stream", + "tokio-tar", + "toml", + "trust-dns-resolver", + "url", + "uuid 1.2.2", +] + +[[package]] +name = "deltachat-fuzz" +version = "0.0.0" +dependencies = [ + "bolero", + "deltachat", + "format-flowed", + "mailparse", +] + +[[package]] +name = "deltachat_derive" +version = "2.0.0" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "derive_builder" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07adf7be193b71cc36b193d0f5fe60b918a3a9db4dad0449f57bcfd519704a3" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f0314b72bed045f3a68671b3c86328386762c93f82d98c65c3cb5e5f573dd68" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "des" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e" +dependencies = [ + "cipher", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer 0.10.3", + "const-oid", + "crypto-common", +] + +[[package]] +name = "ed25519" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "email" +version = "0.0.21" +source = "git+https://github.com/deltachat/rust-email?branch=master#25702df99254d059483b41417cd80696a258df8e" +dependencies = [ + "base64 0.11.0", + "chrono", + "encoded-words", + "encoding", + "lazy_static", + "rand 0.7.3", + "time", + "version_check 0.9.4", +] + +[[package]] +name = "encoded-words" +version = "0.2.0" +source = "git+https://github.com/async-email/encoded-words?branch=master#d55366b36f96e383f39c432aedce42ee8b43f796" +dependencies = [ + "base64 0.12.3", + "charset", + "encoding_rs", + "hex", + "lazy_static", + "regex", + "thiserror", +] + +[[package]] +name = "encoding" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +dependencies = [ + "encoding-index-japanese", + "encoding-index-korean", + "encoding-index-simpchinese", + "encoding-index-singlebyte", + "encoding-index-tradchinese", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" + +[[package]] +name = "encoding_rs" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "entities" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" + +[[package]] +name = "enum-as-inner" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "escaper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a53eb97b7349ba1bdb31839eceafe9aaae8f1d8d944dc589b67fb0b26e1c1666" +dependencies = [ + "entities", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fast-socks5" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2687b5a6108f18ba8621e0e618a3be1dcc2768632dad24b7cea1f87975375a9" +dependencies = [ + "anyhow", + "log", + "thiserror", + "tokio", + "tokio-stream", +] + +[[package]] +name = "fast_chemail" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4" +dependencies = [ + "ascii_utils", +] + +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + +[[package]] +name = "filetime" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys 0.42.0", +] + +[[package]] +name = "flate2" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "format-flowed" +version = "1.0.0" + +[[package]] +name = "futures" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" + +[[package]] +name = "futures-executor" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" + +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" + +[[package]] +name = "futures-task" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" + +[[package]] +name = "futures-util" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check 0.9.4", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "gif" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec7af912d60cdbd3677c1af9352ebae6fb8394d165568a2234df0fa00f87793" + +[[package]] +name = "h2" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +dependencies = [ + "hashbrown 0.11.2", +] + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + +[[package]] +name = "http" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "humansize" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e682e2bd70ecbcce5209f11a992a4ba001fea8e60acf7860ce007629e6d2756" +dependencies = [ + "libm", +] + +[[package]] +name = "hyper" +version = "0.14.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "image" +version = "0.24.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "gif", + "jpeg-decoder", + "num-rational", + "num-traits", + "png", +] + +[[package]] +name = "imap-proto" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f73b1b63179418b20aa81002d616c5f21b4ba257da9bca6989ea64dc573933e0" +dependencies = [ + "nom 7.1.1", +] + +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ipconfig" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd302af1b90f2463a98fa5ad469fc212c8e3175a41c3068601bfa2727591c5be" +dependencies = [ + "socket2", + "widestring", + "winapi", + "winreg", +] + +[[package]] +name = "ipnet" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11b0d96e660696543b251e58030cf9787df56da39dab19ad60eae7353040917e" + +[[package]] +name = "itoa" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" + +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" + +[[package]] +name = "js-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kamadak-exif" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4fc70d0ab7e5b6bafa30216a6b48705ea964cdfc29c050f2412295eba58077" +dependencies = [ + "mutate_once", +] + +[[package]] +name = "keccak" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3afef3b6eff9ce9d8ff9b3601125eec7f0c8cbac7abd14f355d053fa56c98768" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin", +] + +[[package]] +name = "lettre" +version = "0.9.2" +source = "git+https://github.com/deltachat/lettre?branch=master#96555ec428ac114ecfca9934d2fda34c13737e54" +dependencies = [ + "fast_chemail", + "log", +] + +[[package]] +name = "lettre_email" +version = "0.9.2" +source = "git+https://github.com/deltachat/lettre?branch=master#96555ec428ac114ecfca9934d2fda34c13737e54" +dependencies = [ + "base64 0.11.0", + "email", + "lazy_static", + "lettre", + "mime", + "regex", + "time", + "uuid 0.8.2", +] + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "libm" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" + +[[package]] +name = "libsqlite3-sys" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14" +dependencies = [ + "cc", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "link-cplusplus" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +dependencies = [ + "cc", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "mailparse" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cae768a50835557749599277fc59f7c728118724eb34185e8feb633ef266a32" +dependencies = [ + "charset", + "data-encoding", + "quoted_printable", +] + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "md-5" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" +dependencies = [ + "digest 0.10.6", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.42.0", +] + +[[package]] +name = "mutate_once" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b" + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" +dependencies = [ + "memchr", + "version_check 0.1.5", +] + +[[package]] +name = "nom" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2399c9463abc5f909349d8aa9ba080e0b88b3ce2885389b60b993f39b1a56905" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "serde", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239da7f290cfa979f43f85a8efeee9a8a76d0827c356d37f9d3d7254d6b537fb" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssl" +version = "0.10.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-src" +version = "111.24.0+1.1.1s" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3498f259dab01178c6228c6b00dcef0ed2a2d5e20d648c017861227773ea4abd" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7" +dependencies = [ + "autocfg", + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ouroboros" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbb50b356159620db6ac971c6d5c9ab788c9cc38a6f49619fca2a27acb062ca" +dependencies = [ + "aliasable", + "ouroboros_macro", +] + +[[package]] +name = "ouroboros_macro" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0d9d1a6191c4f391f87219d1ea42b23f09ee84d64763cd05ee6ea88d9f384d" +dependencies = [ + "Inflector", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys 0.42.0", +] + +[[package]] +name = "pem-rfc7468" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pgp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "991e3f098483f52c454c7cb16720adc010c2966a8845d3c34aad589cb86d3196" +dependencies = [ + "aes", + "base64 0.13.1", + "bitfield", + "block-padding", + "blowfish", + "buf_redux", + "byteorder", + "cast5", + "cfb-mode", + "chrono", + "cipher", + "crc24", + "derive_builder", + "des", + "digest 0.10.6", + "ed25519-dalek", + "flate2", + "generic-array", + "hex", + "log", + "md-5", + "nom 4.2.3", + "num-bigint-dig", + "num-derive", + "num-traits", + "rand 0.8.5", + "ripemd", + "rsa", + "sha1", + "sha2 0.10.6", + "sha3", + "signature", + "smallvec", + "thiserror", + "twofish", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "pin-project" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff33bdbdfc54cc98a2eca766ebdec3e1b8fb7387523d5c9c9a2891da856f719" +dependencies = [ + "der", + "pkcs8", + "spki", + "zeroize", +] + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + +[[package]] +name = "png" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638" +dependencies = [ + "bitflags", + "crc32fast", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "pretty-hex" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check 0.9.4", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check 0.9.4", +] + +[[package]] +name = "proc-macro2" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "qrcodegen" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142" + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-xml" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "quoted_printable" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20f14e071918cbeefc5edc986a7aa92c425dae244e003a35e1cdddb5ca39b5cb" + +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + +[[package]] +name = "r2d2_sqlite" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fdc8e4da70586127893be32b7adf21326a4c6b1aba907611edf467d13ffe895" +dependencies = [ + "r2d2", + "rusqlite", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.8", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "reqwest" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c" +dependencies = [ + "base64 0.13.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "resolv-conf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname", + "quick-error", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.6", +] + +[[package]] +name = "rsa" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "094052d5470cbcef561cb848a7209968c9f12dfa6d668f4bca048ac5de51099c" +dependencies = [ + "byteorder", + "digest 0.10.6", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "smallvec", + "subtle", + "zeroize", +] + +[[package]] +name = "rusqlite" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85127183a999f7db96d1a976a309eebbfb6ea3b0b400ddd8340190129de6eb7a" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "memchr", + "smallvec", +] + +[[package]] +name = "rust-hsluv" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efe2374f2385cdd8755a446f80b2a646de603c9d8539ca38734879b5c71e378b" + +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + +[[package]] +name = "rustversion" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" + +[[package]] +name = "ryu" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[package]] +name = "sanitize-filename" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c502bdb638f1396509467cb0580ef3b29aa2a45c5d43e5d84928241280296c" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "schannel" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" +dependencies = [ + "lazy_static", + "windows-sys 0.36.1", +] + +[[package]] +name = "scheduled-thread-pool" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "977a7519bff143a44f842fd07e80ad1329295bd71686457f18e496736f4bf9bf" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "scratch" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" + +[[package]] +name = "security-framework" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fed41fc1a24994d044e6db6935e69511a1153b52c15eb42493b26fa87feba0" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "255abe9a125a985c05190d687b320c12f9b1f0b99445e608c21ba0782c719ad8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha-1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.6", +] + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.6", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.6", +] + +[[package]] +name = "sha3" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdf0c33fae925bdc080598b84bc15c55e7b9a4a43b3c704da051f977469691c9" +dependencies = [ + "digest 0.10.6", + "keccak", +] + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +dependencies = [ + "digest 0.10.6", + "rand_core 0.6.4", +] + +[[package]] +name = "slab" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "smawk" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" + +[[package]] +name = "socket2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "stop-token" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af91f480ee899ab2d9f8435bfdfc14d08a5754bd9d3fef1f1a1c23336aad6c8b" +dependencies = [ + "async-channel", + "cfg-if", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "tagger" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aaa6f5d645d1dae4cd0286e9f8bf15b75a31656348e5e106eb1a940abd34b63" + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.42.0", +] + +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-macros" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tar" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50188549787c32c1c3d9c8c71ad7e003ccf2f102489c5a96e385c84760477f4" +dependencies = [ + "filetime", + "futures-core", + "libc", + "redox_syscall", + "tokio", + "tokio-stream", + "xattr", +] + +[[package]] +name = "tokio-util" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1333c76748e868a4d9d1017b5ab53171dfd095f70c712fdb4653a406547f598f" +dependencies = [ + "serde", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "trust-dns-proto" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna 0.2.3", + "ipnet", + "lazy_static", + "rand 0.8.5", + "smallvec", + "thiserror", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "trust-dns-resolver" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aff21aa4dcefb0a1afbfac26deb0adc93888c7d295fb63ab273ef276ba2b7cfe" +dependencies = [ + "cfg-if", + "futures-util", + "ipconfig", + "lazy_static", + "lru-cache", + "parking_lot", + "resolv-conf", + "smallvec", + "thiserror", + "tokio", + "tracing", + "trust-dns-proto", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "twofish" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a78e83a30223c757c3947cd144a31014ff04298d8719ae10d03c31c0448c8013" +dependencies = [ + "cipher", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "unicode-linebreak" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137" +dependencies = [ + "hashbrown 0.12.3", + "regex", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna 0.3.0", + "percent-encoding", +] + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.8", +] + +[[package]] +name = "uuid" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" +dependencies = [ + "getrandom 0.2.8", + "serde", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" + +[[package]] +name = "web-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + +[[package]] +name = "widestring" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc 0.36.1", + "windows_i686_gnu 0.36.1", + "windows_i686_msvc 0.36.1", + "windows_x86_64_gnu 0.36.1", + "windows_x86_64_msvc 0.36.1", +] + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.42.0", + "windows_i686_gnu 0.42.0", + "windows_i686_msvc 0.42.0", + "windows_x86_64_gnu 0.42.0", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.42.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "x25519-dalek" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a0c105152107e3b96f6a00a65e86ce82d9b125230e1c4302940eca58ff71f4f" +dependencies = [ + "curve25519-dalek", + "rand_core 0.5.1", + "zeroize", +] + +[[package]] +name = "xattr" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" +dependencies = [ + "libc", +] + +[[package]] +name = "zeroize" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44bf07cb3e50ea2003396695d58bf46bc9887a1f362260446fad6bc4e79bd36c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 000000000..a3eba6256 --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "deltachat-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[dev-dependencies] +bolero = "0.8" + +[dependencies] +mailparse = "0.13" +deltachat = { path = ".." } +format-flowed = { path = "../format-flowed" } + +[workspace] +members = ["."] + +[[test]] +name = "fuzz_dateparse" +path = "fuzz_targets/fuzz_dateparse.rs" +harness = false + +[[test]] +name = "fuzz_simplify" +path = "fuzz_targets/fuzz_simplify.rs" +harness = false + +[[test]] +name = "fuzz_mailparse" +path = "fuzz_targets/fuzz_mailparse.rs" +harness = false + +[[test]] +name = "fuzz_format_flowed" +path = "fuzz_targets/fuzz_format_flowed.rs" +harness = false diff --git a/fuzz/fuzz_targets/fuzz_dateparse.rs b/fuzz/fuzz_targets/fuzz_dateparse.rs new file mode 100644 index 000000000..e903363db --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_dateparse.rs @@ -0,0 +1,10 @@ +use bolero::check; + +fn main() { + check!().for_each(|data: &[u8]| match std::str::from_utf8(data) { + Ok(input) => { + mailparse::dateparse(input).ok(); + } + Err(_err) => {} + }); +} diff --git a/fuzz/fuzz_targets/fuzz_format_flowed.rs b/fuzz/fuzz_targets/fuzz_format_flowed.rs new file mode 100644 index 000000000..f44bebf94 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_format_flowed.rs @@ -0,0 +1,25 @@ +use bolero::check; +use format_flowed::{format_flowed, unformat_flowed}; + +fn round_trip(input: &str) -> String { + let mut input = format_flowed(input); + input.retain(|c| c != '\r'); + unformat_flowed(&input, false) +} + +fn main() { + check!().for_each(|data: &[u8]| { + if let Ok(input) = std::str::from_utf8(data.into()) { + let mut input = input.to_string(); + + // Only consider inputs that don't contain quotes. + input.retain(|c| c != '>'); + + // Only consider inputs that are the result of unformatting format=flowed text. + // At least this means that lines don't contain any trailing whitespace. + let input = round_trip(&input); + let output = round_trip(&input); + assert_eq!(input, output); + } + }); +} diff --git a/fuzz/fuzz_targets/fuzz_mailparse.rs b/fuzz/fuzz_targets/fuzz_mailparse.rs new file mode 100644 index 000000000..19f2fe097 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_mailparse.rs @@ -0,0 +1,7 @@ +use bolero::check; + +fn main() { + check!().for_each(|data: &[u8]| { + mailparse::parse_mail(data).ok(); + }); +} diff --git a/fuzz/fuzz_targets/fuzz_simplify.rs b/fuzz/fuzz_targets/fuzz_simplify.rs new file mode 100644 index 000000000..cd0a22352 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_simplify.rs @@ -0,0 +1,13 @@ +use bolero::check; + +use deltachat::fuzzing::simplify; + +fn main() { + check!().for_each(|data: &[u8]| match String::from_utf8(data.to_vec()) { + Ok(input) => { + simplify(input.clone(), true); + simplify(input, false); + } + Err(_err) => {} + }); +} diff --git a/src/fuzzing.rs b/src/fuzzing.rs new file mode 100644 index 000000000..85634430c --- /dev/null +++ b/src/fuzzing.rs @@ -0,0 +1,8 @@ +/// Fuzzing target for simplify(). +/// +/// Calls simplify() and panics if simplify() panics. +/// Does not return any vaule to avoid exposing internal crate types. +#[cfg(fuzzing)] +pub fn simplify(mut input: String, is_chat_message: bool) { + crate::simplify::simplify(input, is_chat_message); +} diff --git a/src/lib.rs b/src/lib.rs index a672ec7c0..c69fc8587 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -117,3 +117,6 @@ pub const DCC_MIME_DEBUG: &str = "DCC_MIME_DEBUG"; mod test_utils; #[cfg(test)] mod tests; + +#[cfg(fuzzing)] +pub mod fuzzing; From 7082f9f882c0516431abd6fb4e8d969b80ce736e Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 28 Dec 2022 19:01:48 +0000 Subject: [PATCH 047/132] Fix fuzzing module warnings --- src/fuzzing.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/fuzzing.rs b/src/fuzzing.rs index 85634430c..ad920b9ab 100644 --- a/src/fuzzing.rs +++ b/src/fuzzing.rs @@ -1,8 +1,12 @@ +//! # Fuzzing module. +//! +//! This module exposes private APIs for fuzzing. + /// Fuzzing target for simplify(). /// /// Calls simplify() and panics if simplify() panics. /// Does not return any vaule to avoid exposing internal crate types. #[cfg(fuzzing)] -pub fn simplify(mut input: String, is_chat_message: bool) { +pub fn simplify(input: String, is_chat_message: bool) { crate::simplify::simplify(input, is_chat_message); } From 4e943d52e42786c5a87627d1e30f649a2d72b16c Mon Sep 17 00:00:00 2001 From: Rafael Diniz Date: Wed, 7 Dec 2022 16:20:45 +0300 Subject: [PATCH 048/132] Add mappings for some file types to Viewtype / MIME type Namely: ppt, pptx, xls, heif, heic, avif, txt. But use Viewtype::File for medias without uniform support on all platforms. --- CHANGELOG.md | 1 + python/tests/data/r | 2 ++ python/tests/test_3_offline.py | 17 +++++++++-------- src/message.rs | 14 ++++++++++++-- 4 files changed, 24 insertions(+), 10 deletions(-) create mode 100644 python/tests/data/r diff --git a/CHANGELOG.md b/CHANGELOG.md index 706431cfb..157eb8eb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Move format=flowed support to a separate crate #3869 - cargo: bump quick-xml from 0.23.0 to 0.26.0 #3722 - Add fuzzing tests #3853 +- Add mappings for some file types to Viewtype / MIME type #3881 ### API-Changes - jsonrpc: add python API for webxdc updates #3872 diff --git a/python/tests/data/r b/python/tests/data/r new file mode 100644 index 000000000..3e23ae484 --- /dev/null +++ b/python/tests/data/r @@ -0,0 +1,2 @@ + +hello diff --git a/python/tests/test_3_offline.py b/python/tests/test_3_offline.py index 3a09eebf4..98890287a 100644 --- a/python/tests/test_3_offline.py +++ b/python/tests/test_3_offline.py @@ -450,24 +450,25 @@ class TestOfflineChat: assert msg.filemime == "image/png" @pytest.mark.parametrize( - "typein,typeout", + "fn,typein,typeout", [ - (None, "application/octet-stream"), - ("text/plain", "text/plain"), - ("image/png", "image/png"), + ("r", None, "application/octet-stream"), + ("r.txt", None, "text/plain"), + ("r.txt", "text/plain", "text/plain"), + ("r.txt", "image/png", "image/png"), ], ) - def test_message_file(self, ac1, chat1, data, lp, typein, typeout): + def test_message_file(self, ac1, chat1, data, lp, fn, typein, typeout): lp.sec("sending file") - fn = data.get_path("r.txt") - msg = chat1.send_file(fn, typein) + fp = data.get_path(fn) + msg = chat1.send_file(fp, typein) assert msg assert msg.id > 0 assert msg.is_file() assert os.path.exists(msg.filename) assert msg.filename.endswith(msg.basename) assert msg.filemime == typeout - msg2 = chat1.send_file(fn, typein) + msg2 = chat1.send_file(fp, typein) assert msg2 != msg assert msg2.filename != msg.filename diff --git a/src/message.rs b/src/message.rs index a1e1b0b97..d05b8b366 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1168,6 +1168,7 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> { "3gp" => (Viewtype::Video, "video/3gpp"), "aac" => (Viewtype::Audio, "audio/aac"), "avi" => (Viewtype::Video, "video/x-msvideo"), + "avif" => (Viewtype::File, "image/avif"), // supported since Android 12 / iOS 16 "doc" => (Viewtype::File, "application/msword"), "docx" => ( Viewtype::File, @@ -1176,6 +1177,8 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> { "epub" => (Viewtype::File, "application/epub+zip"), "flac" => (Viewtype::Audio, "audio/flac"), "gif" => (Viewtype::Gif, "image/gif"), + "heic" => (Viewtype::File, "image/heic"), // supported since Android 10 / iOS 11 + "heif" => (Viewtype::File, "image/heif"), // supported since Android 10 / iOS 11 "html" => (Viewtype::File, "text/html"), "htm" => (Viewtype::File, "text/html"), "ico" => (Viewtype::File, "image/vnd.microsoft.icon"), @@ -1200,10 +1203,15 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> { "oga" => (Viewtype::Audio, "audio/ogg"), "ogg" => (Viewtype::Audio, "audio/ogg"), "ogv" => (Viewtype::File, "video/ogg"), - "opus" => (Viewtype::File, "audio/ogg"), // not supported eg. on Android 4 + "opus" => (Viewtype::File, "audio/ogg"), // supported since Android 10 "otf" => (Viewtype::File, "font/otf"), "pdf" => (Viewtype::File, "application/pdf"), "png" => (Viewtype::Image, "image/png"), + "ppt" => (Viewtype::File, "application/vnd.ms-powerpoint"), + "pptx" => ( + Viewtype::File, + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + ), "rar" => (Viewtype::File, "application/vnd.rar"), "rtf" => (Viewtype::File, "application/rtf"), "spx" => (Viewtype::File, "audio/ogg"), // Ogg Speex Profile @@ -1212,6 +1220,7 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> { "tiff" => (Viewtype::File, "image/tiff"), "tif" => (Viewtype::File, "image/tiff"), "ttf" => (Viewtype::File, "font/ttf"), + "txt" => (Viewtype::File, "text/plain"), "vcard" => (Viewtype::File, "text/vcard"), "vcf" => (Viewtype::File, "text/vcard"), "wav" => (Viewtype::File, "audio/wav"), @@ -1221,11 +1230,12 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> { "wmv" => (Viewtype::Video, "video/x-ms-wmv"), "xdc" => (Viewtype::Webxdc, "application/webxdc+zip"), "xhtml" => (Viewtype::File, "application/xhtml+xml"), + "xls" => (Viewtype::File, "application/vnd.ms-excel"), "xlsx" => ( Viewtype::File, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ), - "xml" => (Viewtype::File, "application/vnd.ms-excel"), + "xml" => (Viewtype::File, "application/xml"), "zip" => (Viewtype::File, "application/zip"), _ => { return None; From bf4ad692df79b53d37475aba38a988d38b754050 Mon Sep 17 00:00:00 2001 From: bjoern Date: Fri, 30 Dec 2022 19:53:44 +0100 Subject: [PATCH 049/132] use u32 as id as done elsewhere (#3882) this will avoid some incompatibilities and castingss in UI. --- deltachat-ffi/deltachat.h | 2 +- deltachat-ffi/src/lib.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 4270d0003..4a03f1a8f 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -4763,7 +4763,7 @@ char* dc_contact_get_verifier_addr (dc_contact_t* contact); * we verified the contact ourself. If it is 0, we don't have verifier information or * the contact is not verified. */ -int dc_contact_get_verifier_id (dc_contact_t* contact); +uint32_t dc_contact_get_verifier_id (dc_contact_t* contact); /** diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 51a5d3353..764f1acad 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -3983,10 +3983,10 @@ pub unsafe extern "C" fn dc_contact_get_verifier_addr( } #[no_mangle] -pub unsafe extern "C" fn dc_contact_get_verifier_id(contact: *mut dc_contact_t) -> libc::c_int { +pub unsafe extern "C" fn dc_contact_get_verifier_id(contact: *mut dc_contact_t) -> u32 { if contact.is_null() { eprintln!("ignoring careless call to dc_contact_get_verifier_id()"); - return 0 as libc::c_int; + return 0; } let ffi_contact = &*contact; let ctx = &*ffi_contact.context; @@ -3995,7 +3995,7 @@ pub unsafe extern "C" fn dc_contact_get_verifier_id(contact: *mut dc_contact_t) .unwrap_or_default() .unwrap_or_default(); - contact_id.to_u32() as libc::c_int + contact_id.to_u32() } // dc_lot_t From 45462fb47ef237ef5756b53c5a6e982ca6018371 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 31 Dec 2022 11:46:51 +0000 Subject: [PATCH 050/132] Fix uncaught exception in node JSON-RPC tests Events don't have an `id`, so promises[response.id] does not exist for them. This currently prints a DEP0168 [1] deprecation warning, but will likely return an error in the future. [1] https://nodejs.org/api/all.html#all_deprecations_dep0168-unhandled-exception-in-node-api-callbacks --- .github/workflows/node-tests.yml | 2 ++ CHANGELOG.md | 1 + node/test/test.js | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/node-tests.yml b/.github/workflows/node-tests.yml index 4789de22e..d2f210f39 100644 --- a/.github/workflows/node-tests.yml +++ b/.github/workflows/node-tests.yml @@ -59,6 +59,7 @@ jobs: npm run test env: DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }} + NODE_OPTIONS: '--force-node-api-uncaught-exceptions-policy=true' - name: Run tests on Windows, except lint timeout-minutes: 10 if: runner.os == 'Windows' @@ -67,3 +68,4 @@ jobs: npm run test:mocha env: DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }} + NODE_OPTIONS: '--force-node-api-uncaught-exceptions-policy=true' diff --git a/CHANGELOG.md b/CHANGELOG.md index 157eb8eb3..8ebb83032 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ ### Fixes - Do not add an error if the message is encrypted but not signed #3860 - Do not strip leading spaces from message lines #3867 +- Fix uncaught exception in JSON-RPC tests #3884 ## 1.104.0 diff --git a/node/test/test.js b/node/test/test.js index 2b4bc6d20..93b5839e3 100644 --- a/node/test/test.js +++ b/node/test/test.js @@ -121,7 +121,7 @@ describe('JSON RPC', function () { const promises = {} dc.startJsonRpcHandler((msg) => { const response = JSON.parse(msg) - promises[response.id](response) + if (response.hasOwnProperty('id')) promises[response.id](response) delete promises[response.id] }) const call = (request) => { From b9dbf1873d25c9db512caab1778ae41c2e199662 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 1 Jan 2023 16:37:39 +0000 Subject: [PATCH 051/132] node: do not truncate assertion errors --- node/test/test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/node/test/test.js b/node/test/test.js index 93b5839e3..3d6c93de9 100644 --- a/node/test/test.js +++ b/node/test/test.js @@ -11,6 +11,7 @@ import { mkdtempSync, statSync } from 'fs' import { tmpdir } from 'os' import { Context } from '../dist/context' chai.use(chaiAsPromised) +chai.config.truncateThreshold = 0; // Do not truncate assertion errors. async function createTempUser(url) { const fetch = require('node-fetch') From 4bbb83826cf10e3b7eebd8d71489398c32438c93 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Jan 2023 21:01:06 +0000 Subject: [PATCH 052/132] cargo: bump thiserror from 1.0.37 to 1.0.38 Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.37 to 1.0.38. - [Release notes](https://github.com/dtolnay/thiserror/releases) - [Commits](https://github.com/dtolnay/thiserror/compare/1.0.37...1.0.38) --- updated-dependencies: - dependency-name: thiserror dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 33f9f9eda..cfdf8853c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3482,18 +3482,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" dependencies = [ "proc-macro2", "quote", From bcef1c7a76aa384c0b9d1aa95ed52abd5794d067 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Jan 2023 21:01:13 +0000 Subject: [PATCH 053/132] cargo: bump num_cpus from 1.14.0 to 1.15.0 Bumps [num_cpus](https://github.com/seanmonstar/num_cpus) from 1.14.0 to 1.15.0. - [Release notes](https://github.com/seanmonstar/num_cpus/releases) - [Changelog](https://github.com/seanmonstar/num_cpus/blob/master/CHANGELOG.md) - [Commits](https://github.com/seanmonstar/num_cpus/compare/v1.14.0...v1.15.0) --- updated-dependencies: - dependency-name: num_cpus dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Cargo.lock | 6 +++--- Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 33f9f9eda..4d3ff0b57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2336,11 +2336,11 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ - "hermit-abi 0.1.19", + "hermit-abi 0.2.6", "libc", ] diff --git a/Cargo.toml b/Cargo.toml index a76e6d5c7..f260f6eec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,7 @@ libc = "0.2" log = {version = "0.4.16", optional = true } mailparse = "0.13" native-tls = "0.2" -num_cpus = "1.14" +num_cpus = "1.15" num-derive = "0.3" num-traits = "0.2" once_cell = "1.16.0" From a562348dfae1857cb33bc5b5b92179f46c7c434c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Jan 2023 21:01:22 +0000 Subject: [PATCH 054/132] cargo: bump anyhow from 1.0.66 to 1.0.68 Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.66 to 1.0.68. - [Release notes](https://github.com/dtolnay/anyhow/releases) - [Commits](https://github.com/dtolnay/anyhow/compare/1.0.66...1.0.68) --- updated-dependencies: - dependency-name: anyhow dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 33f9f9eda..577246655 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,9 +86,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.66" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" +checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" [[package]] name = "ascii_utils" From cb4b9fce303c3d75b8033c0689431c0a489fc7a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Jan 2023 21:01:55 +0000 Subject: [PATCH 055/132] cargo: bump syn from 1.0.105 to 1.0.107 Bumps [syn](https://github.com/dtolnay/syn) from 1.0.105 to 1.0.107. - [Release notes](https://github.com/dtolnay/syn/releases) - [Commits](https://github.com/dtolnay/syn/compare/1.0.105...1.0.107) --- updated-dependencies: - dependency-name: syn dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 33f9f9eda..48e8cb505 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3413,9 +3413,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.105" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" dependencies = [ "proc-macro2", "quote", From 4e468fdf2403edafaaa5e5ce72c4d124b29e46ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Jan 2023 21:02:06 +0000 Subject: [PATCH 056/132] cargo: bump humansize from 2.1.2 to 2.1.3 Bumps [humansize](https://github.com/LeopoldArkham/humansize) from 2.1.2 to 2.1.3. - [Release notes](https://github.com/LeopoldArkham/humansize/releases) - [Changelog](https://github.com/LeopoldArkham/humansize/blob/master/changelog.md) - [Commits](https://github.com/LeopoldArkham/humansize/commits) --- updated-dependencies: - dependency-name: humansize dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 33f9f9eda..afdbcc0ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1752,9 +1752,9 @@ dependencies = [ [[package]] name = "humansize" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e682e2bd70ecbcce5209f11a992a4ba001fea8e60acf7860ce007629e6d2756" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" dependencies = [ "libm", ] From 15fad5476ee945b097ef99e590c3e2341d98c31c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Jan 2023 21:02:20 +0000 Subject: [PATCH 057/132] cargo: bump tokio from 1.22.0 to 1.23.0 Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.22.0 to 1.23.0. - [Release notes](https://github.com/tokio-rs/tokio/releases) - [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.22.0...tokio-1.23.0) --- updated-dependencies: - dependency-name: tokio dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Cargo.lock | 6 +++--- deltachat-jsonrpc/Cargo.toml | 4 ++-- deltachat-rpc-server/Cargo.toml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 33f9f9eda..cdecc713f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3538,9 +3538,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.22.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3" +checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46" dependencies = [ "autocfg", "bytes", @@ -3553,7 +3553,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "winapi", + "windows-sys 0.42.0", ] [[package]] diff --git a/deltachat-jsonrpc/Cargo.toml b/deltachat-jsonrpc/Cargo.toml index c8d77c5b9..dbe8b2d17 100644 --- a/deltachat-jsonrpc/Cargo.toml +++ b/deltachat-jsonrpc/Cargo.toml @@ -23,7 +23,7 @@ futures = { version = "0.3.25" } serde_json = "1.0.89" yerpc = { version = "^0.3.1", features = ["anyhow_expose"] } typescript-type-def = { version = "0.5.5", features = ["json_value"] } -tokio = { version = "1.22.0" } +tokio = { version = "1.23.0" } sanitize-filename = "0.4" walkdir = "2.3.2" @@ -32,7 +32,7 @@ axum = { version = "0.6.1", optional = true, features = ["ws"] } env_logger = { version = "0.10.0", optional = true } [dev-dependencies] -tokio = { version = "1.22.0", features = ["full", "rt-multi-thread"] } +tokio = { version = "1.23.0", features = ["full", "rt-multi-thread"] } [features] diff --git a/deltachat-rpc-server/Cargo.toml b/deltachat-rpc-server/Cargo.toml index 49c568bd9..b6f10996d 100644 --- a/deltachat-rpc-server/Cargo.toml +++ b/deltachat-rpc-server/Cargo.toml @@ -21,5 +21,5 @@ futures-lite = "1.12.0" log = "0.4" serde_json = "1.0.89" serde = { version = "1.0", features = ["derive"] } -tokio = { version = "1.22.0", features = ["io-std"] } +tokio = { version = "1.23.0", features = ["io-std"] } yerpc = { version = "0.3.1", features = ["anyhow_expose"] } From 11ca12e43cd4236cdea944109c71a84394d4f181 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Jan 2023 21:02:53 +0000 Subject: [PATCH 058/132] cargo: bump backtrace from 0.3.66 to 0.3.67 Bumps [backtrace](https://github.com/rust-lang/backtrace-rs) from 0.3.66 to 0.3.67. - [Release notes](https://github.com/rust-lang/backtrace-rs/releases) - [Commits](https://github.com/rust-lang/backtrace-rs/compare/0.3.66...0.3.67) --- updated-dependencies: - dependency-name: backtrace dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 33f9f9eda..030b771f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" [[package]] name = "addr2line" -version = "0.17.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" dependencies = [ "gimli", ] @@ -340,15 +340,15 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.66" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" dependencies = [ "addr2line", "cc", "cfg-if", "libc", - "miniz_oxide 0.5.3", + "miniz_oxide 0.6.2", "object", "rustc-demangle", ] @@ -1598,9 +1598,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.26.2" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" +checksum = "dec7af912d60cdbd3677c1af9352ebae6fb8394d165568a2234df0fa00f87793" [[package]] name = "h2" @@ -2346,9 +2346,9 @@ dependencies = [ [[package]] name = "object" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" +checksum = "239da7f290cfa979f43f85a8efeee9a8a76d0827c356d37f9d3d7254d6b537fb" dependencies = [ "memchr", ] From de47aa84662dadb344b8da21da1601d780634bd6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Jan 2023 23:24:38 +0000 Subject: [PATCH 059/132] cargo: bump mailparse from 0.13.8 to 0.14.0 Bumps [mailparse](https://github.com/staktrace/mailparse) from 0.13.8 to 0.14.0. - [Release notes](https://github.com/staktrace/mailparse/releases) - [Commits](https://github.com/staktrace/mailparse/compare/v0.13.8...v0.14.0) --- updated-dependencies: - dependency-name: mailparse dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Cargo.lock | 12 ++++++------ Cargo.toml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3b37c0598..89d0bd083 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -861,9 +861,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" +checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" [[package]] name = "deltachat" @@ -2100,9 +2100,9 @@ dependencies = [ [[package]] name = "mailparse" -version = "0.13.8" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cae768a50835557749599277fc59f7c728118724eb34185e8feb633ef266a32" +checksum = "6b56570f5f8c0047260d1c8b5b331f62eb9c660b9dd4071a8c46f8c7d3f280aa" dependencies = [ "charset", "data-encoding", @@ -2757,9 +2757,9 @@ dependencies = [ [[package]] name = "quoted_printable" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fee2dce59f7a43418e3382c766554c614e06a552d53a8f07ef499ea4b332c0f" +checksum = "20f14e071918cbeefc5edc986a7aa92c425dae244e003a35e1cdddb5ca39b5cb" [[package]] name = "r2d2" diff --git a/Cargo.toml b/Cargo.toml index f260f6eec..1c9d7230e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ kamadak-exif = "0.5" lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" } libc = "0.2" log = {version = "0.4.16", optional = true } -mailparse = "0.13" +mailparse = "0.14" native-tls = "0.2" num_cpus = "1.15" num-derive = "0.3" From 035b711ee337fe0e15bfed34fcbc7580fd01a351 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 1 Jan 2023 18:57:28 +0000 Subject: [PATCH 060/132] Buffer IMAP client writes async-imap does not do its own buffering, but calls flush() after sending each command. Using BufWriter reduces the number of write() system calls used to send a single command. Note that BufWriter is set up on top of TLS streams, because we can't guarantee that TLS libraries flush the stream before waiting for response. --- CHANGELOG.md | 1 + src/imap.rs | 29 +++++----- src/imap/client.rs | 129 +++++++++++++++++++++----------------------- src/imap/session.rs | 12 +++-- src/lib.rs | 1 + src/net.rs | 26 +++++++++ src/socks.rs | 16 ++---- 7 files changed, 118 insertions(+), 96 deletions(-) create mode 100644 src/net.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ebb83032..c58c4f608 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - cargo: bump quick-xml from 0.23.0 to 0.26.0 #3722 - Add fuzzing tests #3853 - Add mappings for some file types to Viewtype / MIME type #3881 +- Buffer IMAP client writes #3888 ### API-Changes - jsonrpc: add python API for webxdc updates #3872 diff --git a/src/imap.rs b/src/imap.rs index 64dd59976..c762cae7b 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -304,22 +304,23 @@ impl Imap { let imap_server: &str = config.lp.server.as_ref(); let imap_port = config.lp.port; - let connection = if let Some(socks5_config) = &config.socks5_config { - Client::connect_insecure_socks5((imap_server, imap_port), socks5_config.clone()) + if let Some(socks5_config) = &config.socks5_config { + if config.lp.security == Socket::Starttls { + Client::connect_starttls_socks5( + imap_server, + imap_port, + socks5_config.clone(), + config.strict_tls, + ) .await + } else { + Client::connect_insecure_socks5((imap_server, imap_port), socks5_config.clone()) + .await + } + } else if config.lp.security == Socket::Starttls { + Client::connect_starttls(imap_server, imap_port, config.strict_tls).await } else { Client::connect_insecure((imap_server, imap_port)).await - }; - - match connection { - Ok(client) => { - if config.lp.security == Socket::Starttls { - client.secure(imap_server, config.strict_tls).await - } else { - Ok(client) - } - } - Err(err) => Err(err), } } else { let config = &self.config; @@ -328,8 +329,8 @@ impl Imap { if let Some(socks5_config) = &config.socks5_config { Client::connect_secure_socks5( - (imap_server, imap_port), imap_server, + imap_port, config.strict_tls, socks5_config.clone(), ) diff --git a/src/imap/client.rs b/src/imap/client.rs index eef6e7929..7a2329c6a 100644 --- a/src/imap/client.rs +++ b/src/imap/client.rs @@ -8,13 +8,13 @@ use anyhow::{Context as _, Result}; use async_imap::Client as ImapClient; use async_imap::Session as ImapSession; -use tokio::net::{self, TcpStream}; -use tokio::time::timeout; -use tokio_io_timeout::TimeoutStream; +use tokio::io::BufWriter; +use tokio::net::ToSocketAddrs; use super::capabilities::Capabilities; use super::session::Session; use crate::login_param::build_tls; +use crate::net::connect_tcp; use crate::socks::Socks5Config; use super::session::SessionStream; @@ -24,7 +24,6 @@ pub(crate) const IMAP_TIMEOUT: Duration = Duration::from_secs(30); #[derive(Debug)] pub(crate) struct Client { - is_secure: bool, inner: ImapClient>, } @@ -93,108 +92,104 @@ impl Client { } pub async fn connect_secure(hostname: &str, port: u16, strict_tls: bool) -> Result { - let tcp_stream = timeout(IMAP_TIMEOUT, TcpStream::connect((hostname, port))).await??; - let mut timeout_stream = TimeoutStream::new(tcp_stream); - timeout_stream.set_write_timeout(Some(IMAP_TIMEOUT)); - timeout_stream.set_read_timeout(Some(IMAP_TIMEOUT)); - let timeout_stream = Box::pin(timeout_stream); - + let tcp_stream = connect_tcp((hostname, port), IMAP_TIMEOUT).await?; let tls = build_tls(strict_tls); - let tls_stream: Box = - Box::new(tls.connect(hostname, timeout_stream).await?); - let mut client = ImapClient::new(tls_stream); + let tls_stream = tls.connect(hostname, tcp_stream).await?; + let buffered_stream = BufWriter::new(tls_stream); + let session_stream: Box = Box::new(buffered_stream); + let mut client = ImapClient::new(session_stream); let _greeting = client .read_response() .await .context("failed to read greeting")?; - Ok(Client { - is_secure: true, - inner: client, - }) + Ok(Client { inner: client }) } - pub async fn connect_insecure(addr: impl net::ToSocketAddrs) -> Result { - let tcp_stream = timeout(IMAP_TIMEOUT, TcpStream::connect(addr)).await??; - let mut timeout_stream = TimeoutStream::new(tcp_stream); - timeout_stream.set_write_timeout(Some(IMAP_TIMEOUT)); - timeout_stream.set_read_timeout(Some(IMAP_TIMEOUT)); - let timeout_stream = Box::pin(timeout_stream); - let stream: Box = Box::new(timeout_stream); - - let mut client = ImapClient::new(stream); + pub async fn connect_insecure(addr: impl ToSocketAddrs) -> Result { + let tcp_stream = connect_tcp(addr, IMAP_TIMEOUT).await?; + let buffered_stream = BufWriter::new(tcp_stream); + let session_stream: Box = Box::new(buffered_stream); + let mut client = ImapClient::new(session_stream); let _greeting = client .read_response() .await .context("failed to read greeting")?; - Ok(Client { - is_secure: false, - inner: client, - }) + Ok(Client { inner: client }) + } + + pub async fn connect_starttls(hostname: &str, port: u16, strict_tls: bool) -> Result { + let tcp_stream = connect_tcp((hostname, port), IMAP_TIMEOUT).await?; + let tls = build_tls(strict_tls); + let tls_stream = tls.connect(hostname, tcp_stream).await?; + let buffered_stream = BufWriter::new(tls_stream); + let session_stream: Box = Box::new(buffered_stream); + let mut client = ImapClient::new(session_stream); + let _greeting = client + .read_response() + .await + .context("failed to read greeting")?; + + Ok(Client { inner: client }) } pub async fn connect_secure_socks5( - target_addr: impl net::ToSocketAddrs, domain: &str, + port: u16, strict_tls: bool, socks5_config: Socks5Config, ) -> Result { - let socks5_stream: Box = - Box::new(socks5_config.connect(target_addr, IMAP_TIMEOUT).await?); - + let socks5_stream = socks5_config.connect((domain, port), IMAP_TIMEOUT).await?; let tls = build_tls(strict_tls); - let tls_stream: Box = - Box::new(tls.connect(domain, socks5_stream).await?); - let mut client = ImapClient::new(tls_stream); - + let tls_stream = tls.connect(domain, socks5_stream).await?; + let buffered_stream = BufWriter::new(tls_stream); + let session_stream: Box = Box::new(buffered_stream); + let mut client = ImapClient::new(session_stream); let _greeting = client .read_response() .await .context("failed to read greeting")?; - Ok(Client { - is_secure: true, - inner: client, - }) + Ok(Client { inner: client }) } pub async fn connect_insecure_socks5( - target_addr: impl net::ToSocketAddrs, + target_addr: impl ToSocketAddrs, socks5_config: Socks5Config, ) -> Result { - let socks5_stream: Box = - Box::new(socks5_config.connect(target_addr, IMAP_TIMEOUT).await?); - - let mut client = ImapClient::new(socks5_stream); + let socks5_stream = socks5_config.connect(target_addr, IMAP_TIMEOUT).await?; + let buffered_stream = BufWriter::new(socks5_stream); + let session_stream: Box = Box::new(buffered_stream); + let mut client = ImapClient::new(session_stream); let _greeting = client .read_response() .await .context("failed to read greeting")?; - Ok(Client { - is_secure: false, - inner: client, - }) + Ok(Client { inner: client }) } - pub async fn secure(self, domain: &str, strict_tls: bool) -> Result { - if self.is_secure { - Ok(self) - } else { - let Client { mut inner, .. } = self; - let tls = build_tls(strict_tls); - inner.run_command_and_check_ok("STARTTLS", None).await?; + pub async fn connect_starttls_socks5( + hostname: &str, + port: u16, + socks5_config: Socks5Config, + strict_tls: bool, + ) -> Result { + let socks5_stream = socks5_config + .connect((hostname, port), IMAP_TIMEOUT) + .await?; + let tls = build_tls(strict_tls); + let tls_stream = tls.connect(hostname, socks5_stream).await?; + let buffered_stream = BufWriter::new(tls_stream); + let session_stream: Box = Box::new(buffered_stream); + let mut client = ImapClient::new(session_stream); + let _greeting = client + .read_response() + .await + .context("failed to read greeting")?; - let stream = inner.into_inner(); - let ssl_stream = tls.connect(domain, stream).await?; - let boxed: Box = Box::new(ssl_stream); - - Ok(Client { - is_secure: true, - inner: ImapClient::new(boxed), - }) - } + Ok(Client { inner: client }) } } diff --git a/src/imap/session.rs b/src/imap/session.rs index a66dd4852..f3dd00a86 100644 --- a/src/imap/session.rs +++ b/src/imap/session.rs @@ -6,6 +6,7 @@ use async_imap::types::Mailbox; use async_imap::Session as ImapSession; use async_native_tls::TlsStream; use fast_socks5::client::Socks5Stream; +use tokio::io::BufWriter; use tokio::net::TcpStream; use tokio_io_timeout::TimeoutStream; @@ -33,12 +34,17 @@ pub(crate) trait SessionStream: fn set_read_timeout(&mut self, timeout: Option); } -impl SessionStream for TlsStream> { +impl SessionStream for Box { + fn set_read_timeout(&mut self, timeout: Option) { + self.as_mut().set_read_timeout(timeout); + } +} +impl SessionStream for TlsStream { fn set_read_timeout(&mut self, timeout: Option) { self.get_mut().set_read_timeout(timeout); } } -impl SessionStream for TlsStream>>> { +impl SessionStream for BufWriter { fn set_read_timeout(&mut self, timeout: Option) { self.get_mut().set_read_timeout(timeout); } @@ -48,7 +54,7 @@ impl SessionStream for Pin>> { self.as_mut().set_read_timeout_pinned(timeout); } } -impl SessionStream for Socks5Stream>>> { +impl SessionStream for Socks5Stream { fn set_read_timeout(&mut self, timeout: Option) { self.get_socket_mut().set_read_timeout(timeout) } diff --git a/src/lib.rs b/src/lib.rs index c69fc8587..a6e8eb8ae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -100,6 +100,7 @@ mod dehtml; mod authres; mod color; pub mod html; +mod net; pub mod plaintext; mod ratelimit; pub mod summary; diff --git a/src/net.rs b/src/net.rs new file mode 100644 index 000000000..53855cb74 --- /dev/null +++ b/src/net.rs @@ -0,0 +1,26 @@ +///! # Common network utilities. +use std::pin::Pin; +use std::time::Duration; + +use anyhow::{Context as _, Result}; +use tokio::net::{TcpStream, ToSocketAddrs}; +use tokio::time::timeout; +use tokio_io_timeout::TimeoutStream; + +/// Returns a TCP connection with read/write timeouts set. +pub(crate) async fn connect_tcp( + addr: impl ToSocketAddrs, + timeout_val: Duration, +) -> Result>>> { + let tcp_stream = timeout(timeout_val, TcpStream::connect(addr)) + .await + .context("connection timeout")? + .context("connection failure")?; + + let mut timeout_stream = TimeoutStream::new(tcp_stream); + timeout_stream.set_write_timeout(Some(timeout_val)); + timeout_stream.set_read_timeout(Some(timeout_val)); + let pinned_stream = Box::pin(timeout_stream); + + Ok(pinned_stream) +} diff --git a/src/socks.rs b/src/socks.rs index 89a9458c6..e7ea20730 100644 --- a/src/socks.rs +++ b/src/socks.rs @@ -4,10 +4,10 @@ use std::fmt; use std::pin::Pin; use std::time::Duration; -use anyhow::{Context as _, Result}; +use crate::net::connect_tcp; +use anyhow::Result; pub use async_smtp::ServerAddress; use tokio::net::{self, TcpStream}; -use tokio::time::timeout; use tokio_io_timeout::TimeoutStream; use crate::context::Context; @@ -59,14 +59,7 @@ impl Socks5Config { target_addr: impl net::ToSocketAddrs, timeout_val: Duration, ) -> Result>>>> { - let tcp_stream = timeout(timeout_val, TcpStream::connect(target_addr)) - .await - .context("connection timeout")? - .context("connection failure")?; - let mut timeout_stream = TimeoutStream::new(tcp_stream); - timeout_stream.set_write_timeout(Some(timeout_val)); - timeout_stream.set_read_timeout(Some(timeout_val)); - let timeout_stream = Box::pin(timeout_stream); + let tcp_stream = connect_tcp(target_addr, timeout_val).await?; let authentication_method = if let Some((username, password)) = self.user_password.as_ref() { @@ -78,8 +71,7 @@ impl Socks5Config { None }; let socks_stream = - Socks5Stream::use_stream(timeout_stream, authentication_method, Config::default()) - .await?; + Socks5Stream::use_stream(tcp_stream, authentication_method, Config::default()).await?; Ok(socks_stream) } From 08af7419aff1529fca0efbbc9345547ce3efaff9 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 1 Jan 2023 18:30:05 +0300 Subject: [PATCH 061/132] Format configuration error with causes --- src/configure.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/configure.rs b/src/configure.rs index 63e85dcd6..8610e97dc 100644 --- a/src/configure.rs +++ b/src/configure.rs @@ -582,7 +582,7 @@ async fn try_imap_one_param( info!(context, "failure: {}", err); return Err(ConfigurationError { config: inf, - msg: err.to_string(), + msg: format!("{:#}", err), }); } Ok(imap) => imap, @@ -593,7 +593,7 @@ async fn try_imap_one_param( info!(context, "failure: {}", err); Err(ConfigurationError { config: inf, - msg: err.to_string(), + msg: format!("{:#}", err), }) } Ok(()) => { @@ -634,7 +634,7 @@ async fn try_smtp_one_param( info!(context, "failure: {}", err); Err(ConfigurationError { config: inf, - msg: err.to_string(), + msg: format!("{:#}", err), }) } else { info!(context, "success: {}", inf); From 9aaf5cf914692aacd7fa9d9ba7bb6fdddcce0f70 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 1 Jan 2023 22:50:20 +0000 Subject: [PATCH 062/132] Disable Nagle's algorithm for TCP connections --- src/net.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/net.rs b/src/net.rs index 53855cb74..6c0c7dd0b 100644 --- a/src/net.rs +++ b/src/net.rs @@ -7,7 +7,11 @@ use tokio::net::{TcpStream, ToSocketAddrs}; use tokio::time::timeout; use tokio_io_timeout::TimeoutStream; -/// Returns a TCP connection with read/write timeouts set. +/// Returns a TCP connection stream with read/write timeouts set +/// and Nagle's algorithm disabled with `TCP_NODELAY`. +/// +/// `TCP_NODELAY` ensures writing to the stream always results in immediate sending of the packet +/// to the network, which is important to reduce the latency of interactive protocols such as IMAP. pub(crate) async fn connect_tcp( addr: impl ToSocketAddrs, timeout_val: Duration, @@ -17,6 +21,9 @@ pub(crate) async fn connect_tcp( .context("connection timeout")? .context("connection failure")?; + // Disable Nagle's algorithm. + tcp_stream.set_nodelay(true)?; + let mut timeout_stream = TimeoutStream::new(tcp_stream); timeout_stream.set_write_timeout(Some(timeout_val)); timeout_stream.set_read_timeout(Some(timeout_val)); From 4489db76c903a27c28886e238a2a3feef316736f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Jan 2023 13:43:57 +0000 Subject: [PATCH 063/132] cargo: bump quote from 1.0.21 to 1.0.23 Bumps [quote](https://github.com/dtolnay/quote) from 1.0.21 to 1.0.23. - [Release notes](https://github.com/dtolnay/quote/releases) - [Commits](https://github.com/dtolnay/quote/compare/1.0.21...1.0.23) --- updated-dependencies: - dependency-name: quote dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 00ca5046f..386c22fcf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2748,9 +2748,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" dependencies = [ "proc-macro2", ] From e6324e3a198a776d15885734ad1aed54469b503b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Jan 2023 13:44:38 +0000 Subject: [PATCH 064/132] cargo: bump libc from 0.2.137 to 0.2.139 Bumps [libc](https://github.com/rust-lang/libc) from 0.2.137 to 0.2.139. - [Release notes](https://github.com/rust-lang/libc/releases) - [Commits](https://github.com/rust-lang/libc/compare/0.2.137...0.2.139) --- updated-dependencies: - dependency-name: libc dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 00ca5046f..194030be5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2030,9 +2030,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.137" +version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" [[package]] name = "libm" From f694d2e150cd66622cb2437391e1a13f09fd98c4 Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 2 Jan 2023 21:26:46 +0000 Subject: [PATCH 065/132] Format configure() logs with error causes --- src/configure.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/configure.rs b/src/configure.rs index 8610e97dc..82403a76a 100644 --- a/src/configure.rs +++ b/src/configure.rs @@ -579,7 +579,7 @@ async fn try_imap_one_param( let mut imap = match Imap::new(param, socks5_config.clone(), addr, provider_strict_tls, r) { Err(err) => { - info!(context, "failure: {}", err); + info!(context, "failure: {:#}", err); return Err(ConfigurationError { config: inf, msg: format!("{:#}", err), @@ -590,7 +590,7 @@ async fn try_imap_one_param( match imap.connect(context).await { Err(err) => { - info!(context, "failure: {}", err); + info!(context, "failure: {:#}", err); Err(ConfigurationError { config: inf, msg: format!("{:#}", err), From 1e5c90ed65655ac717f9d430bf5d71b4d56eaf5f Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 2 Jan 2023 20:01:34 +0000 Subject: [PATCH 066/132] Fix STARTTLS connection --- src/imap/client.rs | 55 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/src/imap/client.rs b/src/imap/client.rs index 7a2329c6a..74624a119 100644 --- a/src/imap/client.rs +++ b/src/imap/client.rs @@ -102,7 +102,7 @@ impl Client { let _greeting = client .read_response() .await - .context("failed to read greeting")?; + .context("failed to read greeting")??; Ok(Client { inner: client }) } @@ -115,22 +115,36 @@ impl Client { let _greeting = client .read_response() .await - .context("failed to read greeting")?; + .context("failed to read greeting")??; Ok(Client { inner: client }) } pub async fn connect_starttls(hostname: &str, port: u16, strict_tls: bool) -> Result { let tcp_stream = connect_tcp((hostname, port), IMAP_TIMEOUT).await?; - let tls = build_tls(strict_tls); - let tls_stream = tls.connect(hostname, tcp_stream).await?; - let buffered_stream = BufWriter::new(tls_stream); - let session_stream: Box = Box::new(buffered_stream); + + // Run STARTTLS command and convert the client back into a stream. + let session_stream: Box = Box::new(tcp_stream); let mut client = ImapClient::new(session_stream); let _greeting = client .read_response() .await - .context("failed to read greeting")?; + .context("failed to read greeting")??; + client + .run_command_and_check_ok("STARTTLS", None) + .await + .context("STARTTLS command failed")?; + let tcp_stream = client.into_inner(); + + let tls = build_tls(strict_tls); + let tls_stream = tls + .connect(hostname, tcp_stream) + .await + .context("STARTTLS upgrade failed")?; + + let buffered_stream = BufWriter::new(tls_stream); + let session_stream: Box = Box::new(buffered_stream); + let client = ImapClient::new(session_stream); Ok(Client { inner: client }) } @@ -150,7 +164,7 @@ impl Client { let _greeting = client .read_response() .await - .context("failed to read greeting")?; + .context("failed to read greeting")??; Ok(Client { inner: client }) } @@ -166,7 +180,7 @@ impl Client { let _greeting = client .read_response() .await - .context("failed to read greeting")?; + .context("failed to read greeting")??; Ok(Client { inner: client }) } @@ -180,15 +194,28 @@ impl Client { let socks5_stream = socks5_config .connect((hostname, port), IMAP_TIMEOUT) .await?; - let tls = build_tls(strict_tls); - let tls_stream = tls.connect(hostname, socks5_stream).await?; - let buffered_stream = BufWriter::new(tls_stream); - let session_stream: Box = Box::new(buffered_stream); + + // Run STARTTLS command and convert the client back into a stream. + let session_stream: Box = Box::new(socks5_stream); let mut client = ImapClient::new(session_stream); let _greeting = client .read_response() .await - .context("failed to read greeting")?; + .context("failed to read greeting")??; + client + .run_command_and_check_ok("STARTTLS", None) + .await + .context("STARTTLS command failed")?; + let socks5_stream = client.into_inner(); + + let tls = build_tls(strict_tls); + let tls_stream = tls + .connect(hostname, socks5_stream) + .await + .context("STARTTLS upgrade failed")?; + let buffered_stream = BufWriter::new(tls_stream); + let session_stream: Box = Box::new(buffered_stream); + let client = ImapClient::new(session_stream); Ok(Client { inner: client }) } From 5b3596987bbaa388b90569e70eced6791846c34b Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 2 Jan 2023 21:31:23 +0000 Subject: [PATCH 067/132] Test that STARTTLS connection works --- .../src/deltachat_rpc_client/pytestplugin.py | 9 +++++++-- deltachat-rpc-client/tests/test_something.py | 10 ++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py index f628fdcfa..4b8562af7 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py @@ -27,12 +27,17 @@ class ACFactory: async def get_unconfigured_bot(self) -> Bot: return Bot(await self.get_unconfigured_account()) - async def new_configured_account(self) -> Account: + async def new_preconfigured_account(self) -> Account: + """Make a new account with configuration options set, but configuration not started.""" credentials = await get_temp_credentials() account = await self.get_unconfigured_account() - assert not await account.is_configured() await account.set_config("addr", credentials["email"]) await account.set_config("mail_pw", credentials["password"]) + assert not await account.is_configured() + return account + + async def new_configured_account(self) -> Account: + account = await self.new_preconfigured_account() await account.configure() assert await account.is_configured() return account diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index 5b2e67af9..d1208042e 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -41,6 +41,16 @@ async def test_acfactory(acfactory) -> None: print("Successful configuration") +@pytest.mark.asyncio +async def test_configure_starttls(acfactory) -> None: + account = await acfactory.new_preconfigured_account() + + # Use STARTTLS + await account.set_config("mail_security", "2") + await account.configure() + assert await account.is_configured() + + @pytest.mark.asyncio async def test_account(acfactory) -> None: alice, bob = await acfactory.get_online_accounts(2) From 7aa7548a51e93faed1cb0f332166fbdbbc828c6f Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 2 Jan 2023 21:34:45 +0000 Subject: [PATCH 068/132] Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c58c4f608..476504be4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - Do not add an error if the message is encrypted but not signed #3860 - Do not strip leading spaces from message lines #3867 - Fix uncaught exception in JSON-RPC tests #3884 +- Fix STARTTLS connection and add a test for it #3907 ## 1.104.0 From e653531934a8e6d1ef45c3f39202c364ba518f95 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Jan 2023 22:36:56 +0000 Subject: [PATCH 069/132] cargo: bump base64 from 0.13.1 to 0.20.0 Bumps [base64](https://github.com/marshallpierce/rust-base64) from 0.13.1 to 0.20.0. - [Release notes](https://github.com/marshallpierce/rust-base64/releases) - [Changelog](https://github.com/marshallpierce/rust-base64/blob/master/RELEASE-NOTES.md) - [Commits](https://github.com/marshallpierce/rust-base64/compare/v0.13.1...v0.20.0) --- updated-dependencies: - dependency-name: base64 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Cargo.lock | 8 +++++++- Cargo.toml | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ff69d5210..fb9565f1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -371,6 +371,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" + [[package]] name = "base64ct" version = "1.5.1" @@ -877,7 +883,7 @@ dependencies = [ "async-smtp", "async_zip", "backtrace", - "base64 0.13.1", + "base64 0.20.0", "bitflags", "chrono", "criterion", diff --git a/Cargo.toml b/Cargo.toml index 1c9d7230e..6e34c46eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ trust-dns-resolver = "0.22" tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] } tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar backtrace = "0.3" -base64 = "0.13" +base64 = "0.20" bitflags = "1.3" chrono = { version = "0.4", default-features=false, features = ["clock", "std"] } dirs = { version = "4", optional=true } From c8f0c6b5f65312129edb9a65e29177d519aad0a2 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 3 Jan 2023 12:06:27 +0000 Subject: [PATCH 070/132] Add more IMAP logs --- src/imap.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/imap.rs b/src/imap.rs index c762cae7b..2df278f29 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -297,6 +297,7 @@ impl Imap { let oauth2 = self.config.lp.oauth2; + info!(context, "Connecting to IMAP server"); let connection_res: Result = if self.config.lp.security == Socket::Starttls || self.config.lp.security == Socket::Plain { @@ -346,6 +347,7 @@ impl Imap { let imap_pw: &str = config.lp.password.as_ref(); let login_res = if oauth2 { + info!(context, "Logging into IMAP server with OAuth 2"); let addr: &str = config.addr.as_ref(); let token = get_oauth2_access_token(context, addr, imap_pw, true) @@ -357,6 +359,7 @@ impl Imap { }; client.authenticate("XOAUTH2", auth).await } else { + info!(context, "Logging into IMAP server with LOGIN"); client.login(imap_user, imap_pw).await }; @@ -372,6 +375,7 @@ impl Imap { "IMAP-LOGIN as {}", self.config.lp.user ))); + info!(context, "Successfully logged into IMAP server"); Ok(()) } @@ -379,7 +383,7 @@ impl Imap { let imap_user = self.config.lp.user.to_owned(); let message = stock_str::cannot_login(context, &imap_user).await; - warn!(context, "{} ({})", message, err); + warn!(context, "{} ({:#})", message, err); let lock = context.wrong_pw_warning_mutex.lock().await; if self.login_failed_once @@ -387,7 +391,7 @@ impl Imap { && context.get_config_bool(Config::NotifyAboutWrongPw).await? { if let Err(e) = context.set_config(Config::NotifyAboutWrongPw, None).await { - warn!(context, "{}", e); + warn!(context, "{:#}", e); } drop(lock); @@ -397,13 +401,13 @@ impl Imap { chat::add_device_msg_with_importance(context, None, Some(&mut msg), true) .await { - warn!(context, "{}", e); + warn!(context, "{:#}", e); } } else { self.login_failed_once = true; } - Err(format_err!("{}\n\n{}", message, err)) + Err(format_err!("{}\n\n{:#}", message, err)) } } } @@ -846,7 +850,7 @@ impl Imap { if let Some(folder) = context.get_config(*config).await? { self.fetch_new_messages(context, &folder, false, true) .await - .context("could not fetch messages")?; + .context("could not fetch existing messages")?; } } } From f0a28b916821cc608509d7b1678bbf591422457e Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 3 Jan 2023 12:08:24 +0000 Subject: [PATCH 071/132] Log the error before triggering reconnect This way "Dropping an IMAP connection" message appears after the cause for connection drop. --- src/scheduler.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scheduler.rs b/src/scheduler.rs index 81d5e7be8..f575bbd4b 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -198,8 +198,8 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_config: Config) .await .context("prepare IMAP connection") { - connection.trigger_reconnect(ctx); warn!(ctx, "{:#}", err); + connection.trigger_reconnect(ctx); return connection.fake_idle(ctx, Some(watch_folder)).await; } From 468356b12040d6078dcfab078c674883ee4591a8 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 3 Jan 2023 12:08:45 +0000 Subject: [PATCH 072/132] Trigger reconnection when failing to fetch existing messages --- CHANGELOG.md | 1 + src/scheduler.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 476504be4..2cbcc49ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ - Do not strip leading spaces from message lines #3867 - Fix uncaught exception in JSON-RPC tests #3884 - Fix STARTTLS connection and add a test for it #3907 +- Trigger reconnection when failing to fetch existing messages #3911 ## 1.104.0 diff --git a/src/scheduler.rs b/src/scheduler.rs index f575bbd4b..d3d3bca2c 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -143,6 +143,7 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne if !fetched_existing_msgs { if let Err(err) = connection.fetch_existing_msgs(&ctx).await { warn!(ctx, "Failed to fetch existing messages: {:#}", err); + connection.trigger_reconnect(&ctx); } } } From ac15b3a5af5ae34eca92e39c9f555bf9516eb0c0 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 3 Jan 2023 01:19:57 +0000 Subject: [PATCH 073/132] Remove `rust-toolchain` file Any Rust toolchain above MSRV should be usable. --- rust-toolchain | 1 - 1 file changed, 1 deletion(-) delete mode 100644 rust-toolchain diff --git a/rust-toolchain b/rust-toolchain deleted file mode 100644 index 940573042..000000000 --- a/rust-toolchain +++ /dev/null @@ -1 +0,0 @@ -1.64.0 From c9ab9d59c2b8200efa18c94b9c26053616d45355 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 3 Jan 2023 13:54:23 +0000 Subject: [PATCH 074/132] Adapt the comment, there is no more `rust-toolchain` --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b57e5b06..ee4abe96c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,7 +69,7 @@ jobs: strategy: matrix: include: - # Currently used Rust version, same as in `rust-toolchain` file. + # Currently used Rust version. - os: ubuntu-latest rust: 1.64.0 python: 3.9 From 5b1278458991d1c38733d17b2319d42fd81c110f Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 3 Jan 2023 15:45:08 +0000 Subject: [PATCH 075/132] ci: update rust toolchain for repl.exe builds --- .github/workflows/repl.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/repl.yml b/.github/workflows/repl.yml index 38b84e921..c0a76fc37 100644 --- a/.github/workflows/repl.yml +++ b/.github/workflows/repl.yml @@ -16,7 +16,7 @@ jobs: - name: Install Rust uses: actions-rs/toolchain@v1 with: - toolchain: 1.50.0 + toolchain: 1.66.0 override: true - name: build From 138e62e1ef95ae1a4b079d19b07b8f68ed0848c7 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 3 Jan 2023 18:52:34 +0000 Subject: [PATCH 076/132] Improve error handling of existing messages fetch and never retry There are at least two user reports that fetching existing messages sometimes results in infinite loop of retrying it. Account is working if set up from the backup, but never starts working if set up from scratch. This change improves error reporting, but also sets FetchedExistingMsgs before actually trying to do it. This way if the operation fails, connection is reestablished, but fetching existing messages is not retried again over and over. --- CHANGELOG.md | 1 + src/imap.rs | 112 ++++++++++++++++++++++++++++------------------- src/scheduler.rs | 10 +++++ 3 files changed, 77 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cbcc49ad..323cfac6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - Fix uncaught exception in JSON-RPC tests #3884 - Fix STARTTLS connection and add a test for it #3907 - Trigger reconnection when failing to fetch existing messages #3911 +- Do not retry fetching existing messages after failure, prevents infinite reconnection loop #3913 ## 1.104.0 diff --git a/src/imap.rs b/src/imap.rs index 2df278f29..cb3843ee0 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -677,7 +677,10 @@ impl Imap { return Ok(false); } - let new_emails = self.select_with_uidvalidity(context, folder).await?; + let new_emails = self + .select_with_uidvalidity(context, folder) + .await + .with_context(|| format!("failed to select folder {}", folder))?; if !new_emails && !fetch_existing_msgs { info!(context, "No new emails in folder {}", folder); @@ -837,9 +840,15 @@ impl Imap { } self.prepare(context).await.context("could not connect")?; - add_all_recipients_as_contacts(context, self, Config::ConfiguredSentboxFolder).await; - add_all_recipients_as_contacts(context, self, Config::ConfiguredMvboxFolder).await; - add_all_recipients_as_contacts(context, self, Config::ConfiguredInboxFolder).await; + add_all_recipients_as_contacts(context, self, Config::ConfiguredSentboxFolder) + .await + .context("failed to get recipients from the sentbox")?; + add_all_recipients_as_contacts(context, self, Config::ConfiguredMvboxFolder) + .await + .context("failed to ge recipients from the movebox")?; + add_all_recipients_as_contacts(context, self, Config::ConfiguredInboxFolder) + .await + .context("failed to get recipients from the inbox")?; if context.get_config_bool(Config::FetchExistingMsgs).await? { for config in &[ @@ -848,6 +857,10 @@ impl Imap { Config::ConfiguredSentboxFolder, ] { if let Some(folder) = context.get_config(*config).await? { + info!( + context, + "Fetching existing messages from folder \"{}\"", folder + ); self.fetch_new_messages(context, &folder, false, true) .await .context("could not fetch existing messages")?; @@ -856,9 +869,6 @@ impl Imap { } info!(context, "Done fetching existing messages."); - context - .set_config_bool(Config::FetchedExistingMsgs, true) - .await?; Ok(()) } } @@ -1207,8 +1217,10 @@ impl Imap { /// Prefetch all messages greater than or equal to `uid_next`. Returns a list of fetch results /// in the order of ascending delivery time to the server (INTERNALDATE). async fn prefetch(&mut self, uid_next: u32) -> Result> { - let session = self.session.as_mut(); - let session = session.context("fetch_after(): IMAP No Connection established")?; + let session = self + .session + .as_mut() + .context("no IMAP connection established")?; // fetch messages with larger UID than the last one seen let set = format!("{}:*", uid_next); @@ -1233,7 +1245,6 @@ impl Imap { } } } - drop(list); Ok(msgs.into_iter().map(|((_, uid), msg)| (uid, msg)).collect()) } @@ -2306,49 +2317,58 @@ impl std::fmt::Display for UidRange { } } } -async fn add_all_recipients_as_contacts(context: &Context, imap: &mut Imap, folder: Config) { - let mailbox = if let Ok(Some(m)) = context.get_config(folder).await { +async fn add_all_recipients_as_contacts( + context: &Context, + imap: &mut Imap, + folder: Config, +) -> Result<()> { + let mailbox = if let Some(m) = context.get_config(folder).await? { m } else { - return; + info!( + context, + "Folder {} is not configured, skipping fetching contacts from it.", folder + ); + return Ok(()); }; - if let Err(e) = imap.select_with_uidvalidity(context, &mailbox).await { - // We are using Anyhow's .context() and to show the inner error, too, we need the {:#}: - warn!(context, "Could not select {}: {:#}", mailbox, e); - return; - } - match imap.get_all_recipients(context).await { - Ok(contacts) => { - let mut any_modified = false; - for contact in contacts { - let display_name_normalized = contact - .display_name - .as_ref() - .map(|s| normalize_name(s)) - .unwrap_or_default(); + imap.select_with_uidvalidity(context, &mailbox) + .await + .with_context(|| format!("could not select {}", mailbox))?; - match Contact::add_or_lookup( - context, - &display_name_normalized, - &contact.addr, - Origin::OutgoingTo, - ) - .await - { - Ok((_, modified)) => { - if modified != Modifier::None { - any_modified = true; - } - } - Err(e) => warn!(context, "Could not add recipient: {}", e), + let contacts = imap + .get_all_recipients(context) + .await + .context("could not get recipients")?; + + let mut any_modified = false; + for contact in contacts { + let display_name_normalized = contact + .display_name + .as_ref() + .map(|s| normalize_name(s)) + .unwrap_or_default(); + + match Contact::add_or_lookup( + context, + &display_name_normalized, + &contact.addr, + Origin::OutgoingTo, + ) + .await + { + Ok((_, modified)) => { + if modified != Modifier::None { + any_modified = true; } } - if any_modified { - context.emit_event(EventType::ContactsChanged(None)); - } + Err(e) => warn!(context, "Could not add recipient: {}", e), } - Err(e) => warn!(context, "Could not add recipients: {}", e), - }; + } + if any_modified { + context.emit_event(EventType::ContactsChanged(None)); + } + + Ok(()) } #[cfg(test)] diff --git a/src/scheduler.rs b/src/scheduler.rs index d3d3bca2c..92e569010 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -141,6 +141,16 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne match ctx.get_config_bool(Config::FetchedExistingMsgs).await { Ok(fetched_existing_msgs) => { if !fetched_existing_msgs { + // Consider it done even if we fail. + // + // This operation is not critical enough to retry, + // especially if the error is persistent. + if let Err(err) = + ctx.set_config_bool(Config::FetchedExistingMsgs, true).await + { + warn!(ctx, "Can't set Config::FetchedExistingMsgs: {:#}", err); + } + if let Err(err) = connection.fetch_existing_msgs(&ctx).await { warn!(ctx, "Failed to fetch existing messages: {:#}", err); connection.trigger_reconnect(&ctx); From 37a212ddc49ff94167f7309fb2553e9903c8269a Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 4 Jan 2023 13:35:11 +0000 Subject: [PATCH 077/132] ci: remove failing actions-rs/toolchain calls It fails because there is no `rust-toolchain` file anymore. ubuntu-latest already has cargo installed, there is no need to reintall it. --- .github/workflows/upload-docs.yml | 1 - .github/workflows/upload-ffi-docs.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/upload-docs.yml b/.github/workflows/upload-docs.yml index fa9b7f6e4..c624cb081 100644 --- a/.github/workflows/upload-docs.yml +++ b/.github/workflows/upload-docs.yml @@ -13,7 +13,6 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: actions-rs/toolchain@v1 - name: Build the documentation with cargo run: | cargo doc --package deltachat --no-deps diff --git a/.github/workflows/upload-ffi-docs.yml b/.github/workflows/upload-ffi-docs.yml index 96c192803..73fa07cb1 100644 --- a/.github/workflows/upload-ffi-docs.yml +++ b/.github/workflows/upload-ffi-docs.yml @@ -13,7 +13,6 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: actions-rs/toolchain@v1 - name: Build the documentation with cargo run: | cargo doc --package deltachat_ffi --no-deps From 1f420777af077f5635ffd2e7c3c28c7dc911b179 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Jan 2023 14:38:48 +0000 Subject: [PATCH 078/132] cargo: bump once_cell from 1.16.0 to 1.17.0 Bumps [once_cell](https://github.com/matklad/once_cell) from 1.16.0 to 1.17.0. - [Release notes](https://github.com/matklad/once_cell/releases) - [Changelog](https://github.com/matklad/once_cell/blob/master/CHANGELOG.md) - [Commits](https://github.com/matklad/once_cell/compare/v1.16.0...v1.17.0) --- updated-dependencies: - dependency-name: once_cell dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- deltachat-ffi/Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ff69d5210..14e347901 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2355,9 +2355,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" [[package]] name = "oorandom" diff --git a/Cargo.toml b/Cargo.toml index 1c9d7230e..4cfd9d809 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,7 @@ native-tls = "0.2" num_cpus = "1.15" num-derive = "0.3" num-traits = "0.2" -once_cell = "1.16.0" +once_cell = "1.17.0" percent-encoding = "2.2" pgp = { version = "0.9", default-features = false } pretty_env_logger = { version = "0.4", optional = true } diff --git a/deltachat-ffi/Cargo.toml b/deltachat-ffi/Cargo.toml index e7cc70998..b6e18901b 100644 --- a/deltachat-ffi/Cargo.toml +++ b/deltachat-ffi/Cargo.toml @@ -24,7 +24,7 @@ tokio = { version = "1", features = ["rt-multi-thread"] } anyhow = "1" thiserror = "1" rand = "0.7" -once_cell = "1.16.0" +once_cell = "1.17.0" [features] default = ["vendored"] From 8ac7f639d8d32a40626a7edb30c957736a40cb95 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Jan 2023 23:50:21 +0000 Subject: [PATCH 079/132] cargo: bump toml from 0.5.9 to 0.5.10 Bumps [toml](https://github.com/toml-rs/toml) from 0.5.9 to 0.5.10. - [Release notes](https://github.com/toml-rs/toml/releases) - [Commits](https://github.com/toml-rs/toml/commits/toml-v0.5.10) --- updated-dependencies: - dependency-name: toml dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 14e347901..373223d0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3641,9 +3641,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +checksum = "1333c76748e868a4d9d1017b5ab53171dfd095f70c712fdb4653a406547f598f" dependencies = [ "serde", ] From ac0fbaad2119f0e02dfceb60ae96deea418eea8f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Jan 2023 23:51:59 +0000 Subject: [PATCH 080/132] cargo: bump quick-xml from 0.26.0 to 0.27.1 Bumps [quick-xml](https://github.com/tafia/quick-xml) from 0.26.0 to 0.27.1. - [Release notes](https://github.com/tafia/quick-xml/releases) - [Changelog](https://github.com/tafia/quick-xml/blob/master/Changelog.md) - [Commits](https://github.com/tafia/quick-xml/compare/v0.26.0...v0.27.1) --- updated-dependencies: - dependency-name: quick-xml dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 14e347901..7b22fdca2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2739,9 +2739,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.26.0" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" +checksum = "ffc053f057dd768a56f62cd7e434c42c831d296968997e9ac1f76ea7c2d14c41" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 4cfd9d809..91d56d4af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ once_cell = "1.17.0" percent-encoding = "2.2" pgp = { version = "0.9", default-features = false } pretty_env_logger = { version = "0.4", optional = true } -quick-xml = "0.26" +quick-xml = "0.27" r2d2 = "0.8" r2d2_sqlite = "0.20" rand = "0.8" From d873f88b5646971060ca9efb49b9f76c45c7dda5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Jan 2023 00:32:24 +0000 Subject: [PATCH 081/132] cargo: bump serde from 1.0.148 to 1.0.152 Bumps [serde](https://github.com/serde-rs/serde) from 1.0.148 to 1.0.152. - [Release notes](https://github.com/serde-rs/serde/releases) - [Commits](https://github.com/serde-rs/serde/compare/v1.0.148...v1.0.152) --- updated-dependencies: - dependency-name: serde dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 373223d0b..ddbd3c710 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3184,18 +3184,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.148" +version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.148" +version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" dependencies = [ "proc-macro2", "quote", From c5d18023463460316395fdb68a910cd5b31cc1d8 Mon Sep 17 00:00:00 2001 From: bjoern Date: Thu, 5 Jan 2023 10:31:47 +0100 Subject: [PATCH 082/132] "archive" consistency and improvements (#3918) * move 'archived link' betweeen pinned and normal cahts or above normal chats * add icon for 'archived chats' link * let get_fresh_msg_cnt() work for DC_CHAT_ID_ARCHIVED_LINK * move 'archived link' topmost * use less noticeable archived-icon * slightly smaller archived icon * update CHANGELOG --- CHANGELOG.md | 2 ++ assets/icon-archive.png | Bin 0 -> 1652 bytes assets/icon-archive.svg | 60 ++++++++++++++++++++++++++++++++++++++ deltachat-ffi/deltachat.h | 6 +++- src/chat.rs | 51 +++++++++++++++++++++++++++----- src/chatlist.rs | 20 +++++-------- 6 files changed, 118 insertions(+), 21 deletions(-) create mode 100644 assets/icon-archive.png create mode 100644 assets/icon-archive.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index 323cfac6a..1ea187ef2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ - Add fuzzing tests #3853 - Add mappings for some file types to Viewtype / MIME type #3881 - Buffer IMAP client writes #3888 +- move `DC_CHAT_ID_ARCHIVED_LINK` to the top of chat lists + and make `dc_get_fresh_msg_cnt()` work for `DC_CHAT_ID_ARCHIVED_LINK` #3918 ### API-Changes - jsonrpc: add python API for webxdc updates #3872 diff --git a/assets/icon-archive.png b/assets/icon-archive.png new file mode 100644 index 0000000000000000000000000000000000000000..95d35e2a5107e14ee900d0b7947b2eb9a38e6a85 GIT binary patch literal 1652 zcmd^ATTI$_6#ikYc*lf9ywywG>Pu%?Y;CKExLGC(>n@h9#T$y^EKHo(YAY(MHFIez zOu~pNxLod^s zvwSgIq9bF_%>Jbz68a|by7K3QiqpfSq$_ueiSe`Fyc-t-pNl$ruwv#er;|rOAaAem zT9olDSg+*dm&UM?Ml`J>!(Te_s6#<1usqHV@2+5L%kxZf9$6+ug0VypMh5W$K)@n+ zG#{Y!1(ySWFBJYf+o5HpI@jjJA9zx2@y=hvX-M(h#wqFaWDo2R8c!X|>Lr0C;?-A3(cn9ul7IZ09IOhH6=T}%l zV!zfbI8w?V?v}wPx?r=}+-`6hrR#z~M1-chBe`Iw6i(=34doBdmlFp5)2ZAM9}?tt zZO7!A8iJ_IOdTtAoyvUMX2@hoTpZY8Bbi(+ngyBMu4c&Q^#-TXwuKoNe)OgZo<62611wKQ842J5wsXpzW($r{M;VFWLe<)XcDs1P zJM~Hqo2_j{z68?K>TR$J8m3T0{EQ%v^&&;T&=ZnwR%gY{lt{OWDX#4izTMEYNUgUB z;u?k^JYK4_E%Koa`5hrFUp^;sZF+E4lY{G4O@-hy9B!&9X*Nouk5zI93VZonMZax=i}#3_4}X#E|2Lf<^LgvH5~-kH)A*>qNdgHY zpY++OOtHb4XK^fbbI7fLtd6^6Vh1-&8=PW_)dKCzO^-8h0|0$5KeZCs|Wh z2;+}mO#oNkx-+&Dz3UMvE?WLt)p2lbnrGHWboC7)h?S2Zkx054 zP|AY9?|YdrYTnW9BMX@6SVV;3Q#uM;++!T|rPU3JH{80{$jHc|?7g?WCnmj)$i0asOjd;z5uQ8K@SaufNoUd8ti3fdUJcqYPtKO^YH3!Z;Q at}{@Dw+Qr~*o5AX0Z1oLW%s;ByZkq}L1Whd literal 0 HcmV?d00001 diff --git a/assets/icon-archive.svg b/assets/icon-archive.svg new file mode 100644 index 000000000..a8ba45d49 --- /dev/null +++ b/assets/icon-archive.svg @@ -0,0 +1,60 @@ + + + + + + + + + + diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 4a03f1a8f..47452f6ac 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -1227,7 +1227,11 @@ int dc_get_msg_cnt (dc_context_t* context, uint32_t ch * Get the number of _fresh_ messages in a chat. * Typically used to implement a badge with a number in the chatlist. * - * If the specified chat is muted, + * As muted archived chats are not unarchived automatically, + * a similar information is needed for the @ref dc_get_chatlist() "archive link" as well: + * here, the number of archived chats containing fresh messages is returned. + * + * If the specified chat is muted or the @ref dc_get_chatlist() "archive link", * the UI should show the badge counter "less obtrusive", * e.g. using "gray" instead of "red" color. * diff --git a/src/chat.rs b/src/chat.rs index 8be73e1ef..4de92e070 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -782,17 +782,35 @@ impl ChatId { // the times are average, no matter if there are fresh messages or not - // and have to be multiplied by the number of items shown at once on the chatlist, // so savings up to 2 seconds are possible on older devices - newer ones will feel "snappier" :) - let count = context - .sql - .count( - "SELECT COUNT(*) + let count = if self.is_archived_link() { + context + .sql + .count( + "SELECT COUNT(DISTINCT(m.chat_id)) + FROM msgs m + LEFT JOIN chats c ON m.chat_id=c.id + WHERE m.state=10 + and m.hidden=0 + AND m.chat_id>9 + AND c.blocked=0 + AND c.archived=1 + ", + paramsv![], + ) + .await? + } else { + context + .sql + .count( + "SELECT COUNT(*) FROM msgs WHERE state=? AND hidden=0 AND chat_id=?;", - paramsv![MessageState::InFresh, self], - ) - .await?; + paramsv![MessageState::InFresh, self], + ) + .await? + }; Ok(count) } @@ -1216,6 +1234,10 @@ impl Chat { if !image_rel.is_empty() { return Ok(Some(get_abs_path(context, image_rel))); } + } else if self.id.is_archived_link() { + if let Ok(image_rel) = get_archive_icon(context).await { + return Ok(Some(get_abs_path(context, image_rel))); + } } else if self.typ == Chattype::Single { let contacts = get_chat_contacts(context, self.id).await?; if let Some(contact_id) = contacts.first() { @@ -1708,6 +1730,21 @@ pub(crate) async fn get_broadcast_icon(context: &Context) -> Result { Ok(icon) } +pub(crate) async fn get_archive_icon(context: &Context) -> Result { + if let Some(icon) = context.sql.get_raw_config("icon-archive").await? { + return Ok(icon); + } + + let icon = include_bytes!("../assets/icon-archive.png"); + let blob = BlobObject::create(context, "icon-archive.png", icon).await?; + let icon = blob.as_name().to_string(); + context + .sql + .set_raw_config("icon-archive", Some(&icon)) + .await?; + Ok(icon) +} + async fn update_special_chat_name( context: &Context, contact_id: ContactId, diff --git a/src/chatlist.rs b/src/chatlist.rs index da7d309a4..df80b15e2 100644 --- a/src/chatlist.rs +++ b/src/chatlist.rs @@ -92,8 +92,6 @@ impl Chatlist { let flag_no_specials = 0 != listflags & DC_GCL_NO_SPECIALS; let flag_add_alldone_hint = 0 != listflags & DC_GCL_ADD_ALLDONE_HINT; - let mut add_archived_link_item = false; - let process_row = |row: &rusqlite::Row| { let chat_id: ChatId = row.get(0)?; let msg_id: Option = row.get(1)?; @@ -123,7 +121,7 @@ impl Chatlist { // // The query shows messages from blocked contacts in // groups. Otherwise it would be hard to follow conversations. - let mut ids = if let Some(query_contact_id) = query_contact_id { + let ids = if let Some(query_contact_id) = query_contact_id { // show chats shared with a given contact context.sql.query_map( "SELECT c.id, m.id @@ -216,7 +214,7 @@ impl Chatlist { } else { ChatId::new(0) }; - let ids = context.sql.query_map( + let mut ids = context.sql.query_map( "SELECT c.id, m.id FROM chats c LEFT JOIN msgs m @@ -236,19 +234,15 @@ impl Chatlist { process_row, process_rows, ).await?; - if !flag_no_specials { - add_archived_link_item = true; + if !flag_no_specials && get_archived_cnt(context).await? > 0 { + if ids.is_empty() && flag_add_alldone_hint { + ids.push((DC_CHAT_ID_ALLDONE_HINT, None)); + } + ids.insert(0, (DC_CHAT_ID_ARCHIVED_LINK, None)); } ids }; - if add_archived_link_item && get_archived_cnt(context).await? > 0 { - if ids.is_empty() && flag_add_alldone_hint { - ids.push((DC_CHAT_ID_ALLDONE_HINT, None)); - } - ids.push((DC_CHAT_ID_ARCHIVED_LINK, None)); - } - Ok(Chatlist { ids }) } From 2b4e32d2cf7460a6f16eb29365464c5566bef5dd Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 4 Jan 2023 14:45:05 +0000 Subject: [PATCH 083/132] Remove unused `KeyType` from `DcKey` trait It always equals Self. --- src/key.rs | 24 ++++++++---------------- src/keyring.rs | 2 +- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/key.rs b/src/key.rs index e2fbe7c82..82ea25667 100644 --- a/src/key.rs +++ b/src/key.rs @@ -30,17 +30,13 @@ pub use pgp::composed::{SignedPublicKey, SignedSecretKey}; /// [SignedSecretKey] types and makes working with them a little /// easier in the deltachat world. pub trait DcKey: Serialize + Deserializable + KeyTrait + Clone { - type KeyType: Serialize + Deserializable + KeyTrait + Clone; - /// Create a key from some bytes. - fn from_slice(bytes: &[u8]) -> Result { - Ok(::from_bytes(Cursor::new( - bytes, - ))?) + fn from_slice(bytes: &[u8]) -> Result { + Ok(::from_bytes(Cursor::new(bytes))?) } /// Create a key from a base64 string. - fn from_base64(data: &str) -> Result { + fn from_base64(data: &str) -> Result { // strip newlines and other whitespace let cleaned: String = data.split_whitespace().collect(); let bytes = base64::decode(cleaned.as_bytes())?; @@ -51,15 +47,15 @@ pub trait DcKey: Serialize + Deserializable + KeyTrait + Clone { /// /// Returns the key and a map of any headers which might have been set in /// the ASCII-armored representation. - fn from_asc(data: &str) -> Result<(Self::KeyType, BTreeMap)> { + fn from_asc(data: &str) -> Result<(Self, BTreeMap)> { let bytes = data.as_bytes(); - Self::KeyType::from_armor_single(Cursor::new(bytes)).context("rPGP error") + Self::from_armor_single(Cursor::new(bytes)).context("rPGP error") } /// Load the users' default key from the database. fn load_self<'a>( context: &'a Context, - ) -> Pin> + 'a + Send>>; + ) -> Pin> + 'a + Send>>; /// Serialise the key as bytes. fn to_bytes(&self) -> Vec { @@ -92,11 +88,9 @@ pub trait DcKey: Serialize + Deserializable + KeyTrait + Clone { } impl DcKey for SignedPublicKey { - type KeyType = SignedPublicKey; - fn load_self<'a>( context: &'a Context, - ) -> Pin> + 'a + Send>> { + ) -> Pin> + 'a + Send>> { Box::pin(async move { let addr = context.get_primary_self_addr().await?; match context @@ -143,11 +137,9 @@ impl DcKey for SignedPublicKey { } impl DcKey for SignedSecretKey { - type KeyType = SignedSecretKey; - fn load_self<'a>( context: &'a Context, - ) -> Pin> + 'a + Send>> { + ) -> Pin> + 'a + Send>> { Box::pin(async move { match context .sql diff --git a/src/keyring.rs b/src/keyring.rs index 192b8de80..fa5e9b5f4 100644 --- a/src/keyring.rs +++ b/src/keyring.rs @@ -19,7 +19,7 @@ where impl Keyring where - T: DcKey, + T: DcKey, { /// New empty keyring. pub fn new() -> Keyring { From 754c7324f51c0d33e1263c16d74d59ed161389cf Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 4 Jan 2023 14:37:12 +0000 Subject: [PATCH 084/132] Print more anyhow errors with their causes --- src/chat.rs | 12 ++++++------ src/context.rs | 4 ++-- src/download.rs | 2 +- src/html.rs | 2 +- src/job.rs | 10 +++++----- src/location.rs | 4 ++-- src/message.rs | 4 ++-- src/mimeparser.rs | 18 +++++++++--------- src/quota.rs | 4 ++-- src/receive_imf.rs | 27 +++++++++++++++------------ src/sql.rs | 8 ++++---- 11 files changed, 49 insertions(+), 46 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index 4de92e070..8a5ee0bb2 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1129,7 +1129,7 @@ impl Chat { } } Err(err) => { - error!(context, "faild to load contacts for {}: {:?}", chat.id, err); + error!(context, "faild to load contacts for {}: {:#}", chat.id, err); } } chat.name = chat_name; @@ -2146,7 +2146,7 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result attach_selfavatar, Err(err) => { - warn!(context, "job: cannot get selfavatar-state: {}", err); + warn!(context, "job: cannot get selfavatar-state: {:#}", err); false } }; @@ -2208,27 +2208,27 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result error!(self, "Failed to start IO: {}", err), + Err(err) => error!(self, "Failed to start IO: {:#}", err), Ok(scheduler) => *lock = Some(scheduler), } } @@ -499,7 +499,7 @@ impl Context { match &*s { RunningState::Running { cancel_sender } => { if let Err(err) = cancel_sender.send(()).await { - warn!(self, "could not cancel ongoing: {:?}", err); + warn!(self, "could not cancel ongoing: {:#}", err); } info!(self, "Signaling the ongoing process to stop ASAP.",); *s = RunningState::ShallStop; diff --git a/src/download.rs b/src/download.rs index de127c7ae..015eb56f1 100644 --- a/src/download.rs +++ b/src/download.rs @@ -132,7 +132,7 @@ impl Job { /// Called in response to `Action::DownloadMsg`. pub(crate) async fn download_msg(&self, context: &Context, imap: &mut Imap) -> Status { if let Err(err) = imap.prepare(context).await { - warn!(context, "download: could not connect: {:?}", err); + warn!(context, "download: could not connect: {:#}", err); return Status::RetryNow; } diff --git a/src/html.rs b/src/html.rs index ac59793fc..279995292 100644 --- a/src/html.rs +++ b/src/html.rs @@ -250,7 +250,7 @@ impl MsgId { if !rawmime.is_empty() { match HtmlMsgParser::from_bytes(context, &rawmime).await { Err(err) => { - warn!(context, "get_html: parser error: {}", err); + warn!(context, "get_html: parser error: {:#}", err); Ok(None) } Ok(parser) => Ok(Some(parser.html)), diff --git a/src/job.rs b/src/job.rs index e3b513fed..689a7cade 100644 --- a/src/job.rs +++ b/src/job.rs @@ -157,7 +157,7 @@ impl Job { /// Synchronizes UIDs for all folders. async fn resync_folders(&mut self, context: &Context, imap: &mut Imap) -> Status { if let Err(err) = imap.prepare(context).await { - warn!(context, "could not connect: {:?}", err); + warn!(context, "could not connect: {:#}", err); return Status::RetryLater; } @@ -246,7 +246,7 @@ pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_ time_offset ); job.save(context).await.unwrap_or_else(|err| { - error!(context, "failed to save job: {}", err); + error!(context, "failed to save job: {:#}", err); }); } else { info!( @@ -254,7 +254,7 @@ pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_ "remove job {} as it exhausted {} retries", job, JOB_RETRIES ); job.delete(context).await.unwrap_or_else(|err| { - error!(context, "failed to delete job: {}", err); + error!(context, "failed to delete job: {:#}", err); }); } } @@ -269,7 +269,7 @@ pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_ } job.delete(context).await.unwrap_or_else(|err| { - error!(context, "failed to delete job: {}", err); + error!(context, "failed to delete job: {:#}", err); }); } } @@ -403,7 +403,7 @@ LIMIT 1; Ok(job) => return Ok(job), Err(err) => { // Remove invalid job from the DB - info!(context, "cleaning up job, because of {}", err); + info!(context, "cleaning up job, because of {:#}", err); // TODO: improve by only doing a single query let id = context diff --git a/src/location.rs b/src/location.rs index bff558912..dc6bf3af6 100644 --- a/src/location.rs +++ b/src/location.rs @@ -337,7 +337,7 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64 ContactId::SELF, ] ).await { - warn!(context, "failed to store location {:?}", err); + warn!(context, "failed to store location {:#}", err); } else { info!(context, "stored location for chat {}", chat_id); continue_streaming = true; @@ -638,7 +638,7 @@ pub(crate) async fn location_loop(context: &Context, interrupt_receiver: Receive loop { let next_event = match maybe_send_locations(context).await { Err(err) => { - warn!(context, "maybe_send_locations failed: {}", err); + warn!(context, "maybe_send_locations failed: {:#}", err); Some(60) // Retry one minute later. } Ok(next_event) => next_event, diff --git a/src/message.rs b/src/message.rs index d05b8b366..e8cc3a936 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1687,7 +1687,7 @@ pub async fn get_unblocked_msg_cnt(context: &Context) -> usize { { Ok(res) => res, Err(err) => { - error!(context, "get_unblocked_msg_cnt() failed. {}", err); + error!(context, "get_unblocked_msg_cnt() failed. {:#}", err); 0 } } @@ -1707,7 +1707,7 @@ pub async fn get_request_msg_cnt(context: &Context) -> usize { { Ok(res) => res, Err(err) => { - error!(context, "get_request_msg_cnt() failed. {}", err); + error!(context, "get_request_msg_cnt() failed. {:#}", err); 0 } } diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 843053930..e7a2ea40e 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -251,7 +251,7 @@ impl MimeMessage { } Ok(None) => (Ok(mail), HashSet::new(), false), Err(err) => { - warn!(context, "decryption failed: {}", err); + warn!(context, "decryption failed: {:#}", err); (Err(err), HashSet::new(), false) } }; @@ -380,7 +380,7 @@ impl MimeMessage { typ: Viewtype::Text, msg_raw: Some(txt.clone()), msg: txt, - error: Some(format!("Decrypting failed: {}", err)), + error: Some(format!("Decrypting failed: {:#}", err)), ..Default::default() }; parser.parts.push(part); @@ -680,7 +680,7 @@ impl MimeMessage { Err(err) => { warn!( context, - "Could not save decoded avatar to blob file: {}", err + "Could not save decoded avatar to blob file: {:#}", err ); None } @@ -987,7 +987,7 @@ impl MimeMessage { let decoded_data = match mail.get_body() { Ok(decoded_data) => decoded_data, Err(err) => { - warn!(context, "Invalid body parsed {:?}", err); + warn!(context, "Invalid body parsed {:#}", err); // Note that it's not always an error - might be no data return Ok(false); } @@ -1007,7 +1007,7 @@ impl MimeMessage { let decoded_data = match mail.get_body() { Ok(decoded_data) => decoded_data, Err(err) => { - warn!(context, "Invalid body parsed {:?}", err); + warn!(context, "Invalid body parsed {:#}", err); // Note that it's not always an error - might be no data return Ok(false); } @@ -1139,7 +1139,7 @@ impl MimeMessage { if filename.starts_with("location") || filename.starts_with("message") { let parsed = location::Kml::parse(decoded_data) .map_err(|err| { - warn!(context, "failed to parse kml part: {}", err); + warn!(context, "failed to parse kml part: {:#}", err); }) .ok(); if filename.starts_with("location") { @@ -1157,7 +1157,7 @@ impl MimeMessage { self.sync_items = context .parse_sync_items(serialized) .map_err(|err| { - warn!(context, "failed to parse sync data: {}", err); + warn!(context, "failed to parse sync data: {:#}", err); }) .ok(); return Ok(()); @@ -1179,7 +1179,7 @@ impl MimeMessage { Err(err) => { error!( context, - "Could not add blob for mime part {}, error {}", filename, err + "Could not add blob for mime part {}, error {:#}", filename, err ); return Ok(()); } @@ -1224,7 +1224,7 @@ impl MimeMessage { Err(err) => { warn!( context, - "PGP key attachment is not an ASCII-armored file: {}", err, + "PGP key attachment is not an ASCII-armored file: {:#}", err ); return Ok(false); } diff --git a/src/quota.rs b/src/quota.rs index eba52899f..9b233a349 100644 --- a/src/quota.rs +++ b/src/quota.rs @@ -134,7 +134,7 @@ impl Context { /// Called in response to `Action::UpdateRecentQuota`. pub(crate) async fn update_recent_quota(&self, imap: &mut Imap) -> Result { if let Err(err) = imap.prepare(self).await { - warn!(self, "could not connect: {:?}", err); + warn!(self, "could not connect: {:#}", err); return Ok(Status::RetryNow); } @@ -162,7 +162,7 @@ impl Context { self.set_config(Config::QuotaExceeding, None).await?; } } - Err(err) => warn!(self, "cannot get highest quota usage: {:?}", err), + Err(err) => warn!(self, "cannot get highest quota usage: {:#}", err), } } diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 41bb20ff9..e8ad015af 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -102,7 +102,7 @@ pub(crate) async fn receive_imf_inner( let mut mime_parser = match MimeMessage::from_bytes_with_partial(context, imf_raw, is_partial_download).await { Err(err) => { - warn!(context, "receive_imf: can't parse MIME: {}", err); + warn!(context, "receive_imf: can't parse MIME: {:#}", err); let msg_ids; if !rfc724_mid.starts_with(GENERATED_PREFIX) { let row_id = context @@ -253,7 +253,7 @@ pub(crate) async fn receive_imf_inner( if from_id == ContactId::SELF { if mime_parser.was_encrypted() { if let Err(err) = context.execute_sync_items(sync_items).await { - warn!(context, "receive_imf cannot execute sync items: {}", err); + warn!(context, "receive_imf cannot execute sync items: {:#}", err); } } else { warn!(context, "sync items are not encrypted."); @@ -268,7 +268,7 @@ pub(crate) async fn receive_imf_inner( .receive_status_update(from_id, insert_msg_id, status_update) .await { - warn!(context, "receive_imf cannot update status: {}", err); + warn!(context, "receive_imf cannot update status: {:#}", err); } } @@ -290,7 +290,10 @@ pub(crate) async fn receive_imf_inner( context.emit_event(EventType::ChatModified(chat_id)); } Err(err) => { - warn!(context, "receive_imf cannot update profile image: {}", err); + warn!( + context, + "receive_imf cannot update profile image: {:#}", err + ); } }; } @@ -317,7 +320,7 @@ pub(crate) async fn receive_imf_inner( ) .await { - warn!(context, "cannot update contact status: {}", err); + warn!(context, "cannot update contact status: {:#}", err); } } @@ -495,7 +498,7 @@ async fn add_parts( securejoin_seen = false; } Err(err) => { - warn!(context, "Error in Secure-Join message handling: {}", err); + warn!(context, "Error in Secure-Join message handling: {:#}", err); chat_id = Some(DC_CHAT_ID_TRASH); securejoin_seen = true; } @@ -730,7 +733,7 @@ async fn add_parts( chat_id = None; } Err(err) => { - warn!(context, "Error in Secure-Join watching: {}", err); + warn!(context, "Error in Secure-Join watching: {:#}", err); chat_id = Some(DC_CHAT_ID_TRASH); } } @@ -870,7 +873,7 @@ async fn add_parts( Err(err) => { warn!( context, - "can't parse ephemeral timer \"{}\": {}", value, err + "can't parse ephemeral timer \"{}\": {:#}", value, err ); EphemeralTimer::Disabled } @@ -926,7 +929,7 @@ async fn add_parts( { warn!( context, - "failed to modify timer for chat {}: {}", chat_id, err + "failed to modify timer for chat {}: {:#}", chat_id, err ); } else { info!( @@ -975,7 +978,7 @@ async fn add_parts( if chat.is_protected() || new_status.is_some() { if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await { - warn!(context, "verification problem: {}", err); + warn!(context, "verification problem: {:#}", err); let s = format!("{}. See 'Info' for more details", err); mime_parser.repl_msg_by_error(&s); } else { @@ -1487,7 +1490,7 @@ async fn create_or_lookup_group( let create_protected = if mime_parser.get_header(HeaderDef::ChatVerified).is_some() { if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await { - warn!(context, "verification problem: {}", err); + warn!(context, "verification problem: {:#}", err); let s = format!("{}. See 'Info' for more details", err); mime_parser.repl_msg_by_error(&s); } @@ -1685,7 +1688,7 @@ async fn apply_group_changes( if mime_parser.get_header(HeaderDef::ChatVerified).is_some() { if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await { - warn!(context, "verification problem: {}", err); + warn!(context, "verification problem: {:#}", err); let s = format!("{}. See 'Info' for more details", err); mime_parser.repl_msg_by_error(&s); } diff --git a/src/sql.rs b/src/sql.rs index f24008390..1203a276e 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -626,26 +626,26 @@ pub async fn housekeeping(context: &Context) -> Result<()> { if let Err(err) = remove_unused_files(context).await { warn!( context, - "Housekeeping: cannot remove unusued files: {}", err + "Housekeeping: cannot remove unusued files: {:#}", err ); } if let Err(err) = start_ephemeral_timers(context).await { warn!( context, - "Housekeeping: cannot start ephemeral timers: {}", err + "Housekeeping: cannot start ephemeral timers: {:#}", err ); } if let Err(err) = prune_tombstones(&context.sql).await { warn!( context, - "Housekeeping: Cannot prune message tombstones: {}", err + "Housekeeping: Cannot prune message tombstones: {:#}", err ); } if let Err(err) = deduplicate_peerstates(&context.sql).await { - warn!(context, "Failed to deduplicate peerstates: {}", err) + warn!(context, "Failed to deduplicate peerstates: {:#}", err) } context.schedule_quota_update().await?; From f69acaa13db838b30dc3dd76f843bb449eb19779 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 4 Jan 2023 15:55:54 +0000 Subject: [PATCH 085/132] Add more logging and improve errors around folder selection --- src/imap.rs | 13 ++++++++++--- src/imap/select_folder.rs | 5 +++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/imap.rs b/src/imap.rs index cb3843ee0..84871cb89 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -554,7 +554,10 @@ impl Imap { folder: &str, ) -> Result { let session = self.session.as_mut().context("no session")?; - let newly_selected = session.select_or_create_folder(context, folder).await?; + let newly_selected = session + .select_or_create_folder(context, folder) + .await + .with_context(|| format!("failed to select or create folder {}", folder))?; let mailbox = session .selected_mailbox .as_mut() @@ -564,8 +567,12 @@ impl Imap { .uid_validity .with_context(|| format!("No UIDVALIDITY for folder {}", folder))?; - let old_uid_validity = get_uidvalidity(context, folder).await?; - let old_uid_next = get_uid_next(context, folder).await?; + let old_uid_validity = get_uidvalidity(context, folder) + .await + .with_context(|| format!("failed to get old UID validity for folder {}", folder))?; + let old_uid_next = get_uid_next(context, folder) + .await + .with_context(|| format!("failed to get old UID NEXT for folder {}", folder))?; if new_uid_validity == old_uid_validity { let new_emails = if newly_selected == NewlySelected::No { diff --git a/src/imap/select_folder.rs b/src/imap/select_folder.rs index 3f04f41a2..facf04ffc 100644 --- a/src/imap/select_folder.rs +++ b/src/imap/select_folder.rs @@ -109,13 +109,14 @@ impl ImapSession { Ok(newly_selected) => Ok(newly_selected), Err(err) => match err { Error::NoFolder(..) => { + info!(context, "Failed to select folder {} because it does not exist, trying to create it.", folder); self.create(folder).await.with_context(|| { format!("Couldn't select folder ('{}'), then create() failed", err) })?; - Ok(self.select_folder(context, Some(folder)).await?) + Ok(self.select_folder(context, Some(folder)).await.with_context(|| format!("failed to select newely created folder {}", folder))?) } - _ => Err(err.into()), + _ => Err(err).with_context(|| format!("failed to select folder {} with error other than NO, not trying to create it", folder)), }, } } From ea81d08c0159f648bc91c83ed3bc5230cc02da30 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 5 Jan 2023 23:15:09 +0000 Subject: [PATCH 086/132] ci: use default Rust toolchain for JSON-RPC tests Default preinstalled toolchain is currently at 1.65.0, there is no need to overwrite it with 1.66.0 for these tests. --- .github/workflows/jsonrpc.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/jsonrpc.yml b/.github/workflows/jsonrpc.yml index e84985229..de05f50d6 100644 --- a/.github/workflows/jsonrpc.yml +++ b/.github/workflows/jsonrpc.yml @@ -19,11 +19,6 @@ jobs: uses: actions/setup-node@v3 with: node-version: 16.x - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - name: Add Rust cache uses: Swatinem/rust-cache@v2 - name: npm install From d8f5e818805874d665ebee421da9bb78671fb73f Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 5 Jan 2023 23:32:43 +0000 Subject: [PATCH 087/132] jsonrpc: increase account request timeout to 60 seconds --- deltachat-jsonrpc/typescript/test/online.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deltachat-jsonrpc/typescript/test/online.ts b/deltachat-jsonrpc/typescript/test/online.ts index 4bf7181c9..06c4c8954 100644 --- a/deltachat-jsonrpc/typescript/test/online.ts +++ b/deltachat-jsonrpc/typescript/test/online.ts @@ -12,7 +12,7 @@ describe("online tests", function () { let accountId1: number, accountId2: number; before(async function () { - this.timeout(12000); + this.timeout(60000); if (!process.env.DCC_NEW_TMP_EMAIL) { if (process.env.COVERAGE && !process.env.COVERAGE_OFFLINE) { console.error( From 10349b7be4f6a4d123f5787b25e8184737201eaa Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 6 Jan 2023 00:39:40 +0000 Subject: [PATCH 088/132] Update provider database --- CHANGELOG.md | 1 + src/provider/data.rs | 82 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ea187ef2..ae2eb0ff3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Buffer IMAP client writes #3888 - move `DC_CHAT_ID_ARCHIVED_LINK` to the top of chat lists and make `dc_get_fresh_msg_cnt()` work for `DC_CHAT_ID_ARCHIVED_LINK` #3918 +- Update provider database ### API-Changes - jsonrpc: add python API for webxdc updates #3872 diff --git a/src/provider/data.rs b/src/provider/data.rs index 3b8b4b642..24dbdf6ef 100644 --- a/src/provider/data.rs +++ b/src/provider/data.rs @@ -526,7 +526,7 @@ static P_GMX_NET: Lazy = Lazy::new(|| Provider { oauth2_authorizer: None, }); -// hermes.radio.md: ac.hermes.radio, ac1.hermes.radio, ac2.hermes.radio, ac3.hermes.radio, ac4.hermes.radio, ac5.hermes.radio, ac6.hermes.radio, ac7.hermes.radio, ac8.hermes.radio, ac9.hermes.radio, ac10.hermes.radio, ac11.hermes.radio, ac12.hermes.radio, ac13.hermes.radio, ac14.hermes.radio, ac15.hermes.radio, ka.hermes.radio, ka1.hermes.radio, ka2.hermes.radio, ka3.hermes.radio, ka4.hermes.radio, ka5.hermes.radio, ka6.hermes.radio, ka7.hermes.radio, ka8.hermes.radio, ka9.hermes.radio, ka10.hermes.radio, ka11.hermes.radio, ka12.hermes.radio, ka13.hermes.radio, ka14.hermes.radio, ka15.hermes.radio, hermes.radio +// hermes.radio.md: ac.hermes.radio, ac1.hermes.radio, ac2.hermes.radio, ac3.hermes.radio, ac4.hermes.radio, ac5.hermes.radio, ac6.hermes.radio, ac7.hermes.radio, ac8.hermes.radio, ac9.hermes.radio, ac10.hermes.radio, ac11.hermes.radio, ac12.hermes.radio, ac13.hermes.radio, ac14.hermes.radio, ac15.hermes.radio, ka.hermes.radio, ka1.hermes.radio, ka2.hermes.radio, ka3.hermes.radio, ka4.hermes.radio, ka5.hermes.radio, ka6.hermes.radio, ka7.hermes.radio, ka8.hermes.radio, ka9.hermes.radio, ka10.hermes.radio, ka11.hermes.radio, ka12.hermes.radio, ka13.hermes.radio, ka14.hermes.radio, ka15.hermes.radio, ec.hermes.radio, ec1.hermes.radio, ec2.hermes.radio, ec3.hermes.radio, ec4.hermes.radio, ec5.hermes.radio, ec6.hermes.radio, ec7.hermes.radio, ec8.hermes.radio, ec9.hermes.radio, ec10.hermes.radio, ec11.hermes.radio, ec12.hermes.radio, ec13.hermes.radio, ec14.hermes.radio, ec15.hermes.radio, hermes.radio static P_HERMES_RADIO: Lazy = Lazy::new(|| Provider { id: "hermes.radio", status: Status::Ok, @@ -902,6 +902,35 @@ static P_NAVER: Lazy = Lazy::new(|| Provider { oauth2_authorizer: None, }); +// nubo.coop.md: nubo.coop +static P_NUBO_COOP: Lazy = Lazy::new(|| Provider { + id: "nubo.coop", + status: Status::Ok, + before_login_hint: "", + after_login_hint: "", + overview_page: "https://providers.delta.chat/nubo-coop", + server: vec![ + Server { + protocol: Imap, + socket: Ssl, + hostname: "mail.nubo.coop", + port: 993, + username_pattern: Email, + }, + Server { + protocol: Smtp, + socket: Ssl, + hostname: "mail.nubo.coop", + port: 465, + username_pattern: Email, + }, + ], + config_defaults: None, + strict_tls: true, + max_smtp_rcpt_to: None, + oauth2_authorizer: None, +}); + // outlook.com.md: hotmail.com, outlook.com, office365.com, outlook.com.tr, live.com, outlook.de static P_OUTLOOK_COM: Lazy = Lazy::new(|| Provider { id: "outlook.com", @@ -931,6 +960,35 @@ static P_OUTLOOK_COM: Lazy = Lazy::new(|| Provider { oauth2_authorizer: None, }); +// ouvaton.coop.md: ouvaton.org +static P_OUVATON_COOP: Lazy = Lazy::new(|| Provider { + id: "ouvaton.coop", + status: Status::Ok, + before_login_hint: "", + after_login_hint: "", + overview_page: "https://providers.delta.chat/ouvaton-coop", + server: vec![ + Server { + protocol: Imap, + socket: Ssl, + hostname: "imap.ouvaton.coop", + port: 993, + username_pattern: Email, + }, + Server { + protocol: Smtp, + socket: Ssl, + hostname: "smtp.ouvaton.coop", + port: 465, + username_pattern: Email, + }, + ], + config_defaults: None, + strict_tls: true, + max_smtp_rcpt_to: None, + oauth2_authorizer: None, +}); + // posteo.md: posteo.de, posteo.af, posteo.at, posteo.be, posteo.ca, posteo.ch, posteo.cl, posteo.co, posteo.co.uk, posteo.com.br, posteo.cr, posteo.cz, posteo.dk, posteo.ee, posteo.es, posteo.eu, posteo.fi, posteo.gl, posteo.gr, posteo.hn, posteo.hr, posteo.hu, posteo.ie, posteo.in, posteo.is, posteo.it, posteo.jp, posteo.la, posteo.li, posteo.lt, posteo.lu, posteo.me, posteo.mx, posteo.my, posteo.net, posteo.nl, posteo.no, posteo.nz, posteo.org, posteo.pe, posteo.pl, posteo.pm, posteo.pt, posteo.ro, posteo.ru, posteo.se, posteo.sg, posteo.si, posteo.tn, posteo.uk, posteo.us static P_POSTEO: Lazy = Lazy::new(|| Provider { id: "posteo", @@ -1659,6 +1717,22 @@ pub(crate) static PROVIDER_DATA: Lazy> ("ka13.hermes.radio", &*P_HERMES_RADIO), ("ka14.hermes.radio", &*P_HERMES_RADIO), ("ka15.hermes.radio", &*P_HERMES_RADIO), + ("ec.hermes.radio", &*P_HERMES_RADIO), + ("ec1.hermes.radio", &*P_HERMES_RADIO), + ("ec2.hermes.radio", &*P_HERMES_RADIO), + ("ec3.hermes.radio", &*P_HERMES_RADIO), + ("ec4.hermes.radio", &*P_HERMES_RADIO), + ("ec5.hermes.radio", &*P_HERMES_RADIO), + ("ec6.hermes.radio", &*P_HERMES_RADIO), + ("ec7.hermes.radio", &*P_HERMES_RADIO), + ("ec8.hermes.radio", &*P_HERMES_RADIO), + ("ec9.hermes.radio", &*P_HERMES_RADIO), + ("ec10.hermes.radio", &*P_HERMES_RADIO), + ("ec11.hermes.radio", &*P_HERMES_RADIO), + ("ec12.hermes.radio", &*P_HERMES_RADIO), + ("ec13.hermes.radio", &*P_HERMES_RADIO), + ("ec14.hermes.radio", &*P_HERMES_RADIO), + ("ec15.hermes.radio", &*P_HERMES_RADIO), ("hermes.radio", &*P_HERMES_RADIO), ("hey.com", &*P_HEY_COM), ("i.ua", &*P_I_UA), @@ -1681,12 +1755,14 @@ pub(crate) static PROVIDER_DATA: Lazy> ("mailo.com", &*P_MAILO_COM), ("nauta.cu", &*P_NAUTA_CU), ("naver.com", &*P_NAVER), + ("nubo.coop", &*P_NUBO_COOP), ("hotmail.com", &*P_OUTLOOK_COM), ("outlook.com", &*P_OUTLOOK_COM), ("office365.com", &*P_OUTLOOK_COM), ("outlook.com.tr", &*P_OUTLOOK_COM), ("live.com", &*P_OUTLOOK_COM), ("outlook.de", &*P_OUTLOOK_COM), + ("ouvaton.org", &*P_OUVATON_COOP), ("posteo.de", &*P_POSTEO), ("posteo.af", &*P_POSTEO), ("posteo.at", &*P_POSTEO), @@ -1861,7 +1937,9 @@ pub(crate) static PROVIDER_IDS: Lazy> = ("mailo.com", &*P_MAILO_COM), ("nauta.cu", &*P_NAUTA_CU), ("naver", &*P_NAVER), + ("nubo.coop", &*P_NUBO_COOP), ("outlook.com", &*P_OUTLOOK_COM), + ("ouvaton.coop", &*P_OUVATON_COOP), ("posteo", &*P_POSTEO), ("protonmail", &*P_PROTONMAIL), ("qq", &*P_QQ), @@ -1891,4 +1969,4 @@ pub(crate) static PROVIDER_IDS: Lazy> = }); pub static PROVIDER_UPDATED: Lazy = - Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2022, 7, 5).unwrap()); + Lazy::new(|| chrono::NaiveDate::from_ymd(2023, 1, 6)); From 3986bb6c4e4d3598b6472cf6f6afd3ef2c096adc Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 6 Jan 2023 00:44:15 +0000 Subject: [PATCH 089/132] Fix provider update script --- src/provider/data.rs | 2 +- src/provider/update.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/provider/data.rs b/src/provider/data.rs index 24dbdf6ef..8c9516272 100644 --- a/src/provider/data.rs +++ b/src/provider/data.rs @@ -1969,4 +1969,4 @@ pub(crate) static PROVIDER_IDS: Lazy> = }); pub static PROVIDER_UPDATED: Lazy = - Lazy::new(|| chrono::NaiveDate::from_ymd(2023, 1, 6)); + Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2023, 1, 6).unwrap()); diff --git a/src/provider/update.py b/src/provider/update.py index 9031e85b4..b67e9fb59 100755 --- a/src/provider/update.py +++ b/src/provider/update.py @@ -190,6 +190,6 @@ if __name__ == "__main__": now = datetime.datetime.utcnow() out_all += "pub static PROVIDER_UPDATED: Lazy = "\ - "Lazy::new(|| chrono::NaiveDate::from_ymd("+str(now.year)+", "+str(now.month)+", "+str(now.day)+"));\n" + "Lazy::new(|| chrono::NaiveDate::from_ymd_opt("+str(now.year)+", "+str(now.month)+", "+str(now.day)+").unwrap());\n" print(out_all) From eba96eb9047048d30b57db59042c3c10fe386e4d Mon Sep 17 00:00:00 2001 From: bjoern Date: Fri, 6 Jan 2023 11:22:30 +0100 Subject: [PATCH 090/132] mark all archived read (#3919) * let marknoticed_chat() work for DC_CHAT_ID_ARCHIVED_LINK * fix test_util::get_last_msg() - the first position may be the archive-link if 'adding specials' is allowed * add a test for the archived-link message counter * update CHANGELOG --- CHANGELOG.md | 1 + src/chat.rs | 159 ++++++++++++++++++++++++++++++++++++++++------ src/test_utils.rs | 4 +- 3 files changed, 141 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae2eb0ff3..580a3c60f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Buffer IMAP client writes #3888 - move `DC_CHAT_ID_ARCHIVED_LINK` to the top of chat lists and make `dc_get_fresh_msg_cnt()` work for `DC_CHAT_ID_ARCHIVED_LINK` #3918 +- make `dc_marknoticed_chat()` work for `DC_CHAT_ID_ARCHIVED_LINK` #3919 - Update provider database ### API-Changes diff --git a/src/chat.rs b/src/chat.rs index 8a5ee0bb2..d22cec77d 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -2431,31 +2431,63 @@ pub(crate) async fn marknoticed_chat_if_older_than( } /// Marks all messages in the chat as noticed. +/// If the given chat-id is the archive-link, marks all messages in all archived chats as noticed. pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()> { // "WHERE" below uses the index `(state, hidden, chat_id)`, see get_fresh_msg_cnt() for reasoning // the additional SELECT statement may speed up things as no write-blocking is needed. - let exists = context - .sql - .exists( - "SELECT COUNT(*) FROM msgs WHERE state=? AND hidden=0 AND chat_id=?;", - paramsv![MessageState::InFresh, chat_id], - ) - .await?; - if !exists { - return Ok(()); - } + if chat_id.is_archived_link() { + let chat_ids_in_archive = context + .sql + .query_map( + "SELECT DISTINCT(m.chat_id) FROM msgs m + LEFT JOIN chats c ON m.chat_id=c.id + WHERE m.state=10 AND m.hidden=0 AND m.chat_id>9 AND c.blocked=0 AND c.archived=1", + paramsv![], + |row| row.get::<_, ChatId>(0), + |ids| ids.collect::, _>>().map_err(Into::into) + ) + .await?; + if chat_ids_in_archive.is_empty() { + return Ok(()); + } - context - .sql - .execute( - "UPDATE msgs - SET state=? - WHERE state=? - AND hidden=0 - AND chat_id=?;", - paramsv![MessageState::InNoticed, MessageState::InFresh, chat_id], - ) - .await?; + context + .sql + .execute( + &format!( + "UPDATE msgs SET state=13 WHERE state=10 AND hidden=0 AND chat_id IN ({});", + sql::repeat_vars(chat_ids_in_archive.len()) + ), + rusqlite::params_from_iter(&chat_ids_in_archive), + ) + .await?; + for chat_id_in_archive in chat_ids_in_archive { + context.emit_event(EventType::MsgsNoticed(chat_id_in_archive)); + } + } else { + let exists = context + .sql + .exists( + "SELECT COUNT(*) FROM msgs WHERE state=? AND hidden=0 AND chat_id=?;", + paramsv![MessageState::InFresh, chat_id], + ) + .await?; + if !exists { + return Ok(()); + } + + context + .sql + .execute( + "UPDATE msgs + SET state=? + WHERE state=? + AND hidden=0 + AND chat_id=?;", + paramsv![MessageState::InNoticed, MessageState::InFresh, chat_id], + ) + .await?; + } context.emit_event(EventType::MsgsNoticed(chat_id)); @@ -4460,6 +4492,91 @@ mod tests { Ok(()) } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_archive_fresh_msgs() -> Result<()> { + let t = TestContext::new_alice().await; + + async fn msg_from(t: &TestContext, name: &str, num: u32) -> Result<()> { + receive_imf( + t, + format!( + "From: {}@example.net\n\ + To: alice@example.org\n\ + Message-ID: <{}@example.org>\n\ + Chat-Version: 1.0\n\ + Date: Sun, 22 Mar 2022 19:37:57 +0000\n\ + \n\ + hello\n", + name, num + ) + .as_bytes(), + false, + ) + .await?; + Ok(()) + } + + // receive some messages in archived+muted chats + msg_from(&t, "bob", 1).await?; + let bob_chat_id = t.get_last_msg().await.get_chat_id(); + bob_chat_id.accept(&t).await?; + set_muted(&t, bob_chat_id, MuteDuration::Forever).await?; + bob_chat_id + .set_visibility(&t, ChatVisibility::Archived) + .await?; + assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 0); + + msg_from(&t, "bob", 2).await?; + assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 1); + + msg_from(&t, "bob", 3).await?; + assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 1); + + msg_from(&t, "claire", 4).await?; + let claire_chat_id = t.get_last_msg().await.get_chat_id(); + claire_chat_id.accept(&t).await?; + set_muted(&t, claire_chat_id, MuteDuration::Forever).await?; + claire_chat_id + .set_visibility(&t, ChatVisibility::Archived) + .await?; + msg_from(&t, "claire", 5).await?; + msg_from(&t, "claire", 6).await?; + msg_from(&t, "claire", 7).await?; + assert_eq!(bob_chat_id.get_fresh_msg_cnt(&t).await?, 2); + assert_eq!(claire_chat_id.get_fresh_msg_cnt(&t).await?, 3); + assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 2); + + // mark one of the archived+muted chats as noticed: check that the archive-link counter is changed as well + marknoticed_chat(&t, claire_chat_id).await?; + assert_eq!(bob_chat_id.get_fresh_msg_cnt(&t).await?, 2); + assert_eq!(claire_chat_id.get_fresh_msg_cnt(&t).await?, 0); + assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 1); + + // receive some more messages + msg_from(&t, "claire", 8).await?; + assert_eq!(bob_chat_id.get_fresh_msg_cnt(&t).await?, 2); + assert_eq!(claire_chat_id.get_fresh_msg_cnt(&t).await?, 1); + assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 2); + assert_eq!(t.get_fresh_msgs().await?.len(), 0); + + msg_from(&t, "dave", 9).await?; + let dave_chat_id = t.get_last_msg().await.get_chat_id(); + dave_chat_id.accept(&t).await?; + assert_eq!(dave_chat_id.get_fresh_msg_cnt(&t).await?, 1); + assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 2); + assert_eq!(t.get_fresh_msgs().await?.len(), 1); + + // mark the archived-link as noticed: check that the real chats are noticed as well + marknoticed_chat(&t, DC_CHAT_ID_ARCHIVED_LINK).await?; + assert_eq!(bob_chat_id.get_fresh_msg_cnt(&t).await?, 0); + assert_eq!(claire_chat_id.get_fresh_msg_cnt(&t).await?, 0); + assert_eq!(dave_chat_id.get_fresh_msg_cnt(&t).await?, 1); + assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 0); + assert_eq!(t.get_fresh_msgs().await?.len(), 1); + + Ok(()) + } + async fn get_chats_from_chat_list(ctx: &Context, listflags: usize) -> Vec { let chatlist = Chatlist::try_load(ctx, listflags, None, None) .await diff --git a/src/test_utils.rs b/src/test_utils.rs index ecf219d5c..8204602e8 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -21,7 +21,7 @@ use crate::chat::{self, Chat, ChatId}; use crate::chatlist::Chatlist; use crate::config::Config; use crate::constants::Chattype; -use crate::constants::{DC_GCM_ADDDAYMARKER, DC_MSG_ID_DAYMARKER}; +use crate::constants::{DC_GCL_NO_SPECIALS, DC_GCM_ADDDAYMARKER, DC_MSG_ID_DAYMARKER}; use crate::contact::{Contact, ContactId, Modifier, Origin}; use crate::context::Context; use crate::events::{Event, EventType, Events}; @@ -502,7 +502,7 @@ impl TestContext { /// Gets the most recent message over all chats. pub async fn get_last_msg(&self) -> Message { - let chats = Chatlist::try_load(&self.ctx, 0, None, None) + let chats = Chatlist::try_load(&self.ctx, DC_GCL_NO_SPECIALS, 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): From f4c674fa98c93e2ec5727e51337aba0f9d317356 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 5 Jan 2023 22:59:12 +0000 Subject: [PATCH 091/132] python: set reasonable timeouts for account requests `requests` library does not have a timeout at all by default. --- .../src/deltachat_rpc_client/pytestplugin.py | 5 ++++- python/src/deltachat/testplugin.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py index 4b8562af7..6bfd446d1 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py @@ -12,8 +12,11 @@ from .rpc import Rpc async def get_temp_credentials() -> dict: url = os.getenv("DCC_NEW_TMP_EMAIL") assert url, "Failed to get online account, DCC_NEW_TMP_EMAIL is not set" + + # Replace default 5 minute timeout with a 1 minute timeout. + timeout = aiohttp.ClientTimeout(total=60) async with aiohttp.ClientSession() as session: - async with session.post(url) as response: + async with session.post(url, timeout=timeout) as response: return json.loads(await response.text()) diff --git a/python/src/deltachat/testplugin.py b/python/src/deltachat/testplugin.py index 335e384a5..ed92917b6 100644 --- a/python/src/deltachat/testplugin.py +++ b/python/src/deltachat/testplugin.py @@ -176,7 +176,7 @@ class TestProcess: try: yield self._configlist[index] except IndexError: - res = requests.post(liveconfig_opt) + res = requests.post(liveconfig_opt, timeout=60) if res.status_code != 200: pytest.fail("newtmpuser count={} code={}: '{}'".format(index, res.status_code, res.text)) d = res.json() From 6e40fd8000fd39a283eb888c759d272c3548455e Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 6 Jan 2023 02:12:36 +0000 Subject: [PATCH 092/132] message: derive `Ord` for `MessageState` --- src/message.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/message.rs b/src/message.rs index e8cc3a936..35c69f9b2 100644 --- a/src/message.rs +++ b/src/message.rs @@ -586,7 +586,7 @@ impl Message { } pub fn is_sent(&self) -> bool { - self.state as i32 >= MessageState::OutDelivered as i32 + self.state >= MessageState::OutDelivered } pub fn is_forwarded(&self) -> bool { @@ -899,6 +899,8 @@ impl Message { Copy, PartialEq, Eq, + PartialOrd, + Ord, FromPrimitive, ToPrimitive, ToSql, From ecc7758788651c0c2df22e757b87ae85be47adda Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 31 Dec 2022 12:14:27 +0000 Subject: [PATCH 093/132] Add documentation to `contact` module --- src/contact.rs | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/contact.rs b/src/contact.rs index baf74b4ac..612b0eee8 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -1,7 +1,5 @@ //! Contacts module -#![allow(missing_docs)] - use std::cmp::Reverse; use std::collections::BinaryHeap; use std::convert::{TryFrom, TryInto}; @@ -48,12 +46,18 @@ const SEEN_RECENTLY_SECONDS: i64 = 600; pub struct ContactId(u32); impl ContactId { + /// Undefined contact. Used as a placeholder for trashed messages. pub const UNDEFINED: ContactId = ContactId::new(0); + /// The owner of the account. /// /// The email-address is set by `set_config` using "addr". pub const SELF: ContactId = ContactId::new(1); + + /// ID of the contact for info messages. pub const INFO: ContactId = ContactId::new(2); + + /// ID of the contact for device messages. pub const DEVICE: ContactId = ContactId::new(5); const LAST_SPECIAL: ContactId = ContactId::new(9); @@ -177,6 +181,8 @@ pub struct Contact { )] #[repr(u32)] pub enum Origin { + /// Unknown origin. Can be used as a minimum origin to specify that the caller does not care + /// about origin of the contact. Unknown = 0, /// The contact is a mailing list address, needed to unblock mailing lists @@ -257,12 +263,13 @@ pub(crate) enum Modifier { Created, } +/// Verification status of the contact. #[derive(Debug, PartialEq, Eq, Clone, Copy, FromPrimitive)] #[repr(u8)] pub enum VerifiedStatus { /// Contact is not verified. Unverified = 0, - // TODO: is this a thing? + /// SELF has verified the fingerprint of a contact. Currently unused. Verified = 1, /// SELF and contact have verified their fingerprints in both directions; in the UI typically checkmarks are shown. BidirectVerified = 2, @@ -275,6 +282,7 @@ impl Default for VerifiedStatus { } impl Contact { + /// Loads a contact snapshot from the database. pub async fn load_from_db(context: &Context, contact_id: ContactId) -> Result { let mut contact = context .sql @@ -847,6 +855,7 @@ impl Contact { Ok(()) } + /// Returns number of blocked contacts. pub async fn get_blocked_cnt(context: &Context) -> Result { let count = context .sql @@ -1138,7 +1147,7 @@ impl Contact { Ok(VerifiedStatus::Unverified) } - /// Return the address that verified the given contact + /// Returns the address that verified the given contact. pub async fn get_verifier_addr( context: &Context, contact_id: &ContactId, @@ -1150,6 +1159,7 @@ impl Contact { .and_then(|peerstate| peerstate.get_verifier().map(|addr| addr.to_owned()))) } + /// Returns the ContactId that verified the given contact. pub async fn get_verifier_id( context: &Context, contact_id: &ContactId, @@ -1162,7 +1172,7 @@ impl Contact { } } - /// Return the ContactId that verified the given contact + /// Returns the number of real (i.e. non-special) contacts in the database. pub async fn get_real_cnt(context: &Context) -> Result { if !context.sql.is_open().await { return Ok(0); @@ -1178,6 +1188,7 @@ impl Contact { Ok(count) } + /// Returns true if a contact with this ID exists. pub async fn real_exists_by_id(context: &Context, contact_id: ContactId) -> Result { if contact_id.is_special() { return Ok(false); @@ -1193,6 +1204,7 @@ impl Contact { Ok(exists) } + /// Updates the origin of the contact, but only if new origin is higher than the current one. pub async fn scaleup_origin_by_id( context: &Context, contact_id: ContactId, @@ -1453,6 +1465,7 @@ fn cat_fingerprint( } } +/// Compares two email addresses, normalizing them beforehand. pub fn addr_cmp(addr1: &str, addr2: &str) -> bool { let norm1 = addr_normalize(addr1).to_lowercase(); let norm2 = addr_normalize(addr2).to_lowercase(); From 8e65e794bca56ca5dbe8aadbecc23dcd508e543e Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 28 Dec 2022 23:03:38 +0000 Subject: [PATCH 094/132] format-flowed: make quotes round-trip --- CHANGELOG.md | 1 + format-flowed/src/lib.rs | 76 ++++++++++++++++++------- fuzz/fuzz_targets/fuzz_format_flowed.rs | 5 +- src/message.rs | 5 ++ 4 files changed, 62 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 580a3c60f..bf0e1b21b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ - Fix STARTTLS connection and add a test for it #3907 - Trigger reconnection when failing to fetch existing messages #3911 - Do not retry fetching existing messages after failure, prevents infinite reconnection loop #3913 +- Ensure format=flowed formatting is always reversible on the receiver side #3880 ## 1.104.0 diff --git a/format-flowed/src/lib.rs b/format-flowed/src/lib.rs index 52b6827e5..10f9400b5 100644 --- a/format-flowed/src/lib.rs +++ b/format-flowed/src/lib.rs @@ -24,7 +24,7 @@ fn format_line_flowed(line: &str, prefix: &str) -> String { let mut result = String::new(); let mut buffer = prefix.to_string(); - let mut after_space = false; + let mut after_space = prefix.ends_with(' '); for c in line.chars() { if c == ' ' { @@ -55,7 +55,7 @@ fn format_line_flowed(line: &str, prefix: &str) -> String { result + &buffer } -/// Returns text formatted according to RFC 3767 (format=flowed). +/// Returns text formatted according to RFC 3676 (format=flowed). /// /// This function accepts text separated by LF, but returns text /// separated by CRLF. @@ -70,23 +70,20 @@ pub fn format_flowed(text: &str) -> String { result += "\r\n"; } - let line_no_prefix = line - .strip_prefix('>') - .map(|line| line.strip_prefix(' ').unwrap_or(line)); - let is_quote = line_no_prefix.is_some(); - let line = line_no_prefix.unwrap_or(line).trim_end(); - let prefix = if is_quote { "> " } else { "" }; + let line = line.trim_end(); + let quote_depth = line.chars().take_while(|&c| c == '>').count(); + let (prefix, mut line) = line.split_at(quote_depth); - if prefix.len() + line.len() > 78 { - result += &format_line_flowed(line, prefix); - } else { - result += prefix; - if prefix.is_empty() && (line.starts_with('>') || line.starts_with(' ')) { - // Space stuffing, see RFC 3676 - result.push(' '); + let mut prefix = prefix.to_string(); + + if quote_depth > 0 { + if let Some(s) = line.strip_prefix(' ') { + line = s; + prefix += " "; } - result += line; } + + result += &format_line_flowed(line, &prefix); } result @@ -111,9 +108,6 @@ pub fn format_flowed_quote(text: &str) -> String { /// /// Lines must be separated by single LF. /// -/// Quote processing is not supported, it is assumed that they are -/// deleted during simplification. -/// /// Signature separator line is not processed here, it is assumed to /// be stripped beforehand. pub fn unformat_flowed(text: &str, delsp: bool) -> String { @@ -121,6 +115,12 @@ pub fn unformat_flowed(text: &str, delsp: bool) -> String { let mut skip_newline = true; for line in text.split('\n') { + let line = if !result.is_empty() && skip_newline { + line.trim_start_matches('>') + } else { + line + }; + // Revert space-stuffing let line = line.strip_prefix(' ').unwrap_or(line); @@ -150,8 +150,20 @@ mod tests { #[test] fn test_format_flowed() { + let text = ""; + assert_eq!(format_flowed(text), ""); + let text = "Foo bar baz"; - assert_eq!(format_flowed(text), "Foo bar baz"); + assert_eq!(format_flowed(text), text); + + let text = ">Foo bar"; + assert_eq!(format_flowed(text), text); + + let text = "> Foo bar"; + assert_eq!(format_flowed(text), text); + + let text = ">\n\nA"; + assert_eq!(format_flowed(text), ">\r\n\r\nA"); let text = "This is the Autocrypt Setup Message used to transfer your key between clients.\n\ \n\ @@ -165,17 +177,33 @@ mod tests { let text = "> A quote"; assert_eq!(format_flowed(text), "> A quote"); + let text = "> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > A"; + assert_eq!( + format_flowed(text), + "> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > \r\n> A" + ); + // Test space stuffing of wrapped lines let text = "> This is the Autocrypt Setup Message used to transfer your key between clients.\n\ > \n\ > To decrypt and use your key, open the message in an Autocrypt-compliant client and enter the setup code presented on the generating device."; let expected = "> This is the Autocrypt Setup Message used to transfer your key between \r\n\ > clients.\r\n\ - > \r\n\ + >\r\n\ > To decrypt and use your key, open the message in an Autocrypt-compliant \r\n\ > client and enter the setup code presented on the generating device."; assert_eq!(format_flowed(text), expected); + let text = ">> This is the Autocrypt Setup Message used to transfer your key between clients.\n\ + >> \n\ + >> To decrypt and use your key, open the message in an Autocrypt-compliant client and enter the setup code presented on the generating device."; + let expected = ">> This is the Autocrypt Setup Message used to transfer your key between \r\n\ + >> clients.\r\n\ + >>\r\n\ + >> To decrypt and use your key, open the message in an Autocrypt-compliant \r\n\ + >> client and enter the setup code presented on the generating device."; + assert_eq!(format_flowed(text), expected); + // Test space stuffing of spaces. let text = " Foo bar baz"; assert_eq!(format_flowed(text), " Foo bar baz"); @@ -202,6 +230,12 @@ mod tests { let text = " Foo bar"; let expected = " Foo bar"; assert_eq!(unformat_flowed(text, false), expected); + + let text = + "> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > \n> A"; + let expected = + "> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > A"; + assert_eq!(unformat_flowed(text, false), expected); } #[test] diff --git a/fuzz/fuzz_targets/fuzz_format_flowed.rs b/fuzz/fuzz_targets/fuzz_format_flowed.rs index f44bebf94..8f779a468 100644 --- a/fuzz/fuzz_targets/fuzz_format_flowed.rs +++ b/fuzz/fuzz_targets/fuzz_format_flowed.rs @@ -10,10 +10,7 @@ fn round_trip(input: &str) -> String { fn main() { check!().for_each(|data: &[u8]| { if let Ok(input) = std::str::from_utf8(data.into()) { - let mut input = input.to_string(); - - // Only consider inputs that don't contain quotes. - input.retain(|c| c != '>'); + let input = input.trim().to_string(); // Only consider inputs that are the result of unformatting format=flowed text. // At least this means that lines don't contain any trailing whitespace. diff --git a/src/message.rs b/src/message.rs index 35c69f9b2..678415378 100644 --- a/src/message.rs +++ b/src/message.rs @@ -2390,6 +2390,11 @@ mod tests { let received = bob.recv_msg(&sent).await; assert_eq!(received.text.as_deref(), Some(text)); + let text = "> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > A"; + let sent = alice.send_text(chat.id, text).await; + let received = bob.recv_msg(&sent).await; + assert_eq!(received.text.as_deref(), Some(text)); + let python_program = "\ def hello(): return 'Hello, world!'"; From 9eb7a84d832d2f4f4495b0ca909159ca7954b7b7 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 6 Jan 2023 16:34:49 +0000 Subject: [PATCH 095/132] Use TestContextManager for test_format_flowed_round_trip test --- src/message.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/message.rs b/src/message.rs index 678415378..5535666bd 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1889,7 +1889,7 @@ mod tests { use crate::chatlist::Chatlist; use crate::receive_imf::receive_imf; use crate::test_utils as test; - use crate::test_utils::TestContext; + use crate::test_utils::{TestContext, TestContextManager}; use super::*; @@ -2376,8 +2376,9 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_format_flowed_round_trip() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; let chat = alice.create_chat(&bob).await; let text = " Foo bar"; From 4f8593f46a053876d62445aac39a6a66762985d0 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 6 Jan 2023 21:58:47 +0000 Subject: [PATCH 096/132] Update base64 API usage --- src/tools.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/tools.rs b/src/tools.rs index b766dc73e..b02ed2fea 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -277,6 +277,12 @@ async fn maybe_warn_on_outdated(context: &Context, now: i64, approx_compile_time /// - for INCOMING messages, the ID is taken from the Chat-Group-ID-header or from the Message-ID in the In-Reply-To: or References:-Header /// - the group-id should be a string with the characters [a-zA-Z0-9\-_] pub(crate) fn create_id() -> String { + const URL_SAFE_ENGINE: base64::engine::fast_portable::FastPortable = + base64::engine::fast_portable::FastPortable::from( + &base64::alphabet::URL_SAFE, + base64::engine::fast_portable::NO_PAD, + ); + // ThreadRng implements CryptoRng trait and is supposed to be cryptographically secure. let mut rng = thread_rng(); @@ -285,7 +291,7 @@ pub(crate) fn create_id() -> String { rng.fill(&mut arr[..]); // Take 11 base64 characters containing 66 random bits. - base64::encode_config(arr, base64::URL_SAFE) + base64::encode_engine(arr, &URL_SAFE_ENGINE) .chars() .take(11) .collect() From 3df5f6110c22c810b8910bf69e8eb6893056bf4e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Jan 2023 23:10:29 +0000 Subject: [PATCH 097/132] cargo: bump tokio from 1.23.0 to 1.23.1 Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.23.0 to 1.23.1. - [Release notes](https://github.com/tokio-rs/tokio/releases) - [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.23.0...tokio-1.23.1) --- updated-dependencies: - dependency-name: tokio dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- deltachat-jsonrpc/Cargo.toml | 4 ++-- deltachat-rpc-server/Cargo.toml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fb0d51376..5b2c268a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3544,9 +3544,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.23.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46" +checksum = "38a54aca0c15d014013256222ba0ebed095673f89345dd79119d912eb561b7a8" dependencies = [ "autocfg", "bytes", diff --git a/deltachat-jsonrpc/Cargo.toml b/deltachat-jsonrpc/Cargo.toml index dbe8b2d17..49c322197 100644 --- a/deltachat-jsonrpc/Cargo.toml +++ b/deltachat-jsonrpc/Cargo.toml @@ -23,7 +23,7 @@ futures = { version = "0.3.25" } serde_json = "1.0.89" yerpc = { version = "^0.3.1", features = ["anyhow_expose"] } typescript-type-def = { version = "0.5.5", features = ["json_value"] } -tokio = { version = "1.23.0" } +tokio = { version = "1.23.1" } sanitize-filename = "0.4" walkdir = "2.3.2" @@ -32,7 +32,7 @@ axum = { version = "0.6.1", optional = true, features = ["ws"] } env_logger = { version = "0.10.0", optional = true } [dev-dependencies] -tokio = { version = "1.23.0", features = ["full", "rt-multi-thread"] } +tokio = { version = "1.23.1", features = ["full", "rt-multi-thread"] } [features] diff --git a/deltachat-rpc-server/Cargo.toml b/deltachat-rpc-server/Cargo.toml index b6f10996d..2a7ce674c 100644 --- a/deltachat-rpc-server/Cargo.toml +++ b/deltachat-rpc-server/Cargo.toml @@ -21,5 +21,5 @@ futures-lite = "1.12.0" log = "0.4" serde_json = "1.0.89" serde = { version = "1.0", features = ["derive"] } -tokio = { version = "1.23.0", features = ["io-std"] } +tokio = { version = "1.23.1", features = ["io-std"] } yerpc = { version = "0.3.1", features = ["anyhow_expose"] } From 087b4289e5443758b2b28d73d089fa69be0ecfb5 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sat, 7 Jan 2023 00:34:47 +0100 Subject: [PATCH 098/132] jsonrpc: add fresh message count to the archive-link chatlistitem variant (#3920) this is a followup to #3918 I went with option "C" from my comment: https://github.com/deltachat/deltachat-core-rust/pull/3918#issuecomment-1371224339 - Archive link is (still) very different from a normal chat, so most of the options would be empty / not relevant - avatar path is not needed as desktop renders it differently anyway, it's not really a chat like saved messages or device messages where it made more sense for the core to supply the icon, vs. using the svg directly. - translating the string in the coreas stock-string is more annoying than doing it from the ui, especially when this special pseudo chat has different rendering anyway so also no need to provide a name property --- CHANGELOG.md | 1 + deltachat-jsonrpc/src/api/types/chat_list.rs | 15 ++++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf0e1b21b..37715b863 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ ### API-Changes - jsonrpc: add python API for webxdc updates #3872 +- jsonrpc: add fresh message count to ChatListItemFetchResult::ArchiveLink - Add ffi functions to retrieve `verified by` information #3786 ### Fixes diff --git a/deltachat-jsonrpc/src/api/types/chat_list.rs b/deltachat-jsonrpc/src/api/types/chat_list.rs index 45933770f..83b84dd3d 100644 --- a/deltachat-jsonrpc/src/api/types/chat_list.rs +++ b/deltachat-jsonrpc/src/api/types/chat_list.rs @@ -48,12 +48,10 @@ pub enum ChatListItemFetchResult { dm_chat_contact: Option, was_seen_recently: bool, }, - ArchiveLink, #[serde(rename_all = "camelCase")] - Error { - id: u32, - error: String, - }, + ArchiveLink { fresh_message_counter: usize }, + #[serde(rename_all = "camelCase")] + Error { id: u32, error: String }, } pub(crate) async fn get_chat_list_item_by_id( @@ -66,8 +64,12 @@ pub(crate) async fn get_chat_list_item_by_id( _ => Some(MsgId::new(entry.1)), }; + let fresh_message_counter = chat_id.get_fresh_msg_cnt(ctx).await?; + if chat_id.is_archived_link() { - return Ok(ChatListItemFetchResult::ArchiveLink); + return Ok(ChatListItemFetchResult::ArchiveLink { + fresh_message_counter, + }); } let chat = Chat::load_from_db(ctx, chat_id).await?; @@ -111,7 +113,6 @@ pub(crate) async fn get_chat_list_item_by_id( (None, false) }; - let fresh_message_counter = chat_id.get_fresh_msg_cnt(ctx).await?; let color = color_int_to_hex_string(chat.get_color(ctx).await?); Ok(ChatListItemFetchResult::ChatListItem { From 58ba107d011032f7567c083f67d536613c27916e Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 6 Jan 2023 19:22:57 +0000 Subject: [PATCH 099/132] Resultify Message::get_filebytes() --- CHANGELOG.md | 1 + deltachat-ffi/src/lib.rs | 2 ++ deltachat-jsonrpc/src/api/types/message.rs | 2 +- src/chat.rs | 2 +- src/message.rs | 13 +++++++------ src/mimefactory.rs | 2 +- src/mimeparser.rs | 2 +- src/tools.rs | 10 ++++------ 8 files changed, 18 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37715b863..b28312450 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - jsonrpc: add python API for webxdc updates #3872 - jsonrpc: add fresh message count to ChatListItemFetchResult::ArchiveLink - Add ffi functions to retrieve `verified by` information #3786 +- resultify `Message::get_filebytes()` #3925 ### Fixes - Do not add an error if the message is encrypted but not signed #3860 diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 764f1acad..7de6e4cd8 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -3309,6 +3309,8 @@ pub unsafe extern "C" fn dc_msg_get_filebytes(msg: *mut dc_msg_t) -> u64 { let ctx = &*ffi_msg.context; block_on(ffi_msg.message.get_filebytes(ctx)) + .unwrap_or_log_default(ctx, "Cannot get file size") + .unwrap_or_default() } #[no_mangle] diff --git a/deltachat-jsonrpc/src/api/types/message.rs b/deltachat-jsonrpc/src/api/types/message.rs index c7e9cfe8d..0f35f0c6f 100644 --- a/deltachat-jsonrpc/src/api/types/message.rs +++ b/deltachat-jsonrpc/src/api/types/message.rs @@ -105,7 +105,7 @@ impl MessageObject { let sender_contact = Contact::load_from_db(context, message.get_from_id()).await?; let sender = ContactObject::try_from_dc_contact(context, sender_contact).await?; - let file_bytes = message.get_filebytes(context).await; + let file_bytes = message.get_filebytes(context).await?.unwrap_or_default(); let override_sender_name = message.get_override_sender_name(); let webxdc_info = if message.get_viewtype() == Viewtype::Webxdc { diff --git a/src/chat.rs b/src/chat.rs index d22cec77d..618319cb1 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -5202,7 +5202,7 @@ mod tests { assert_eq!(msg.get_filename(), Some(filename.to_string())); assert_eq!(msg.get_width(), w); assert_eq!(msg.get_height(), h); - assert!(msg.get_filebytes(&bob).await > 250); + assert!(msg.get_filebytes(&bob).await?.unwrap() > 250); Ok(()) } diff --git a/src/message.rs b/src/message.rs index 5535666bd..b1581137f 100644 --- a/src/message.rs +++ b/src/message.rs @@ -494,11 +494,12 @@ impl Message { .map(|name| name.to_string_lossy().to_string()) } - pub async fn get_filebytes(&self, context: &Context) -> u64 { - match self.param.get_path(Param::File, context) { - Ok(Some(path)) => get_filebytes(context, &path).await, - Ok(None) => 0, - Err(_) => 0, + /// Returns the size of the file in bytes, if applicable. + pub async fn get_filebytes(&self, context: &Context) -> Result> { + if let Some(path) = self.param.get_path(Param::File, context)? { + Ok(Some(get_filebytes(context, &path).await?)) + } else { + Ok(None) } } @@ -1102,7 +1103,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result { } if let Some(path) = msg.get_file(context) { - let bytes = get_filebytes(context, &path).await; + let bytes = get_filebytes(context, &path).await?; ret += &format!("\nFile: {}, {}, bytes\n", path.display(), bytes); } diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 43f3e5886..5d7d51fe8 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1432,7 +1432,7 @@ fn recipients_contain_addr(recipients: &[(String, String)], addr: &str) -> bool async fn is_file_size_okay(context: &Context, msg: &Message) -> Result { match msg.param.get_path(Param::File, context)? { Some(path) => { - let bytes = get_filebytes(context, &path).await; + let bytes = get_filebytes(context, &path).await?; Ok(bytes <= UPPER_LIMIT_FILE_SIZE) } None => Ok(false), diff --git a/src/mimeparser.rs b/src/mimeparser.rs index e7a2ea40e..c83709673 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -3145,7 +3145,7 @@ On 2020-10-25, Bob wrote: assert_eq!(msg.is_dc_message, MessengerMessage::No); assert_eq!(msg.chat_blocked, Blocked::Request); assert_eq!(msg.state, MessageState::InFresh); - assert_eq!(msg.get_filebytes(&t).await, 2115); + assert_eq!(msg.get_filebytes(&t).await.unwrap().unwrap(), 2115); assert!(msg.get_file(&t).is_some()); assert_eq!(msg.get_filename().unwrap(), "avatar64x64.png"); assert_eq!(msg.get_width(), 64); diff --git a/src/tools.rs b/src/tools.rs index b02ed2fea..e9c34f5e1 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -364,12 +364,10 @@ pub(crate) fn get_abs_path(context: &Context, path: impl AsRef) -> PathBuf } } -pub(crate) async fn get_filebytes(context: &Context, path: impl AsRef) -> u64 { +pub(crate) async fn get_filebytes(context: &Context, path: impl AsRef) -> Result { let path_abs = get_abs_path(context, &path); - match fs::metadata(&path_abs).await { - Ok(meta) => meta.len(), - Err(_err) => 0, - } + let meta = fs::metadata(&path_abs).await?; + Ok(meta.len()) } pub(crate) async fn delete_file(context: &Context, path: impl AsRef) -> bool { @@ -1055,7 +1053,7 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true"; .is_ok()); assert!(file_exist!(context, "$BLOBDIR/foobar")); assert!(!file_exist!(context, "$BLOBDIR/foobarx")); - assert_eq!(get_filebytes(context, "$BLOBDIR/foobar").await, 7); + assert_eq!(get_filebytes(context, "$BLOBDIR/foobar").await.unwrap(), 7); let abs_path = context .get_blobdir() From f2f211908dc7c7e73b60e8c0f36c471342d6a136 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 7 Jan 2023 15:58:34 +0000 Subject: [PATCH 100/132] cargo: bump serde_json from 1.0.89 to 1.0.91 Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.89 to 1.0.91. - [Release notes](https://github.com/serde-rs/json/releases) - [Commits](https://github.com/serde-rs/json/compare/v1.0.89...v1.0.91) --- updated-dependencies: - dependency-name: serde_json dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- deltachat-jsonrpc/Cargo.toml | 2 +- deltachat-rpc-server/Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5b2c268a9..0c0c3b778 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3210,9 +3210,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.89" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" +checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" dependencies = [ "itoa", "ryu", diff --git a/deltachat-jsonrpc/Cargo.toml b/deltachat-jsonrpc/Cargo.toml index 49c322197..36e543b3e 100644 --- a/deltachat-jsonrpc/Cargo.toml +++ b/deltachat-jsonrpc/Cargo.toml @@ -20,7 +20,7 @@ tempfile = "3.3.0" log = "0.4" async-channel = { version = "1.8.0" } futures = { version = "0.3.25" } -serde_json = "1.0.89" +serde_json = "1.0.91" yerpc = { version = "^0.3.1", features = ["anyhow_expose"] } typescript-type-def = { version = "0.5.5", features = ["json_value"] } tokio = { version = "1.23.1" } diff --git a/deltachat-rpc-server/Cargo.toml b/deltachat-rpc-server/Cargo.toml index 2a7ce674c..b6f839dd9 100644 --- a/deltachat-rpc-server/Cargo.toml +++ b/deltachat-rpc-server/Cargo.toml @@ -19,7 +19,7 @@ anyhow = "1" env_logger = { version = "0.10.0" } futures-lite = "1.12.0" log = "0.4" -serde_json = "1.0.89" +serde_json = "1.0.91" serde = { version = "1.0", features = ["derive"] } tokio = { version = "1.23.1", features = ["io-std"] } yerpc = { version = "0.3.1", features = ["anyhow_expose"] } From 15674111b440b816814ebf8544abf7052c61ca54 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 7 Jan 2023 16:39:18 +0000 Subject: [PATCH 101/132] Bump tokio from 1.23.0 to 1.24.1 in /fuzz Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.23.0 to 1.24.1. - [Release notes](https://github.com/tokio-rs/tokio/releases) - [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.23.0...tokio-1.24.1) --- updated-dependencies: - dependency-name: tokio dependency-type: indirect ... Signed-off-by: dependabot[bot] --- fuzz/Cargo.lock | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index b2f3f5f36..46118154d 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -233,6 +233,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" + [[package]] name = "base64ct" version = "1.5.3" @@ -702,7 +708,7 @@ dependencies = [ "async-smtp", "async_zip", "backtrace", - "base64 0.13.1", + "base64 0.20.0", "bitflags", "chrono", "deltachat_derive", @@ -719,7 +725,7 @@ dependencies = [ "kamadak-exif", "lettre_email", "libc", - "mailparse", + "mailparse 0.14.0", "native-tls", "num-derive", "num-traits", @@ -764,7 +770,7 @@ dependencies = [ "bolero", "deltachat", "format-flowed", - "mailparse", + "mailparse 0.13.8", ] [[package]] @@ -1691,6 +1697,17 @@ dependencies = [ "quoted_printable", ] +[[package]] +name = "mailparse" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b56570f5f8c0047260d1c8b5b331f62eb9c660b9dd4071a8c46f8c7d3f280aa" +dependencies = [ + "charset", + "data-encoding", + "quoted_printable", +] + [[package]] name = "match_cfg" version = "0.1.0" @@ -1887,9 +1904,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" [[package]] name = "opaque-debug" @@ -2195,9 +2212,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quick-xml" -version = "0.26.0" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" +checksum = "ffc053f057dd768a56f62cd7e434c42c831d296968997e9ac1f76ea7c2d14c41" dependencies = [ "memchr", ] @@ -2846,9 +2863,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.23.0" +version = "1.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46" +checksum = "1d9f76183f91ecfb55e1d7d5602bd1d979e38a3a522fe900241cf195624d67ae" dependencies = [ "autocfg", "bytes", From 847611aed49a76959f484537cbd6cd5f4a1362cf Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sat, 7 Jan 2023 23:37:54 +0100 Subject: [PATCH 102/132] improve error of node tests (#3928) remove unused imports and fail without revealing the token --- node/test/test.js | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/node/test/test.js b/node/test/test.js index 3d6c93de9..d76566b04 100644 --- a/node/test/test.js +++ b/node/test/test.js @@ -1,33 +1,29 @@ // @ts-check -import DeltaChat, { Message } from '../dist' -import binding from '../binding' +import DeltaChat from '../dist' -import { deepEqual, deepStrictEqual, strictEqual } from 'assert' +import { deepStrictEqual, strictEqual } from 'assert' import chai, { expect } from 'chai' import chaiAsPromised from 'chai-as-promised' import { EventId2EventName, C } from '../dist/constants' import { join } from 'path' -import { mkdtempSync, statSync } from 'fs' -import { tmpdir } from 'os' +import { statSync } from 'fs' import { Context } from '../dist/context' +import fetch from 'node-fetch' chai.use(chaiAsPromised) -chai.config.truncateThreshold = 0; // Do not truncate assertion errors. +chai.config.truncateThreshold = 0 // Do not truncate assertion errors. async function createTempUser(url) { - const fetch = require('node-fetch') - async function postData(url = '') { // Default options are marked with * const response = await fetch(url, { method: 'POST', // *GET, POST, PUT, DELETE, etc. - mode: 'cors', // no-cors, *cors, same-origin - cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached - credentials: 'same-origin', // include, *same-origin, omit headers: { 'cache-control': 'no-cache', }, - referrerPolicy: 'no-referrer', // no-referrer, *client }) + if (!response.ok) { + throw new Error('request failed: ' + response.body.read()) + } return response.json() // parses JSON response into native JavaScript objects } From 872cd10b8b3ec112ef2a9c4d4033e42a411d9084 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 8 Jan 2023 09:58:53 +0000 Subject: [PATCH 103/132] Update tokio to 1.24.1 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0c0c3b778..9afad7882 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3544,9 +3544,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.23.1" +version = "1.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38a54aca0c15d014013256222ba0ebed095673f89345dd79119d912eb561b7a8" +checksum = "1d9f76183f91ecfb55e1d7d5602bd1d979e38a3a522fe900241cf195624d67ae" dependencies = [ "autocfg", "bytes", From 120a7a3090e46727b21c518ffb618cc643ffff82 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 8 Jan 2023 16:18:08 +0000 Subject: [PATCH 104/132] Prepare 1.105.0 release --- CHANGELOG.md | 9 +++++++++ Cargo.lock | 8 ++++---- Cargo.toml | 2 +- deltachat-ffi/Cargo.toml | 2 +- deltachat-jsonrpc/Cargo.toml | 2 +- deltachat-jsonrpc/typescript/package.json | 2 +- deltachat-rpc-server/Cargo.toml | 2 +- package.json | 2 +- 8 files changed, 19 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b28312450..d0ca83889 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## Unreleased +### Changes + +### API-Changes + +### Fixes + + +## 1.105.0 + ### Changes - Validate signatures in try_decrypt() even if the message isn't encrypted #3859 - Don't parse the message again after detached signatures validation #3862 diff --git a/Cargo.lock b/Cargo.lock index 9afad7882..abe7cd0d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -873,7 +873,7 @@ checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" [[package]] name = "deltachat" -version = "1.104.0" +version = "1.105.0" dependencies = [ "ansi_term", "anyhow", @@ -947,7 +947,7 @@ dependencies = [ [[package]] name = "deltachat-jsonrpc" -version = "1.104.0" +version = "1.105.0" dependencies = [ "anyhow", "async-channel", @@ -969,7 +969,7 @@ dependencies = [ [[package]] name = "deltachat-rpc-server" -version = "1.104.0" +version = "1.105.0" dependencies = [ "anyhow", "deltachat-jsonrpc", @@ -992,7 +992,7 @@ dependencies = [ [[package]] name = "deltachat_ffi" -version = "1.104.0" +version = "1.105.0" dependencies = [ "anyhow", "deltachat", diff --git a/Cargo.toml b/Cargo.toml index e8f36abe3..549d49dbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat" -version = "1.104.0" +version = "1.105.0" edition = "2021" license = "MPL-2.0" rust-version = "1.63" diff --git a/deltachat-ffi/Cargo.toml b/deltachat-ffi/Cargo.toml index b6e18901b..c3c3dbdf1 100644 --- a/deltachat-ffi/Cargo.toml +++ b/deltachat-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat_ffi" -version = "1.104.0" +version = "1.105.0" description = "Deltachat FFI" edition = "2018" readme = "README.md" diff --git a/deltachat-jsonrpc/Cargo.toml b/deltachat-jsonrpc/Cargo.toml index 36e543b3e..63677512c 100644 --- a/deltachat-jsonrpc/Cargo.toml +++ b/deltachat-jsonrpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-jsonrpc" -version = "1.104.0" +version = "1.105.0" description = "DeltaChat JSON-RPC API" edition = "2021" default-run = "deltachat-jsonrpc-server" diff --git a/deltachat-jsonrpc/typescript/package.json b/deltachat-jsonrpc/typescript/package.json index 9937dbabb..9c1df45e3 100644 --- a/deltachat-jsonrpc/typescript/package.json +++ b/deltachat-jsonrpc/typescript/package.json @@ -48,5 +48,5 @@ }, "type": "module", "types": "dist/deltachat.d.ts", - "version": "1.104.0" + "version": "1.105.0" } \ No newline at end of file diff --git a/deltachat-rpc-server/Cargo.toml b/deltachat-rpc-server/Cargo.toml index b6f839dd9..a45572b74 100644 --- a/deltachat-rpc-server/Cargo.toml +++ b/deltachat-rpc-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-rpc-server" -version = "1.104.0" +version = "1.105.0" description = "DeltaChat JSON-RPC server" edition = "2021" readme = "README.md" diff --git a/package.json b/package.json index 3122efe66..09b9e179d 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.104.0" + "version": "1.105.0" } \ No newline at end of file From 0e849609f419843860493909e6fc669c7c4b78df Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 9 Jan 2023 16:20:56 +0000 Subject: [PATCH 105/132] ci: python: do not try to upload source distributions They are not useful because you still need a statically linked rust library besides the C FFI module, and they are not built by tox 4 without changes. --- scripts/concourse/docs_wheels.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/scripts/concourse/docs_wheels.yml b/scripts/concourse/docs_wheels.yml index d902947bc..bd2074d4b 100644 --- a/scripts/concourse/docs_wheels.yml +++ b/scripts/concourse/docs_wheels.yml @@ -104,9 +104,6 @@ jobs: outputs: - name: py-docs path: ./python/doc/_build/ - # Source packages - - name: py-dist - path: ./python/.docker-tox/dist/ # Binary wheels - name: py-wheels path: ./python/.docker-tox/wheelhouse/ @@ -145,7 +142,6 @@ jobs: config: inputs: - name: py-wheels - - name: py-dist image_resource: type: registry-image source: @@ -162,7 +158,6 @@ jobs: devpi use https://m.devpi.net/dc/master devpi login ((devpi.login)) --password ((devpi.password)) devpi upload py-wheels/*manylinux201* - devpi upload py-dist/* - name: python-aarch64 plan: From 07f2e28eca7ccd3e44d5e7a39b059dcce17563a8 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Mon, 9 Jan 2023 22:12:33 +0100 Subject: [PATCH 106/132] fix: only send contact changed event for recently seen if it is relevant (#3938) (no events for contacts that are already offline again) This dramatically speeds up replay after backup import on desktop. --- CHANGELOG.md | 2 +- src/contact.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0ca83889..cf4f53026 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ ### API-Changes ### Fixes - +- fix: only send contact changed event for recently seen if it is relevant (not too old to matter) #3938 ## 1.105.0 diff --git a/src/contact.rs b/src/contact.rs index 612b0eee8..4f2189467 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -1416,6 +1416,7 @@ pub(crate) async fn update_last_seen( ) .await? > 0 + && timestamp > time() - SEEN_RECENTLY_SECONDS { context.interrupt_recently_seen(contact_id, timestamp).await; } From 8d119713bce74d14e19e40e6a315cbc36d677d67 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Mon, 9 Jan 2023 22:46:32 +0100 Subject: [PATCH 107/132] Print chats when a test failed (#3937) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Print chats after a test failed again E.g. ``` ========== Chats of bob: ========== Single#Chat#10: alice@example.org [alice@example.org] -------------------------------------------------------------------------------- Msg#10: (Contact#Contact#10): hellooo [FRESH] Msg#11: (Contact#Contact#10): hellooo without mailing list [FRESH] -------------------------------------------------------------------------------- ========== Chats of alice: ========== Single#Chat#10: bob@example.net [bob@example.net] -------------------------------------------------------------------------------- Msg#10: Me (Contact#Contact#Self): hellooo √ Msg#11: Me (Contact#Contact#Self): hellooo without mailing list √ -------------------------------------------------------------------------------- ``` I found this very useful sometimes, so, let's try to re-introduce it (it was removed in #3449) * Add failing test for TestContext::drop() * Do not panic in TestContext::drop() if runtime is dropped Co-authored-by: link2xt --- src/test_utils.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/test_utils.rs b/src/test_utils.rs index 8204602e8..cf19dafd8 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -14,6 +14,7 @@ use chat::ChatItem; use once_cell::sync::Lazy; use rand::Rng; use tempfile::{tempdir, TempDir}; +use tokio::runtime::Handle; use tokio::sync::RwLock; use tokio::task; @@ -263,7 +264,6 @@ impl TestContext { Self::builder().configure_fiona().build().await } - #[allow(dead_code)] /// Print current chat state. pub async fn print_chats(&self) { println!("\n========== Chats of {}: ==========", self.name()); @@ -702,6 +702,19 @@ impl Deref for TestContext { } } +impl Drop for TestContext { + fn drop(&mut self) { + task::block_in_place(move || { + if let Ok(handle) = Handle::try_current() { + // Print the chats if runtime still exists. + handle.block_on(async move { + self.print_chats().await; + }); + } + }); + } +} + pub enum LogEvent { /// Logged event. Event(Event), @@ -1079,4 +1092,12 @@ mod tests { bob.ctx.emit_event(EventType::Info("there".into())); // panic!("Both fail"); } + + /// Checks that dropping the `TestContext` after the runtime does not panic, + /// e.g. that `TestContext::drop` does not assume the runtime still exists. + #[test] + fn test_new_test_context() { + let runtime = tokio::runtime::Runtime::new().expect("unable to create tokio runtime"); + runtime.block_on(TestContext::new()); + } } From 2b8923931ed64839c446ebbb1789263c6f1bec84 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 10 Jan 2023 00:00:23 +0000 Subject: [PATCH 108/132] Add more context to IMAP errors --- src/imap.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/imap.rs b/src/imap.rs index 84871cb89..94d4223d2 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -698,9 +698,11 @@ impl Imap { let old_uid_next = get_uid_next(context, folder).await?; let msgs = if fetch_existing_msgs { - self.prefetch_existing_msgs().await? + self.prefetch_existing_msgs() + .await + .context("prefetch_existing_msgs")? } else { - self.prefetch(old_uid_next).await? + self.prefetch(old_uid_next).await.context("prefetch")? }; let read_cnt = msgs.len(); @@ -763,7 +765,7 @@ impl Imap { fetch_response.flags(), show_emails, ) - .await? + .await.context("prefetch_should_download")? { match download_limit { Some(download_limit) => uids_fetch.push(( @@ -799,7 +801,8 @@ impl Imap { fetch_partially, fetch_existing_msgs, ) - .await?; + .await + .context("fetch_many_msgs")?; received_msgs.extend(received_msgs_in_batch); largest_uid_fetched = max( largest_uid_fetched, From 01fe88e3371ef66ffa2f11776aaad0cd99324ce7 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 10 Jan 2023 01:36:05 +0000 Subject: [PATCH 109/132] Save modified .toml if absolute paths were replaced with relative Previously this required adding or removing an account. --- CHANGELOG.md | 1 + src/accounts.rs | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf4f53026..942ac2a86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixes - fix: only send contact changed event for recently seen if it is relevant (not too old to matter) #3938 +- Immediately save `accounts.toml` if it was modified by a migration from absolute paths to relative paths #3943 ## 1.105.0 diff --git a/src/accounts.rs b/src/accounts.rs index 2380de04e..8de0a7da8 100644 --- a/src/accounts.rs +++ b/src/accounts.rs @@ -367,13 +367,20 @@ impl Config { // Previous versions of the core stored absolute paths in account config. // Convert them to relative paths. + let mut modified = false; for account in &mut inner.accounts { if let Ok(new_dir) = account.dir.strip_prefix(dir) { account.dir = new_dir.to_path_buf(); + modified = true; } } - Ok(Config { file, inner }) + let config = Self { file, inner }; + if modified { + config.sync().await?; + } + + Ok(config) } /// Loads all accounts defined in the configuration file. From 68cd8dbc3e605731913247c218eaf168e38f6ab3 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Tue, 10 Jan 2023 14:18:40 +0100 Subject: [PATCH 110/132] Only send IncomingMsgBunch if there are more than 0 new messages (#3941) --- CHANGELOG.md | 1 + src/imap.rs | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 942ac2a86..b754ae5ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased ### Changes +- Only send IncomingMsgBunch if there are more than 0 new messages #3941 ### API-Changes diff --git a/src/imap.rs b/src/imap.rs index 94d4223d2..8bf71b4c3 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -828,11 +828,13 @@ impl Imap { info!(context, "{} mails read from \"{}\".", read_cnt, folder); - let msg_ids = received_msgs + let msg_ids: Vec = received_msgs .iter() .flat_map(|m| m.msg_ids.clone()) .collect(); - context.emit_event(EventType::IncomingMsgBunch { msg_ids }); + if !msg_ids.is_empty() { + context.emit_event(EventType::IncomingMsgBunch { msg_ids }); + } chat::mark_old_messages_as_noticed(context, received_msgs).await?; From 5ae6c2539413212608dec8880bd734778e1740e3 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 10 Jan 2023 12:41:17 +0000 Subject: [PATCH 111/132] Remove aheader module dependency on mailparse --- src/aheader.rs | 31 +++---------------------------- src/decrypt.rs | 24 ++++++++++++++++++++---- 2 files changed, 23 insertions(+), 32 deletions(-) diff --git a/src/aheader.rs b/src/aheader.rs index 9899c1978..ac9a54f60 100644 --- a/src/aheader.rs +++ b/src/aheader.rs @@ -4,11 +4,9 @@ use anyhow::{bail, Context as _, Error, Result}; use std::collections::BTreeMap; +use std::fmt; use std::str::FromStr; -use std::{fmt, str}; -use crate::contact::addr_cmp; -use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::key::{DcKey, SignedPublicKey}; /// Possible values for encryption preference @@ -36,7 +34,7 @@ impl fmt::Display for EncryptPreference { } } -impl str::FromStr for EncryptPreference { +impl FromStr for EncryptPreference { type Err = Error; fn from_str(s: &str) -> Result { @@ -69,29 +67,6 @@ impl Aheader { prefer_encrypt, } } - - /// Tries to parse Autocrypt header. - /// - /// If there is none, returns None. If the header is present but cannot be parsed, returns an - /// error. - pub fn from_headers( - wanted_from: &str, - headers: &[mailparse::MailHeader<'_>], - ) -> Result> { - if let Some(value) = headers.get_header_value(HeaderDef::Autocrypt) { - let header = Self::from_str(&value)?; - if !addr_cmp(&header.addr, wanted_from) { - bail!( - "Autocrypt header address {:?} is not {:?}", - header.addr, - wanted_from - ); - } - Ok(Some(header)) - } else { - Ok(None) - } - } } impl fmt::Display for Aheader { @@ -118,7 +93,7 @@ impl fmt::Display for Aheader { } } -impl str::FromStr for Aheader { +impl FromStr for Aheader { type Err = Error; fn from_str(s: &str) -> Result { diff --git a/src/decrypt.rs b/src/decrypt.rs index 68bcba737..b7f831ab0 100644 --- a/src/decrypt.rs +++ b/src/decrypt.rs @@ -1,6 +1,7 @@ //! End-to-end decryption support. use std::collections::HashSet; +use std::str::FromStr; use anyhow::Result; use mailparse::ParsedMail; @@ -13,7 +14,6 @@ use crate::context::Context; use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey}; use crate::keyring::Keyring; -use crate::log::LogExt; use crate::peerstate::Peerstate; use crate::pgp; @@ -72,9 +72,25 @@ pub(crate) async fn prepare_decryption( }); } - let autocrypt_header = Aheader::from_headers(from, &mail.headers) - .ok_or_log_msg(context, "Failed to parse Autocrypt header") - .flatten(); + let autocrypt_header = + if let Some(autocrypt_header_value) = mail.headers.get_header_value(HeaderDef::Autocrypt) { + match Aheader::from_str(&autocrypt_header_value) { + Ok(header) if addr_cmp(&header.addr, from) => Some(header), + Ok(header) => { + warn!( + context, + "Autocrypt header address {:?} is not {:?}.", header.addr, from + ); + None + } + Err(err) => { + warn!(context, "Failed to parse Autocrypt header: {:#}.", err); + None + } + } + } else { + None + }; let dkim_results = handle_authres(context, mail, from, message_time).await?; From e215b4d919ced5631c16067061bff942bb6f35e8 Mon Sep 17 00:00:00 2001 From: link2xt Date: Mon, 9 Jan 2023 22:28:06 +0000 Subject: [PATCH 112/132] Return Option from Contact::add_or_lookup() This allows to distinguish exceptions, such as database errors, from invalid user input. For example, if the From: field of the message does not look like an email address, the mail should be ignored. But if there is a database failure while writing a new contact for the address, this error should be bubbled up. --- CHANGELOG.md | 2 ++ python/tests/test_1_online.py | 14 +++++++++ src/chat.rs | 12 +++++-- src/contact.rs | 56 ++++++++++++++++++++++++--------- src/imap.rs | 36 +++++++++++++++------ src/mimefactory.rs | 1 + src/peerstate.rs | 13 +++++--- src/qr.rs | 22 +++++++++---- src/reaction.rs | 1 + src/receive_imf.rs | 59 ++++++++++++++++++++++++++--------- src/receive_imf/tests.rs | 7 ++++- src/securejoin.rs | 3 +- src/test_utils.rs | 6 +++- 13 files changed, 176 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b754ae5ed..6383f3f7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ ### Fixes - fix: only send contact changed event for recently seen if it is relevant (not too old to matter) #3938 - Immediately save `accounts.toml` if it was modified by a migration from absolute paths to relative paths #3943 +- Do not treat invalid email addresses as an exception #3942 + ## 1.105.0 diff --git a/python/tests/test_1_online.py b/python/tests/test_1_online.py index 07da89134..954812d3b 100644 --- a/python/tests/test_1_online.py +++ b/python/tests/test_1_online.py @@ -939,6 +939,20 @@ def test_dont_show_emails(acfactory, lp): ac1.get_config("configured_addr") ), ) + ac1.direct_imap.append( + "Spam", + """ + From: delta + Subject: subj + To: {} + Message-ID: + Content-Type: text/plain; charset=utf-8 + + Unknown & malformed message in Spam + """.format( + ac1.get_config("configured_addr") + ), + ) ac1.direct_imap.append( "Spam", """ diff --git a/src/chat.rs b/src/chat.rs index 618319cb1..8a2fb2a42 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -4693,7 +4693,9 @@ mod tests { assert!(!shall_attach_selfavatar(&t, chat_id).await?); let (contact_id, _) = - Contact::add_or_lookup(&t, "", "foo@bar.org", Origin::IncomingUnknownTo).await?; + Contact::add_or_lookup(&t, "", "foo@bar.org", Origin::IncomingUnknownTo) + .await? + .unwrap(); add_contact_to_chat(&t, chat_id, contact_id).await?; assert!(!shall_attach_selfavatar(&t, chat_id).await?); t.set_config(Config::Selfavatar, None).await?; // setting to None also forces re-sending @@ -4940,7 +4942,9 @@ mod tests { bob.set_config(Config::ShowEmails, Some("2")).await?; let (contact_id, _) = - Contact::add_or_lookup(&alice, "", "bob@example.net", Origin::ManuallyCreated).await?; + Contact::add_or_lookup(&alice, "", "bob@example.net", Origin::ManuallyCreated) + .await? + .unwrap(); let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; let alice_chat = Chat::load_from_db(&alice, alice_chat_id).await?; @@ -5651,7 +5655,9 @@ mod tests { async fn test_create_for_contact_with_blocked() -> Result<()> { let t = TestContext::new().await; let (contact_id, _) = - Contact::add_or_lookup(&t, "", "foo@bar.org", Origin::ManuallyCreated).await?; + Contact::add_or_lookup(&t, "", "foo@bar.org", Origin::ManuallyCreated) + .await? + .unwrap(); // create a blocked chat let chat_id_orig = diff --git a/src/contact.rs b/src/contact.rs index 4f2189467..b0e33a52c 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -376,12 +376,14 @@ impl Contact { /// May result in a `#DC_EVENT_CONTACTS_CHANGED` event. pub async fn create(context: &Context, name: &str, addr: &str) -> Result { let name = improve_single_line_input(name); - ensure!(!addr.is_empty(), "Cannot create contact with empty address"); let (name, addr) = sanitize_name_and_addr(&name, addr); let (contact_id, sth_modified) = - Contact::add_or_lookup(context, &name, &addr, Origin::ManuallyCreated).await?; + Contact::add_or_lookup(context, &name, &addr, Origin::ManuallyCreated) + .await + .context("add_or_lookup")? + .with_context(|| format!("can't create a contact with address {:?}", addr))?; let blocked = Contact::is_blocked_load(context, contact_id).await?; match sth_modified { Modifier::None => {} @@ -466,12 +468,14 @@ impl Contact { /// Depending on the origin, both, "row_name" and "row_authname" are updated from "name". /// /// Returns the contact_id and a `Modifier` value indicating if a modification occurred. + /// + /// Returns None if the contact with such address cannot exist. pub(crate) async fn add_or_lookup( context: &Context, name: &str, addr: &str, mut origin: Origin, - ) -> Result<(ContactId, Modifier)> { + ) -> Result> { let mut sth_modified = Modifier::None; ensure!(!addr.is_empty(), "Can not add_or_lookup empty address"); @@ -480,7 +484,7 @@ impl Contact { let addr = addr_normalize(addr).to_string(); if context.is_self_addr(&addr).await? { - return Ok((ContactId::SELF, sth_modified)); + return Ok(Some((ContactId::SELF, sth_modified))); } if !may_be_valid_addr(&addr) { @@ -490,7 +494,7 @@ impl Contact { addr, if !name.is_empty() { name } else { "" }, ); - bail!("Bad address supplied: {:?}", addr); + return Ok(None); } let mut name = name; @@ -653,7 +657,7 @@ impl Contact { } } - Ok((ContactId::new(row_id), sth_modified)) + Ok(Some((ContactId::new(row_id), sth_modified))) } /// Add a number of contacts. @@ -686,7 +690,10 @@ impl Contact { "Failed to add address {} from address book: {}", addr, err ); } - Ok((_, modified)) => { + Ok(None) => { + warn!(context, "Cannot create contact with address {}.", addr); + } + Ok(Some((_, modified))) => { if modified != Modifier::None { modify_cnt += 1 } @@ -1697,7 +1704,8 @@ mod tests { "user@example.org", Origin::IncomingReplyTo, ) - .await?; + .await? + .unwrap(); assert_ne!(id, ContactId::UNDEFINED); let contact = Contact::load_from_db(&context.ctx, id).await.unwrap(); @@ -1725,7 +1733,8 @@ mod tests { "user@example.org", Origin::ManuallyCreated, ) - .await?; + .await? + .unwrap(); assert_eq!(contact_bob_id, id); assert_eq!(modified, Modifier::Modified); let contact = Contact::load_from_db(&context.ctx, id).await.unwrap(); @@ -1775,6 +1784,7 @@ mod tests { let (contact_id, sth_modified) = Contact::add_or_lookup(&t, "bla foo", "one@eins.org", Origin::IncomingUnknownTo) .await + .unwrap() .unwrap(); assert!(!contact_id.is_special()); assert_eq!(sth_modified, Modifier::Modified); @@ -1790,6 +1800,7 @@ mod tests { let (contact_id_test, sth_modified) = Contact::add_or_lookup(&t, "Real one", " one@eins.org ", Origin::ManuallyCreated) .await + .unwrap() .unwrap(); assert_eq!(contact_id, contact_id_test); assert_eq!(sth_modified, Modifier::Modified); @@ -1802,6 +1813,7 @@ mod tests { let (contact_id, sth_modified) = Contact::add_or_lookup(&t, "", "three@drei.sam", Origin::IncomingUnknownTo) .await + .unwrap() .unwrap(); assert!(!contact_id.is_special()); assert_eq!(sth_modified, Modifier::None); @@ -1819,6 +1831,7 @@ mod tests { Origin::IncomingUnknownFrom, ) .await + .unwrap() .unwrap(); assert_eq!(contact_id, contact_id_test); assert_eq!(sth_modified, Modifier::Modified); @@ -1830,6 +1843,7 @@ mod tests { let (contact_id_test, sth_modified) = Contact::add_or_lookup(&t, "schnucki", "three@drei.sam", Origin::ManuallyCreated) .await + .unwrap() .unwrap(); assert_eq!(contact_id, contact_id_test); assert_eq!(sth_modified, Modifier::Modified); @@ -1842,6 +1856,7 @@ mod tests { let (contact_id, sth_modified) = Contact::add_or_lookup(&t, "", "alice@w.de", Origin::IncomingUnknownTo) .await + .unwrap() .unwrap(); assert!(!contact_id.is_special()); assert_eq!(sth_modified, Modifier::None); @@ -1979,7 +1994,8 @@ mod tests { // Create Bob contact let (contact_id, _) = Contact::add_or_lookup(&alice, "Bob", "bob@example.net", Origin::ManuallyCreated) - .await?; + .await? + .unwrap(); let chat = alice .create_chat_with_contact("Bob", "bob@example.net") .await; @@ -2055,6 +2071,7 @@ mod tests { let (contact_id, sth_modified) = Contact::add_or_lookup(&t, "bob1", "bob@example.org", Origin::IncomingUnknownFrom) .await + .unwrap() .unwrap(); assert!(!contact_id.is_special()); assert_eq!(sth_modified, Modifier::Created); @@ -2067,6 +2084,7 @@ mod tests { let (contact_id, sth_modified) = Contact::add_or_lookup(&t, "bob2", "bob@example.org", Origin::IncomingUnknownFrom) .await + .unwrap() .unwrap(); assert!(!contact_id.is_special()); assert_eq!(sth_modified, Modifier::Modified); @@ -2089,6 +2107,7 @@ mod tests { let (contact_id, sth_modified) = Contact::add_or_lookup(&t, "bob4", "bob@example.org", Origin::IncomingUnknownFrom) .await + .unwrap() .unwrap(); assert!(!contact_id.is_special()); assert_eq!(sth_modified, Modifier::Modified); @@ -2118,6 +2137,7 @@ mod tests { Origin::IncomingUnknownFrom, ) .await + .unwrap() .unwrap(); assert_eq!(contact_id, contact_id_same); assert_eq!(sth_modified, Modifier::Modified); @@ -2134,6 +2154,7 @@ mod tests { Origin::IncomingUnknownFrom, ) .await + .unwrap() .unwrap(); assert_eq!(contact_id, contact_id_same); assert_eq!(sth_modified, Modifier::Modified); @@ -2154,7 +2175,8 @@ mod tests { // Incoming message from Bob. let (contact_id, sth_modified) = Contact::add_or_lookup(&t, "Bob", "bob@example.org", Origin::IncomingUnknownFrom) - .await?; + .await? + .unwrap(); assert_eq!(sth_modified, Modifier::Created); let contact = Contact::load_from_db(&t, contact_id).await?; assert_eq!(contact.get_display_name(), "Bob"); @@ -2162,7 +2184,8 @@ mod tests { // Incoming message from someone else with "Not Bob" in the "To:" field. let (contact_id_same, sth_modified) = Contact::add_or_lookup(&t, "Not Bob", "bob@example.org", Origin::IncomingUnknownTo) - .await?; + .await? + .unwrap(); assert_eq!(contact_id, contact_id_same); assert_eq!(sth_modified, Modifier::Modified); let contact = Contact::load_from_db(&t, contact_id).await?; @@ -2171,7 +2194,8 @@ mod tests { // Incoming message from Bob, changing the name back. let (contact_id_same, sth_modified) = Contact::add_or_lookup(&t, "Bob", "bob@example.org", Origin::IncomingUnknownFrom) - .await?; + .await? + .unwrap(); assert_eq!(contact_id, contact_id_same); assert_eq!(sth_modified, Modifier::Modified); // This was None until the bugfix let contact = Contact::load_from_db(&t, contact_id).await?; @@ -2312,7 +2336,8 @@ mod tests { let (contact_bob_id, _modified) = Contact::add_or_lookup(&alice, "Bob", "bob@example.net", Origin::ManuallyCreated) - .await?; + .await? + .unwrap(); let encrinfo = Contact::get_encrinfo(&alice, contact_bob_id).await?; assert_eq!(encrinfo, "No encryption"); @@ -2471,7 +2496,8 @@ CCCB 5AA9 F6E1 141C 9431 let (contact_id, _) = Contact::add_or_lookup(&alice, "Bob", "bob@example.net", Origin::ManuallyCreated) - .await?; + .await? + .unwrap(); let contact = Contact::load_from_db(&alice, contact_id).await?; assert_eq!(contact.last_seen(), 0); diff --git a/src/imap.rs b/src/imap.rs index 8bf71b4c3..c2bcd1f7a 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -1737,7 +1737,19 @@ async fn should_move_out_of_spam( }; // No chat found. let (from_id, blocked_contact, _origin) = - from_field_to_contact_id(context, &from, true).await?; + match from_field_to_contact_id(context, &from, true) + .await + .context("from_field_to_contact_id")? + { + Some(res) => res, + None => { + warn!( + context, + "Contact with From address {:?} cannot exist, not moving out of spam", from + ); + return Ok(false); + } + }; if blocked_contact { // Contact is blocked, leave the message in spam. return Ok(false); @@ -2027,7 +2039,10 @@ pub(crate) async fn prefetch_should_download( None => return Ok(false), }; let (_from_id, blocked_contact, origin) = - from_field_to_contact_id(context, &from, true).await?; + match from_field_to_contact_id(context, &from, true).await? { + Some(res) => res, + None => return Ok(false), + }; // prevent_rename=true as this might be a mailing list message and in this case it would be bad if we rename the contact. // (prevent_rename is the last argument of from_field_to_contact_id()) @@ -2347,14 +2362,14 @@ async fn add_all_recipients_as_contacts( .await .with_context(|| format!("could not select {}", mailbox))?; - let contacts = imap + let recipients = imap .get_all_recipients(context) .await .context("could not get recipients")?; let mut any_modified = false; - for contact in contacts { - let display_name_normalized = contact + for recipient in recipients { + let display_name_normalized = recipient .display_name .as_ref() .map(|s| normalize_name(s)) @@ -2363,17 +2378,20 @@ async fn add_all_recipients_as_contacts( match Contact::add_or_lookup( context, &display_name_normalized, - &contact.addr, + &recipient.addr, Origin::OutgoingTo, ) - .await + .await? { - Ok((_, modified)) => { + Some((_, modified)) => { if modified != Modifier::None { any_modified = true; } } - Err(e) => warn!(context, "Could not add recipient: {}", e), + None => warn!( + context, + "Could not add contact for recipient with address {:?}", recipient.addr + ), } } if any_modified { diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 5d7d51fe8..bde304869 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1821,6 +1821,7 @@ mod tests { Contact::add_or_lookup(&t, "Dave", "dave@example.com", Origin::ManuallyCreated) .await .unwrap() + .unwrap() .0; let chat_id = ChatId::create_for_contact(&t, contact_id).await.unwrap(); diff --git a/src/peerstate.rs b/src/peerstate.rs index 2699bf6eb..6224118d9 100644 --- a/src/peerstate.rs +++ b/src/peerstate.rs @@ -542,14 +542,17 @@ impl Peerstate { if (chat.typ == Chattype::Group && chat.is_protected()) || chat.typ == Chattype::Broadcast { - chat::remove_from_chat_contacts_table(context, *chat_id, contact_id).await?; - - let (new_contact_id, _) = + if let Some((new_contact_id, _)) = Contact::add_or_lookup(context, "", new_addr, Origin::IncomingUnknownFrom) + .await? + { + chat::remove_from_chat_contacts_table(context, *chat_id, contact_id) + .await?; + chat::add_to_chat_contacts_table(context, *chat_id, &[new_contact_id]) .await?; - chat::add_to_chat_contacts_table(context, *chat_id, &[new_contact_id]).await?; - context.emit_event(EventType::ChatModified(*chat_id)); + context.emit_event(EventType::ChatModified(*chat_id)); + } } } diff --git a/src/qr.rs b/src/qr.rs index 2813c762d..c1e250211 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -221,10 +221,15 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result { .context("Can't load peerstate")?; if let (Some(addr), Some(invitenumber), Some(authcode)) = (&addr, invitenumber, authcode) { - let contact_id = Contact::add_or_lookup(context, &name, addr, Origin::UnhandledQrScan) + let (contact_id, _) = Contact::add_or_lookup(context, &name, addr, Origin::UnhandledQrScan) .await - .map(|(id, _)| id) - .with_context(|| format!("failed to add or lookup contact for address {:?}", addr))?; + .with_context(|| format!("failed to add or lookup contact for address {:?}", addr))? + .with_context(|| { + format!( + "do not want to lookup contact for invalid address {:?}", + addr + ) + })?; if let (Some(grpid), Some(grpname)) = (grpid, grpname) { if context @@ -287,10 +292,13 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result { } } else if let Some(addr) = addr { if let Some(peerstate) = peerstate { - let contact_id = + let (contact_id, _) = Contact::add_or_lookup(context, &name, &peerstate.addr, Origin::UnhandledQrScan) .await - .map(|(id, _)| id)?; + .context("add_or_lookup")? + .with_context(|| { + format!("Not looking up contact for address {:?}", &peerstate.addr) + })?; let chat = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Request) .await .context("Failed to create (new) chat for contact")?; @@ -620,7 +628,9 @@ impl Qr { draft: Option, ) -> Result { let (contact_id, _) = - Contact::add_or_lookup(context, &name, &addr, Origin::UnhandledQrScan).await?; + Contact::add_or_lookup(context, &name, &addr, Origin::UnhandledQrScan) + .await? + .context("QR code does not contain a valid address")?; Ok(Qr::Addr { contact_id, draft }) } } diff --git a/src/reaction.rs b/src/reaction.rs index 72a096030..6777c5375 100644 --- a/src/reaction.rs +++ b/src/reaction.rs @@ -368,6 +368,7 @@ Can we chat at 1pm pacific, today?" let bob_id = Contact::add_or_lookup(&alice, "", "bob@example.net", Origin::ManuallyCreated) .await? + .unwrap() .0; let bob_reaction = reactions.get(bob_id); assert!(bob_reaction.is_empty()); // Bob has not reacted to message yet. diff --git a/src/receive_imf.rs b/src/receive_imf.rs index e8ad015af..c22cfb348 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -170,7 +170,16 @@ pub(crate) async fn receive_imf_inner( // If this is a mailing list email (i.e. list_id_header is some), don't change the displayname because in // a mailing list the sender displayname sometimes does not belong to the sender email address. let (from_id, _from_id_blocked, incoming_origin) = - from_field_to_contact_id(context, &mime_parser.from, prevent_rename).await?; + match from_field_to_contact_id(context, &mime_parser.from, prevent_rename).await? { + Some(contact_id_res) => contact_id_res, + None => { + warn!( + context, + "receive_imf: From field does not contain an acceptable address" + ); + return Ok(None); + } + }; let incoming = from_id != ContactId::SELF; @@ -373,22 +382,28 @@ pub async fn from_field_to_contact_id( context: &Context, from: &SingleInfo, prevent_rename: bool, -) -> Result<(ContactId, bool, Origin)> { +) -> Result> { let display_name = if prevent_rename { Some("") } else { from.display_name.as_deref() }; - let from_id = add_or_lookup_contact_by_addr( + let from_id = if let Some(from_id) = add_or_lookup_contact_by_addr( context, display_name, &from.addr, Origin::IncomingUnknownFrom, ) - .await?; + .await + .context("add_or_lookup_contact_by_addr")? + { + from_id + } else { + return Ok(None); + }; if from_id == ContactId::SELF { - Ok((ContactId::SELF, false, Origin::OutgoingBcc)) + Ok(Some((ContactId::SELF, false, Origin::OutgoingBcc))) } else { let mut from_id_blocked = false; let mut incoming_origin = Origin::Unknown; @@ -396,7 +411,7 @@ pub async fn from_field_to_contact_id( from_id_blocked = contact.blocked; incoming_origin = contact.origin; } - Ok((from_id, from_id_blocked, incoming_origin)) + Ok(Some((from_id, from_id_blocked, incoming_origin))) } } @@ -1928,8 +1943,11 @@ async fn apply_mailinglist_changes( } let listid = &chat.grpid; - let (contact_id, _) = - Contact::add_or_lookup(context, "", list_post, Origin::Hidden).await?; + let contact_id = + match Contact::add_or_lookup(context, "", list_post, Origin::Hidden).await? { + Some((contact_id, _)) => contact_id, + None => return Ok(()), + }; let mut contact = Contact::load_from_db(context, contact_id).await?; if contact.param.get(Param::ListId) != Some(listid) { contact.param.set(Param::ListId, listid); @@ -2268,28 +2286,39 @@ async fn add_or_lookup_contacts_by_address_list( continue; } let display_name = info.display_name.as_deref(); - contact_ids - .insert(add_or_lookup_contact_by_addr(context, display_name, addr, origin).await?); + if let Some(contact_id) = + add_or_lookup_contact_by_addr(context, display_name, addr, origin).await? + { + contact_ids.insert(contact_id); + } else { + warn!(context, "Contact with address {:?} cannot exist.", addr); + } } Ok(contact_ids.into_iter().collect::>()) } /// Add contacts to database on receiving messages. +/// +/// Returns `None` if the address can't be a valid email address. async fn add_or_lookup_contact_by_addr( context: &Context, display_name: Option<&str>, addr: &str, origin: Origin, -) -> Result { +) -> Result> { if context.is_self_addr(addr).await? { - return Ok(ContactId::SELF); + return Ok(Some(ContactId::SELF)); } let display_name_normalized = display_name.map(normalize_name).unwrap_or_default(); - let (row_id, _modified) = - Contact::add_or_lookup(context, &display_name_normalized, addr, origin).await?; - Ok(row_id) + if let Some((contact_id, _modified)) = + Contact::add_or_lookup(context, &display_name_normalized, addr, origin).await? + { + Ok(Some(contact_id)) + } else { + Ok(None) + } } #[cfg(test)] diff --git a/src/receive_imf/tests.rs b/src/receive_imf/tests.rs index a92c8aead..12a3901b0 100644 --- a/src/receive_imf/tests.rs +++ b/src/receive_imf/tests.rs @@ -429,6 +429,7 @@ async fn test_escaped_recipients() { Contact::add_or_lookup(&t, "Carl", "carl@host.tld", Origin::IncomingUnknownFrom) .await .unwrap() + .unwrap() .0; receive_imf( @@ -471,6 +472,7 @@ async fn test_cc_to_contact() { Contact::add_or_lookup(&t, "garabage", "carl@host.tld", Origin::IncomingUnknownFrom) .await .unwrap() + .unwrap() .0; receive_imf( @@ -2058,6 +2060,7 @@ async fn test_duplicate_message() -> Result<()> { Origin::IncomingUnknownFrom, ) .await? + .unwrap() .0; let first_message = b"Received: from [127.0.0.1] @@ -2111,6 +2114,7 @@ async fn test_ignore_footer_status_from_mailinglist() -> Result<()> { t.set_config(Config::ShowEmails, Some("2")).await?; let bob_id = Contact::add_or_lookup(&t, "", "bob@example.net", Origin::IncomingUnknownCc) .await? + .unwrap() .0; let bob = Contact::load_from_db(&t, bob_id).await?; assert_eq!(bob.get_status(), ""); @@ -2529,7 +2533,8 @@ Second thread."#; "fiona@example.net", Origin::IncomingUnknownTo, ) - .await?; + .await? + .unwrap(); chat::add_contact_to_chat(&alice, alice_first_msg.chat_id, alice_fiona_contact_id).await?; let alice_first_invite = alice.pop_sent_msg().await; diff --git a/src/securejoin.rs b/src/securejoin.rs index 77d743c17..f335f2e27 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -1006,7 +1006,8 @@ mod tests { "bob@example.net", Origin::ManuallyCreated, ) - .await?; + .await? + .unwrap(); let contact_bob = Contact::load_from_db(&alice.ctx, contact_bob_id).await?; assert_eq!( contact_bob.is_verified(&alice.ctx).await?, diff --git a/src/test_utils.rs b/src/test_utils.rs index cf19dafd8..4ae732591 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -529,7 +529,11 @@ impl TestContext { let (contact_id, modified) = Contact::add_or_lookup(self, &name, &addr, Origin::MailinglistAddress) .await - .unwrap(); + .expect("add_or_lookup") + .expect(&format!( + "contact with address {:?} cannot be created", + &addr + )); match modified { Modifier::None => (), Modifier::Modified => warn!(&self.ctx, "Contact {} modified by TestContext", &addr), From 1cde09c312f4dc2e72ee492548f4edab4a1a1752 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 6 Jan 2023 02:37:01 +0000 Subject: [PATCH 113/132] Add missing documentation to the `message` module --- src/message.rs | 112 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 104 insertions(+), 8 deletions(-) diff --git a/src/message.rs b/src/message.rs index b1581137f..63bf1450f 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1,7 +1,5 @@ //! # Messages and their identifiers. -#![allow(missing_docs)] - use std::collections::BTreeSet; use std::path::{Path, PathBuf}; @@ -237,11 +235,18 @@ impl Default for MessengerMessage { /// If you want an update, you have to recreate the object. #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct Message { + /// Message ID. pub(crate) id: MsgId, + + /// `From:` contact ID. pub(crate) from_id: ContactId, + + /// ID of the first contact in the `To:` header. pub(crate) to_id: ContactId, pub(crate) chat_id: ChatId, pub(crate) viewtype: Viewtype, + + /// State of the message. pub(crate) state: MessageState, pub(crate) download_state: DownloadState, pub(crate) hidden: bool, @@ -263,6 +268,7 @@ pub struct Message { } impl Message { + /// Creates a new message with given view type. pub fn new(viewtype: Viewtype) -> Self { Message { viewtype, @@ -270,6 +276,7 @@ impl Message { } } + /// Loads message with given ID from the database. pub async fn load_from_db(context: &Context, id: MsgId) -> Result { ensure!( !id.is_special(), @@ -366,6 +373,12 @@ impl Message { Ok(msg) } + /// Returns the MIME type of an attached file if it exists. + /// + /// If the MIME type is not known, the function guesses the MIME type + /// from the extension. `application/octet-stream` is used as a fallback + /// if MIME type is not known, but `None` is only returned if no file + /// is attached. pub fn get_filemime(&self) -> Option { if let Some(m) = self.param.get(Param::MimeType) { return Some(m.to_string()); @@ -380,11 +393,12 @@ impl Message { None } + /// Returns the full path to the file associated with a message. pub fn get_file(&self, context: &Context) -> Option { self.param.get_path(Param::File, context).unwrap_or(None) } - pub async fn try_calc_and_set_dimensions(&mut self, context: &Context) -> Result<()> { + pub(crate) async fn try_calc_and_set_dimensions(&mut self, context: &Context) -> Result<()> { if self.viewtype.has_file() { let file_param = self.param.get_path(Param::File, context)?; if let Some(path_and_filename) = file_param { @@ -442,6 +456,8 @@ impl Message { self.param.set_float(Param::SetLongitude, longitude); } + /// Returns the message timestamp for display in the UI + /// as a unix timestamp in seconds. pub fn get_timestamp(&self) -> i64 { if 0 != self.timestamp_sent { self.timestamp_sent @@ -450,10 +466,12 @@ impl Message { } } + /// Returns the message ID. pub fn get_id(&self) -> MsgId { self.id } + /// Returns the ID of the contact who wrote the message. pub fn get_from_id(&self) -> ContactId { self.from_id } @@ -463,30 +481,40 @@ impl Message { self.chat_id } + /// Returns the type of the message. pub fn get_viewtype(&self) -> Viewtype { self.viewtype } + /// Returns the state of the message. pub fn get_state(&self) -> MessageState { self.state } + /// Returns the message receive time as a unix timestamp in seconds. pub fn get_received_timestamp(&self) -> i64 { self.timestamp_rcvd } + /// Returns the timestamp of the message for sorting. pub fn get_sort_timestamp(&self) -> i64 { self.timestamp_sort } + /// Returns the text of the message. pub fn get_text(&self) -> Option { self.text.as_ref().map(|s| s.to_string()) } + /// Returns message subject. pub fn get_subject(&self) -> &str { &self.subject } + /// Returns base file name without the path. + /// The base file name includes the extension. + /// + /// To get the full path, use [`Self::get_file()`]. pub fn get_filename(&self) -> Option { self.param .get(Param::File) @@ -503,18 +531,22 @@ impl Message { } } + /// Returns width of associated image or video file. pub fn get_width(&self) -> i32 { self.param.get_int(Param::Width).unwrap_or_default() } + /// Returns height of associated image or video file. pub fn get_height(&self) -> i32 { self.param.get_int(Param::Height).unwrap_or_default() } + /// Returns duration of associated audio or video file. pub fn get_duration(&self) -> i32 { self.param.get_int(Param::Duration).unwrap_or_default() } + /// Returns true if padlock indicating message encryption should be displayed in the UI. pub fn get_showpadlock(&self) -> bool { self.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() != 0 } @@ -524,10 +556,12 @@ impl Message { self.param.get_bool(Param::Bot).unwrap_or_default() } + /// Return the ephemeral timer duration for a message. pub fn get_ephemeral_timer(&self) -> EphemeralTimer { self.ephemeral_timer } + /// Returns the timestamp of the epehemeral message removal. pub fn get_ephemeral_timestamp(&self) -> i64 { self.ephemeral_timestamp } @@ -565,6 +599,7 @@ impl Message { // C-data in the Java code (i.e. a `long` storing a C pointer) // - We can't make a param `SenderDisplayname` for messages as sometimes the display name of a contact changes, and we want to show // the same display name over all messages from the same sender. + /// Returns the name that should be shown over the message instead of the contact display ame. pub fn get_override_sender_name(&self) -> Option { self.param .get(Param::OverrideSenderDisplayname) @@ -573,11 +608,15 @@ impl Message { // Exposing this function over the ffi instead of get_override_sender_name() would mean that at least Android Java code has // to handle raw C-data (as it is done for msg_get_summary()) - pub fn get_sender_name(&self, contact: &Contact) -> String { + pub(crate) fn get_sender_name(&self, contact: &Contact) -> String { self.get_override_sender_name() .unwrap_or_else(|| contact.get_display_name().to_string()) } + /// Returns true if a message has a deviating timestamp. + /// + /// A message has a deviating timestamp when it is sent on + /// another day as received/sorted by. pub fn has_deviating_timestamp(&self) -> bool { let cnv_to_local = gm2local_offset(); let sort_timestamp = self.get_sort_timestamp() + cnv_to_local; @@ -586,14 +625,18 @@ impl Message { sort_timestamp / 86400 != send_timestamp / 86400 } + /// Returns true if the message was successfully delivered to the outgoing server or even + /// received a read receipt. pub fn is_sent(&self) -> bool { self.state >= MessageState::OutDelivered } + /// Returns true if the message is a forwarded message. pub fn is_forwarded(&self) -> bool { 0 != self.param.get_int(Param::Forwarded).unwrap_or_default() } + /// Returns true if the message is an informational message. pub fn is_info(&self) -> bool { let cmd = self.param.get_cmd(); self.from_id == ContactId::INFO @@ -601,10 +644,12 @@ impl Message { || cmd != SystemMessage::Unknown && cmd != SystemMessage::AutocryptSetupMessage } + /// Returns the type of an informational message. pub fn get_info_type(&self) -> SystemMessage { self.param.get_cmd() } + /// Returns true if the message is a system message. pub fn is_system_message(&self) -> bool { let cmd = self.param.get_cmd(); cmd != SystemMessage::Unknown @@ -622,6 +667,7 @@ impl Message { self.viewtype.has_file() && self.state == MessageState::OutPreparing } + /// Returns true if the message is an Autocrypt Setup Message. pub fn is_setupmessage(&self) -> bool { if self.viewtype != Viewtype::File { return false; @@ -630,6 +676,9 @@ impl Message { self.param.get_cmd() == SystemMessage::AutocryptSetupMessage } + /// Returns the first characters of the setup code. + /// + /// This is used to pre-fill the first entry field of the setup code. pub async fn get_setupcodebegin(&self, context: &Context) -> Option { if !self.is_setupmessage() { return None; @@ -650,7 +699,7 @@ impl Message { // add room to a webrtc_instance as defined by the corresponding config-value; // the result may still be prefixed by the type - pub fn create_webrtc_instance(instance: &str, room: &str) -> String { + pub(crate) fn create_webrtc_instance(instance: &str, room: &str) -> String { let (videochat_type, mut url) = Message::parse_webrtc_instance(instance); // make sure, there is a scheme in the url @@ -707,6 +756,7 @@ impl Message { } } + /// Returns videochat URL if the message is a videochat invitation. pub fn get_videochat_url(&self) -> Option { if self.viewtype == Viewtype::VideochatInvitation { if let Some(instance) = self.param.get(Param::WebrtcRoom) { @@ -716,6 +766,7 @@ impl Message { None } + /// Returns videochat type if the message is a videochat invitation. pub fn get_videochat_type(&self) -> Option { if self.viewtype == Viewtype::VideochatInvitation { if let Some(instance) = self.param.get(Param::WebrtcRoom) { @@ -725,10 +776,16 @@ impl Message { None } + /// Sets or unsets message text. pub fn set_text(&mut self, text: Option) { self.text = text; } + /// Sets the file associated with a message. + /// + /// This function does not use the file or check if it exists, + /// the file will only be used when the message is prepared + /// for sending. pub fn set_file(&mut self, file: impl ToString, filemime: Option<&str>) { self.param.set(Param::File, file); if let Some(filemime) = filemime { @@ -746,11 +803,13 @@ impl Message { } } + /// Sets the dimensions of associated image or video file. pub fn set_dimension(&mut self, width: i32, height: i32) { self.param.set_int(Param::Width, width); self.param.set_int(Param::Height, height); } + /// Sets the duration of associated audio or video file. pub fn set_duration(&mut self, duration: i32) { self.param.set_int(Param::Duration, duration); } @@ -760,6 +819,8 @@ impl Message { self.param.set_int(Param::Reaction, 1); } + /// Changes the message width, height or duration, + /// and stores it into the database. pub async fn latefiling_mediasize( &mut self, context: &Context, @@ -824,10 +885,12 @@ impl Message { Ok(()) } + /// Returns quoted message text, if any. pub fn quoted_text(&self) -> Option { self.param.get(Param::Quote).map(|s| s.to_string()) } + /// Returns quoted message, if any. pub async fn quoted_message(&self, context: &Context) -> Result> { if self.param.get(Param::Quote).is_some() && !self.is_forwarded() { return self.parent(context).await; @@ -835,6 +898,10 @@ impl Message { Ok(None) } + /// Returns parent message according to the `In-Reply-To` header + /// if it exists in the database and is not trashed. + /// + /// `References` header is not taken into account. pub async fn parent(&self, context: &Context) -> Result> { if let Some(in_reply_to) = &self.in_reply_to { if let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await? { @@ -855,6 +922,7 @@ impl Message { self.param.set_int(Param::ForcePlaintext, 1); } + /// Updates `param` column of the message in the database without changing other columns. pub async fn update_param(&self, context: &Context) -> Result<()> { context .sql @@ -894,6 +962,9 @@ impl Message { } } +/// State of the message. +/// For incoming messages, stores the information on whether the message was read or not. +/// For outgoing message, the message could be pending, already delivered or confirmed. #[derive( Debug, Clone, @@ -911,6 +982,7 @@ impl Message { )] #[repr(u32)] pub enum MessageState { + /// Undefined message state. Undefined = 0, /// Incoming *fresh* message. Fresh messages are neither noticed @@ -981,6 +1053,7 @@ impl std::fmt::Display for MessageState { } impl MessageState { + /// Returns true if the message can transition to `OutFailed` state from the current state. pub fn can_fail(self) -> bool { use MessageState::*; matches!( @@ -988,6 +1061,8 @@ impl MessageState { OutPreparing | OutPending | OutDelivered | OutMdnRcvd // OutMdnRcvd can still fail because it could be a group message and only some recipients failed. ) } + + /// Returns true for any outgoing message states. pub fn is_outgoing(self) -> bool { use MessageState::*; matches!( @@ -997,6 +1072,7 @@ impl MessageState { } } +/// Returns detailed message information in a multi-line text form. pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result { let msg = Message::load_from_db(context, msg_id).await?; let rawtxt: Option = context @@ -1161,7 +1237,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result { Ok(ret) } -pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> { +pub(crate) fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> { let extension: &str = &path.extension()?.to_str()?.to_lowercase(); let info = match extension { // before using viewtype other than Viewtype::File, @@ -1274,6 +1350,9 @@ pub async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result Result<()> { for msg_id in msg_ids.iter() { let msg = Message::load_from_db(context, *msg_id).await?; @@ -1321,6 +1400,7 @@ async fn delete_poi_location(context: &Context, location_id: u32) -> Result<()> Ok(()) } +/// Marks requested messages as seen. pub async fn markseen_msgs(context: &Context, msg_ids: Vec) -> Result<()> { if msg_ids.is_empty() { return Ok(()); @@ -1453,7 +1533,8 @@ pub(crate) async fn update_msg_state( // Context functions to work with messages -pub async fn exists(context: &Context, msg_id: MsgId) -> Result { +/// Returns true if given message ID exists in the database and is not trashed. +pub(crate) async fn exists(context: &Context, msg_id: MsgId) -> Result { if msg_id.is_special() { return Ok(false); } @@ -1470,7 +1551,7 @@ pub async fn exists(context: &Context, msg_id: MsgId) -> Result { } } -pub async fn set_msg_failed(context: &Context, msg_id: MsgId, error: &str) { +pub(crate) async fn set_msg_failed(context: &Context, msg_id: MsgId, error: &str) { if let Ok(mut msg) = Message::load_from_db(context, msg_id).await { if msg.state.can_fail() { msg.state = MessageState::OutFailed; @@ -1716,6 +1797,20 @@ pub async fn get_request_msg_cnt(context: &Context) -> usize { } } +/// Estimates the number of messages that will be deleted +/// by the options `delete_device_after` or `delete_server_after`. +/// This is typically used to show the estimated impact to the user +/// before actually enabling deletion of old messages. +/// +/// If `from_server` is true, +/// estimate deletion count for server, +/// otherwise estimate deletion count for device. +/// +/// Count messages older than the given number of `seconds`. +/// +/// Returns the number of messages that are older than the given number of seconds. +/// This includes e-mails downloaded due to the `show_emails` option. +/// Messages in the "saved messages" folder are not counted as they will not be deleted automatically. pub async fn estimate_deletion_cnt( context: &Context, from_server: bool, @@ -1804,6 +1899,7 @@ pub(crate) async fn rfc724_mid_exists( )] #[repr(u32)] pub enum Viewtype { + /// Unknown message type. Unknown = 0, /// Text message. From 554090b03e3b75f5af7a7df496204fed2fa5a2b1 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 10 Jan 2023 20:57:14 +0000 Subject: [PATCH 114/132] Prepare 1.106.0 --- CHANGELOG.md | 4 +--- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- deltachat-ffi/Cargo.toml | 2 +- deltachat-jsonrpc/Cargo.toml | 2 +- deltachat-jsonrpc/typescript/package.json | 2 +- deltachat-rpc-server/Cargo.toml | 2 +- package.json | 2 +- 8 files changed, 11 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6383f3f7b..5ef009c26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,10 @@ # Changelog -## Unreleased +## 1.106.0 ### Changes - Only send IncomingMsgBunch if there are more than 0 new messages #3941 -### API-Changes - ### Fixes - fix: only send contact changed event for recently seen if it is relevant (not too old to matter) #3938 - Immediately save `accounts.toml` if it was modified by a migration from absolute paths to relative paths #3943 diff --git a/Cargo.lock b/Cargo.lock index abe7cd0d1..8bc55496c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -873,7 +873,7 @@ checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" [[package]] name = "deltachat" -version = "1.105.0" +version = "1.106.0" dependencies = [ "ansi_term", "anyhow", @@ -947,7 +947,7 @@ dependencies = [ [[package]] name = "deltachat-jsonrpc" -version = "1.105.0" +version = "1.106.0" dependencies = [ "anyhow", "async-channel", @@ -969,7 +969,7 @@ dependencies = [ [[package]] name = "deltachat-rpc-server" -version = "1.105.0" +version = "1.106.0" dependencies = [ "anyhow", "deltachat-jsonrpc", @@ -992,7 +992,7 @@ dependencies = [ [[package]] name = "deltachat_ffi" -version = "1.105.0" +version = "1.106.0" dependencies = [ "anyhow", "deltachat", diff --git a/Cargo.toml b/Cargo.toml index 549d49dbe..2ba38945e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat" -version = "1.105.0" +version = "1.106.0" edition = "2021" license = "MPL-2.0" rust-version = "1.63" diff --git a/deltachat-ffi/Cargo.toml b/deltachat-ffi/Cargo.toml index c3c3dbdf1..3488ba82b 100644 --- a/deltachat-ffi/Cargo.toml +++ b/deltachat-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat_ffi" -version = "1.105.0" +version = "1.106.0" description = "Deltachat FFI" edition = "2018" readme = "README.md" diff --git a/deltachat-jsonrpc/Cargo.toml b/deltachat-jsonrpc/Cargo.toml index 63677512c..763548287 100644 --- a/deltachat-jsonrpc/Cargo.toml +++ b/deltachat-jsonrpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-jsonrpc" -version = "1.105.0" +version = "1.106.0" description = "DeltaChat JSON-RPC API" edition = "2021" default-run = "deltachat-jsonrpc-server" diff --git a/deltachat-jsonrpc/typescript/package.json b/deltachat-jsonrpc/typescript/package.json index 9c1df45e3..480685078 100644 --- a/deltachat-jsonrpc/typescript/package.json +++ b/deltachat-jsonrpc/typescript/package.json @@ -48,5 +48,5 @@ }, "type": "module", "types": "dist/deltachat.d.ts", - "version": "1.105.0" + "version": "1.106.0" } \ No newline at end of file diff --git a/deltachat-rpc-server/Cargo.toml b/deltachat-rpc-server/Cargo.toml index a45572b74..753b99c81 100644 --- a/deltachat-rpc-server/Cargo.toml +++ b/deltachat-rpc-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-rpc-server" -version = "1.105.0" +version = "1.106.0" description = "DeltaChat JSON-RPC server" edition = "2021" readme = "README.md" diff --git a/package.json b/package.json index 09b9e179d..25771812e 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.105.0" + "version": "1.106.0" } \ No newline at end of file From 6642083f52120ca1130753bef2ad17789a648f60 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 10 Jan 2023 21:17:30 +0000 Subject: [PATCH 115/132] Clippy fix --- src/test_utils.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/test_utils.rs b/src/test_utils.rs index 4ae732591..d8542fe2f 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -530,10 +530,7 @@ impl TestContext { Contact::add_or_lookup(self, &name, &addr, Origin::MailinglistAddress) .await .expect("add_or_lookup") - .expect(&format!( - "contact with address {:?} cannot be created", - &addr - )); + .unwrap_or_else(|| panic!("contact with address {:?} cannot be created", &addr)); match modified { Modifier::None => (), Modifier::Modified => warn!(&self.ctx, "Contact {} modified by TestContext", &addr), From 6d9d31cad13cb0314a94b31c92b474e1d0c57a86 Mon Sep 17 00:00:00 2001 From: iequidoo Date: Wed, 11 Jan 2023 11:55:42 -0300 Subject: [PATCH 116/132] Add timeouts to HTTP requests (#3908) --- CHANGELOG.md | 1 + src/configure/read_url.rs | 2 +- src/http.rs | 12 ++++++++++++ src/lib.rs | 1 + src/oauth2.rs | 11 +++++++++-- src/qr.rs | 2 +- 6 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 src/http.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ef009c26..f36e6cf43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - fix: only send contact changed event for recently seen if it is relevant (not too old to matter) #3938 - Immediately save `accounts.toml` if it was modified by a migration from absolute paths to relative paths #3943 - Do not treat invalid email addresses as an exception #3942 +- Add timeouts to HTTP requests #3948 ## 1.105.0 diff --git a/src/configure/read_url.rs b/src/configure/read_url.rs index f275025db..b0cdd989f 100644 --- a/src/configure/read_url.rs +++ b/src/configure/read_url.rs @@ -16,7 +16,7 @@ pub async fn read_url(context: &Context, url: &str) -> anyhow::Result { } pub async fn read_url_inner(context: &Context, url: &str) -> anyhow::Result { - let client = reqwest::Client::new(); + let client = crate::http::get_client()?; let mut url = url.to_string(); // Follow up to 10 http-redirects diff --git a/src/http.rs b/src/http.rs new file mode 100644 index 000000000..4f0c3fb35 --- /dev/null +++ b/src/http.rs @@ -0,0 +1,12 @@ +//! # HTTP module. + +use anyhow::Result; +use std::time::Duration; + +const HTTP_TIMEOUT: Duration = Duration::from_secs(30); + +pub(crate) fn get_client() -> Result { + Ok(reqwest::ClientBuilder::new() + .timeout(HTTP_TIMEOUT) + .build()?) +} diff --git a/src/lib.rs b/src/lib.rs index a6e8eb8ae..831f6609e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,6 +66,7 @@ mod decrypt; pub mod download; mod e2ee; pub mod ephemeral; +mod http; mod imap; pub mod imex; mod scheduler; diff --git a/src/oauth2.rs b/src/oauth2.rs index d48cb45a6..ab0b4eef5 100644 --- a/src/oauth2.rs +++ b/src/oauth2.rs @@ -158,7 +158,7 @@ pub async fn get_oauth2_access_token( } // ... and POST - let client = reqwest::Client::new(); + let client = crate::http::get_client()?; let response: Response = match client.post(post_url).form(&post_param).send().await { Ok(resp) => match resp.json().await { @@ -284,7 +284,14 @@ impl Oauth2 { // "verified_email": true, // "picture": "https://lh4.googleusercontent.com/-Gj5jh_9R0BY/AAAAAAAAAAI/AAAAAAAAAAA/IAjtjfjtjNA/photo.jpg" // } - let response = match reqwest::get(userinfo_url).await { + let client = match crate::http::get_client() { + Ok(cl) => cl, + Err(err) => { + warn!(context, "failed to get HTTP client: {}", err); + return None; + } + }; + let response = match client.get(userinfo_url).send().await { Ok(response) => response, Err(err) => { warn!(context, "failed to get userinfo: {}", err); diff --git a/src/qr.rs b/src/qr.rs index c1e250211..7cd4fc28c 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -381,7 +381,7 @@ struct CreateAccountErrorResponse { #[allow(clippy::indexing_slicing)] async fn set_account_from_qr(context: &Context, qr: &str) -> Result<()> { let url_str = &qr[DCACCOUNT_SCHEME.len()..]; - let response = reqwest::Client::new().post(url_str).send().await?; + let response = crate::http::get_client()?.post(url_str).send().await?; let response_status = response.status(); let response_text = response.text().await.with_context(|| { format!( From 89c8d26968a3a937abd9bb72ae573ab75ac54a8b Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 11 Jan 2023 04:19:28 +0000 Subject: [PATCH 117/132] Add ContactAddress type --- src/chat.rs | 30 ++-- src/contact.rs | 316 +++++++++++++++++++++++++-------------- src/imap.rs | 28 ++-- src/mimefactory.rs | 17 ++- src/peerstate.rs | 32 ++-- src/qr.rs | 48 +++--- src/reaction.rs | 16 +- src/receive_imf.rs | 69 +++++---- src/receive_imf/tests.rs | 55 +++---- src/securejoin.rs | 6 +- src/test_utils.rs | 10 +- 11 files changed, 373 insertions(+), 254 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index 8a2fb2a42..cef50d3d1 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -3629,7 +3629,7 @@ mod tests { use crate::chatlist::{get_archived_cnt, Chatlist}; use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS}; - use crate::contact::Contact; + use crate::contact::{Contact, ContactAddress}; use crate::receive_imf::receive_imf; use crate::test_utils::TestContext; @@ -4692,10 +4692,13 @@ mod tests { let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; assert!(!shall_attach_selfavatar(&t, chat_id).await?); - let (contact_id, _) = - Contact::add_or_lookup(&t, "", "foo@bar.org", Origin::IncomingUnknownTo) - .await? - .unwrap(); + let (contact_id, _) = Contact::add_or_lookup( + &t, + "", + ContactAddress::new("foo@bar.org")?, + Origin::IncomingUnknownTo, + ) + .await?; add_contact_to_chat(&t, chat_id, contact_id).await?; assert!(!shall_attach_selfavatar(&t, chat_id).await?); t.set_config(Config::Selfavatar, None).await?; // setting to None also forces re-sending @@ -4941,10 +4944,8 @@ mod tests { alice.set_config(Config::ShowEmails, Some("2")).await?; bob.set_config(Config::ShowEmails, Some("2")).await?; - let (contact_id, _) = - Contact::add_or_lookup(&alice, "", "bob@example.net", Origin::ManuallyCreated) - .await? - .unwrap(); + let alice_bob_contact = alice.add_or_lookup_contact(&bob).await; + let contact_id = alice_bob_contact.id; let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; let alice_chat = Chat::load_from_db(&alice, alice_chat_id).await?; @@ -5654,10 +5655,13 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_create_for_contact_with_blocked() -> Result<()> { let t = TestContext::new().await; - let (contact_id, _) = - Contact::add_or_lookup(&t, "", "foo@bar.org", Origin::ManuallyCreated) - .await? - .unwrap(); + let (contact_id, _) = Contact::add_or_lookup( + &t, + "", + ContactAddress::new("foo@bar.org")?, + Origin::ManuallyCreated, + ) + .await?; // create a blocked chat let chat_id_orig = diff --git a/src/contact.rs b/src/contact.rs index b0e33a52c..1f5233fbf 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -4,6 +4,7 @@ use std::cmp::Reverse; use std::collections::BinaryHeap; use std::convert::{TryFrom, TryInto}; use std::fmt; +use std::ops::Deref; use std::path::PathBuf; use std::time::{SystemTime, UNIX_EPOCH}; @@ -36,6 +37,51 @@ use crate::{chat, stock_str}; /// Time during which a contact is considered as seen recently. const SEEN_RECENTLY_SECONDS: i64 = 600; +/// Valid contact address. +#[derive(Debug, Clone, Copy)] +pub(crate) struct ContactAddress<'a>(&'a str); + +impl Deref for ContactAddress<'_> { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.0 + } +} + +impl AsRef for ContactAddress<'_> { + fn as_ref(&self) -> &str { + self.0 + } +} + +impl fmt::Display for ContactAddress<'_> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl<'a> ContactAddress<'a> { + /// Constructs a new contact address from string, + /// normalizing and validating it. + pub fn new(s: &'a str) -> Result { + let addr = addr_normalize(s); + if !may_be_valid_addr(addr) { + bail!("invalid address {:?}", s); + } + Ok(Self(addr)) + } +} + +/// Allow converting [`ContactAddress`] to an SQLite type. +impl rusqlite::types::ToSql for ContactAddress<'_> { + fn to_sql(&self) -> rusqlite::Result { + let val = rusqlite::types::Value::Text(self.0.to_string()); + let out = rusqlite::types::ToSqlOutput::Owned(val); + Ok(out) + } +} + /// Contact ID, including reserved IDs. /// /// Some contact IDs are reserved to identify special contacts. This @@ -378,12 +424,12 @@ impl Contact { let name = improve_single_line_input(name); let (name, addr) = sanitize_name_and_addr(&name, addr); + let addr = ContactAddress::new(&addr)?; let (contact_id, sth_modified) = - Contact::add_or_lookup(context, &name, &addr, Origin::ManuallyCreated) + Contact::add_or_lookup(context, &name, addr, Origin::ManuallyCreated) .await - .context("add_or_lookup")? - .with_context(|| format!("can't create a contact with address {:?}", addr))?; + .context("add_or_lookup")?; let blocked = Contact::is_blocked_load(context, contact_id).await?; match sth_modified { Modifier::None => {} @@ -473,28 +519,16 @@ impl Contact { pub(crate) async fn add_or_lookup( context: &Context, name: &str, - addr: &str, + addr: ContactAddress<'_>, mut origin: Origin, - ) -> Result> { + ) -> Result<(ContactId, Modifier)> { let mut sth_modified = Modifier::None; ensure!(!addr.is_empty(), "Can not add_or_lookup empty address"); ensure!(origin != Origin::Unknown, "Missing valid origin"); - let addr = addr_normalize(addr).to_string(); - if context.is_self_addr(&addr).await? { - return Ok(Some((ContactId::SELF, sth_modified))); - } - - if !may_be_valid_addr(&addr) { - warn!( - context, - "Bad address \"{}\" for contact \"{}\".", - addr, - if !name.is_empty() { name } else { "" }, - ); - return Ok(None); + return Ok((ContactId::SELF, sth_modified)); } let mut name = name; @@ -555,7 +589,7 @@ impl Contact { || row_authname.is_empty()); row_id = u32::try_from(id)?; - if origin as i32 >= row_origin as i32 && addr != row_addr { + if origin >= row_origin && addr.as_ref() != row_addr { update_addr = true; } if update_name || update_authname || update_addr || origin > row_origin { @@ -657,7 +691,7 @@ impl Contact { } } - Ok(Some((ContactId::new(row_id), sth_modified))) + Ok((ContactId::new(row_id), sth_modified)) } /// Add a number of contacts. @@ -683,21 +717,25 @@ impl Contact { for (name, addr) in split_address_book(addr_book).into_iter() { let (name, addr) = sanitize_name_and_addr(name, addr); let name = normalize_name(&name); - match Contact::add_or_lookup(context, &name, &addr, Origin::AddressBook).await { - Err(err) => { - warn!( - context, - "Failed to add address {} from address book: {}", addr, err - ); - } - Ok(None) => { - warn!(context, "Cannot create contact with address {}.", addr); - } - Ok(Some((_, modified))) => { - if modified != Modifier::None { - modify_cnt += 1 + match ContactAddress::new(&addr) { + Ok(addr) => { + match Contact::add_or_lookup(context, &name, addr, Origin::AddressBook).await { + Ok((_, modified)) => { + if modified != Modifier::None { + modify_cnt += 1 + } + } + Err(err) => { + warn!( + context, + "Failed to add address {} from address book: {}", addr, err + ); + } } } + Err(err) => { + warn!(context, "{:#}.", err); + } } } if modify_cnt > 0 { @@ -1701,11 +1739,10 @@ mod tests { let (id, _modified) = Contact::add_or_lookup( &context.ctx, "bob", - "user@example.org", + ContactAddress::new("user@example.org")?, Origin::IncomingReplyTo, ) - .await? - .unwrap(); + .await?; assert_ne!(id, ContactId::UNDEFINED); let contact = Contact::load_from_db(&context.ctx, id).await.unwrap(); @@ -1730,11 +1767,10 @@ mod tests { let (contact_bob_id, modified) = Contact::add_or_lookup( &context.ctx, "someone", - "user@example.org", + ContactAddress::new("user@example.org")?, Origin::ManuallyCreated, ) - .await? - .unwrap(); + .await?; assert_eq!(contact_bob_id, id); assert_eq!(modified, Modifier::Modified); let contact = Contact::load_from_db(&context.ctx, id).await.unwrap(); @@ -1766,6 +1802,18 @@ mod tests { Ok(()) } + #[test] + fn test_contact_address() -> Result<()> { + let alice_addr = "alice@example.org"; + let contact_address = ContactAddress::new(alice_addr)?; + assert_eq!(contact_address.as_ref(), alice_addr); + + let invalid_addr = "<> foobar"; + assert!(ContactAddress::new(invalid_addr).is_err()); + + Ok(()) + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_add_or_lookup() { // add some contacts, this also tests add_address_book() @@ -1781,11 +1829,14 @@ mod tests { assert_eq!(Contact::add_address_book(&t, book).await.unwrap(), 4); // check first added contact, this modifies authname because it is empty - let (contact_id, sth_modified) = - Contact::add_or_lookup(&t, "bla foo", "one@eins.org", Origin::IncomingUnknownTo) - .await - .unwrap() - .unwrap(); + let (contact_id, sth_modified) = Contact::add_or_lookup( + &t, + "bla foo", + ContactAddress::new("one@eins.org").unwrap(), + Origin::IncomingUnknownTo, + ) + .await + .unwrap(); assert!(!contact_id.is_special()); assert_eq!(sth_modified, Modifier::Modified); let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); @@ -1797,11 +1848,14 @@ mod tests { assert_eq!(contact.get_name_n_addr(), "Name one (one@eins.org)"); // modify first added contact - let (contact_id_test, sth_modified) = - Contact::add_or_lookup(&t, "Real one", " one@eins.org ", Origin::ManuallyCreated) - .await - .unwrap() - .unwrap(); + let (contact_id_test, sth_modified) = Contact::add_or_lookup( + &t, + "Real one", + ContactAddress::new(" one@eins.org ").unwrap(), + Origin::ManuallyCreated, + ) + .await + .unwrap(); assert_eq!(contact_id, contact_id_test); assert_eq!(sth_modified, Modifier::Modified); let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); @@ -1810,11 +1864,14 @@ mod tests { assert!(!contact.is_blocked()); // check third added contact (contact without name) - let (contact_id, sth_modified) = - Contact::add_or_lookup(&t, "", "three@drei.sam", Origin::IncomingUnknownTo) - .await - .unwrap() - .unwrap(); + let (contact_id, sth_modified) = Contact::add_or_lookup( + &t, + "", + ContactAddress::new("three@drei.sam").unwrap(), + Origin::IncomingUnknownTo, + ) + .await + .unwrap(); assert!(!contact_id.is_special()); assert_eq!(sth_modified, Modifier::None); let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); @@ -1827,11 +1884,10 @@ mod tests { let (contact_id_test, sth_modified) = Contact::add_or_lookup( &t, "m. serious", - "three@drei.sam", + ContactAddress::new("three@drei.sam").unwrap(), Origin::IncomingUnknownFrom, ) .await - .unwrap() .unwrap(); assert_eq!(contact_id, contact_id_test); assert_eq!(sth_modified, Modifier::Modified); @@ -1840,11 +1896,14 @@ mod tests { assert!(!contact.is_blocked()); // manually edit name of third contact (does not changed authorized name) - let (contact_id_test, sth_modified) = - Contact::add_or_lookup(&t, "schnucki", "three@drei.sam", Origin::ManuallyCreated) - .await - .unwrap() - .unwrap(); + let (contact_id_test, sth_modified) = Contact::add_or_lookup( + &t, + "schnucki", + ContactAddress::new("three@drei.sam").unwrap(), + Origin::ManuallyCreated, + ) + .await + .unwrap(); assert_eq!(contact_id, contact_id_test); assert_eq!(sth_modified, Modifier::Modified); let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); @@ -1853,11 +1912,14 @@ mod tests { assert!(!contact.is_blocked()); // Fourth contact: - let (contact_id, sth_modified) = - Contact::add_or_lookup(&t, "", "alice@w.de", Origin::IncomingUnknownTo) - .await - .unwrap() - .unwrap(); + let (contact_id, sth_modified) = Contact::add_or_lookup( + &t, + "", + ContactAddress::new("alice@w.de").unwrap(), + Origin::IncomingUnknownTo, + ) + .await + .unwrap(); assert!(!contact_id.is_special()); assert_eq!(sth_modified, Modifier::None); let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); @@ -1992,10 +2054,13 @@ mod tests { assert!(Contact::delete(&alice, ContactId::SELF).await.is_err()); // Create Bob contact - let (contact_id, _) = - Contact::add_or_lookup(&alice, "Bob", "bob@example.net", Origin::ManuallyCreated) - .await? - .unwrap(); + let (contact_id, _) = Contact::add_or_lookup( + &alice, + "Bob", + ContactAddress::new("bob@example.net")?, + Origin::ManuallyCreated, + ) + .await?; let chat = alice .create_chat_with_contact("Bob", "bob@example.net") .await; @@ -2068,11 +2133,14 @@ mod tests { let t = TestContext::new().await; // incoming mail `From: bob1 ` - this should init authname - let (contact_id, sth_modified) = - Contact::add_or_lookup(&t, "bob1", "bob@example.org", Origin::IncomingUnknownFrom) - .await - .unwrap() - .unwrap(); + let (contact_id, sth_modified) = Contact::add_or_lookup( + &t, + "bob1", + ContactAddress::new("bob@example.org").unwrap(), + Origin::IncomingUnknownFrom, + ) + .await + .unwrap(); assert!(!contact_id.is_special()); assert_eq!(sth_modified, Modifier::Created); let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); @@ -2081,11 +2149,14 @@ mod tests { assert_eq!(contact.get_display_name(), "bob1"); // incoming mail `From: bob2 ` - this should update authname - let (contact_id, sth_modified) = - Contact::add_or_lookup(&t, "bob2", "bob@example.org", Origin::IncomingUnknownFrom) - .await - .unwrap() - .unwrap(); + let (contact_id, sth_modified) = Contact::add_or_lookup( + &t, + "bob2", + ContactAddress::new("bob@example.org").unwrap(), + Origin::IncomingUnknownFrom, + ) + .await + .unwrap(); assert!(!contact_id.is_special()); assert_eq!(sth_modified, Modifier::Modified); let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); @@ -2104,11 +2175,14 @@ mod tests { assert_eq!(contact.get_display_name(), "bob3"); // incoming mail `From: bob4 ` - this should update authname, manually given name is still "bob3" - let (contact_id, sth_modified) = - Contact::add_or_lookup(&t, "bob4", "bob@example.org", Origin::IncomingUnknownFrom) - .await - .unwrap() - .unwrap(); + let (contact_id, sth_modified) = Contact::add_or_lookup( + &t, + "bob4", + ContactAddress::new("bob@example.org").unwrap(), + Origin::IncomingUnknownFrom, + ) + .await + .unwrap(); assert!(!contact_id.is_special()); assert_eq!(sth_modified, Modifier::Modified); let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); @@ -2133,11 +2207,10 @@ mod tests { let (contact_id_same, sth_modified) = Contact::add_or_lookup( &t, "claire1", - "claire@example.org", + ContactAddress::new("claire@example.org").unwrap(), Origin::IncomingUnknownFrom, ) .await - .unwrap() .unwrap(); assert_eq!(contact_id, contact_id_same); assert_eq!(sth_modified, Modifier::Modified); @@ -2150,11 +2223,10 @@ mod tests { let (contact_id_same, sth_modified) = Contact::add_or_lookup( &t, "claire2", - "claire@example.org", + ContactAddress::new("claire@example.org").unwrap(), Origin::IncomingUnknownFrom, ) .await - .unwrap() .unwrap(); assert_eq!(contact_id, contact_id_same); assert_eq!(sth_modified, Modifier::Modified); @@ -2173,29 +2245,38 @@ mod tests { let t = TestContext::new().await; // Incoming message from Bob. - let (contact_id, sth_modified) = - Contact::add_or_lookup(&t, "Bob", "bob@example.org", Origin::IncomingUnknownFrom) - .await? - .unwrap(); + let (contact_id, sth_modified) = Contact::add_or_lookup( + &t, + "Bob", + ContactAddress::new("bob@example.org")?, + Origin::IncomingUnknownFrom, + ) + .await?; assert_eq!(sth_modified, Modifier::Created); let contact = Contact::load_from_db(&t, contact_id).await?; assert_eq!(contact.get_display_name(), "Bob"); // Incoming message from someone else with "Not Bob" in the "To:" field. - let (contact_id_same, sth_modified) = - Contact::add_or_lookup(&t, "Not Bob", "bob@example.org", Origin::IncomingUnknownTo) - .await? - .unwrap(); + let (contact_id_same, sth_modified) = Contact::add_or_lookup( + &t, + "Not Bob", + ContactAddress::new("bob@example.org")?, + Origin::IncomingUnknownTo, + ) + .await?; assert_eq!(contact_id, contact_id_same); assert_eq!(sth_modified, Modifier::Modified); let contact = Contact::load_from_db(&t, contact_id).await?; assert_eq!(contact.get_display_name(), "Not Bob"); // Incoming message from Bob, changing the name back. - let (contact_id_same, sth_modified) = - Contact::add_or_lookup(&t, "Bob", "bob@example.org", Origin::IncomingUnknownFrom) - .await? - .unwrap(); + let (contact_id_same, sth_modified) = Contact::add_or_lookup( + &t, + "Bob", + ContactAddress::new("bob@example.org")?, + Origin::IncomingUnknownFrom, + ) + .await?; assert_eq!(contact_id, contact_id_same); assert_eq!(sth_modified, Modifier::Modified); // This was None until the bugfix let contact = Contact::load_from_db(&t, contact_id).await?; @@ -2218,9 +2299,14 @@ mod tests { assert_eq!(contact.get_display_name(), "dave1"); // incoming mail `From: dave2 ` - this should update authname - Contact::add_or_lookup(&t, "dave2", "dave@example.org", Origin::IncomingUnknownFrom) - .await - .unwrap(); + Contact::add_or_lookup( + &t, + "dave2", + ContactAddress::new("dave@example.org").unwrap(), + Origin::IncomingUnknownFrom, + ) + .await + .unwrap(); let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); assert_eq!(contact.get_authname(), "dave2"); assert_eq!(contact.get_name(), "dave1"); @@ -2334,10 +2420,13 @@ mod tests { let encrinfo = Contact::get_encrinfo(&alice, ContactId::DEVICE).await; assert!(encrinfo.is_err()); - let (contact_bob_id, _modified) = - Contact::add_or_lookup(&alice, "Bob", "bob@example.net", Origin::ManuallyCreated) - .await? - .unwrap(); + let (contact_bob_id, _modified) = Contact::add_or_lookup( + &alice, + "Bob", + ContactAddress::new("bob@example.net")?, + Origin::ManuallyCreated, + ) + .await?; let encrinfo = Contact::get_encrinfo(&alice, contact_bob_id).await?; assert_eq!(encrinfo, "No encryption"); @@ -2494,10 +2583,13 @@ CCCB 5AA9 F6E1 141C 9431 async fn test_last_seen() -> Result<()> { let alice = TestContext::new_alice().await; - let (contact_id, _) = - Contact::add_or_lookup(&alice, "Bob", "bob@example.net", Origin::ManuallyCreated) - .await? - .unwrap(); + let (contact_id, _) = Contact::add_or_lookup( + &alice, + "Bob", + ContactAddress::new("bob@example.net")?, + Origin::ManuallyCreated, + ) + .await?; let contact = Contact::load_from_db(&alice, contact_id).await?; assert_eq!(contact.last_seen(), 0); diff --git a/src/imap.rs b/src/imap.rs index c2bcd1f7a..fe8e3a1fb 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -22,7 +22,7 @@ use crate::config::Config; use crate::constants::{ Blocked, Chattype, ShowEmails, DC_FETCH_EXISTING_MSGS_COUNT, DC_FOLDERS_CONFIGURED_VERSION, }; -use crate::contact::{normalize_name, Contact, ContactId, Modifier, Origin}; +use crate::contact::{normalize_name, Contact, ContactAddress, ContactId, Modifier, Origin}; use crate::context::Context; use crate::events::EventType; use crate::headerdef::{HeaderDef, HeaderDefMap}; @@ -2375,23 +2375,23 @@ async fn add_all_recipients_as_contacts( .map(|s| normalize_name(s)) .unwrap_or_default(); - match Contact::add_or_lookup( - context, - &display_name_normalized, - &recipient.addr, - Origin::OutgoingTo, - ) - .await? - { - Some((_, modified)) => { + match ContactAddress::new(&recipient.addr) { + Err(err) => warn!( + context, + "Could not add contact for recipient with address {:?}: {:#}", recipient.addr, err + ), + Ok(recipient_addr) => { + let (_, modified) = Contact::add_or_lookup( + context, + &display_name_normalized, + recipient_addr, + Origin::OutgoingTo, + ) + .await?; if modified != Modifier::None { any_modified = true; } } - None => warn!( - context, - "Could not add contact for recipient with address {:?}", recipient.addr - ), } } if any_modified { diff --git a/src/mimefactory.rs b/src/mimefactory.rs index bde304869..7025ad52c 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1490,7 +1490,7 @@ mod tests { ProtectionStatus, }; use crate::chatlist::Chatlist; - use crate::contact::Origin; + use crate::contact::{ContactAddress, Origin}; use crate::mimeparser::MimeMessage; use crate::receive_imf::receive_imf; use crate::test_utils::{get_chat_msg, TestContext}; @@ -1817,12 +1817,15 @@ mod tests { } async fn first_subject_str(t: TestContext) -> String { - let contact_id = - Contact::add_or_lookup(&t, "Dave", "dave@example.com", Origin::ManuallyCreated) - .await - .unwrap() - .unwrap() - .0; + let contact_id = Contact::add_or_lookup( + &t, + "Dave", + ContactAddress::new("dave@example.com").unwrap(), + Origin::ManuallyCreated, + ) + .await + .unwrap() + .0; let chat_id = ChatId::create_for_contact(&t, contact_id).await.unwrap(); diff --git a/src/peerstate.rs b/src/peerstate.rs index 6224118d9..761fec50f 100644 --- a/src/peerstate.rs +++ b/src/peerstate.rs @@ -9,7 +9,7 @@ use crate::aheader::{Aheader, EncryptPreference}; use crate::chat::{self, Chat}; use crate::chatlist::Chatlist; use crate::constants::Chattype; -use crate::contact::{addr_cmp, Contact, Origin}; +use crate::contact::{addr_cmp, Contact, ContactAddress, Origin}; use crate::context::Context; use crate::events::EventType; use crate::key::{DcKey, Fingerprint, SignedPublicKey}; @@ -542,16 +542,30 @@ impl Peerstate { if (chat.typ == Chattype::Group && chat.is_protected()) || chat.typ == Chattype::Broadcast { - if let Some((new_contact_id, _)) = - Contact::add_or_lookup(context, "", new_addr, Origin::IncomingUnknownFrom) - .await? - { - chat::remove_from_chat_contacts_table(context, *chat_id, contact_id) - .await?; - chat::add_to_chat_contacts_table(context, *chat_id, &[new_contact_id]) + match ContactAddress::new(new_addr) { + Ok(new_addr) => { + let (new_contact_id, _) = Contact::add_or_lookup( + context, + "", + new_addr, + Origin::IncomingUnknownFrom, + ) .await?; + chat::remove_from_chat_contacts_table(context, *chat_id, contact_id) + .await?; + chat::add_to_chat_contacts_table(context, *chat_id, &[new_contact_id]) + .await?; - context.emit_event(EventType::ChatModified(*chat_id)); + context.emit_event(EventType::ChatModified(*chat_id)); + } + Err(err) => { + warn!( + context, + "New address {:?} is not vaild, not doing AEAP: {:#}.", + new_addr, + err + ) + } } } } diff --git a/src/qr.rs b/src/qr.rs index 7cd4fc28c..c6b5e18b7 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -14,7 +14,9 @@ use std::collections::BTreeMap; use crate::chat::{self, get_chat_id_by_grpid, ChatIdBlocked}; use crate::config::Config; use crate::constants::Blocked; -use crate::contact::{addr_normalize, may_be_valid_addr, Contact, ContactId, Origin}; +use crate::contact::{ + addr_normalize, may_be_valid_addr, Contact, ContactAddress, ContactId, Origin, +}; use crate::context::Context; use crate::key::Fingerprint; use crate::message::Message; @@ -221,19 +223,14 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result { .context("Can't load peerstate")?; if let (Some(addr), Some(invitenumber), Some(authcode)) = (&addr, invitenumber, authcode) { + let addr = ContactAddress::new(addr)?; let (contact_id, _) = Contact::add_or_lookup(context, &name, addr, Origin::UnhandledQrScan) .await - .with_context(|| format!("failed to add or lookup contact for address {:?}", addr))? - .with_context(|| { - format!( - "do not want to lookup contact for invalid address {:?}", - addr - ) - })?; + .with_context(|| format!("failed to add or lookup contact for address {:?}", addr))?; if let (Some(grpid), Some(grpname)) = (grpid, grpname) { if context - .is_self_addr(addr) + .is_self_addr(&addr) .await .with_context(|| format!("can't check if address {:?} is our address", addr))? { @@ -266,7 +263,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result { authcode, }) } - } else if context.is_self_addr(addr).await? { + } else if context.is_self_addr(&addr).await? { if token::exists(context, token::Namespace::InviteNumber, &invitenumber).await { Ok(Qr::WithdrawVerifyContact { contact_id, @@ -292,13 +289,11 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result { } } else if let Some(addr) = addr { if let Some(peerstate) = peerstate { + let peerstate_addr = ContactAddress::new(&peerstate.addr)?; let (contact_id, _) = - Contact::add_or_lookup(context, &name, &peerstate.addr, Origin::UnhandledQrScan) + Contact::add_or_lookup(context, &name, peerstate_addr, Origin::UnhandledQrScan) .await - .context("add_or_lookup")? - .with_context(|| { - format!("Not looking up contact for address {:?}", &peerstate.addr) - })?; + .context("add_or_lookup")?; let chat = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Request) .await .context("Failed to create (new) chat for contact")?; @@ -538,11 +533,11 @@ async fn decode_mailto(context: &Context, qr: &str) -> Result { }; let addr = normalize_address(addr)?; - let name = "".to_string(); + let name = ""; Qr::from_address( context, name, - addr, + &addr, if draft.is_empty() { None } else { Some(draft) }, ) .await @@ -562,8 +557,8 @@ async fn decode_smtp(context: &Context, qr: &str) -> Result { }; let addr = normalize_address(addr)?; - let name = "".to_string(); - Qr::from_address(context, name, addr, None).await + let name = ""; + Qr::from_address(context, name, &addr, None).await } /// Extract address for the matmsg scheme. @@ -587,8 +582,8 @@ async fn decode_matmsg(context: &Context, qr: &str) -> Result { }; let addr = normalize_address(addr)?; - let name = "".to_string(); - Qr::from_address(context, name, addr, None).await + let name = ""; + Qr::from_address(context, name, &addr, None).await } static VCARD_NAME_RE: Lazy = @@ -617,20 +612,19 @@ async fn decode_vcard(context: &Context, qr: &str) -> Result { bail!("Bad e-mail address"); }; - Qr::from_address(context, name, addr, None).await + Qr::from_address(context, &name, &addr, None).await } impl Qr { pub async fn from_address( context: &Context, - name: String, - addr: String, + name: &str, + addr: &str, draft: Option, ) -> Result { + let addr = ContactAddress::new(addr)?; let (contact_id, _) = - Contact::add_or_lookup(context, &name, &addr, Origin::UnhandledQrScan) - .await? - .context("QR code does not contain a valid address")?; + Contact::add_or_lookup(context, name, addr, Origin::UnhandledQrScan).await?; Ok(Qr::Addr { contact_id, draft }) } } diff --git a/src/reaction.rs b/src/reaction.rs index 6777c5375..f858f6172 100644 --- a/src/reaction.rs +++ b/src/reaction.rs @@ -286,11 +286,11 @@ pub async fn get_msg_reactions(context: &Context, msg_id: MsgId) -> Result from_addr, + Err(err) => { + warn!( + context, + "Cannot create a contact for the given From field: {:#}.", err + ); + return Ok(None); + } + }; + + let from_id = add_or_lookup_contact_by_addr( context, display_name, - &from.addr, + from_addr, Origin::IncomingUnknownFrom, ) - .await - .context("add_or_lookup_contact_by_addr")? - { - from_id - } else { - return Ok(None); - }; + .await?; if from_id == ContactId::SELF { Ok(Some((ContactId::SELF, false, Origin::OutgoingBcc))) @@ -1943,11 +1950,15 @@ async fn apply_mailinglist_changes( } let listid = &chat.grpid; - let contact_id = - match Contact::add_or_lookup(context, "", list_post, Origin::Hidden).await? { - Some((contact_id, _)) => contact_id, - None => return Ok(()), - }; + let list_post = match ContactAddress::new(list_post) { + Ok(list_post) => list_post, + Err(err) => { + warn!(context, "Invalid List-Post: {:#}.", err); + return Ok(()); + } + }; + let (contact_id, _) = + Contact::add_or_lookup(context, "", list_post, Origin::Hidden).await?; let mut contact = Contact::load_from_db(context, contact_id).await?; if contact.param.get(Param::ListId) != Some(listid) { contact.param.set(Param::ListId, listid); @@ -1955,7 +1966,7 @@ async fn apply_mailinglist_changes( } if let Some(old_list_post) = chat.param.get(Param::ListPost) { - if list_post != old_list_post { + if list_post.as_ref() != old_list_post { // Apparently the mailing list is using a different List-Post header in each message. // Make the mailing list read-only because we would't know which message the user wants to reply to. chat.param.remove(Param::ListPost); @@ -2286,9 +2297,9 @@ async fn add_or_lookup_contacts_by_address_list( continue; } let display_name = info.display_name.as_deref(); - if let Some(contact_id) = - add_or_lookup_contact_by_addr(context, display_name, addr, origin).await? - { + if let Ok(addr) = ContactAddress::new(addr) { + let contact_id = + add_or_lookup_contact_by_addr(context, display_name, addr, origin).await?; contact_ids.insert(contact_id); } else { warn!(context, "Contact with address {:?} cannot exist.", addr); @@ -2299,26 +2310,20 @@ async fn add_or_lookup_contacts_by_address_list( } /// Add contacts to database on receiving messages. -/// -/// Returns `None` if the address can't be a valid email address. async fn add_or_lookup_contact_by_addr( context: &Context, display_name: Option<&str>, - addr: &str, + addr: ContactAddress<'_>, origin: Origin, -) -> Result> { - if context.is_self_addr(addr).await? { - return Ok(Some(ContactId::SELF)); +) -> Result { + if context.is_self_addr(&addr).await? { + return Ok(ContactId::SELF); } let display_name_normalized = display_name.map(normalize_name).unwrap_or_default(); - if let Some((contact_id, _modified)) = - Contact::add_or_lookup(context, &display_name_normalized, addr, origin).await? - { - Ok(Some(contact_id)) - } else { - Ok(None) - } + let (contact_id, _modified) = + Contact::add_or_lookup(context, &display_name_normalized, addr, origin).await?; + Ok(contact_id) } #[cfg(test)] diff --git a/src/receive_imf/tests.rs b/src/receive_imf/tests.rs index 12a3901b0..cd5dcd30e 100644 --- a/src/receive_imf/tests.rs +++ b/src/receive_imf/tests.rs @@ -425,12 +425,15 @@ async fn test_escaped_recipients() { .await .unwrap(); - let carl_contact_id = - Contact::add_or_lookup(&t, "Carl", "carl@host.tld", Origin::IncomingUnknownFrom) - .await - .unwrap() - .unwrap() - .0; + let carl_contact_id = Contact::add_or_lookup( + &t, + "Carl", + ContactAddress::new("carl@host.tld").unwrap(), + Origin::IncomingUnknownFrom, + ) + .await + .unwrap() + .0; receive_imf( &t, @@ -468,12 +471,15 @@ async fn test_cc_to_contact() { .await .unwrap(); - let carl_contact_id = - Contact::add_or_lookup(&t, "garabage", "carl@host.tld", Origin::IncomingUnknownFrom) - .await - .unwrap() - .unwrap() - .0; + let carl_contact_id = Contact::add_or_lookup( + &t, + "garabage", + ContactAddress::new("carl@host.tld").unwrap(), + Origin::IncomingUnknownFrom, + ) + .await + .unwrap() + .0; receive_imf( &t, @@ -2056,11 +2062,10 @@ async fn test_duplicate_message() -> Result<()> { let bob_contact_id = Contact::add_or_lookup( &alice, "Bob", - "bob@example.org", + ContactAddress::new("bob@example.org").unwrap(), Origin::IncomingUnknownFrom, ) .await? - .unwrap() .0; let first_message = b"Received: from [127.0.0.1] @@ -2112,10 +2117,14 @@ Second signature"; async fn test_ignore_footer_status_from_mailinglist() -> Result<()> { let t = TestContext::new_alice().await; t.set_config(Config::ShowEmails, Some("2")).await?; - let bob_id = Contact::add_or_lookup(&t, "", "bob@example.net", Origin::IncomingUnknownCc) - .await? - .unwrap() - .0; + let bob_id = Contact::add_or_lookup( + &t, + "", + ContactAddress::new("bob@example.net").unwrap(), + Origin::IncomingUnknownCc, + ) + .await? + .0; let bob = Contact::load_from_db(&t, bob_id).await?; assert_eq!(bob.get_status(), ""); assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 0); @@ -2527,14 +2536,8 @@ Second thread."#; // Alice adds Fiona to both ad hoc groups. let fiona = TestContext::new_fiona().await; - let (alice_fiona_contact_id, _) = Contact::add_or_lookup( - &alice, - "Fiona", - "fiona@example.net", - Origin::IncomingUnknownTo, - ) - .await? - .unwrap(); + let alice_fiona_contact = alice.add_or_lookup_contact(&fiona).await; + let alice_fiona_contact_id = alice_fiona_contact.id; chat::add_contact_to_chat(&alice, alice_first_msg.chat_id, alice_fiona_contact_id).await?; let alice_first_invite = alice.pop_sent_msg().await; diff --git a/src/securejoin.rs b/src/securejoin.rs index f335f2e27..420966ceb 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -710,6 +710,7 @@ mod tests { use crate::chat::ProtectionStatus; use crate::chatlist::Chatlist; use crate::constants::{Chattype, DC_GCM_ADDDAYMARKER}; + use crate::contact::ContactAddress; use crate::peerstate::Peerstate; use crate::receive_imf::receive_imf; use crate::test_utils::{TestContext, TestContextManager}; @@ -1003,11 +1004,10 @@ mod tests { let (contact_bob_id, _modified) = Contact::add_or_lookup( &alice.ctx, "Bob", - "bob@example.net", + ContactAddress::new("bob@example.net")?, Origin::ManuallyCreated, ) - .await? - .unwrap(); + .await?; let contact_bob = Contact::load_from_db(&alice.ctx, contact_bob_id).await?; assert_eq!( contact_bob.is_verified(&alice.ctx).await?, diff --git a/src/test_utils.rs b/src/test_utils.rs index d8542fe2f..f5bce1cf3 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -23,7 +23,7 @@ use crate::chatlist::Chatlist; use crate::config::Config; use crate::constants::Chattype; use crate::constants::{DC_GCL_NO_SPECIALS, DC_GCM_ADDDAYMARKER, DC_MSG_ID_DAYMARKER}; -use crate::contact::{Contact, ContactId, Modifier, Origin}; +use crate::contact::{Contact, ContactAddress, ContactId, Modifier, Origin}; use crate::context::Context; use crate::events::{Event, EventType, Events}; use crate::key::{self, DcKey, KeyPair, KeyPairUse}; @@ -523,14 +523,14 @@ impl TestContext { .await .unwrap_or_default() .unwrap_or_default(); - let addr = other.ctx.get_primary_self_addr().await.unwrap(); + let primary_self_addr = other.ctx.get_primary_self_addr().await.unwrap(); + let addr = ContactAddress::new(&primary_self_addr).unwrap(); // MailinglistAddress is the lowest allowed origin, we'd prefer to not modify the // origin when creating this contact. let (contact_id, modified) = - Contact::add_or_lookup(self, &name, &addr, Origin::MailinglistAddress) + Contact::add_or_lookup(self, &name, addr, Origin::MailinglistAddress) .await - .expect("add_or_lookup") - .unwrap_or_else(|| panic!("contact with address {:?} cannot be created", &addr)); + .expect("add_or_lookup"); match modified { Modifier::None => (), Modifier::Modified => warn!(&self.ctx, "Contact {} modified by TestContext", &addr), From 790512d52a01da18eb75e11ed01971f8f3c55e18 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 11 Jan 2023 23:19:07 +0000 Subject: [PATCH 118/132] Reduce code indentation --- src/imap.rs | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/imap.rs b/src/imap.rs index fe8e3a1fb..f8b75574f 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -2375,23 +2375,28 @@ async fn add_all_recipients_as_contacts( .map(|s| normalize_name(s)) .unwrap_or_default(); - match ContactAddress::new(&recipient.addr) { - Err(err) => warn!( - context, - "Could not add contact for recipient with address {:?}: {:#}", recipient.addr, err - ), - Ok(recipient_addr) => { - let (_, modified) = Contact::add_or_lookup( + let recipient_addr = match ContactAddress::new(&recipient.addr) { + Err(err) => { + warn!( context, - &display_name_normalized, - recipient_addr, - Origin::OutgoingTo, - ) - .await?; - if modified != Modifier::None { - any_modified = true; - } + "Could not add contact for recipient with address {:?}: {:#}", + recipient.addr, + err + ); + continue; } + Ok(recipient_addr) => recipient_addr, + }; + + let (_, modified) = Contact::add_or_lookup( + context, + &display_name_normalized, + recipient_addr, + Origin::OutgoingTo, + ) + .await?; + if modified != Modifier::None { + any_modified = true; } } if any_modified { From f460043e8759784eaf65c06f140f32b04d3cb150 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 12 Jan 2023 12:37:41 +0000 Subject: [PATCH 119/132] Add missing documentation to webxdc module --- src/webxdc.rs | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/webxdc.rs b/src/webxdc.rs index 7d4bb4d03..000da4fb6 100644 --- a/src/webxdc.rs +++ b/src/webxdc.rs @@ -1,7 +1,5 @@ //! # Handle webxdc messages. -#![allow(missing_docs)] - use std::convert::TryFrom; use std::path::Path; @@ -31,6 +29,7 @@ use crate::{chat, EventType}; /// In the future, that may be useful to avoid new Webxdc being loaded on old Delta Chats. const WEBXDC_API_VERSION: u32 = 1; +/// Suffix used to recognize webxdc files. pub const WEBXDC_SUFFIX: &str = "xdc"; const WEBXDC_DEFAULT_ICON: &str = "__webxdc__/default-icon.png"; @@ -55,20 +54,44 @@ const WEBXDC_RECEIVING_LIMIT: u64 = 4194304; #[derive(Debug, Deserialize)] #[non_exhaustive] struct WebxdcManifest { + /// Webxdc name, used on icons or page titles. name: Option, + + /// Minimum API version required to run this webxdc. min_api: Option, + + /// Optional URL of webxdc source code. source_code_url: Option, + + /// If the webxdc requests network access. request_internet_access: Option, } /// Parsed information from WebxdcManifest and fallbacks. #[derive(Debug, Serialize)] pub struct WebxdcInfo { + /// The name of the app. + /// Defaults to filename if not set in the manifest. pub name: String, + + /// Filename of the app icon. pub icon: String, + + /// If the webxdc represents a document and allows to edit it, + /// this is the document name. + /// Otherwise an empty string. pub document: String, + + /// Short description of the webxdc state. + /// For example, "7 votes". pub summary: String, + + /// URL of webxdc source code or an empty string. pub source_code_url: String, + + /// If the webxdc is allowed to access the network. + /// It should request access, be encrypted + /// and sent to self for this. pub internet_access: bool, } From f0e900b885ec8a6a6fd253c6b69f1c57db8924e3 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 11 Jan 2023 23:33:33 +0000 Subject: [PATCH 120/132] Cleanup constants module --- src/constants.rs | 35 +++++------------------------------ src/smtp/send.rs | 6 +++++- 2 files changed, 10 insertions(+), 31 deletions(-) diff --git a/src/constants.rs b/src/constants.rs index dde6219f5..a7c60a8dd 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -68,6 +68,7 @@ impl Default for MediaQuality { } } +/// Type of the key to generate. #[derive( Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql, )] @@ -118,13 +119,13 @@ pub const DC_GCL_VERIFIED_ONLY: u32 = 0x01; pub const DC_GCL_ADD_SELF: u32 = 0x02; // unchanged user avatars are resent to the recipients every some days -pub const DC_RESEND_USER_AVATAR_DAYS: i64 = 14; +pub(crate) const DC_RESEND_USER_AVATAR_DAYS: i64 = 14; // warn about an outdated app after a given number of days. // as we use the "provider-db generation date" as reference (that might not be updated very often) // and as not all system get speedy updates, // do not use too small value that will annoy users checking for nonexistant updates. -pub const DC_OUTDATED_WARNING_DAYS: i64 = 365; +pub(crate) const DC_OUTDATED_WARNING_DAYS: i64 = 365; /// 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: ChatId = ChatId::new(3); @@ -169,7 +170,7 @@ pub const DC_MSG_ID_DAYMARKER: u32 = 9; pub const DC_MSG_ID_LAST_SPECIAL: u32 = 9; /// String that indicates that something is left out or truncated. -pub const DC_ELLIPSIS: &str = "[...]"; +pub(crate) const DC_ELLIPSIS: &str = "[...]"; // how many lines desktop can display when fullscreen (fullscreen at zoomlevel 1x) // (taken from "subjective" testing what looks ok) pub const DC_DESIRED_TEXT_LINES: usize = 38; @@ -186,11 +187,6 @@ pub const DC_DESIRED_TEXT_LINE_LEN: usize = 100; /// `char`s), not Unicode Grapheme Clusters. pub const DC_DESIRED_TEXT_LEN: usize = DC_DESIRED_TEXT_LINE_LEN * DC_DESIRED_TEXT_LINES; -// Flags for empty server job - -pub const DC_EMPTY_MVBOX: u32 = 0x01; -pub const DC_EMPTY_INBOX: u32 = 0x02; - // Flags for configuring IMAP and SMTP servers. // These flags are optional // and may be set together with the username, password etc. @@ -220,21 +216,7 @@ pub const BALANCED_IMAGE_SIZE: u32 = 1280; pub const WORSE_IMAGE_SIZE: u32 = 640; // this value can be increased if the folder configuration is changed and must be redone on next program start -pub const DC_FOLDERS_CONFIGURED_VERSION: i32 = 3; - -// if more recipients are needed in SMTP's `RCPT TO:` header, recipient-list is splitted to chunks. -// this does not affect MIME'e `To:` header. -// can be overwritten by the setting `max_smtp_rcpt_to` in provider-db. -pub const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50; - -pub const DC_JOB_DELETE_MSG_ON_IMAP: i32 = 110; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)] -#[repr(u8)] -pub enum KeyType { - Public = 0, - Private = 1, -} +pub(crate) const DC_FOLDERS_CONFIGURED_VERSION: i32 = 3; #[cfg(test)] mod tests { @@ -262,13 +244,6 @@ mod tests { assert_eq!(KeyGenType::Ed25519, KeyGenType::from_i32(2).unwrap()); } - #[test] - fn test_keytype_values() { - // values may be written to disk and must not change - assert_eq!(KeyType::Public, KeyType::from_i32(0).unwrap()); - assert_eq!(KeyType::Private, KeyType::from_i32(1).unwrap()); - } - #[test] fn test_showemails_values() { // values may be written to disk and must not change diff --git a/src/smtp/send.rs b/src/smtp/send.rs index 829e114b0..6f09d1346 100644 --- a/src/smtp/send.rs +++ b/src/smtp/send.rs @@ -4,13 +4,17 @@ use super::Smtp; use async_smtp::{EmailAddress, Envelope, SendableEmail, Transport}; use crate::config::Config; -use crate::constants::DEFAULT_MAX_SMTP_RCPT_TO; use crate::context::Context; use crate::events::EventType; use std::time::Duration; pub type Result = std::result::Result; +// if more recipients are needed in SMTP's `RCPT TO:` header, recipient-list is splitted to chunks. +// this does not affect MIME'e `To:` header. +// can be overwritten by the setting `max_smtp_rcpt_to` in provider-db. +pub(crate) const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50; + #[derive(Debug, thiserror::Error)] pub enum Error { #[error("Envelope error: {}", _0)] From 0b07dafe771c85a84d74f7d74f5e3060c06d9532 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Thu, 12 Jan 2023 17:13:27 +0100 Subject: [PATCH 121/132] add verified-by api to jsonrpc (#3946) also refactor it so that it is not a static method anymore (would have resulted in two load-Contact-from-db-calls in jsonrpc) --- CHANGELOG.md | 5 +++++ deltachat-ffi/src/lib.rs | 15 ++++++--------- deltachat-jsonrpc/src/api/types/contact.rs | 18 ++++++++++++++++++ src/contact.rs | 20 ++++++-------------- 4 files changed, 35 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f36e6cf43..1bddc660f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Unreleased + +### API-Changes +- jsonrpc: add verified-by information to `Contact`-Object + ## 1.106.0 ### Changes diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 7de6e4cd8..5ad070e83 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -3975,13 +3975,10 @@ pub unsafe extern "C" fn dc_contact_get_verifier_addr( } let ffi_contact = &*contact; let ctx = &*ffi_contact.context; - block_on(Contact::get_verifier_addr( - ctx, - &ffi_contact.contact.get_id(), - )) - .log_err(ctx, "failed to get verifier for contact") - .unwrap_or_default() - .strdup() + block_on(ffi_contact.contact.get_verifier_addr(ctx)) + .log_err(ctx, "failed to get verifier for contact") + .unwrap_or_default() + .strdup() } #[no_mangle] @@ -3992,12 +3989,12 @@ pub unsafe extern "C" fn dc_contact_get_verifier_id(contact: *mut dc_contact_t) } let ffi_contact = &*contact; let ctx = &*ffi_contact.context; - let contact_id = block_on(Contact::get_verifier_id(ctx, &ffi_contact.contact.get_id())) + let verifier_contact_id = block_on(ffi_contact.contact.get_verifier_id(ctx)) .log_err(ctx, "failed to get verifier") .unwrap_or_default() .unwrap_or_default(); - contact_id.to_u32() + verifier_contact_id.to_u32() } // dc_lot_t diff --git a/deltachat-jsonrpc/src/api/types/contact.rs b/deltachat-jsonrpc/src/api/types/contact.rs index 4ed4cf435..d67fc7fb1 100644 --- a/deltachat-jsonrpc/src/api/types/contact.rs +++ b/deltachat-jsonrpc/src/api/types/contact.rs @@ -20,6 +20,10 @@ pub struct ContactObject { name_and_addr: String, is_blocked: bool, is_verified: bool, + /// the address that verified this contact + verifier_addr: Option, + /// the id of the contact that verified this contact + verifier_id: Option, /// the contact's last seen timestamp last_seen: i64, was_seen_recently: bool, @@ -36,6 +40,18 @@ impl ContactObject { }; let is_verified = contact.is_verified(context).await? == VerifiedStatus::BidirectVerified; + let (verifier_addr, verifier_id) = if is_verified { + ( + contact.get_verifier_addr(context).await?, + contact + .get_verifier_id(context) + .await? + .map(|contact_id| contact_id.to_u32()), + ) + } else { + (None, None) + }; + Ok(ContactObject { address: contact.get_addr().to_owned(), color: color_int_to_hex_string(contact.get_color()), @@ -48,6 +64,8 @@ impl ContactObject { name_and_addr: contact.get_name_n_addr(), is_blocked: contact.is_blocked(), is_verified, + verifier_addr, + verifier_id, last_seen: contact.last_seen(), was_seen_recently: contact.was_seen_recently(), }) diff --git a/src/contact.rs b/src/contact.rs index 1f5233fbf..95a713b5b 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -1192,24 +1192,16 @@ impl Contact { Ok(VerifiedStatus::Unverified) } - /// Returns the address that verified the given contact. - pub async fn get_verifier_addr( - context: &Context, - contact_id: &ContactId, - ) -> Result> { - let contact = Contact::load_from_db(context, *contact_id).await?; - - Ok(Peerstate::from_addr(context, contact.get_addr()) + /// Returns the address that verified the contact. + pub async fn get_verifier_addr(&self, context: &Context) -> Result> { + Ok(Peerstate::from_addr(context, self.get_addr()) .await? .and_then(|peerstate| peerstate.get_verifier().map(|addr| addr.to_owned()))) } - /// Returns the ContactId that verified the given contact. - pub async fn get_verifier_id( - context: &Context, - contact_id: &ContactId, - ) -> Result> { - let verifier_addr = Contact::get_verifier_addr(context, contact_id).await?; + /// Returns the ContactId that verified the contact. + pub async fn get_verifier_id(&self, context: &Context) -> Result> { + let verifier_addr = self.get_verifier_addr(context).await?; if let Some(addr) = verifier_addr { Ok(Contact::lookup_id_by_addr(context, &addr, Origin::AddressBook).await?) } else { From a5354ded3f97519a580f61db14326d658a82500c Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 12 Jan 2023 17:42:19 +0000 Subject: [PATCH 122/132] ci: disable fail-fast This setting is true by default and causes Windows build to cancel when Linux fails due to flaky test and vice versa. Cancelled test then has to be restarted from scratch even though it was not going to fail. --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee4abe96c..81c0b0077 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,6 +67,7 @@ jobs: build_and_test: name: Build and test strategy: + fail-fast: false matrix: include: # Currently used Rust version. From 3b9a48ff5f452b93720c2c229a24729585c54677 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 12 Jan 2023 15:22:11 +0000 Subject: [PATCH 123/132] python: remove "data1=0" from INFO/WARNING/ERROR events display --- python/src/deltachat/events.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/python/src/deltachat/events.py b/python/src/deltachat/events.py index 72b84a44a..49ef23f96 100644 --- a/python/src/deltachat/events.py +++ b/python/src/deltachat/events.py @@ -30,7 +30,14 @@ class FFIEvent: self.data2 = data2 def __str__(self): - return "{name} data1={data1} data2={data2}".format(**self.__dict__) + if self.name == "DC_EVENT_INFO": + return "INFO {data2}".format(data2=self.data2) + elif self.name == "DC_EVENT_WARNING": + return "WARNING {data2}".format(data2=self.data2) + elif self.name == "DC_EVENT_ERROR": + return "ERROR {data2}".format(data2=self.data2) + else: + return "{name} data1={data1} data2={data2}".format(**self.__dict__) class FFIEventLogger: From 27c6cfc958576ebf074064b1163e9428bcc9a3b6 Mon Sep 17 00:00:00 2001 From: iequidoo Date: Mon, 2 Jan 2023 13:09:44 -0300 Subject: [PATCH 124/132] Log messages in info!() if DCC_MIME_DEBUG is set Using println!() leads to reordered output on terminal. Moreover, println!() prints to stdout which is not for logging. --- src/mimefactory.rs | 16 +++++++++++++--- src/mimeparser.rs | 7 +++++-- src/receive_imf.rs | 9 ++++++--- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 7025ad52c..04c3543ab 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -722,9 +722,11 @@ impl<'a> MimeFactory<'a> { )); if std::env::var(crate::DCC_MIME_DEBUG).is_ok() { - info!(context, "mimefactory: outgoing message mime:"); - let raw_message = message.clone().build().as_string(); - println!("{}", raw_message); + info!( + context, + "mimefactory: unencrypted message mime-body:\n{}", + message.clone().build().as_string(), + ); } let encrypted = encrypt_helper @@ -782,6 +784,14 @@ impl<'a> MimeFactory<'a> { .into_iter() .fold(outer_message, |message, header| message.header(header)); + if std::env::var(crate::DCC_MIME_DEBUG).is_ok() { + info!( + context, + "mimefactory: outgoing message mime-body:\n{}", + outer_message.clone().build().as_string(), + ); + } + let MimeFactory { last_added_location_id, .. diff --git a/src/mimeparser.rs b/src/mimeparser.rs index c83709673..34318a143 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -244,8 +244,11 @@ impl MimeMessage { mail_raw = raw; let decrypted_mail = mailparse::parse_mail(&mail_raw)?; if std::env::var(crate::DCC_MIME_DEBUG).is_ok() { - info!(context, "decrypted message mime-body:"); - println!("{}", String::from_utf8_lossy(&mail_raw)); + info!( + context, + "decrypted message mime-body:\n{}", + String::from_utf8_lossy(&mail_raw), + ); } (Ok(decrypted_mail), signatures, true) } diff --git a/src/receive_imf.rs b/src/receive_imf.rs index f09941441..971ce7e41 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -94,9 +94,12 @@ pub(crate) async fn receive_imf_inner( ) -> Result> { info!(context, "Receiving message, seen={}...", seen); - if std::env::var(crate::DCC_MIME_DEBUG).unwrap_or_default() == "2" { - info!(context, "receive_imf: incoming message mime-body:"); - println!("{}", String::from_utf8_lossy(imf_raw)); + if std::env::var(crate::DCC_MIME_DEBUG).is_ok() { + info!( + context, + "receive_imf: incoming message mime-body:\n{}", + String::from_utf8_lossy(imf_raw), + ); } let mut mime_parser = From d64498884589dd7eda966150500cce24f58286b2 Mon Sep 17 00:00:00 2001 From: iequidoo Date: Mon, 2 Jan 2023 13:09:56 -0300 Subject: [PATCH 125/132] Securejoin: Fix adding and handling Autocrypt-Gossip headers (#3836) - If bcc_self is set, gossip headers must be added despite of the number of group members. - If another device observes Secure-Join, instead of looking for Secure-Join-Fingerprint in "vg-member-added"/"vc-contact-confirm" messages it must use keys from Autocrypt-Gossip headers as described in the Countermitm doc (https://countermitm.readthedocs.io/en/latest/new.html#joining-a-verified-group-secure-join). --- CHANGELOG.md | 4 ++ src/mimefactory.rs | 4 +- src/securejoin.rs | 102 ++++++++++++++++++++++++++++++++++++--------- 3 files changed, 89 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bddc660f..91263fafd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,13 @@ ## Unreleased +### Fixes +- Securejoin: Fix adding and handling Autocrypt-Gossip headers #3914 + ### API-Changes - jsonrpc: add verified-by information to `Contact`-Object + ## 1.106.0 ### Changes diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 04c3543ab..b0918d484 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -689,7 +689,9 @@ impl<'a> MimeFactory<'a> { .fold(message, |message, header| message.header(header)); // Add gossip headers in chats with multiple recipients - if peerstates.len() > 1 && self.should_do_gossip(context).await? { + if (peerstates.len() > 1 || context.get_config_bool(Config::BccSelf).await?) + && self.should_do_gossip(context).await? + { for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) { if peerstate.peek_key(min_verified).is_some() { if let Some(header) = peerstate.render_gossip_header(min_verified) { diff --git a/src/securejoin.rs b/src/securejoin.rs index 420966ceb..92ddb0733 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -579,36 +579,98 @@ pub(crate) async fn observe_securejoin_on_other_device( .await?; return Ok(HandshakeMessage::Ignore); } - let fingerprint: Fingerprint = - match mime_message.get_header(HeaderDef::SecureJoinFingerprint) { - Some(fp) => fp.parse()?, + let addr = Contact::load_from_db(context, contact_id) + .await? + .get_addr() + .to_string(); + if mime_message.gossiped_addr.contains(&addr) { + let mut peerstate = match Peerstate::from_addr(context, &addr).await? { + Some(p) => p, None => { could_not_establish_secure_connection( - context, - contact_id, - info_chat_id(context, contact_id).await?, - "Fingerprint not provided, please update Delta Chat on all your devices.", - ) - .await?; + context, + contact_id, + info_chat_id(context, contact_id).await?, + &format!("No peerstate in db for '{}' at step {}", &addr, step), + ) + .await?; return Ok(HandshakeMessage::Ignore); } }; - if mark_peer_as_verified( - context, - &fingerprint, - Contact::load_from_db(context, contact_id) - .await? - .get_addr() - .to_owned(), - ) - .await - .is_err() + let fingerprint = match peerstate.gossip_key_fingerprint.clone() { + Some(fp) => fp, + None => { + could_not_establish_secure_connection( + context, + contact_id, + info_chat_id(context, contact_id).await?, + &format!( + "No gossip key fingerprint in db for '{}' at step {}", + &addr, step, + ), + ) + .await?; + return Ok(HandshakeMessage::Ignore); + } + }; + if peerstate.set_verified( + PeerstateKeyType::GossipKey, + &fingerprint, + PeerstateVerifiedStatus::BidirectVerified, + addr, + ) { + peerstate.prefer_encrypt = EncryptPreference::Mutual; + peerstate.save_to_db(&context.sql).await.unwrap_or_default(); + } else { + could_not_establish_secure_connection( + context, + contact_id, + info_chat_id(context, contact_id).await?, + &format!( + "Could not mark peer as verified for fingerprint {} at step {}", + fingerprint.hex(), + step, + ), + ) + .await?; + return Ok(HandshakeMessage::Ignore); + } + } else if let Some(fingerprint) = + mime_message.get_header(HeaderDef::SecureJoinFingerprint) { + // FIXME: Old versions of DC send this header instead of gossips. Remove this + // eventually. + let fingerprint = fingerprint.parse()?; + if mark_peer_as_verified( + context, + &fingerprint, + Contact::load_from_db(context, contact_id) + .await? + .get_addr() + .to_owned(), + ) + .await + .is_err() + { + could_not_establish_secure_connection( + context, + contact_id, + info_chat_id(context, contact_id).await?, + format!("Fingerprint mismatch on observing {}.", step).as_ref(), + ) + .await?; + return Ok(HandshakeMessage::Ignore); + } + } else { could_not_establish_secure_connection( context, contact_id, info_chat_id(context, contact_id).await?, - format!("Fingerprint mismatch on observing {}.", step).as_ref(), + &format!( + "No gossip header for '{}' at step {}, please update Delta Chat on all \ + your devices.", + &addr, step, + ), ) .await?; return Ok(HandshakeMessage::Ignore); From 13b2fe8d30e161a754d89b67aa6884096e674e94 Mon Sep 17 00:00:00 2001 From: iequidoo Date: Tue, 3 Jan 2023 21:12:27 -0300 Subject: [PATCH 126/132] import_self_keys(): Log set_self_key() error as DC_EVENT_INFO It isn't an error actually since we just skip the file. --- src/imex.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/imex.rs b/src/imex.rs index ea4033959..e265e2f6b 100644 --- a/src/imex.rs +++ b/src/imex.rs @@ -656,7 +656,7 @@ async fn import_self_keys(context: &Context, dir: &Path) -> Result<()> { Ok(buf) => { let armored = std::string::String::from_utf8_lossy(&buf); if let Err(err) = set_self_key(context, &armored, set_default, false).await { - error!(context, "set_self_key: {}", err); + info!(context, "set_self_key: {}", err); continue; } } From 57d7df530b8a592ccd86bc3ba4fc13400f4b3f30 Mon Sep 17 00:00:00 2001 From: iequidoo Date: Thu, 29 Dec 2022 18:55:13 -0300 Subject: [PATCH 127/132] Add a test that a new verified member is seen on the second device going online (#3836) - Alice has two devices, the second is offline. - Alice creates a verified group and sends a QR invitation to Bob. - Bob joins the group and sends a message there. Alice sees it. - Alice's second devices goes online, but doesn't see Bob in the group. --- python/tests/test_0_complex_or_slow.py | 42 ++++++++++++++++++++++++++ python/tests/test_1_online.py | 2 +- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/python/tests/test_0_complex_or_slow.py b/python/tests/test_0_complex_or_slow.py index 034839ee5..48e4467fc 100644 --- a/python/tests/test_0_complex_or_slow.py +++ b/python/tests/test_0_complex_or_slow.py @@ -492,3 +492,45 @@ def test_multidevice_sync_seen(acfactory, lp): assert ac1_clone_message.is_in_seen # Test that the timer is started on the second device after synchronizing the seen status. assert "Expires: " in ac1_clone_message.get_message_info() + + +def test_see_new_verified_member_after_going_online(acfactory, tmpdir, lp): + """The test for the bug #3836: + - Alice has two devices, the second is offline. + - Alice creates a verified group and sends a QR invitation to Bob. + - Bob joins the group and sends a message there. Alice sees it. + - Alice's second devices goes online, but doesn't see Bob in the group. + """ + ac1, ac2 = acfactory.get_online_accounts(2) + ac2_addr = ac2.get_config("addr") + ac1_offl = acfactory.new_online_configuring_account(cloned_from=ac1) + for ac in [ac1, ac1_offl]: + ac.set_config("bcc_self", "1") + acfactory.bring_accounts_online() + dir = tmpdir.mkdir("exportdir") + ac1.export_self_keys(dir.strpath) + ac1_offl.import_self_keys(dir.strpath) + ac1_offl.stop_io() + + lp.sec("ac1: create verified-group QR, ac2 scans and joins") + chat = ac1.create_group_chat("hello", verified=True) + assert chat.is_protected() + qr = chat.get_join_qr() + lp.sec("ac2: start QR-code based join-group protocol") + chat2 = ac2.qr_join_chat(qr) + ac1._evtracker.wait_securejoin_inviter_progress(1000) + + lp.sec("ac2: sending message") + msg_out = chat2.send_text("hello") + + lp.sec("ac1: receiving message") + msg_in = ac1._evtracker.wait_next_incoming_message() + assert msg_in.text == msg_out.text + assert msg_in.get_sender_contact().addr == ac2_addr + + lp.sec("ac1_offl: going online, receiving message") + ac1_offl.start_io() + ac1_offl._evtracker.wait_securejoin_inviter_progress(1000) + msg_in = ac1_offl._evtracker.wait_next_incoming_message() + assert msg_in.text == msg_out.text + assert msg_in.get_sender_contact().addr == ac2_addr diff --git a/python/tests/test_1_online.py b/python/tests/test_1_online.py index 954812d3b..9634a470a 100644 --- a/python/tests/test_1_online.py +++ b/python/tests/test_1_online.py @@ -88,7 +88,7 @@ def test_export_import_self_keys(acfactory, tmpdir, lp): lp.indent(dir.strpath + os.sep + name) lp.sec("importing into existing account") ac2.import_self_keys(dir.strpath) - (key_id2,) = ac2._evtracker.get_info_regex_groups(r".*stored.*KeyId\((.*)\).*", check_error=False) + (key_id2,) = ac2._evtracker.get_info_regex_groups(r".*stored.*KeyId\((.*)\).*") assert key_id2 == key_id From 8dc6ff268d75f2da9847af8b37bc8e8157dc1e16 Mon Sep 17 00:00:00 2001 From: iequidoo Date: Wed, 4 Jan 2023 22:39:29 -0300 Subject: [PATCH 128/132] check_verified_properties(): Don't ignore fails of Peerstate::set_verified() - Return Result from set_verified() so that it can't be missed. - Pass Fingerprint to set_verified() by value to avoid cloning it inside. This optimises out an extra clone() if we already have a value that can be moved at the caller side. However, this may add an extra clone() if set_verified() fails, but let's not optimise the fail scenario. --- src/peerstate.rs | 31 +++++++++++++++++------------- src/receive_imf.rs | 4 ++-- src/securejoin.rs | 39 +++++++++++++++++--------------------- src/securejoin/bobstate.rs | 2 +- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/peerstate.rs b/src/peerstate.rs index 761fec50f..10c561287 100644 --- a/src/peerstate.rs +++ b/src/peerstate.rs @@ -17,7 +17,7 @@ use crate::message::Message; use crate::mimeparser::SystemMessage; use crate::sql::Sql; use crate::stock_str; -use anyhow::{Context as _, Result}; +use anyhow::{Context as _, Error, Result}; use num_traits::FromPrimitive; #[derive(Debug)] @@ -369,43 +369,48 @@ impl Peerstate { /// verifier: /// The address which verifies the given contact /// If we are verifying the contact, use that contacts address - /// Returns whether the value of the key has changed pub fn set_verified( &mut self, which_key: PeerstateKeyType, - fingerprint: &Fingerprint, + fingerprint: Fingerprint, verified: PeerstateVerifiedStatus, verifier: String, - ) -> bool { + ) -> Result<()> { if verified == PeerstateVerifiedStatus::BidirectVerified { match which_key { PeerstateKeyType::PublicKey => { if self.public_key_fingerprint.is_some() - && self.public_key_fingerprint.as_ref().unwrap() == fingerprint + && self.public_key_fingerprint.as_ref().unwrap() == &fingerprint { self.verified_key = self.public_key.clone(); - self.verified_key_fingerprint = self.public_key_fingerprint.clone(); + self.verified_key_fingerprint = Some(fingerprint); self.verifier = Some(verifier); - true + Ok(()) } else { - false + Err(Error::msg(format!( + "{} is not peer's public key fingerprint", + fingerprint, + ))) } } PeerstateKeyType::GossipKey => { if self.gossip_key_fingerprint.is_some() - && self.gossip_key_fingerprint.as_ref().unwrap() == fingerprint + && self.gossip_key_fingerprint.as_ref().unwrap() == &fingerprint { self.verified_key = self.gossip_key.clone(); - self.verified_key_fingerprint = self.gossip_key_fingerprint.clone(); + self.verified_key_fingerprint = Some(fingerprint); self.verifier = Some(verifier); - true + Ok(()) } else { - false + Err(Error::msg(format!( + "{} is not peer's gossip key fingerprint", + fingerprint, + ))) } } } } else { - false + Err(Error::msg("BidirectVerified required")) } } diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 971ce7e41..990eebcef 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -2178,10 +2178,10 @@ async fn check_verified_properties( if let Some(fp) = fp { peerstate.set_verified( PeerstateKeyType::GossipKey, - &fp, + fp, PeerstateVerifiedStatus::BidirectVerified, contact.get_addr().to_owned(), - ); + )?; peerstate.save_to_db(&context.sql).await?; is_verified = true; } diff --git a/src/securejoin.rs b/src/securejoin.rs index 92ddb0733..eff6abd56 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -415,7 +415,7 @@ pub(crate) async fn handle_securejoin_handshake( .await? .get_addr() .to_owned(); - if mark_peer_as_verified(context, &fingerprint, contact_addr) + if mark_peer_as_verified(context, fingerprint.clone(), contact_addr) .await .is_err() { @@ -613,28 +613,23 @@ pub(crate) async fn observe_securejoin_on_other_device( return Ok(HandshakeMessage::Ignore); } }; - if peerstate.set_verified( + if let Err(err) = peerstate.set_verified( PeerstateKeyType::GossipKey, - &fingerprint, + fingerprint, PeerstateVerifiedStatus::BidirectVerified, addr, ) { - peerstate.prefer_encrypt = EncryptPreference::Mutual; - peerstate.save_to_db(&context.sql).await.unwrap_or_default(); - } else { could_not_establish_secure_connection( context, contact_id, info_chat_id(context, contact_id).await?, - &format!( - "Could not mark peer as verified for fingerprint {} at step {}", - fingerprint.hex(), - step, - ), + &format!("Could not mark peer as verified at step {}: {}", step, err), ) .await?; return Ok(HandshakeMessage::Ignore); } + peerstate.prefer_encrypt = EncryptPreference::Mutual; + peerstate.save_to_db(&context.sql).await.unwrap_or_default(); } else if let Some(fingerprint) = mime_message.get_header(HeaderDef::SecureJoinFingerprint) { @@ -643,7 +638,7 @@ pub(crate) async fn observe_securejoin_on_other_device( let fingerprint = fingerprint.parse()?; if mark_peer_as_verified( context, - &fingerprint, + fingerprint, Contact::load_from_db(context, contact_id) .await? .get_addr() @@ -715,25 +710,25 @@ async fn could_not_establish_secure_connection( async fn mark_peer_as_verified( context: &Context, - fingerprint: &Fingerprint, + fingerprint: Fingerprint, verifier: String, ) -> Result<(), Error> { - if let Some(ref mut peerstate) = Peerstate::from_fingerprint(context, fingerprint).await? { - if peerstate.set_verified( + if let Some(ref mut peerstate) = Peerstate::from_fingerprint(context, &fingerprint).await? { + if let Err(err) = peerstate.set_verified( PeerstateKeyType::PublicKey, fingerprint, PeerstateVerifiedStatus::BidirectVerified, verifier, ) { - peerstate.prefer_encrypt = EncryptPreference::Mutual; - peerstate.save_to_db(&context.sql).await.unwrap_or_default(); - return Ok(()); + error!(context, "Could not mark peer as verified: {}", err); + return Err(err); } + peerstate.prefer_encrypt = EncryptPreference::Mutual; + peerstate.save_to_db(&context.sql).await.unwrap_or_default(); + Ok(()) + } else { + bail!("no peerstate in db for fingerprint {}", fingerprint.hex()); } - bail!( - "could not mark peer as verified for fingerprint {}", - fingerprint.hex() - ); } /* ****************************************************************************** diff --git a/src/securejoin/bobstate.rs b/src/securejoin/bobstate.rs index e1145a006..36a43e02d 100644 --- a/src/securejoin/bobstate.rs +++ b/src/securejoin/bobstate.rs @@ -368,7 +368,7 @@ impl BobState { } mark_peer_as_verified( context, - self.invite.fingerprint(), + self.invite.fingerprint().clone(), mime_message.from.addr.to_string(), ) .await?; From 5f883a44458cbc76d0ae49ef86bb3ae51bc2ab84 Mon Sep 17 00:00:00 2001 From: iequidoo Date: Fri, 6 Jan 2023 15:54:56 -0300 Subject: [PATCH 129/132] Prepare to remove "vc-contact-confirm-received", "vg-member-added-received" messages from Securejoin protocol --- deltachat-ffi/deltachat.h | 2 +- python/tests/test_0_complex_or_slow.py | 3 +++ src/events.rs | 2 +- src/mimefactory.rs | 11 +++++++++++ src/securejoin.rs | 12 +++++++++--- 5 files changed, 25 insertions(+), 5 deletions(-) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 47452f6ac..507fbe76c 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -5811,7 +5811,7 @@ void dc_event_unref(dc_event_t* event); * @param data2 (int) The progress as: * 300=vg-/vc-request received, typically shown as "bob@addr joins". * 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified". - * 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol. + * 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol. * 1000=Protocol finished for this contact. */ #define DC_EVENT_SECUREJOIN_INVITER_PROGRESS 2060 diff --git a/python/tests/test_0_complex_or_slow.py b/python/tests/test_0_complex_or_slow.py index 48e4467fc..43e2b16c7 100644 --- a/python/tests/test_0_complex_or_slow.py +++ b/python/tests/test_0_complex_or_slow.py @@ -521,6 +521,9 @@ def test_see_new_verified_member_after_going_online(acfactory, tmpdir, lp): ac1._evtracker.wait_securejoin_inviter_progress(1000) lp.sec("ac2: sending message") + # Message can be sent only after a receipt of "vg-member-added" message. Just wait for + # "Member Me () added by ." message. + ac2._evtracker.wait_next_incoming_message() msg_out = chat2.send_text("hello") lp.sec("ac1: receiving message") diff --git a/src/events.rs b/src/events.rs index b2744f4d5..6d2962341 100644 --- a/src/events.rs +++ b/src/events.rs @@ -283,7 +283,7 @@ pub enum EventType { /// @param data2 (int) Progress as: /// 300=vg-/vc-request received, typically shown as "bob@addr joins". /// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified". - /// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol. + /// 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol. /// 1000=Protocol finished for this contact. SecurejoinInviterProgress { contact_id: ContactId, diff --git a/src/mimefactory.rs b/src/mimefactory.rs index b0918d484..7f1f7b5d2 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -917,6 +917,17 @@ impl<'a> MimeFactory<'a> { "Secure-Join".to_string(), "vg-member-added".to_string(), )); + // FIXME: Old clients require Secure-Join-Fingerprint header. Remove this + // eventually. + let fingerprint = Peerstate::from_addr(context, email_to_add) + .await? + .context("No peerstate found in db")? + .public_key_fingerprint + .context("No public key fingerprint in db for the member to add")?; + headers.protected.push(Header::new( + "Secure-Join-Fingerprint".into(), + fingerprint.hex(), + )); } } SystemMessage::GroupNameChanged => { diff --git a/src/securejoin.rs b/src/securejoin.rs index eff6abd56..4da710ac4 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -455,6 +455,8 @@ pub(crate) async fn handle_securejoin_handshake( } None => bail!("Chat {} not found", &field_grpid), } + inviter_progress!(context, contact_id, 800); + inviter_progress!(context, contact_id, 1000); } else { // Alice -> Bob secure_connection_established( @@ -503,9 +505,6 @@ pub(crate) async fn handle_securejoin_handshake( return Ok(HandshakeMessage::Ignore); } if join_vg { - // Responsible for showing "$Bob securely joined $group" message - inviter_progress!(context, contact_id, 800); - inviter_progress!(context, contact_id, 1000); let field_grpid = mime_message .get_header(HeaderDef::SecureJoinGroup) .map(|s| s.as_str()) @@ -670,6 +669,12 @@ pub(crate) async fn observe_securejoin_on_other_device( .await?; return Ok(HandshakeMessage::Ignore); } + if step.as_str() == "vg-member-added" { + inviter_progress!(context, contact_id, 800); + } + if step.as_str() == "vg-member-added" || step.as_str() == "vc-contact-confirm" { + inviter_progress!(context, contact_id, 1000); + } Ok(if step.as_str() == "vg-member-added" { HandshakeMessage::Propagate } else { @@ -768,6 +773,7 @@ mod tests { use crate::chatlist::Chatlist; use crate::constants::{Chattype, DC_GCM_ADDDAYMARKER}; use crate::contact::ContactAddress; + use crate::contact::VerifiedStatus; use crate::peerstate::Peerstate; use crate::receive_imf::receive_imf; use crate::test_utils::{TestContext, TestContextManager}; From 1c44135b412da0bae26835ca10e4793f402ae008 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 12 Jan 2023 14:41:41 +0000 Subject: [PATCH 130/132] Remove deprecated `attach_selfavatar` config According to the comment it was added in Dec 2019 with an intention to remove it "after some time". --- CHANGELOG.md | 1 + src/chat.rs | 17 ++++++++--------- src/config.rs | 3 --- src/mimefactory.rs | 1 + 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91263fafd..33d183b9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ### API-Changes - jsonrpc: add verified-by information to `Contact`-Object +- Remove `attach_selfavatar` config #3951 ## 1.106.0 diff --git a/src/chat.rs b/src/chat.rs index cef50d3d1..0b54efc50 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -2902,14 +2902,12 @@ pub(crate) async fn add_contact_to_chat_ex( Ok(true) } +/// Returns true if an avatar should be attached in the given chat. +/// +/// This function does not check if the avatar is set. +/// If avatar is not set and this function returns `true`, +/// a `Chat-User-Avatar: 0` header should be sent to reset the avatar. pub(crate) async fn shall_attach_selfavatar(context: &Context, chat_id: ChatId) -> Result { - // versions before 12/2019 already allowed to set selfavatar, however, it was never sent to others. - // to avoid sending out previously set selfavatars unexpectedly we added this additional check. - // it can be removed after some time. - if !context.sql.get_raw_config_bool("attach_selfavatar").await? { - return Ok(false); - } - let timestamp_some_days_ago = time() - DC_RESEND_USER_AVATAR_DAYS * 24 * 60 * 60; let needs_attach = context .sql @@ -4700,12 +4698,13 @@ mod tests { ) .await?; add_contact_to_chat(&t, chat_id, contact_id).await?; - assert!(!shall_attach_selfavatar(&t, chat_id).await?); - t.set_config(Config::Selfavatar, None).await?; // setting to None also forces re-sending assert!(shall_attach_selfavatar(&t, chat_id).await?); chat_id.set_selfavatar_timestamp(&t, time()).await?; assert!(!shall_attach_selfavatar(&t, chat_id).await?); + + t.set_config(Config::Selfavatar, None).await?; // setting to None also forces re-sending + assert!(shall_attach_selfavatar(&t, chat_id).await?); Ok(()) } diff --git a/src/config.rs b/src/config.rs index 40cddcc8d..41b884fab 100644 --- a/src/config.rs +++ b/src/config.rs @@ -292,9 +292,6 @@ impl Context { self.sql .execute("UPDATE contacts SET selfavatar_sent=0;", paramsv![]) .await?; - self.sql - .set_raw_config_bool("attach_selfavatar", true) - .await?; match value { Some(value) => { let mut blob = BlobObject::new_from_path(self, value.as_ref()).await?; diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 7f1f7b5d2..bd198329d 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -76,6 +76,7 @@ pub struct MimeFactory<'a> { /// and must be deleted if the message is actually queued for sending. sync_ids_to_delete: Option, + /// True if the avatar should be attached. attach_selfavatar: bool, } From 0053e143e77a45eeffb5c260051d349712ab158c Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 12 Jan 2023 20:51:07 +0000 Subject: [PATCH 131/132] Do not emit ChatModified event when user avatar is updated --- src/receive_imf.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 990eebcef..a28601cd4 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -290,7 +290,7 @@ pub(crate) async fn receive_imf_inner( .update_contacts_timestamp(from_id, Param::AvatarTimestamp, sent_timestamp) .await? { - match contact::set_profile_image( + if let Err(err) = contact::set_profile_image( context, from_id, avatar_action, @@ -298,15 +298,10 @@ pub(crate) async fn receive_imf_inner( ) .await { - Ok(()) => { - context.emit_event(EventType::ChatModified(chat_id)); - } - Err(err) => { - warn!( - context, - "receive_imf cannot update profile image: {:#}", err - ); - } + warn!( + context, + "receive_imf cannot update profile image: {:#}", err + ); }; } } From 5b265dbc1c2e497643b0c0af6247a89334ea7bf7 Mon Sep 17 00:00:00 2001 From: bjoern Date: Fri, 13 Jan 2023 18:25:20 +0100 Subject: [PATCH 132/132] remove comma from unit in message-details (#3954) it shoud read "filename.ext, 123 bytes" and not "filename.ext, 123, bytes" --- src/message.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/message.rs b/src/message.rs index 63bf1450f..48819a218 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1180,7 +1180,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result { if let Some(path) = msg.get_file(context) { let bytes = get_filebytes(context, &path).await?; - ret += &format!("\nFile: {}, {}, bytes\n", path.display(), bytes); + ret += &format!("\nFile: {}, {} bytes\n", path.display(), bytes); } if msg.viewtype != Viewtype::Text {