mirror of
https://github.com/chatmail/core.git
synced 2026-04-26 09:56:35 +03:00
Merge branch 'master' into fix3782
This commit is contained in:
@@ -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)]
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -355,7 +355,6 @@ mod tests {
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::e2ee;
|
||||
use crate::message;
|
||||
|
||||
@@ -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");
|
||||
|
||||
341
src/chat.rs
341
src/chat.rs
@@ -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 =
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
353
src/contact.rs
353
src/contact.rs
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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-----";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
13
src/http.rs
Normal 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()?)
|
||||
}
|
||||
102
src/imap.rs
102
src/imap.rs
@@ -22,7 +22,7 @@ use crate::config::Config;
|
||||
use crate::constants::{
|
||||
Blocked, Chattype, ShowEmails, DC_FETCH_EXISTING_MSGS_COUNT, DC_FOLDERS_CONFIGURED_VERSION,
|
||||
};
|
||||
use crate::contact::{normalize_name, Contact, ContactId, Modifier, Origin};
|
||||
use crate::contact::{normalize_name, Contact, ContactAddress, ContactId, Modifier, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
11
src/job.rs
11
src/job.rs
@@ -157,7 +157,7 @@ impl Job {
|
||||
/// Synchronizes UIDs for all folders.
|
||||
async fn resync_folders(&mut self, context: &Context, imap: &mut Imap) -> Status {
|
||||
if let Err(err) = imap.prepare(context).await {
|
||||
warn!(context, "could not connect: {:?}", err);
|
||||
warn!(context, "could not connect: {:#}", err);
|
||||
return Status::RetryLater;
|
||||
}
|
||||
|
||||
@@ -246,7 +246,7 @@ pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_
|
||||
time_offset
|
||||
);
|
||||
job.save(context).await.unwrap_or_else(|err| {
|
||||
error!(context, "failed to save job: {}", err);
|
||||
error!(context, "failed to save job: {:#}", err);
|
||||
});
|
||||
} else {
|
||||
info!(
|
||||
@@ -254,7 +254,7 @@ pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_
|
||||
"remove job {} as it exhausted {} retries", job, JOB_RETRIES
|
||||
);
|
||||
job.delete(context).await.unwrap_or_else(|err| {
|
||||
error!(context, "failed to delete job: {}", err);
|
||||
error!(context, "failed to delete job: {:#}", err);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -269,7 +269,7 @@ pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_
|
||||
}
|
||||
|
||||
job.delete(context).await.unwrap_or_else(|err| {
|
||||
error!(context, "failed to delete job: {}", err);
|
||||
error!(context, "failed to delete job: {:#}", err);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -403,7 +403,7 @@ LIMIT 1;
|
||||
Ok(job) => return Ok(job),
|
||||
Err(err) => {
|
||||
// Remove invalid job from the DB
|
||||
info!(context, "cleaning up job, because of {}", err);
|
||||
info!(context, "cleaning up job, because of {:#}", err);
|
||||
|
||||
// TODO: improve by only doing a single query
|
||||
let id = context
|
||||
@@ -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) {
|
||||
|
||||
36
src/key.rs
36
src/key.rs
@@ -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);
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ where
|
||||
|
||||
impl<T> Keyring<T>
|
||||
where
|
||||
T: DcKey<KeyType = T>,
|
||||
T: DcKey,
|
||||
{
|
||||
/// New empty keyring.
|
||||
pub fn new() -> Keyring<T> {
|
||||
|
||||
@@ -66,6 +66,7 @@ mod decrypt;
|
||||
pub mod download;
|
||||
mod e2ee;
|
||||
pub mod ephemeral;
|
||||
mod http;
|
||||
mod imap;
|
||||
pub mod imex;
|
||||
mod scheduler;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]
|
||||
|
||||
150
src/message.rs
150
src/message.rs
@@ -1,7 +1,5 @@
|
||||
//! # Messages and their identifiers.
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -237,11 +235,18 @@ impl Default for MessengerMessage {
|
||||
/// If you want an update, you have to recreate the object.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct Message {
|
||||
/// Message ID.
|
||||
pub(crate) id: MsgId,
|
||||
|
||||
/// `From:` contact ID.
|
||||
pub(crate) from_id: ContactId,
|
||||
|
||||
/// ID of the first contact in the `To:` header.
|
||||
pub(crate) to_id: ContactId,
|
||||
pub(crate) chat_id: ChatId,
|
||||
pub(crate) viewtype: Viewtype,
|
||||
|
||||
/// State of the message.
|
||||
pub(crate) state: MessageState,
|
||||
pub(crate) download_state: DownloadState,
|
||||
pub(crate) hidden: bool,
|
||||
@@ -263,6 +268,7 @@ pub struct Message {
|
||||
}
|
||||
|
||||
impl Message {
|
||||
/// Creates a new message with given view type.
|
||||
pub fn new(viewtype: Viewtype) -> Self {
|
||||
Message {
|
||||
viewtype,
|
||||
@@ -270,6 +276,7 @@ impl Message {
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads message with given ID from the database.
|
||||
pub async fn load_from_db(context: &Context, id: MsgId) -> Result<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!'";
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
169
src/net.rs
169
src/net.rs
@@ -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)?;
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
112
src/peerstate.rs
112
src/peerstate.rs
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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)
|
||||
|
||||
53
src/qr.rs
53
src/qr.rs
@@ -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<()> {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)]
|
||||
|
||||
26
src/socks.rs
26
src/socks.rs
@@ -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)
|
||||
}
|
||||
|
||||
11
src/sql.rs
11
src/sql.rs
@@ -626,26 +626,26 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
|
||||
if let Err(err) = remove_unused_files(context).await {
|
||||
warn!(
|
||||
context,
|
||||
"Housekeeping: cannot remove unusued files: {}", err
|
||||
"Housekeeping: cannot remove unusued files: {:#}", err
|
||||
);
|
||||
}
|
||||
|
||||
if let Err(err) = start_ephemeral_timers(context).await {
|
||||
warn!(
|
||||
context,
|
||||
"Housekeeping: cannot start ephemeral timers: {}", err
|
||||
"Housekeeping: cannot start ephemeral timers: {:#}", err
|
||||
);
|
||||
}
|
||||
|
||||
if let Err(err) = prune_tombstones(&context.sql).await {
|
||||
warn!(
|
||||
context,
|
||||
"Housekeeping: Cannot prune message tombstones: {}", err
|
||||
"Housekeeping: Cannot prune message tombstones: {:#}", err
|
||||
);
|
||||
}
|
||||
|
||||
if let Err(err) = deduplicate_peerstates(&context.sql).await {
|
||||
warn!(context, "Failed to deduplicate peerstates: {}", err)
|
||||
warn!(context, "Failed to deduplicate peerstates: {:#}", err)
|
||||
}
|
||||
|
||||
context.schedule_quota_update().await?;
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)]
|
||||
|
||||
12
src/sync.rs
12
src/sync.rs
@@ -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<()> {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
25
src/tools.rs
25
src/tools.rs
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
Reference in New Issue
Block a user