Merge branch 'master' into fix3782

This commit is contained in:
Sebastian Klähn
2023-01-23 11:10:41 +01:00
committed by GitHub
141 changed files with 2311 additions and 1196 deletions

View File

@@ -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.
@@ -502,7 +509,6 @@ impl AccountConfig {
#[cfg(test)]
mod tests {
use super::*;
use crate::stock_str::{self, StockMessage};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -2,13 +2,12 @@
//!
//! Parse and create [Autocrypt-headers](https://autocrypt.org/en/latest/level1.html#the-autocrypt-header).
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 anyhow::{bail, Context as _, Error, Result};
use crate::key::{DcKey, SignedPublicKey};
/// Possible values for encryption preference
@@ -36,7 +35,7 @@ impl fmt::Display for EncryptPreference {
}
}
impl str::FromStr for EncryptPreference {
impl FromStr for EncryptPreference {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
@@ -69,29 +68,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<Option<Self>> {
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 +94,7 @@ impl fmt::Display for Aheader {
}
}
impl str::FromStr for Aheader {
impl FromStr for Aheader {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {

View File

@@ -355,7 +355,6 @@ mod tests {
use tokio::io::AsyncReadExt;
use super::*;
use crate::aheader::EncryptPreference;
use crate::e2ee;
use crate::message;

View File

@@ -499,17 +499,15 @@ fn encoded_img_exceeds_bytes(
#[cfg(test)]
mod tests {
use fs::File;
use anyhow::Result;
use fs::File;
use image::{GenericImageView, Pixel};
use super::*;
use crate::chat::{self, create_group_chat, ProtectionStatus};
use crate::message::Message;
use crate::test_utils::{self, TestContext};
use super::*;
fn check_image_size(path: impl AsRef<Path>, width: u32, height: u32) -> image::DynamicImage {
tokio::task::block_in_place(move || {
let img = image::open(path).expect("failed to open image");

View File

@@ -531,20 +531,68 @@ impl ChatId {
Ok(())
}
// Unarchives a chat that is archived and not muted.
// Needed when a message is added to a chat so that the chat gets a normal visibility again.
// Sending an appropriate event is up to the caller.
pub async fn unarchive_if_not_muted(self, context: &Context) -> Result<()> {
/// Unarchives a chat that is archived and not muted.
/// Needed after a message is added to a chat so that the chat gets a normal visibility again.
/// `msg_state` is the state of the message. Matters only for incoming messages currently. For
/// multiple outgoing messages the function may be called once with MessageState::Undefined.
/// Sending an appropriate event is up to the caller.
/// Also emits DC_EVENT_MSGS_CHANGED for DC_CHAT_ID_ARCHIVED_LINK when the number of archived
/// chats with unread messages increases (which is possible if the chat is muted).
pub async fn unarchive_if_not_muted(
self,
context: &Context,
msg_state: MessageState,
) -> Result<()> {
if msg_state != MessageState::InFresh {
context
.sql
.execute(
"UPDATE chats SET archived=0 WHERE id=? AND archived=1 \
AND NOT(muted_until=-1 OR muted_until>?)",
paramsv![self, time()],
)
.await?;
return Ok(());
}
let chat = Chat::load_from_db(context, self).await?;
if chat.visibility != ChatVisibility::Archived {
return Ok(());
}
if chat.is_muted() {
let unread_cnt = context
.sql
.count(
"SELECT COUNT(*)
FROM msgs
WHERE state=?
AND hidden=0
AND chat_id=?",
paramsv![MessageState::InFresh, self],
)
.await?;
if unread_cnt == 1 {
// Added the first unread message in the chat.
context.emit_msgs_changed(DC_CHAT_ID_ARCHIVED_LINK, MsgId::new(0));
}
return Ok(());
}
context
.sql
.execute(
"UPDATE chats SET archived=0 WHERE id=? AND archived=1 AND NOT(muted_until=-1 OR muted_until>?)",
paramsv![self, time()],
)
.execute("UPDATE chats SET archived=0 WHERE id=?", paramsv![self])
.await?;
Ok(())
}
/// Emits an appropriate event for a message. `important` is whether a notification should be
/// shown.
pub(crate) fn emit_msg_event(self, context: &Context, msg_id: MsgId, important: bool) {
if important {
context.emit_incoming_msg(self, msg_id);
} else {
context.emit_msgs_changed(self, msg_id);
}
}
/// Deletes a chat.
pub async fn delete(self, context: &Context) -> Result<()> {
ensure!(
@@ -1129,7 +1177,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;
@@ -2011,7 +2059,9 @@ async fn prepare_msg_common(
msg.state = change_state_to;
prepare_msg_blob(context, msg).await?;
chat_id.unarchive_if_not_muted(context).await?;
if !msg.hidden {
chat_id.unarchive_if_not_muted(context, msg.state).await?;
}
msg.id = chat
.prepare_msg_raw(
context,
@@ -2148,7 +2198,7 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
let attach_selfavatar = match shall_attach_selfavatar(context, msg.chat_id).await {
Ok(attach_selfavatar) => attach_selfavatar,
Err(err) => {
warn!(context, "job: cannot get selfavatar-state: {}", err);
warn!(context, "job: cannot get selfavatar-state: {:#}", err);
false
}
};
@@ -2210,27 +2260,27 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
if 0 != rendered_msg.last_added_location_id {
if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, time()).await {
error!(context, "Failed to set kml sent_timestamp: {:?}", err);
error!(context, "Failed to set kml sent_timestamp: {:#}", err);
}
if !msg.hidden {
if let Err(err) =
location::set_msg_location_id(context, msg.id, rendered_msg.last_added_location_id)
.await
{
error!(context, "Failed to set msg_location_id: {:?}", err);
error!(context, "Failed to set msg_location_id: {:#}", err);
}
}
}
if let Some(sync_ids) = rendered_msg.sync_ids_to_delete {
if let Err(err) = context.delete_sync_ids(sync_ids).await {
error!(context, "Failed to delete sync ids: {:?}", err);
error!(context, "Failed to delete sync ids: {:#}", err);
}
}
if attach_selfavatar {
if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, time()).await {
error!(context, "Failed to set selfavatar timestamp: {:?}", err);
error!(context, "Failed to set selfavatar timestamp: {:#}", err);
}
}
@@ -2433,31 +2483,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::<Result<Vec<_>, _>>().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));
@@ -2873,14 +2955,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<bool> {
// 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
@@ -3174,7 +3254,9 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
let mut created_msgs: Vec<MsgId> = Vec::new();
let mut curr_timestamp: i64;
chat_id.unarchive_if_not_muted(context).await?;
chat_id
.unarchive_if_not_muted(context, MessageState::Undefined)
.await?;
if let Ok(mut chat) = Chat::load_from_db(context, chat_id).await {
if let Some(reason) = chat.why_cant_send(context).await? {
bail!("cannot send to {}: {}", chat_id, reason);
@@ -3377,7 +3459,6 @@ pub async fn add_device_msg_with_importance(
let rfc724_mid = create_outgoing_rfc724_mid(None, "@device");
msg.try_calc_and_set_dimensions(context).await.ok();
prepare_msg_blob(context, msg).await?;
chat_id.unarchive_if_not_muted(context).await?;
let timestamp_sent = create_smeared_timestamp(context).await;
@@ -3397,6 +3478,7 @@ pub async fn add_device_msg_with_importance(
}
}
let state = MessageState::InFresh;
let row_id = context
.sql
.insert(
@@ -3420,7 +3502,7 @@ pub async fn add_device_msg_with_importance(
timestamp_sent,
timestamp_sent, // timestamp_sent equals timestamp_rcvd
msg.viewtype,
MessageState::InFresh,
state,
msg.text.as_ref().cloned().unwrap_or_default(),
msg.param.to_string(),
rfc724_mid,
@@ -3429,6 +3511,9 @@ pub async fn add_device_msg_with_importance(
.await?;
msg_id = MsgId::new(u32::try_from(row_id)?);
if !msg.hidden {
chat_id.unarchive_if_not_muted(context, state).await?;
}
}
if let Some(label) = label {
@@ -3442,11 +3527,7 @@ pub async fn add_device_msg_with_importance(
}
if !msg_id.is_unset() {
if important {
context.emit_incoming_msg(chat_id, msg_id);
} else {
context.emit_msgs_changed(chat_id, msg_id);
}
chat_id.emit_msg_event(context, msg_id, important);
}
Ok(msg_id)
@@ -3597,13 +3678,13 @@ pub(crate) async fn update_msg_text_and_timestamp(
#[cfg(test)]
mod tests {
use super::*;
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, TestContextManager};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_chat_info() {
let t = TestContext::new().await;
@@ -4501,6 +4582,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<ChatId> {
let chatlist = Chatlist::try_load(ctx, listflags, None, None)
.await
@@ -4569,6 +4735,46 @@ mod tests {
assert_eq!(chatlist, vec![chat_id3, chat_id2, chat_id1]);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_pinned_after_new_msgs() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat_id = alice.create_chat(&bob).await.id;
let bob_chat_id = bob.create_chat(&alice).await.id;
assert!(alice_chat_id
.set_visibility(&alice, ChatVisibility::Pinned)
.await
.is_ok());
assert_eq!(
Chat::load_from_db(&alice, alice_chat_id)
.await?
.get_visibility(),
ChatVisibility::Pinned,
);
send_text_msg(&alice, alice_chat_id, "hi!".into()).await?;
assert_eq!(
Chat::load_from_db(&alice, alice_chat_id)
.await?
.get_visibility(),
ChatVisibility::Pinned,
);
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("hi!".into()));
let sent_msg = bob.send_msg(bob_chat_id, &mut msg).await;
let msg = alice.recv_msg(&sent_msg).await;
assert_eq!(msg.chat_id, alice_chat_id);
assert_eq!(
Chat::load_from_db(&alice, alice_chat_id)
.await?
.get_visibility(),
ChatVisibility::Pinned,
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_chat_name() {
let t = TestContext::new().await;
@@ -4616,15 +4822,21 @@ 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?;
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
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(())
}
@@ -4863,8 +5075,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?;
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?;
@@ -5126,7 +5338,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(())
}
@@ -5574,8 +5786,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?;
let (contact_id, _) = Contact::add_or_lookup(
&t,
"",
ContactAddress::new("foo@bar.org")?,
Origin::ManuallyCreated,
)
.await?;
// create a blocked chat
let chat_id_orig =

View File

@@ -365,7 +365,6 @@ pub async fn get_archived_cnt(context: &Context) -> Result<usize> {
#[cfg(test)]
mod tests {
use super::*;
use crate::chat::{create_group_chat, get_chat_contacts, ProtectionStatus};
use crate::message::Viewtype;
use crate::receive_imf::receive_imf;

View File

@@ -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?;
@@ -443,16 +440,15 @@ fn get_config_keys_string() -> String {
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
use std::string::ToString;
use num_traits::FromPrimitive;
use super::*;
use crate::constants;
use crate::test_utils::TestContext;
use num_traits::FromPrimitive;
#[test]
fn test_to_string() {
assert_eq!(Config::MailServer.to_string(), "mail_server");

View File

@@ -6,9 +6,12 @@ mod read_url;
mod server_params;
use anyhow::{bail, ensure, Context as _, Result};
use auto_mozilla::moz_autoconfigure;
use auto_outlook::outlk_autodiscover;
use futures::FutureExt;
use futures_lite::FutureExt as _;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use server_params::{expand_param_vector, ServerParams};
use tokio::task;
use crate::config::Config;
@@ -28,10 +31,6 @@ use crate::stock_str;
use crate::tools::{time, EmailAddress};
use crate::{chat, e2ee, provider};
use auto_mozilla::moz_autoconfigure;
use auto_outlook::outlk_autodiscover;
use server_params::{expand_param_vector, ServerParams};
macro_rules! progress {
($context:tt, $progress:expr, $comment:expr) => {
assert!(
@@ -565,13 +564,18 @@ async fn try_imap_one_param(
provider_strict_tls: bool,
) -> Result<Imap, ConfigurationError> {
let inf = format!(
"imap: {}@{}:{} security={} certificate_checks={} oauth2={}",
"imap: {}@{}:{} security={} certificate_checks={} oauth2={} socks5_config={}",
param.user,
param.server,
param.port,
param.security,
param.certificate_checks,
param.oauth2
param.oauth2,
if let Some(socks5_config) = socks5_config {
socks5_config.to_string()
} else {
"None".to_string()
}
);
info!(context, "Trying: {}", inf);
@@ -661,6 +665,7 @@ async fn nicer_configuration_error(context: &Context, errors: Vec<ConfigurationE
if errors.iter().all(|e| {
e.msg.to_lowercase().contains("could not resolve")
|| e.msg.to_lowercase().contains("no dns resolution results")
|| e.msg
.to_lowercase()
.contains("temporary failure in name resolution")

View File

@@ -1,17 +1,16 @@
//! # Thunderbird's Autoconfiguration implementation
//!
//! Documentation: <https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration>
use quick_xml::events::{BytesStart, Event};
use std::io::BufRead;
use std::str::FromStr;
use crate::context::Context;
use crate::login_param::LoginParam;
use crate::provider::{Protocol, Socket};
use quick_xml::events::{BytesStart, Event};
use super::read_url::read_url;
use super::{Error, ServerParams};
use crate::context::Context;
use crate::login_param::LoginParam;
use crate::provider::{Protocol, Socket};
#[derive(Debug)]
struct Server {

View File

@@ -3,15 +3,14 @@
//! This module implements autoconfiguration via POX (Plain Old XML) interface to Autodiscover
//! Service. Newer SOAP interface, introduced in Exchange 2010, is not used.
use quick_xml::events::Event;
use std::io::BufRead;
use crate::context::Context;
use crate::provider::{Protocol, Socket};
use quick_xml::events::Event;
use super::read_url::read_url;
use super::{Error, ServerParams};
use crate::context::Context;
use crate::provider::{Protocol, Socket};
/// Result of parsing a single `Protocol` tag.
///

View File

@@ -16,7 +16,7 @@ pub async fn read_url(context: &Context, url: &str) -> anyhow::Result<String> {
}
pub async fn read_url_inner(context: &Context, url: &str) -> anyhow::Result<String> {
let client = reqwest::Client::new();
let client = crate::http::get_client()?;
let mut url = url.to_string();
// Follow up to 10 http-redirects

View File

@@ -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

View File

@@ -1,11 +1,10 @@
//! Contacts module
#![allow(missing_docs)]
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};
@@ -38,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<str> 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<Self> {
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<rusqlite::types::ToSqlOutput> {
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
@@ -48,12 +92,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 +227,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 +309,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 +328,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<Self> {
let mut contact = context
.sql
@@ -368,12 +422,14 @@ impl Contact {
/// May result in a `#DC_EVENT_CONTACTS_CHANGED` event.
pub async fn create(context: &Context, name: &str, addr: &str) -> Result<ContactId> {
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 addr = ContactAddress::new(&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")?;
let blocked = Contact::is_blocked_load(context, contact_id).await?;
match sth_modified {
Modifier::None => {}
@@ -458,10 +514,12 @@ 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,
addr: ContactAddress<'_>,
mut origin: Origin,
) -> Result<(ContactId, Modifier)> {
let mut sth_modified = Modifier::None;
@@ -469,22 +527,10 @@ impl Contact {
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((ContactId::SELF, sth_modified));
}
if !may_be_valid_addr(&addr) {
warn!(
context,
"Bad address \"{}\" for contact \"{}\".",
addr,
if !name.is_empty() { name } else { "<unset>" },
);
bail!("Bad address supplied: {:?}", addr);
}
let mut name = name;
#[allow(clippy::collapsible_if)]
if origin <= Origin::OutgoingTo {
@@ -543,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 {
@@ -671,18 +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((_, 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 {
@@ -847,6 +900,7 @@ impl Contact {
Ok(())
}
/// Returns number of blocked contacts.
pub async fn get_blocked_cnt(context: &Context) -> Result<usize> {
let count = context
.sql
@@ -1138,23 +1192,16 @@ 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<Option<String>> {
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<Option<String>> {
Ok(Peerstate::from_addr(context, self.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<Option<ContactId>> {
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<Option<ContactId>> {
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 {
@@ -1162,7 +1209,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<usize> {
if !context.sql.is_open().await {
return Ok(0);
@@ -1178,6 +1225,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<bool> {
if contact_id.is_special() {
return Ok(false);
@@ -1193,6 +1241,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,
@@ -1404,6 +1453,7 @@ pub(crate) async fn update_last_seen(
)
.await?
> 0
&& timestamp > time() - SEEN_RECENTLY_SECONDS
{
context.interrupt_recently_seen(contact_id, timestamp).await;
}
@@ -1453,6 +1503,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();
@@ -1561,6 +1612,9 @@ impl RecentlySeenLoop {
context,
"Error receiving an interruption in recently seen loop: {}", err
);
// Maybe the sender side is closed.
// Terminate the loop to avoid looping indefinitely.
return;
}
Ok(Ok(RecentlySeenInterrupt {
contact_id,
@@ -1602,7 +1656,6 @@ impl RecentlySeenLoop {
#[cfg(test)]
mod tests {
use super::*;
use crate::chat::{get_chat_contacts, send_text_msg, Chat};
use crate::chatlist::Chatlist;
use crate::receive_imf::receive_imf;
@@ -1680,7 +1733,7 @@ mod tests {
let (id, _modified) = Contact::add_or_lookup(
&context.ctx,
"bob",
"user@example.org",
ContactAddress::new("user@example.org")?,
Origin::IncomingReplyTo,
)
.await?;
@@ -1708,7 +1761,7 @@ 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?;
@@ -1743,6 +1796,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()
@@ -1758,10 +1823,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();
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();
@@ -1773,10 +1842,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();
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();
@@ -1785,10 +1858,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();
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();
@@ -1801,7 +1878,7 @@ 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
@@ -1813,10 +1890,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();
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();
@@ -1825,10 +1906,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();
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();
@@ -1963,9 +2048,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?;
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;
@@ -2038,10 +2127,14 @@ mod tests {
let t = TestContext::new().await;
// incoming mail `From: bob1 <bob@example.org>` - this should init authname
let (contact_id, sth_modified) =
Contact::add_or_lookup(&t, "bob1", "bob@example.org", Origin::IncomingUnknownFrom)
.await
.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();
@@ -2050,10 +2143,14 @@ mod tests {
assert_eq!(contact.get_display_name(), "bob1");
// incoming mail `From: bob2 <bob@example.org>` - this should update authname
let (contact_id, sth_modified) =
Contact::add_or_lookup(&t, "bob2", "bob@example.org", Origin::IncomingUnknownFrom)
.await
.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();
@@ -2072,10 +2169,14 @@ mod tests {
assert_eq!(contact.get_display_name(), "bob3");
// incoming mail `From: bob4 <bob@example.org>` - 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();
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();
@@ -2100,7 +2201,7 @@ 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
@@ -2116,7 +2217,7 @@ 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
@@ -2138,26 +2239,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?;
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" <bob@example.org> in the "To:" field.
let (contact_id_same, sth_modified) =
Contact::add_or_lookup(&t, "Not Bob", "bob@example.org", Origin::IncomingUnknownTo)
.await?;
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?;
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?;
@@ -2180,9 +2293,14 @@ mod tests {
assert_eq!(contact.get_display_name(), "dave1");
// incoming mail `From: dave2 <dave@example.org>` - 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");
@@ -2296,9 +2414,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?;
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");
@@ -2455,9 +2577,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?;
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);
@@ -2504,4 +2630,27 @@ Hi."#;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verified_by_none() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
let contact = Contact::get_by_id(&alice, contact_id).await?;
assert!(contact.get_verifier_addr(&alice).await?.is_none());
assert!(contact.get_verifier_id(&alice).await?.is_none());
// Receive a message from Bob to create a peerstate.
let chat = bob.create_chat(&alice).await;
let sent_msg = bob.send_text(chat.id, "moin").await;
alice.recv_msg(&sent_msg).await;
let contact = Contact::get_by_id(&alice, contact_id).await?;
assert!(contact.get_verifier_addr(&alice).await?.is_none());
assert!(contact.get_verifier_id(&alice).await?.is_none());
Ok(())
}
}

View File

@@ -383,7 +383,7 @@ impl Context {
let mut lock = self.inner.scheduler.write().await;
if lock.is_none() {
match Scheduler::start(self.clone()).await {
Err(err) => 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;
@@ -861,8 +861,13 @@ pub fn get_version_str() -> &'static str {
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
use anyhow::Context as _;
use strum::IntoEnumIterator;
use tempfile::tempdir;
use super::*;
use crate::chat::{
get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat, ChatId, MuteDuration,
};
@@ -873,10 +878,6 @@ mod tests {
use crate::receive_imf::receive_imf;
use crate::test_utils::TestContext;
use crate::tools::create_outgoing_rfc724_mid;
use anyhow::Context as _;
use std::time::Duration;
use strum::IntoEnumIterator;
use tempfile::tempdir;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_wrong_db() -> Result<()> {

View File

@@ -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?;
@@ -328,11 +344,10 @@ pub(crate) async fn get_autocrypt_peerstate(
#[cfg(test)]
mod tests {
use super::*;
use crate::receive_imf::receive_imf;
use crate::test_utils::TestContext;
use super::*;
#[test]
fn test_has_decrypted_pgp_armor() {
let data = b" -----BEGIN PGP MESSAGE-----";

View File

@@ -1,9 +1,11 @@
//! # Download large messages manually.
use std::cmp::max;
use std::collections::BTreeMap;
use anyhow::{anyhow, Result};
use deltachat_derive::{FromSql, ToSql};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use crate::config::Config;
use crate::context::Context;
@@ -14,7 +16,6 @@ use crate::mimeparser::{MimeMessage, Part};
use crate::param::Params;
use crate::tools::time;
use crate::{job_try, stock_str, EventType};
use std::cmp::max;
/// Download limits should not be used below `MIN_DOWNLOAD_LIMIT`.
///
@@ -132,7 +133,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;
}
@@ -264,14 +265,13 @@ impl MimeMessage {
mod tests {
use num_traits::FromPrimitive;
use super::*;
use crate::chat::{get_chat_msgs, send_msg};
use crate::ephemeral::Timer;
use crate::message::Viewtype;
use crate::receive_imf::receive_imf_inner;
use crate::test_utils::TestContext;
use super::*;
#[test]
fn test_downloadstate_values() {
// values may be written to disk and must not change

View File

@@ -144,13 +144,12 @@ pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
#[cfg(test)]
mod tests {
use super::*;
use crate::chat;
use crate::message::{Message, Viewtype};
use crate::param::Param;
use crate::test_utils::{bob_keypair, TestContext};
use super::*;
mod ensure_secret_key_exists {
use super::*;

View File

@@ -64,6 +64,7 @@
#![allow(missing_docs)]
use std::cmp::max;
use std::convert::{TryFrom, TryInto};
use std::num::ParseIntError;
use std::str::FromStr;
@@ -86,7 +87,6 @@ use crate::mimeparser::SystemMessage;
use crate::sql::{self, params_iter};
use crate::stock_str;
use crate::tools::{duration_to_str, time};
use std::cmp::max;
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
pub enum Timer {

View File

@@ -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,

View File

@@ -7,12 +7,14 @@
//! `MsgId.get_html()` will return HTML -
//! this allows nice quoting, handling linebreaks properly etc.
use futures::future::FutureExt;
use std::future::Future;
use std::pin::Pin;
use anyhow::{Context as _, Result};
use futures::future::FutureExt;
use lettre_email::mime::{self, Mime};
use lettre_email::PartBuilder;
use mailparse::ParsedContentType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::message::{Message, MsgId};
@@ -20,8 +22,6 @@ use crate::mimeparser::parse_message_id;
use crate::param::Param::SendHtml;
use crate::plaintext::PlainText;
use crate::{context::Context, message};
use lettre_email::PartBuilder;
use mailparse::ParsedContentType;
impl Message {
/// Check if the message can be retrieved as HTML.
@@ -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)),

13
src/http.rs Normal file
View File

@@ -0,0 +1,13 @@
//! # HTTP module.
use std::time::Duration;
use anyhow::Result;
const HTTP_TIMEOUT: Duration = Duration::from_secs(30);
pub(crate) fn get_client() -> Result<reqwest::Client> {
Ok(reqwest::ClientBuilder::new()
.timeout(HTTP_TIMEOUT)
.build()?)
}

View File

@@ -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};
@@ -308,6 +308,7 @@ impl Imap {
if let Some(socks5_config) = &config.socks5_config {
if config.lp.security == Socket::Starttls {
Client::connect_starttls_socks5(
context,
imap_server,
imap_port,
socks5_config.clone(),
@@ -315,13 +316,18 @@ impl Imap {
)
.await
} else {
Client::connect_insecure_socks5((imap_server, imap_port), socks5_config.clone())
.await
Client::connect_insecure_socks5(
context,
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
Client::connect_starttls(context, imap_server, imap_port, config.strict_tls).await
} else {
Client::connect_insecure((imap_server, imap_port)).await
Client::connect_insecure(context, imap_server, imap_port).await
}
} else {
let config = &self.config;
@@ -330,6 +336,7 @@ impl Imap {
if let Some(socks5_config) = &config.socks5_config {
Client::connect_secure_socks5(
context,
imap_server,
imap_port,
config.strict_tls,
@@ -337,7 +344,7 @@ impl Imap {
)
.await
} else {
Client::connect_secure(imap_server, imap_port, config.strict_tls).await
Client::connect_secure(context, imap_server, imap_port, config.strict_tls).await
}
};
@@ -554,7 +561,10 @@ impl Imap {
folder: &str,
) -> Result<bool> {
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 +574,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 {
@@ -691,9 +705,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();
@@ -756,7 +772,7 @@ impl Imap {
fetch_response.flags(),
show_emails,
)
.await?
.await.context("prefetch_should_download")?
{
match download_limit {
Some(download_limit) => uids_fetch.push((
@@ -792,7 +808,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,
@@ -818,11 +835,13 @@ impl Imap {
info!(context, "{} mails read from \"{}\".", read_cnt, folder);
let msg_ids = received_msgs
let msg_ids: Vec<MsgId> = 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?;
@@ -1725,7 +1744,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);
@@ -2015,7 +2046,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())
@@ -2335,33 +2369,41 @@ 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))
.unwrap_or_default();
match Contact::add_or_lookup(
let recipient_addr = match ContactAddress::new(&recipient.addr) {
Err(err) => {
warn!(
context,
"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,
&contact.addr,
recipient_addr,
Origin::OutgoingTo,
)
.await
{
Ok((_, modified)) => {
if modified != Modifier::None {
any_modified = true;
}
}
Err(e) => warn!(context, "Could not add recipient: {}", e),
.await?;
if modified != Modifier::None {
any_modified = true;
}
}
if any_modified {

View File

@@ -4,21 +4,18 @@ use std::{
};
use anyhow::{Context as _, Result};
use async_imap::Client as ImapClient;
use async_imap::Session as ImapSession;
use tokio::io::BufWriter;
use tokio::net::ToSocketAddrs;
use super::capabilities::Capabilities;
use super::session::Session;
use super::session::SessionStream;
use crate::context::Context;
use crate::login_param::build_tls;
use crate::net::connect_tcp;
use crate::socks::Socks5Config;
use super::session::SessionStream;
/// IMAP write and read timeout in seconds.
pub(crate) const IMAP_TIMEOUT: Duration = Duration::from_secs(30);
@@ -91,8 +88,13 @@ impl Client {
Ok(Session::new(session, capabilities))
}
pub async fn connect_secure(hostname: &str, port: u16, strict_tls: bool) -> Result<Self> {
let tcp_stream = connect_tcp((hostname, port), IMAP_TIMEOUT).await?;
pub async fn connect_secure(
context: &Context,
hostname: &str,
port: u16,
strict_tls: bool,
) -> Result<Self> {
let tcp_stream = connect_tcp(context, hostname, port, IMAP_TIMEOUT, strict_tls).await?;
let tls = build_tls(strict_tls);
let tls_stream = tls.connect(hostname, tcp_stream).await?;
let buffered_stream = BufWriter::new(tls_stream);
@@ -107,8 +109,8 @@ impl Client {
Ok(Client { inner: client })
}
pub async fn connect_insecure(addr: impl ToSocketAddrs) -> Result<Self> {
let tcp_stream = connect_tcp(addr, IMAP_TIMEOUT).await?;
pub async fn connect_insecure(context: &Context, hostname: &str, port: u16) -> Result<Self> {
let tcp_stream = connect_tcp(context, hostname, port, IMAP_TIMEOUT, false).await?;
let buffered_stream = BufWriter::new(tcp_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = ImapClient::new(session_stream);
@@ -120,12 +122,16 @@ impl Client {
Ok(Client { inner: client })
}
pub async fn connect_starttls(hostname: &str, port: u16, strict_tls: bool) -> Result<Self> {
let tcp_stream = connect_tcp((hostname, port), IMAP_TIMEOUT).await?;
pub async fn connect_starttls(
context: &Context,
hostname: &str,
port: u16,
strict_tls: bool,
) -> Result<Self> {
let tcp_stream = connect_tcp(context, hostname, port, IMAP_TIMEOUT, strict_tls).await?;
// Run STARTTLS command and convert the client back into a stream.
let session_stream: Box<dyn SessionStream> = Box::new(tcp_stream);
let mut client = ImapClient::new(session_stream);
let mut client = ImapClient::new(tcp_stream);
let _greeting = client
.read_response()
.await
@@ -150,12 +156,15 @@ impl Client {
}
pub async fn connect_secure_socks5(
context: &Context,
domain: &str,
port: u16,
strict_tls: bool,
socks5_config: Socks5Config,
) -> Result<Self> {
let socks5_stream = socks5_config.connect((domain, port), IMAP_TIMEOUT).await?;
let socks5_stream = socks5_config
.connect(context, domain, port, IMAP_TIMEOUT, strict_tls)
.await?;
let tls = build_tls(strict_tls);
let tls_stream = tls.connect(domain, socks5_stream).await?;
let buffered_stream = BufWriter::new(tls_stream);
@@ -170,10 +179,14 @@ impl Client {
}
pub async fn connect_insecure_socks5(
target_addr: impl ToSocketAddrs,
context: &Context,
domain: &str,
port: u16,
socks5_config: Socks5Config,
) -> Result<Self> {
let socks5_stream = socks5_config.connect(target_addr, IMAP_TIMEOUT).await?;
let socks5_stream = socks5_config
.connect(context, domain, port, IMAP_TIMEOUT, false)
.await?;
let buffered_stream = BufWriter::new(socks5_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = ImapClient::new(session_stream);
@@ -186,18 +199,18 @@ impl Client {
}
pub async fn connect_starttls_socks5(
context: &Context,
hostname: &str,
port: u16,
socks5_config: Socks5Config,
strict_tls: bool,
) -> Result<Self> {
let socks5_stream = socks5_config
.connect((hostname, port), IMAP_TIMEOUT)
.connect(context, hostname, port, IMAP_TIMEOUT, strict_tls)
.await?;
// Run STARTTLS command and convert the client back into a stream.
let session_stream: Box<dyn SessionStream> = Box::new(socks5_stream);
let mut client = ImapClient::new(session_stream);
let mut client = ImapClient::new(socks5_stream);
let _greeting = client
.read_response()
.await

View File

@@ -1,12 +1,12 @@
use super::Imap;
use std::time::{Duration, SystemTime};
use anyhow::{bail, Context as _, Result};
use async_channel::Receiver;
use async_imap::extensions::idle::IdleResponse;
use futures_lite::FutureExt;
use std::time::{Duration, SystemTime};
use super::session::Session;
use super::Imap;
use crate::imap::client::IMAP_TIMEOUT;
use crate::{context::Context, scheduler::InterruptInfo};

View File

@@ -3,13 +3,12 @@ use std::{collections::BTreeMap, time::Instant};
use anyhow::{Context as _, Result};
use futures::stream::StreamExt;
use super::{get_folder_meaning, get_folder_meaning_by_name};
use crate::config::Config;
use crate::imap::Imap;
use crate::log::LogExt;
use crate::{context::Context, imap::FolderMeaning};
use super::{get_folder_meaning, get_folder_meaning_by_name};
impl Imap {
/// Returns true if folders were scanned, false if scanning was postponed.
pub(crate) async fn scan_folders(&mut self, context: &Context) -> Result<bool> {

View File

@@ -1,8 +1,8 @@
use super::session::Session as ImapSession;
use crate::context::Context;
use anyhow::Context as _;
use super::session::Session as ImapSession;
use crate::context::Context;
type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
@@ -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)),
},
}
}

View File

@@ -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;
}
}
@@ -769,14 +769,13 @@ where
#[cfg(test)]
mod tests {
use super::*;
use ::pgp::armor::BlockType;
use super::*;
use crate::pgp::{split_armored_data, HEADER_AUTOCRYPT, HEADER_SETUPCODE};
use crate::stock_str::StockMessage;
use crate::test_utils::{alice_keypair, TestContext};
use ::pgp::armor::BlockType;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_render_setup_file() {
let t = TestContext::new_alice().await;

View File

@@ -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
@@ -424,7 +424,6 @@ LIMIT 1;
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::TestContext;
async fn insert_job(context: &Context, foreign_id: i64, valid: bool) {

View File

@@ -11,6 +11,7 @@ use anyhow::{ensure, Context as _, Result};
use futures::Future;
use num_traits::FromPrimitive;
use pgp::composed::Deserializable;
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
use pgp::ser::Serialize;
use pgp::types::{KeyTrait, SecretKeyTrait};
use tokio::runtime::Handle;
@@ -18,11 +19,9 @@ use tokio::runtime::Handle;
use crate::config::Config;
use crate::constants::KeyGenType;
use crate::context::Context;
use crate::tools::{time, EmailAddress};
// Re-export key types
pub use crate::pgp::KeyPair;
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
use crate::tools::{time, EmailAddress};
/// Convenience trait for working with keys.
///
@@ -30,17 +29,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<Self::KeyType> {
Ok(<Self::KeyType as Deserializable>::from_bytes(Cursor::new(
bytes,
))?)
fn from_slice(bytes: &[u8]) -> Result<Self> {
Ok(<Self as Deserializable>::from_bytes(Cursor::new(bytes))?)
}
/// Create a key from a base64 string.
fn from_base64(data: &str) -> Result<Self::KeyType> {
fn from_base64(data: &str) -> Result<Self> {
// strip newlines and other whitespace
let cleaned: String = data.split_whitespace().collect();
let bytes = base64::decode(cleaned.as_bytes())?;
@@ -51,15 +46,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<String, String>)> {
fn from_asc(data: &str) -> Result<(Self, BTreeMap<String, String>)> {
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<Box<dyn Future<Output = Result<Self::KeyType>> + 'a + Send>>;
) -> Pin<Box<dyn Future<Output = Result<Self>> + 'a + Send>>;
/// Serialise the key as bytes.
fn to_bytes(&self) -> Vec<u8> {
@@ -92,11 +87,9 @@ pub trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
}
impl DcKey for SignedPublicKey {
type KeyType = SignedPublicKey;
fn load_self<'a>(
context: &'a Context,
) -> Pin<Box<dyn Future<Output = Result<Self::KeyType>> + 'a + Send>> {
) -> Pin<Box<dyn Future<Output = Result<Self>> + 'a + Send>> {
Box::pin(async move {
let addr = context.get_primary_self_addr().await?;
match context
@@ -143,11 +136,9 @@ impl DcKey for SignedPublicKey {
}
impl DcKey for SignedSecretKey {
type KeyType = SignedSecretKey;
fn load_self<'a>(
context: &'a Context,
) -> Pin<Box<dyn Future<Output = Result<Self::KeyType>> + 'a + Send>> {
) -> Pin<Box<dyn Future<Output = Result<Self>> + 'a + Send>> {
Box::pin(async move {
match context
.sql
@@ -398,11 +389,12 @@ impl std::str::FromStr for Fingerprint {
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::{alice_keypair, TestContext};
use std::sync::Arc;
use once_cell::sync::Lazy;
use std::sync::Arc;
use super::*;
use crate::test_utils::{alice_keypair, TestContext};
static KEYPAIR: Lazy<KeyPair> = Lazy::new(alice_keypair);

View File

@@ -19,7 +19,7 @@ where
impl<T> Keyring<T>
where
T: DcKey<KeyType = T>,
T: DcKey,
{
/// New empty keyring.
pub fn new() -> Keyring<T> {

View File

@@ -66,6 +66,7 @@ mod decrypt;
pub mod download;
mod e2ee;
pub mod ephemeral;
mod http;
mod imap;
pub mod imex;
mod scheduler;

View File

@@ -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,

View File

@@ -155,9 +155,10 @@ impl<T, E: std::fmt::Display> LogExt<T, E> for Result<T, E> {
#[cfg(test)]
mod tests {
use crate::test_utils::TestContext;
use anyhow::Result;
use crate::test_utils::TestContext;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_last_error() -> Result<()> {
let t = TestContext::new().await;

View File

@@ -332,7 +332,6 @@ pub fn build_tls(strict_tls: bool) -> async_native_tls::TlsConnector {
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::TestContext;
#[test]

View File

@@ -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<Message> {
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<String> {
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<PathBuf> {
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<String> {
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<String> {
self.param
.get(Param::File)
@@ -494,26 +522,31 @@ 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<Option<u64>> {
if let Some(path) = self.param.get_path(Param::File, context)? {
Ok(Some(get_filebytes(context, &path).await?))
} else {
Ok(None)
}
}
/// 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
}
@@ -523,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
}
@@ -564,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<String> {
self.param
.get(Param::OverrideSenderDisplayname)
@@ -572,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;
@@ -585,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 as i32 >= MessageState::OutDelivered as i32
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
@@ -600,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
@@ -621,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;
@@ -629,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<String> {
if !self.is_setupmessage() {
return None;
@@ -649,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
@@ -706,6 +756,7 @@ impl Message {
}
}
/// Returns videochat URL if the message is a videochat invitation.
pub fn get_videochat_url(&self) -> Option<String> {
if self.viewtype == Viewtype::VideochatInvitation {
if let Some(instance) = self.param.get(Param::WebrtcRoom) {
@@ -715,6 +766,7 @@ impl Message {
None
}
/// Returns videochat type if the message is a videochat invitation.
pub fn get_videochat_type(&self) -> Option<VideochatType> {
if self.viewtype == Viewtype::VideochatInvitation {
if let Some(instance) = self.param.get(Param::WebrtcRoom) {
@@ -724,10 +776,16 @@ impl Message {
None
}
/// Sets or unsets message text.
pub fn set_text(&mut self, text: Option<String>) {
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 {
@@ -745,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);
}
@@ -759,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,
@@ -823,10 +885,12 @@ impl Message {
Ok(())
}
/// Returns quoted message text, if any.
pub fn quoted_text(&self) -> Option<String> {
self.param.get(Param::Quote).map(|s| s.to_string())
}
/// Returns quoted message, if any.
pub async fn quoted_message(&self, context: &Context) -> Result<Option<Message>> {
if self.param.get(Param::Quote).is_some() && !self.is_forwarded() {
return self.parent(context).await;
@@ -834,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<Option<Message>> {
if let Some(in_reply_to) = &self.in_reply_to {
if let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await? {
@@ -854,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
@@ -893,12 +962,17 @@ 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,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
FromPrimitive,
ToPrimitive,
ToSql,
@@ -908,6 +982,7 @@ impl Message {
)]
#[repr(u32)]
pub enum MessageState {
/// Undefined message state.
Undefined = 0,
/// Incoming *fresh* message. Fresh messages are neither noticed
@@ -978,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!(
@@ -985,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!(
@@ -994,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<String> {
let msg = Message::load_from_db(context, msg_id).await?;
let rawtxt: Option<String> = context
@@ -1100,8 +1179,8 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
}
if let Some(path) = msg.get_file(context) {
let bytes = get_filebytes(context, &path).await;
ret += &format!("\nFile: {}, {}, bytes\n", path.display(), bytes);
let bytes = get_filebytes(context, &path).await?;
ret += &format!("\nFile: {}, {} bytes\n", path.display(), bytes);
}
if msg.viewtype != Viewtype::Text {
@@ -1158,7 +1237,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
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,
@@ -1271,6 +1350,9 @@ pub async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result<Vec<u8
Ok(headers)
}
/// Deletes requested messages
/// by moving them to the trash chat
/// and scheduling for deletion on IMAP.
pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
for msg_id in msg_ids.iter() {
let msg = Message::load_from_db(context, *msg_id).await?;
@@ -1318,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<MsgId>) -> Result<()> {
if msg_ids.is_empty() {
return Ok(());
@@ -1450,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<bool> {
/// 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<bool> {
if msg_id.is_special() {
return Ok(false);
}
@@ -1467,7 +1551,7 @@ pub async fn exists(context: &Context, msg_id: MsgId) -> Result<bool> {
}
}
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;
@@ -1687,7 +1771,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,12 +1791,26 @@ 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
}
}
}
/// 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,
@@ -1801,6 +1899,7 @@ pub(crate) async fn rfc724_mid_exists(
)]
#[repr(u32)]
pub enum Viewtype {
/// Unknown message type.
Unknown = 0,
/// Text message.
@@ -1883,13 +1982,12 @@ impl Viewtype {
mod tests {
use num_traits::FromPrimitive;
use super::*;
use crate::chat::{marknoticed_chat, ChatItem};
use crate::chatlist::Chatlist;
use crate::receive_imf::receive_imf;
use crate::test_utils as test;
use crate::test_utils::TestContext;
use super::*;
use crate::test_utils::{TestContext, TestContextManager};
#[test]
fn test_guess_msgtype_from_suffix() {
@@ -2374,8 +2472,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";
@@ -2388,6 +2487,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!'";

View File

@@ -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<String>,
/// True if the avatar should be attached.
attach_selfavatar: bool,
}
@@ -689,7 +690,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) {
@@ -722,9 +725,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 +787,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,
..
@@ -905,6 +918,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 => {
@@ -1432,7 +1456,7 @@ fn recipients_contain_addr(recipients: &[(String, String)], addr: &str) -> bool
async fn is_file_size_okay(context: &Context, msg: &Message) -> Result<bool> {
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),
@@ -1484,18 +1508,17 @@ fn maybe_encode_words(words: &str) -> String {
mod tests {
use mailparse::{addrparse_header, MailHeaderMap};
use super::*;
use crate::chat::ChatId;
use crate::chat::{
self, add_contact_to_chat, create_group_chat, remove_contact_from_chat, send_text_msg,
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};
use super::*;
#[test]
fn test_render_email_address() {
let display_name = "ä space";
@@ -1817,11 +1840,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()
.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();

View File

@@ -246,14 +246,17 @@ 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)
}
Ok(None) => (Ok(mail), HashSet::new(), false),
Err(err) => {
warn!(context, "decryption failed: {}", err);
warn!(context, "decryption failed: {:#}", err);
(Err(err), HashSet::new(), false)
}
};
@@ -382,7 +385,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);
@@ -682,7 +685,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
}
@@ -989,7 +992,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);
}
@@ -1009,7 +1012,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);
}
@@ -1141,7 +1144,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") {
@@ -1159,7 +1162,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(());
@@ -1181,7 +1184,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(());
}
@@ -1226,7 +1229,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);
}
@@ -1952,6 +1955,8 @@ where
mod tests {
#![allow(clippy::indexing_slicing)]
use mailparse::ParsedMail;
use super::*;
use crate::{
chatlist::Chatlist,
@@ -1961,7 +1966,6 @@ mod tests {
receive_imf::receive_imf,
test_utils::TestContext,
};
use mailparse::ParsedMail;
impl AvatarAction {
pub fn is_change(&self) -> bool {
@@ -3147,7 +3151,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);

View File

@@ -1,25 +1,180 @@
///! # Common network utilities.
use std::net::{IpAddr, SocketAddr};
use std::pin::Pin;
use std::str::FromStr;
use std::time::Duration;
use anyhow::{Context as _, Result};
use tokio::net::{TcpStream, ToSocketAddrs};
use anyhow::{Context as _, Error, Result};
use tokio::net::{lookup_host, TcpStream};
use tokio::time::timeout;
use tokio_io_timeout::TimeoutStream;
use crate::context::Context;
use crate::tools::time;
async fn connect_tcp_inner(addr: SocketAddr, timeout_val: Duration) -> Result<TcpStream> {
let tcp_stream = timeout(timeout_val, TcpStream::connect(addr))
.await
.context("connection timeout")?
.context("connection failure")?;
Ok(tcp_stream)
}
async fn lookup_host_with_timeout(
hostname: &str,
port: u16,
timeout_val: Duration,
) -> Result<Vec<SocketAddr>> {
let res = timeout(timeout_val, lookup_host((hostname, port)))
.await
.context("DNS lookup timeout")?
.context("DNS lookup failure")?;
Ok(res.collect())
}
/// Looks up hostname and port using DNS and updates the address resolution cache.
///
/// If `load_cache` is true, appends cached results not older than 30 days to the end.
async fn lookup_host_with_cache(
context: &Context,
hostname: &str,
port: u16,
timeout_val: Duration,
load_cache: bool,
) -> Result<Vec<SocketAddr>> {
let now = time();
let mut resolved_addrs = match lookup_host_with_timeout(hostname, port, timeout_val).await {
Ok(res) => res,
Err(err) => {
warn!(
context,
"DNS resolution for {}:{} failed: {:#}.", hostname, port, err
);
Vec::new()
}
};
for addr in resolved_addrs.iter() {
let ip_string = addr.ip().to_string();
if ip_string == hostname {
// IP address resolved into itself, not interesting to cache.
continue;
}
info!(context, "Resolved {}:{} into {}.", hostname, port, &addr);
// Update the cache.
context
.sql
.execute(
"INSERT INTO dns_cache
(hostname, address, timestamp)
VALUES (?, ?, ?)
ON CONFLICT (hostname, address)
DO UPDATE SET timestamp=excluded.timestamp",
paramsv![hostname, ip_string, now],
)
.await?;
}
if load_cache {
for cached_address in context
.sql
.query_map(
"SELECT address
FROM dns_cache
WHERE hostname = ?
AND ? < timestamp + 30 * 24 * 3600
ORDER BY timestamp DESC",
paramsv![hostname, now],
|row| {
let address: String = row.get(0)?;
Ok(address)
},
|rows| {
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await?
{
match IpAddr::from_str(&cached_address) {
Ok(ip_addr) => {
let addr = SocketAddr::new(ip_addr, port);
if !resolved_addrs.contains(&addr) {
resolved_addrs.push(addr);
}
}
Err(err) => {
warn!(
context,
"Failed to parse cached address {:?}: {:#}.", cached_address, err
);
}
}
}
}
Ok(resolved_addrs)
}
/// 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.
///
/// If `load_cache` is true, may use cached DNS results.
/// Because the cache may be poisoned with incorrect results by networks hijacking DNS requests,
/// this option should only be used when connection is authenticated,
/// for example using TLS.
/// If TLS is not used or invalid TLS certificates are allowed,
/// this option should be disabled.
pub(crate) async fn connect_tcp(
addr: impl ToSocketAddrs,
context: &Context,
host: &str,
port: u16,
timeout_val: Duration,
load_cache: bool,
) -> Result<Pin<Box<TimeoutStream<TcpStream>>>> {
let tcp_stream = timeout(timeout_val, TcpStream::connect(addr))
.await
.context("connection timeout")?
.context("connection failure")?;
let mut tcp_stream = None;
let mut last_error = None;
for resolved_addr in
lookup_host_with_cache(context, host, port, timeout_val, load_cache).await?
{
match connect_tcp_inner(resolved_addr, timeout_val).await {
Ok(stream) => {
tcp_stream = Some(stream);
// Maximize priority of this cached entry.
context
.sql
.execute(
"UPDATE dns_cache
SET timestamp = ?
WHERE address = ?",
paramsv![time(), resolved_addr.ip().to_string()],
)
.await?;
break;
}
Err(err) => {
warn!(
context,
"Failed to connect to {}: {:#}.", resolved_addr, err
);
last_error = Some(err);
}
}
}
let tcp_stream = match tcp_stream {
Some(tcp_stream) => tcp_stream,
None => {
return Err(last_error.unwrap_or_else(|| Error::msg("no DNS resolution results")));
}
};
// Disable Nagle's algorithm.
tcp_stream.set_nodelay(true)?;

View File

@@ -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);
@@ -345,7 +352,6 @@ fn normalize_addr(addr: &str) -> &str {
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::TestContext;
#[test]

View File

@@ -435,14 +435,13 @@ impl<'a> ParamsFile<'a> {
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
use std::str::FromStr;
use anyhow::Result;
use tokio::fs;
use super::*;
use crate::test_utils::TestContext;
#[test]

View File

@@ -3,13 +3,15 @@
#![allow(missing_docs)]
use std::collections::HashSet;
use std::fmt;
use anyhow::{Context as _, Error, Result};
use num_traits::FromPrimitive;
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};
@@ -17,8 +19,6 @@ use crate::message::Message;
use crate::mimeparser::SystemMessage;
use crate::sql::Sql;
use crate::stock_str;
use anyhow::{Context as _, Result};
use num_traits::FromPrimitive;
#[derive(Debug)]
pub enum PeerstateKeyType {
@@ -35,6 +35,7 @@ pub enum PeerstateVerifiedStatus {
}
/// Peerstate represents the state of an Autocrypt peer.
#[derive(Debug, PartialEq, Eq)]
pub struct Peerstate {
pub addr: String,
pub last_seen: i64,
@@ -52,44 +53,6 @@ pub struct Peerstate {
pub verifier: Option<String>,
}
impl PartialEq for Peerstate {
fn eq(&self, other: &Peerstate) -> bool {
self.addr == other.addr
&& self.last_seen == other.last_seen
&& self.last_seen_autocrypt == other.last_seen_autocrypt
&& self.prefer_encrypt == other.prefer_encrypt
&& self.public_key == other.public_key
&& self.public_key_fingerprint == other.public_key_fingerprint
&& self.gossip_key == other.gossip_key
&& self.gossip_timestamp == other.gossip_timestamp
&& self.gossip_key_fingerprint == other.gossip_key_fingerprint
&& self.verified_key == other.verified_key
&& self.verified_key_fingerprint == other.verified_key_fingerprint
&& self.fingerprint_changed == other.fingerprint_changed
}
}
impl Eq for Peerstate {}
impl fmt::Debug for Peerstate {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Peerstate")
.field("addr", &self.addr)
.field("last_seen", &self.last_seen)
.field("last_seen_autocrypt", &self.last_seen_autocrypt)
.field("prefer_encrypt", &self.prefer_encrypt)
.field("public_key", &self.public_key)
.field("public_key_fingerprint", &self.public_key_fingerprint)
.field("gossip_key", &self.gossip_key)
.field("gossip_timestamp", &self.gossip_timestamp)
.field("gossip_key_fingerprint", &self.gossip_key_fingerprint)
.field("verified_key", &self.verified_key)
.field("verified_key_fingerprint", &self.verified_key_fingerprint)
.field("fingerprint_changed", &self.fingerprint_changed)
.finish()
}
}
impl Peerstate {
pub fn from_header(header: &Aheader, message_time: i64) -> Self {
Peerstate {
@@ -223,7 +186,10 @@ impl Peerstate {
.transpose()
.unwrap_or_default(),
fingerprint_changed: false,
verifier: row.get("verifier")?,
verifier: {
let verifier: Option<String> = row.get("verifier")?;
verifier.filter(|verifier| !verifier.is_empty())
},
};
Ok(res)
@@ -369,43 +335,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"))
}
}
@@ -450,7 +421,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,
self.verifier.as_deref().unwrap_or(""),
],
)
.await?;
@@ -542,14 +513,31 @@ 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, _) =
Contact::add_or_lookup(context, "", new_addr, Origin::IncomingUnknownFrom)
match ContactAddress::new(new_addr) {
Ok(new_addr) => {
let (new_contact_id, _) = Contact::add_or_lookup(
context,
"",
new_addr,
Origin::IncomingUnknownFrom,
)
.await?;
chat::add_to_chat_contacts_table(context, *chat_id, &[new_contact_id]).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
)
}
}
}
}

View File

@@ -382,11 +382,12 @@ pub async fn symm_decrypt<T: std::io::Read + std::io::Seek>(
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::{alice_keypair, bob_keypair};
use once_cell::sync::Lazy;
use tokio::sync::OnceCell;
use super::*;
use crate::test_utils::{alice_keypair, bob_keypair};
#[test]
fn test_split_armored_data_1() {
let (typ, _headers, base64) = split_armored_data(

View File

@@ -2,9 +2,10 @@
#![allow(missing_docs)]
use crate::simplify::split_lines;
use once_cell::sync::Lazy;
use crate::simplify::split_lines;
#[derive(Debug)]
pub struct PlainText {
pub text: String,

View File

@@ -4,13 +4,14 @@
mod data;
use crate::config::Config;
use crate::context::Context;
use crate::provider::data::{PROVIDER_DATA, PROVIDER_IDS, PROVIDER_UPDATED};
use anyhow::Result;
use chrono::{NaiveDateTime, NaiveTime};
use trust_dns_resolver::{config, AsyncResolver, TokioAsyncResolver};
use crate::config::Config;
use crate::context::Context;
use crate::provider::data::{PROVIDER_DATA, PROVIDER_IDS, PROVIDER_UPDATED};
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum Status {
@@ -195,10 +196,11 @@ pub fn get_provider_update_timestamp() -> i64 {
mod tests {
#![allow(clippy::indexing_slicing)]
use chrono::NaiveDate;
use super::*;
use crate::test_utils::TestContext;
use crate::tools::time;
use chrono::NaiveDate;
#[test]
fn test_get_provider_by_domain_unexistant() {

View File

@@ -1,12 +1,13 @@
// file generated by src/provider/update.py
use std::collections::HashMap;
use once_cell::sync::Lazy;
use crate::provider::Protocol::*;
use crate::provider::Socket::*;
use crate::provider::UsernamePattern::*;
use crate::provider::{Config, ConfigDefault, Oauth2Authorizer, Provider, Server, Status};
use std::collections::HashMap;
use once_cell::sync::Lazy;
// 163.md: 163.com
static P_163: Lazy<Provider> = Lazy::new(|| Provider {
@@ -526,7 +527,7 @@ static P_GMX_NET: Lazy<Provider> = 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<Provider> = Lazy::new(|| Provider {
id: "hermes.radio",
status: Status::Ok,
@@ -902,6 +903,35 @@ static P_NAVER: Lazy<Provider> = Lazy::new(|| Provider {
oauth2_authorizer: None,
});
// nubo.coop.md: nubo.coop
static P_NUBO_COOP: Lazy<Provider> = 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<Provider> = Lazy::new(|| Provider {
id: "outlook.com",
@@ -931,6 +961,35 @@ static P_OUTLOOK_COM: Lazy<Provider> = Lazy::new(|| Provider {
oauth2_authorizer: None,
});
// ouvaton.coop.md: ouvaton.org
static P_OUVATON_COOP: Lazy<Provider> = 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<Provider> = Lazy::new(|| Provider {
id: "posteo",
@@ -1659,6 +1718,22 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
("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 +1756,14 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
("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 +1938,9 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
("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 +1970,4 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
});
pub static PROVIDER_UPDATED: Lazy<chrono::NaiveDate> =
Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2022, 7, 5).unwrap());
Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2023, 1, 6).unwrap());

View File

@@ -190,6 +190,6 @@ if __name__ == "__main__":
now = datetime.datetime.utcnow()
out_all += "pub static PROVIDER_UPDATED: Lazy<chrono::NaiveDate> = "\
"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)

View File

@@ -3,18 +3,21 @@
#![allow(missing_docs)]
mod dclogin_scheme;
pub use dclogin_scheme::LoginOptions;
use std::collections::BTreeMap;
use anyhow::{anyhow, bail, ensure, Context as _, Error, Result};
pub use dclogin_scheme::LoginOptions;
use once_cell::sync::Lazy;
use percent_encoding::percent_decode_str;
use serde::Deserialize;
use std::collections::BTreeMap;
use self::dclogin_scheme::configure_from_login_qr;
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;
@@ -22,8 +25,6 @@ use crate::peerstate::Peerstate;
use crate::tools::time;
use crate::{token, EventType};
use self::dclogin_scheme::configure_from_login_qr;
const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase
const DCACCOUNT_SCHEME: &str = "DCACCOUNT:";
pub(super) const DCLOGIN_SCHEME: &str = "DCLOGIN:";
@@ -221,14 +222,14 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
.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 addr = ContactAddress::new(addr)?;
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))?;
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))?
{
@@ -261,7 +262,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
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,
@@ -287,10 +288,11 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
}
} else if let Some(addr) = addr {
if let Some(peerstate) = peerstate {
let contact_id =
Contact::add_or_lookup(context, &name, &peerstate.addr, Origin::UnhandledQrScan)
let peerstate_addr = ContactAddress::new(&peerstate.addr)?;
let (contact_id, _) =
Contact::add_or_lookup(context, &name, peerstate_addr, Origin::UnhandledQrScan)
.await
.map(|(id, _)| id)?;
.context("add_or_lookup")?;
let chat = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Request)
.await
.context("Failed to create (new) chat for contact")?;
@@ -373,7 +375,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!(
@@ -530,11 +532,11 @@ async fn decode_mailto(context: &Context, qr: &str) -> Result<Qr> {
};
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
@@ -554,8 +556,8 @@ async fn decode_smtp(context: &Context, qr: &str) -> Result<Qr> {
};
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.
@@ -579,8 +581,8 @@ async fn decode_matmsg(context: &Context, qr: &str) -> Result<Qr> {
};
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<regex::Regex> =
@@ -609,18 +611,19 @@ async fn decode_vcard(context: &Context, qr: &str) -> Result<Qr> {
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<String>,
) -> Result<Self> {
let addr = ContactAddress::new(addr)?;
let (contact_id, _) =
Contact::add_or_lookup(context, &name, &addr, Origin::UnhandledQrScan).await?;
Contact::add_or_lookup(context, name, addr, Origin::UnhandledQrScan).await?;
Ok(Qr::Addr { contact_id, draft })
}
}
@@ -638,14 +641,14 @@ fn normalize_address(addr: &str) -> Result<String, Error> {
#[cfg(test)]
mod tests {
use super::*;
use anyhow::Result;
use super::*;
use crate::aheader::EncryptPreference;
use crate::chat::{create_group_chat, ProtectionStatus};
use crate::key::DcKey;
use crate::securejoin::get_securejoin_qr;
use crate::test_utils::{alice_keypair, TestContext};
use anyhow::Result;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_http() -> Result<()> {

View File

@@ -1,13 +1,13 @@
use std::collections::HashMap;
use crate::config::Config;
use crate::context::Context;
use crate::provider::Socket;
use crate::{contact, login_param::CertificateChecks};
use anyhow::{bail, Context as _, Result};
use num_traits::cast::ToPrimitive;
use super::{Qr, DCLOGIN_SCHEME};
use crate::config::Config;
use crate::context::Context;
use crate::provider::Socket;
use crate::{contact, login_param::CertificateChecks};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LoginOptions {
@@ -221,9 +221,10 @@ pub(crate) async fn configure_from_login_qr(
#[cfg(test)]
mod test {
use anyhow::{self, bail};
use super::{decode_login, LoginOptions};
use crate::{login_param::CertificateChecks, provider::Socket, qr::Qr};
use anyhow::{self, bail};
macro_rules! login_options_just_pw {
($pw: expr) => {

View File

@@ -2,9 +2,10 @@
#![allow(missing_docs)]
use std::collections::BTreeMap;
use anyhow::{anyhow, Context as _, Result};
use async_imap::types::{Quota, QuotaResource};
use std::collections::BTreeMap;
use crate::chat::add_device_msg_with_importance;
use crate::config::Config;
@@ -134,7 +135,7 @@ impl Context {
/// Called in response to `Action::UpdateRecentQuota`.
pub(crate) async fn update_recent_quota(&self, imap: &mut Imap) -> Result<Status> {
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 +163,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),
}
}

View File

@@ -287,10 +287,9 @@ pub async fn get_msg_reactions(context: &Context, msg_id: MsgId) -> Result<React
mod tests {
use super::*;
use crate::chat::get_chat_msgs;
use crate::config::Config;
use crate::constants::DC_CHAT_ID_TRASH;
use crate::contact::{Contact, Origin};
use crate::contact::{Contact, ContactAddress, Origin};
use crate::download::DownloadState;
use crate::message::MessageState;
use crate::receive_imf::{receive_imf, receive_imf_inner};
@@ -366,9 +365,14 @@ Can we chat at 1pm pacific, today?"
let contacts = reactions.contacts();
assert_eq!(contacts.len(), 0);
let bob_id = Contact::add_or_lookup(&alice, "", "bob@example.net", Origin::ManuallyCreated)
.await?
.0;
let bob_id = Contact::add_or_lookup(
&alice,
"",
ContactAddress::new("bob@example.net")?,
Origin::ManuallyCreated,
)
.await?
.0;
let bob_reaction = reactions.get(bob_id);
assert!(bob_reaction.is_empty()); // Bob has not reacted to message yet.

View File

@@ -16,7 +16,7 @@ use crate::chat::{self, is_contact_in_chat, Chat, ChatId, ChatIdBlocked, Protect
use crate::config::Config;
use crate::constants::{Blocked, Chattype, ShowEmails, DC_CHAT_ID_TRASH};
use crate::contact::{
may_be_valid_addr, normalize_name, Contact, ContactId, Origin, VerifiedStatus,
may_be_valid_addr, normalize_name, Contact, ContactAddress, ContactId, Origin, VerifiedStatus,
};
use crate::context::Context;
use crate::download::DownloadState;
@@ -94,15 +94,18 @@ pub(crate) async fn receive_imf_inner(
) -> Result<Option<ReceivedMsg>> {
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 =
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
@@ -170,7 +173,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;
@@ -253,7 +265,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 +280,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);
}
}
@@ -278,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,
@@ -286,12 +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
);
};
}
}
@@ -317,7 +327,7 @@ pub(crate) async fn receive_imf_inner(
)
.await
{
warn!(context, "cannot update contact status: {}", err);
warn!(context, "cannot update contact status: {:#}", err);
}
}
@@ -346,11 +356,7 @@ pub(crate) async fn receive_imf_inner(
} else if !chat_id.is_trash() {
let fresh = received_msg.state == MessageState::InFresh;
for msg_id in &received_msg.msg_ids {
if incoming && fresh {
context.emit_incoming_msg(chat_id, *msg_id);
} else {
context.emit_msgs_changed(chat_id, *msg_id);
};
chat_id.emit_msg_event(context, *msg_id, incoming && fresh);
}
}
@@ -366,26 +372,39 @@ pub(crate) async fn receive_imf_inner(
/// Also returns whether it is blocked or not and its origin.
///
/// * `prevent_rename`: passed through to `add_or_lookup_contacts_by_address_list()`
///
/// Returns `None` if From field does not contain a valid contact address.
pub async fn from_field_to_contact_id(
context: &Context,
from: &SingleInfo,
prevent_rename: bool,
) -> Result<(ContactId, bool, Origin)> {
) -> Result<Option<(ContactId, bool, Origin)>> {
let display_name = if prevent_rename {
Some("")
} else {
from.display_name.as_deref()
};
let from_addr = match ContactAddress::new(&from.addr) {
Ok(from_addr) => 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?;
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;
@@ -393,7 +412,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)))
}
}
@@ -495,7 +514,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 +749,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 +889,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 +945,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 +994,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 {
@@ -1216,7 +1235,7 @@ SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id,
replace_msg_id.delete_from_db(context).await?;
}
chat_id.unarchive_if_not_muted(context).await?;
chat_id.unarchive_if_not_muted(context, state).await?;
info!(
context,
@@ -1487,7 +1506,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);
}
@@ -1713,7 +1732,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);
}
@@ -1953,6 +1972,13 @@ async fn apply_mailinglist_changes(
}
let listid = &chat.grpid;
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?;
@@ -1962,7 +1988,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);
@@ -2171,10 +2197,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;
}
@@ -2293,8 +2319,13 @@ 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 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);
}
}
Ok(contact_ids.into_iter().collect::<Vec<ContactId>>())
@@ -2304,17 +2335,17 @@ async fn add_or_lookup_contacts_by_address_list(
async fn add_or_lookup_contact_by_addr(
context: &Context,
display_name: Option<&str>,
addr: &str,
addr: ContactAddress<'_>,
origin: Origin,
) -> Result<ContactId> {
if context.is_self_addr(addr).await? {
if context.is_self_addr(&addr).await? {
return Ok(ContactId::SELF);
}
let display_name_normalized = display_name.map(normalize_name).unwrap_or_default();
let (row_id, _modified) =
let (contact_id, _modified) =
Contact::add_or_lookup(context, &display_name_normalized, addr, origin).await?;
Ok(row_id)
Ok(contact_id)
}
#[cfg(test)]

View File

@@ -1,7 +1,6 @@
use tokio::fs;
use super::*;
use crate::aheader::EncryptPreference;
use crate::chat::get_chat_contacts;
use crate::chat::{get_chat_msgs, ChatItem, ChatVisibility};
@@ -425,11 +424,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()
.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,
@@ -467,11 +470,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()
.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,
@@ -2054,7 +2061,7 @@ 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?
@@ -2109,9 +2116,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?
.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);
@@ -2523,13 +2535,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?;
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;

View File

@@ -4,6 +4,7 @@ use futures::try_join;
use futures_lite::FutureExt;
use tokio::task;
use self::connectivity::ConnectivityStore;
use crate::config::Config;
use crate::contact::{ContactId, RecentlySeenLoop};
use crate::context::Context;
@@ -17,8 +18,6 @@ use crate::sql;
use crate::tools::time;
use crate::tools::{duration_to_str, maybe_add_time_based_warnings};
use self::connectivity::ConnectivityStore;
pub(crate) mod connectivity;
/// Job and connection scheduler.

View File

@@ -3,6 +3,8 @@
use core::fmt;
use std::{ops::Deref, sync::Arc};
use anyhow::{anyhow, Result};
use humansize::{format_size, BINARY};
use tokio::sync::{Mutex, RwLockReadGuard};
use crate::events::EventType;
@@ -13,8 +15,6 @@ use crate::quota::{
use crate::tools::time;
use crate::{config::Config, scheduler::Scheduler, stock_str, tools};
use crate::{context::Context, log::LogExt};
use anyhow::{anyhow, Result};
use humansize::{format_size, BINARY};
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumProperty, PartialOrd, Ord)]
pub enum Connectivity {

View File

@@ -30,10 +30,11 @@ mod bob;
mod bobstate;
mod qrinvite;
use crate::token::Namespace;
use bobstate::BobState;
use qrinvite::QrInvite;
use crate::token::Namespace;
pub const NON_ALPHANUMERIC_WITHOUT_DOT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'.');
macro_rules! inviter_progress {
@@ -415,7 +416,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()
{
@@ -455,6 +456,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 +506,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())
@@ -579,40 +579,103 @@ 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 let Err(err) = peerstate.set_verified(
PeerstateKeyType::GossipKey,
fingerprint,
PeerstateVerifiedStatus::BidirectVerified,
addr,
) {
could_not_establish_secure_connection(
context,
contact_id,
info_chat_id(context, contact_id).await?,
&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)
{
// 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);
}
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 {
@@ -653,25 +716,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()
);
}
/* ******************************************************************************
@@ -705,11 +768,12 @@ fn encrypted_and_signed(
#[cfg(test)]
mod tests {
use super::*;
use crate::chat;
use crate::chat::ProtectionStatus;
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};
@@ -1003,7 +1067,7 @@ 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?;

View File

@@ -5,6 +5,9 @@
use anyhow::{Context as _, Result};
use super::bobstate::{BobHandshakeStage, BobState};
use super::qrinvite::QrInvite;
use super::HandshakeMessage;
use crate::chat::{is_contact_in_chat, ChatId, ProtectionStatus};
use crate::constants::{Blocked, Chattype};
use crate::contact::Contact;
@@ -14,10 +17,6 @@ use crate::mimeparser::MimeMessage;
use crate::tools::time;
use crate::{chat, stock_str};
use super::bobstate::{BobHandshakeStage, BobState};
use super::qrinvite::QrInvite;
use super::HandshakeMessage;
/// Starts the securejoin protocol with the QR `invite`.
///
/// This will try to start the securejoin protocol for the given QR `invite`. If it

View File

@@ -10,6 +10,8 @@
use anyhow::{Error, Result};
use rusqlite::Connection;
use super::qrinvite::QrInvite;
use super::{encrypted_and_signed, fingerprint_equals_sender, mark_peer_as_verified};
use crate::chat::{self, ChatId};
use crate::contact::{Contact, Origin};
use crate::context::Context;
@@ -21,9 +23,6 @@ use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::param::Param;
use crate::sql::Sql;
use super::qrinvite::QrInvite;
use super::{encrypted_and_signed, fingerprint_equals_sender, mark_peer_as_verified};
/// The stage of the [`BobState`] securejoin handshake protocol state machine.
///
/// This does not concern itself with user interactions, only represents what happened to
@@ -368,7 +367,7 @@ impl BobState {
}
mark_peer_as_verified(
context,
self.invite.fingerprint(),
self.invite.fingerprint().clone(),
mime_message.from.addr.to_string(),
)
.await?;

View File

@@ -285,9 +285,10 @@ fn is_plain_quote(buf: &str) -> bool {
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use super::*;
proptest! {
#[test]
// proptest does not support [[:graphical:][:space:]] regex.

View File

@@ -1,16 +1,21 @@
//! # SMTP message sending
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;
use async_smtp::{EmailAddress, Envelope, SendableEmail, Transport};
use super::Smtp;
use crate::config::Config;
use crate::context::Context;
use crate::events::EventType;
pub type Result<T> = std::result::Result<T, Error>;
// 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)]

View File

@@ -4,15 +4,17 @@ use std::fmt;
use std::pin::Pin;
use std::time::Duration;
use crate::net::connect_tcp;
use anyhow::Result;
pub use async_smtp::ServerAddress;
use tokio::net::{self, TcpStream};
use fast_socks5::client::{Config, Socks5Stream};
use fast_socks5::util::target_addr::ToTargetAddr;
use fast_socks5::AuthenticationMethod;
use fast_socks5::Socks5Command;
use tokio::net::TcpStream;
use tokio_io_timeout::TimeoutStream;
use crate::context::Context;
use fast_socks5::client::{Config, Socks5Stream};
use fast_socks5::AuthenticationMethod;
use crate::net::connect_tcp;
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub struct Socks5Config {
@@ -54,12 +56,18 @@ impl Socks5Config {
}
}
/// If `load_dns_cache` is true, loads cached DNS resolution results.
/// Use this only if the connection is going to be protected with TLS checks.
pub async fn connect(
&self,
target_addr: impl net::ToSocketAddrs,
context: &Context,
target_host: &str,
target_port: u16,
timeout_val: Duration,
load_dns_cache: bool,
) -> Result<Socks5Stream<Pin<Box<TimeoutStream<TcpStream>>>>> {
let tcp_stream = connect_tcp(target_addr, timeout_val).await?;
let tcp_stream =
connect_tcp(context, &self.host, self.port, timeout_val, load_dns_cache).await?;
let authentication_method = if let Some((username, password)) = self.user_password.as_ref()
{
@@ -70,8 +78,12 @@ impl Socks5Config {
} else {
None
};
let socks_stream =
let mut socks_stream =
Socks5Stream::use_stream(tcp_stream, authentication_method, Config::default()).await?;
let target_addr = (target_host, target_port).to_target_addr()?;
socks_stream
.request(Socks5Command::TCPConnect, target_addr)
.await?;
Ok(socks_stream)
}

View File

@@ -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?;
@@ -874,11 +874,10 @@ pub fn repeat_vars(count: usize) -> String {
mod tests {
use async_channel as channel;
use super::*;
use crate::config::Config;
use crate::{test_utils::TestContext, EventType};
use super::*;
#[test]
fn test_maybe_add_file() {
let mut files = Default::default();

View File

@@ -671,6 +671,18 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
)
.await?;
}
if dbversion < 97 {
sql.execute_migration(
"CREATE TABLE dns_cache (
hostname TEXT NOT NULL,
address TEXT NOT NULL, -- IPv4 or IPv6 address
timestamp INTEGER NOT NULL,
UNIQUE (hostname, address)
)",
97,
)
.await?;
}
let new_version = sql
.get_raw_config_int(VERSION_CFG)

View File

@@ -6,6 +6,7 @@ use std::collections::HashMap;
use std::sync::Arc;
use anyhow::{bail, Result};
use humansize::{format_size, BINARY};
use strum::EnumProperty as EnumPropertyTrait;
use strum_macros::EnumProperty;
use tokio::sync::RwLock;
@@ -19,7 +20,6 @@ use crate::context::Context;
use crate::message::{Message, Viewtype};
use crate::param::Param;
use crate::tools::timestamp_to_str;
use humansize::{format_size, BINARY};
#[derive(Debug, Clone)]
pub struct StockStrings {
@@ -1308,13 +1308,12 @@ impl Accounts {
mod tests {
use num_traits::ToPrimitive;
use super::*;
use crate::chat::delete_and_reset_all_device_msgs;
use crate::chat::Chat;
use crate::chatlist::Chatlist;
use crate::test_utils::TestContext;
use super::*;
#[test]
fn test_enum_mapping() {
assert_eq!(StockMessage::NoMessages.to_usize().unwrap(), 1);

View File

@@ -1,5 +1,8 @@
//! # Message summary for chatlist.
use std::borrow::Cow;
use std::fmt;
use crate::chat::Chat;
use crate::constants::Chattype;
use crate::contact::{Contact, ContactId};
@@ -9,8 +12,6 @@ use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::stock_str;
use crate::tools::truncate;
use std::borrow::Cow;
use std::fmt;
/// Prefix displayed before message and separated by ":" in the chatlist.
#[derive(Debug)]

View File

@@ -1,5 +1,10 @@
//! # Synchronize items between devices.
use anyhow::Result;
use lettre_email::mime::{self};
use lettre_email::PartBuilder;
use serde::{Deserialize, Serialize};
use crate::chat::{Chat, ChatId};
use crate::config::Config;
use crate::constants::Blocked;
@@ -12,10 +17,6 @@ use crate::sync::SyncData::{AddQrToken, DeleteQrToken};
use crate::token::Namespace;
use crate::tools::time;
use crate::{chat, stock_str, token};
use anyhow::Result;
use lettre_email::mime::{self};
use lettre_email::PartBuilder;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct QrTokenData {
@@ -260,12 +261,13 @@ impl Context {
#[cfg(test)]
mod tests {
use anyhow::bail;
use super::*;
use crate::chat::Chat;
use crate::chatlist::Chatlist;
use crate::test_utils::TestContext;
use crate::token::Namespace;
use anyhow::bail;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_is_sync_sending_enabled() -> Result<()> {

View File

@@ -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;
@@ -21,8 +22,8 @@ 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::contact::{Contact, ContactId, Modifier, Origin};
use crate::constants::{DC_GCL_NO_SPECIALS, DC_GCM_ADDDAYMARKER, DC_MSG_ID_DAYMARKER};
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};
@@ -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());
@@ -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):
@@ -523,13 +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
.unwrap();
.expect("add_or_lookup");
match modified {
Modifier::None => (),
Modifier::Modified => warn!(&self.ctx, "Contact {} modified by TestContext", &addr),
@@ -702,6 +703,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 +1093,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());
}
}

View File

@@ -9,7 +9,6 @@ use std::fmt;
use std::io::Cursor;
use std::path::{Path, PathBuf};
use std::str::from_utf8;
use std::time::{Duration, SystemTime};
use anyhow::{bail, Error, Result};
@@ -277,6 +276,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 +290,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()
@@ -358,12 +363,10 @@ pub(crate) fn get_abs_path(context: &Context, path: impl AsRef<Path>) -> PathBuf
}
}
pub(crate) async fn get_filebytes(context: &Context, path: impl AsRef<Path>) -> u64 {
pub(crate) async fn get_filebytes(context: &Context, path: impl AsRef<Path>) -> Result<u64> {
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<Path>) -> bool {
@@ -699,7 +702,6 @@ mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::{
config::Config, message::get_msg_info, receive_imf::receive_imf, test_utils::TestContext,
};
@@ -1004,11 +1006,12 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true";
assert_eq!(EmailAddress::new("@d.tt").is_ok(), false);
}
use crate::chatlist::Chatlist;
use crate::{chat, test_utils};
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use proptest::prelude::*;
use crate::chatlist::Chatlist;
use crate::{chat, test_utils};
proptest! {
#[test]
fn test_truncate(
@@ -1049,7 +1052,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()

View File

@@ -1,10 +1,11 @@
//! # Functions to update timestamps.
use anyhow::Result;
use crate::chat::{Chat, ChatId};
use crate::contact::{Contact, ContactId};
use crate::context::Context;
use crate::param::{Param, Params};
use anyhow::Result;
impl Context {
/// Updates a contact's timestamp, if reasonable.

View File

@@ -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<String>,
/// Minimum API version required to run this webxdc.
min_api: Option<u32>,
/// Optional URL of webxdc source code.
source_code_url: Option<String>,
/// If the webxdc requests network access.
request_internet_access: Option<bool>,
}
/// 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,
}
@@ -747,6 +770,7 @@ impl Message {
#[cfg(test)]
mod tests {
use super::*;
use crate::chat::{
add_contact_to_chat, create_broadcast_list, create_group_chat, forward_msgs,
remove_contact_from_chat, resend_msgs, send_msg, send_text_msg, ChatId, ProtectionStatus,
@@ -758,8 +782,6 @@ mod tests {
use crate::receive_imf::{receive_imf, receive_imf_inner};
use crate::test_utils::TestContext;
use super::*;
#[allow(clippy::assertions_on_constants)]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_webxdc_file_limits() -> Result<()> {