mirror of
https://github.com/chatmail/core.git
synced 2026-04-06 15:42:10 +03:00
Compare commits
21 Commits
v2.25.0
...
iequidoo/h
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
267026b44e | ||
|
|
22da92c563 | ||
|
|
357f7107a6 | ||
|
|
d96b4beff1 | ||
|
|
56c605fa0a | ||
|
|
2e9fd1c25d | ||
|
|
1b1a5f170e | ||
|
|
1946603be6 | ||
|
|
c43b622c23 | ||
|
|
73bf6983b9 | ||
|
|
aaa0f8e245 | ||
|
|
5a1e0e8824 | ||
|
|
cf5b145ce0 | ||
|
|
dd11a0e29a | ||
|
|
3d86cb5953 | ||
|
|
75eb94e44f | ||
|
|
7fef812b1e | ||
|
|
5f174ceaf2 | ||
|
|
06b038ab5d | ||
|
|
b20da3cb0e | ||
|
|
a3328ea2de |
@@ -197,12 +197,10 @@ and then run the script.
|
||||
Language bindings are available for:
|
||||
|
||||
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
|
||||
- -> libdeltachat is going to be deprecated and only exists because Android, iOS and Ubuntu Touch are still using it. If you build a new project, then please use the jsonrpc api instead.
|
||||
- **JS**: \[[📂 source](./deltachat-rpc-client) | [📦 npm](https://www.npmjs.com/package/@deltachat/jsonrpc-client) | [📚 docs](https://js.jsonrpc.delta.chat/)\]
|
||||
- **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\]
|
||||
- **Go**
|
||||
- over jsonrpc: \[[📂 source](https://github.com/deltachat/deltachat-rpc-client-go/)\]
|
||||
- over cffi[^1]: \[[📂 source](https://github.com/deltachat/go-deltachat/)\]
|
||||
- **Free Pascal**[^1] \[[📂 source](https://github.com/deltachat/deltachat-fp/)\]
|
||||
- **Go** \[[📂 source](https://github.com/deltachat/deltachat-rpc-client-go/)\]
|
||||
- **Java** and **Swift** (contained in the Android/iOS repos)
|
||||
|
||||
The following "frontend" projects make use of the Rust-library
|
||||
@@ -215,5 +213,3 @@ or its language bindings:
|
||||
- [Telepathy](https://code.ur.gs/lupine/telepathy-padfoot/)
|
||||
- [Ubuntu Touch](https://codeberg.org/lk108/deltatouch)
|
||||
- several **Bots**
|
||||
|
||||
[^1]: Out of date / unmaintained, if you like those languages feel free to start maintaining them. If you have questions we'll help you, please ask in the issues.
|
||||
|
||||
@@ -6,7 +6,6 @@ use deltachat::chat::{Chat, ChatId};
|
||||
use deltachat::constants::Chattype;
|
||||
use deltachat::contact::{Contact, ContactId};
|
||||
use deltachat::context::Context;
|
||||
use num_traits::cast::ToPrimitive;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
@@ -46,7 +45,7 @@ pub struct FullChat {
|
||||
archived: bool,
|
||||
pinned: bool,
|
||||
// subtitle - will be moved to frontend because it uses translation functions
|
||||
chat_type: u32,
|
||||
chat_type: JsonrpcChatType,
|
||||
is_unpromoted: bool,
|
||||
is_self_talk: bool,
|
||||
contacts: Vec<ContactObject>,
|
||||
@@ -130,7 +129,7 @@ impl FullChat {
|
||||
profile_image, //BLOBS ?
|
||||
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
||||
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
|
||||
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
||||
chat_type: chat.get_type().into(),
|
||||
is_unpromoted: chat.is_unpromoted(),
|
||||
is_self_talk: chat.is_self_talk(),
|
||||
contacts,
|
||||
@@ -192,7 +191,7 @@ pub struct BasicChat {
|
||||
profile_image: Option<String>, //BLOBS ?
|
||||
archived: bool,
|
||||
pinned: bool,
|
||||
chat_type: u32,
|
||||
chat_type: JsonrpcChatType,
|
||||
is_unpromoted: bool,
|
||||
is_self_talk: bool,
|
||||
color: String,
|
||||
@@ -220,7 +219,7 @@ impl BasicChat {
|
||||
profile_image, //BLOBS ?
|
||||
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
||||
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
|
||||
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
||||
chat_type: chat.get_type().into(),
|
||||
is_unpromoted: chat.is_unpromoted(),
|
||||
is_self_talk: chat.is_self_talk(),
|
||||
color,
|
||||
@@ -274,3 +273,37 @@ impl JsonrpcChatVisibility {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, PartialEq, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "ChatType")]
|
||||
pub enum JsonrpcChatType {
|
||||
Single,
|
||||
Group,
|
||||
Mailinglist,
|
||||
OutBroadcast,
|
||||
InBroadcast,
|
||||
}
|
||||
|
||||
impl From<Chattype> for JsonrpcChatType {
|
||||
fn from(chattype: Chattype) -> Self {
|
||||
match chattype {
|
||||
Chattype::Single => JsonrpcChatType::Single,
|
||||
Chattype::Group => JsonrpcChatType::Group,
|
||||
Chattype::Mailinglist => JsonrpcChatType::Mailinglist,
|
||||
Chattype::OutBroadcast => JsonrpcChatType::OutBroadcast,
|
||||
Chattype::InBroadcast => JsonrpcChatType::InBroadcast,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JsonrpcChatType> for Chattype {
|
||||
fn from(chattype: JsonrpcChatType) -> Self {
|
||||
match chattype {
|
||||
JsonrpcChatType::Single => Chattype::Single,
|
||||
JsonrpcChatType::Group => Chattype::Group,
|
||||
JsonrpcChatType::Mailinglist => Chattype::Mailinglist,
|
||||
JsonrpcChatType::OutBroadcast => Chattype::OutBroadcast,
|
||||
JsonrpcChatType::InBroadcast => Chattype::InBroadcast,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use num_traits::cast::ToPrimitive;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
use super::chat::JsonrpcChatType;
|
||||
use super::color_int_to_hex_string;
|
||||
use super::message::MessageViewtype;
|
||||
|
||||
@@ -23,7 +24,7 @@ pub enum ChatListItemFetchResult {
|
||||
name: String,
|
||||
avatar_path: Option<String>,
|
||||
color: String,
|
||||
chat_type: u32,
|
||||
chat_type: JsonrpcChatType,
|
||||
last_updated: Option<i64>,
|
||||
summary_text1: String,
|
||||
summary_text2: String,
|
||||
@@ -151,7 +152,7 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
name: chat.get_name().to_owned(),
|
||||
avatar_path,
|
||||
color,
|
||||
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
||||
chat_type: chat.get_type().into(),
|
||||
last_updated,
|
||||
summary_text1,
|
||||
summary_text2,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use deltachat::{Event as CoreEvent, EventType as CoreEventType};
|
||||
use num_traits::ToPrimitive;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
use super::chat::JsonrpcChatType;
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Event {
|
||||
@@ -307,7 +308,7 @@ pub enum EventType {
|
||||
/// The type of the joined chat.
|
||||
/// This can take the same values
|
||||
/// as `BasicChat.chatType` ([`crate::api::types::chat::BasicChat::chat_type`]).
|
||||
chat_type: u32,
|
||||
chat_type: JsonrpcChatType,
|
||||
/// ID of the chat in case of success.
|
||||
chat_id: u32,
|
||||
|
||||
@@ -570,7 +571,7 @@ impl From<CoreEventType> for EventType {
|
||||
progress,
|
||||
} => SecurejoinInviterProgress {
|
||||
contact_id: contact_id.to_u32(),
|
||||
chat_type: chat_type.to_u32().unwrap_or(0),
|
||||
chat_type: chat_type.into(),
|
||||
chat_id: chat_id.to_u32(),
|
||||
progress,
|
||||
},
|
||||
|
||||
@@ -16,6 +16,7 @@ use num_traits::cast::ToPrimitive;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
use super::chat::JsonrpcChatType;
|
||||
use super::color_int_to_hex_string;
|
||||
use super::contact::ContactObject;
|
||||
use super::reactions::JsonrpcReactions;
|
||||
@@ -531,7 +532,7 @@ pub struct MessageSearchResult {
|
||||
chat_profile_image: Option<String>,
|
||||
chat_color: String,
|
||||
chat_name: String,
|
||||
chat_type: u32,
|
||||
chat_type: JsonrpcChatType,
|
||||
is_chat_contact_request: bool,
|
||||
is_chat_archived: bool,
|
||||
message: String,
|
||||
@@ -569,7 +570,7 @@ impl MessageSearchResult {
|
||||
chat_id: chat.id.to_u32(),
|
||||
chat_name: chat.get_name().to_owned(),
|
||||
chat_color,
|
||||
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
||||
chat_type: chat.get_type().into(),
|
||||
chat_profile_image,
|
||||
is_chat_contact_request: chat.is_contact_request(),
|
||||
is_chat_archived: chat.get_visibility() == ChatVisibility::Archived,
|
||||
|
||||
@@ -45,15 +45,30 @@ const constants = data
|
||||
key.startsWith("DC_SOCKET_") ||
|
||||
key.startsWith("DC_LP_AUTH_") ||
|
||||
key.startsWith("DC_PUSH_") ||
|
||||
key.startsWith("DC_TEXT1_")
|
||||
key.startsWith("DC_TEXT1_") ||
|
||||
key.startsWith("DC_CHAT_TYPE")
|
||||
);
|
||||
})
|
||||
.map((row) => {
|
||||
return ` ${row.key}: ${row.value}`;
|
||||
return ` export const ${row.key} = ${row.value};`;
|
||||
})
|
||||
.join(",\n");
|
||||
.join("\n");
|
||||
|
||||
writeFileSync(
|
||||
resolve(__dirname, "../generated/constants.ts"),
|
||||
`// Generated!\n\nexport enum C {\n${constants.replace(/:/g, " =")},\n}\n`,
|
||||
`// Generated!
|
||||
|
||||
export namespace C {
|
||||
${constants}
|
||||
/** @deprecated 10-8-2025 compare string directly with \`== "Group"\` */
|
||||
export const DC_CHAT_TYPE_GROUP = "Group";
|
||||
/** @deprecated 10-8-2025 compare string directly with \`== "InBroadcast"\`*/
|
||||
export const DC_CHAT_TYPE_IN_BROADCAST = "InBroadcast";
|
||||
/** @deprecated 10-8-2025 compare string directly with \`== "Mailinglist"\` */
|
||||
export const DC_CHAT_TYPE_MAILINGLIST = "Mailinglist";
|
||||
/** @deprecated 10-8-2025 compare string directly with \`== "OutBroadcast"\` */
|
||||
export const DC_CHAT_TYPE_OUT_BROADCAST = "OutBroadcast";
|
||||
/** @deprecated 10-8-2025 compare string directly with \`== "Single"\` */
|
||||
export const DC_CHAT_TYPE_SINGLE = "Single";
|
||||
}\n`,
|
||||
);
|
||||
|
||||
@@ -399,9 +399,10 @@ class Account:
|
||||
next_msg_ids = self._rpc.get_next_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||
|
||||
@futuremethod
|
||||
def wait_next_messages(self) -> list[Message]:
|
||||
"""Wait for new messages and return a list of them."""
|
||||
next_msg_ids = self._rpc.wait_next_msgs(self.id)
|
||||
next_msg_ids = yield self._rpc.wait_next_msgs.future(self.id)
|
||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||
|
||||
def wait_for_incoming_msg_event(self):
|
||||
|
||||
@@ -91,19 +91,17 @@ class ChatId(IntEnum):
|
||||
LAST_SPECIAL = 9
|
||||
|
||||
|
||||
class ChatType(IntEnum):
|
||||
class ChatType(str, Enum):
|
||||
"""Chat type."""
|
||||
|
||||
UNDEFINED = 0
|
||||
|
||||
SINGLE = 100
|
||||
SINGLE = "Single"
|
||||
"""1:1 chat, i.e. a direct chat with a single contact"""
|
||||
|
||||
GROUP = 120
|
||||
GROUP = "Group"
|
||||
|
||||
MAILINGLIST = 140
|
||||
MAILINGLIST = "Mailinglist"
|
||||
|
||||
OUT_BROADCAST = 160
|
||||
OUT_BROADCAST = "OutBroadcast"
|
||||
"""Outgoing broadcast channel, called "Channel" in the UI.
|
||||
|
||||
The user can send into this channel,
|
||||
@@ -115,7 +113,7 @@ class ChatType(IntEnum):
|
||||
which would make it hard to grep for it.
|
||||
"""
|
||||
|
||||
IN_BROADCAST = 165
|
||||
IN_BROADCAST = "InBroadcast"
|
||||
"""Incoming broadcast channel, called "Channel" in the UI.
|
||||
|
||||
This channel is read-only,
|
||||
|
||||
@@ -484,22 +484,21 @@ def test_wait_next_messages(acfactory) -> None:
|
||||
# There are no old messages and the call returns immediately.
|
||||
assert not bot.wait_next_messages()
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
# Bot starts waiting for messages.
|
||||
next_messages_task = executor.submit(bot.wait_next_messages)
|
||||
# Bot starts waiting for messages.
|
||||
next_messages_task = bot.wait_next_messages.future()
|
||||
|
||||
alice_contact_bot = alice.create_contact(bot, "Bot")
|
||||
alice_chat_bot = alice_contact_bot.create_chat()
|
||||
alice_chat_bot.send_text("Hello!")
|
||||
alice_contact_bot = alice.create_contact(bot, "Bot")
|
||||
alice_chat_bot = alice_contact_bot.create_chat()
|
||||
alice_chat_bot.send_text("Hello!")
|
||||
|
||||
next_messages = next_messages_task.result()
|
||||
next_messages = next_messages_task()
|
||||
|
||||
if len(next_messages) == E2EE_INFO_MSGS:
|
||||
next_messages += bot.wait_next_messages()
|
||||
if len(next_messages) == E2EE_INFO_MSGS:
|
||||
next_messages += bot.wait_next_messages()
|
||||
|
||||
assert len(next_messages) == 1 + E2EE_INFO_MSGS
|
||||
snapshot = next_messages[0 + E2EE_INFO_MSGS].get_snapshot()
|
||||
assert snapshot.text == "Hello!"
|
||||
assert len(next_messages) == 1 + E2EE_INFO_MSGS
|
||||
snapshot = next_messages[0 + E2EE_INFO_MSGS].get_snapshot()
|
||||
assert snapshot.text == "Hello!"
|
||||
|
||||
|
||||
def test_import_export_backup(acfactory, tmp_path) -> None:
|
||||
|
||||
@@ -18,7 +18,7 @@ use tokio::time::{Duration, sleep};
|
||||
|
||||
use crate::context::{Context, ContextBuilder};
|
||||
use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::log::{info, warn};
|
||||
use crate::log::warn;
|
||||
use crate::push::PushSubscriber;
|
||||
use crate::stock_str::StockStrings;
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ use crate::config::Config;
|
||||
use crate::constants::{self, MediaQuality};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::{LogExt, error, info, warn};
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::message::Viewtype;
|
||||
use crate::tools::sanitize_filename;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::log::{info, warn};
|
||||
use crate::log::warn;
|
||||
use crate::message::{self, Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::net::dns::lookup_host_with_cache;
|
||||
|
||||
59
src/chat.rs
59
src/chat.rs
@@ -32,7 +32,7 @@ use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers};
|
||||
use crate::events::EventType;
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::location;
|
||||
use crate::log::{LogExt, error, info, warn};
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::logged_debug_assert;
|
||||
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimefactory::MimeFactory;
|
||||
@@ -1643,36 +1643,37 @@ impl Chat {
|
||||
|
||||
/// Returns true if the chat is encrypted.
|
||||
pub async fn is_encrypted(&self, context: &Context) -> Result<bool> {
|
||||
let is_encrypted = match self.typ {
|
||||
Chattype::Single => {
|
||||
match context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT cc.contact_id, c.fingerprint<>''
|
||||
let is_encrypted = self.is_self_talk()
|
||||
|| match self.typ {
|
||||
Chattype::Single => {
|
||||
match context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT cc.contact_id, c.fingerprint<>''
|
||||
FROM chats_contacts cc LEFT JOIN contacts c
|
||||
ON c.id=cc.contact_id
|
||||
WHERE cc.chat_id=?
|
||||
",
|
||||
(self.id,),
|
||||
|row| {
|
||||
let id: ContactId = row.get(0)?;
|
||||
let is_key: bool = row.get(1)?;
|
||||
Ok((id, is_key))
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some((id, is_key)) => is_key || id == ContactId::DEVICE,
|
||||
None => true,
|
||||
(self.id,),
|
||||
|row| {
|
||||
let id: ContactId = row.get(0)?;
|
||||
let is_key: bool = row.get(1)?;
|
||||
Ok((id, is_key))
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some((id, is_key)) => is_key || id == ContactId::DEVICE,
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
Chattype::Group => {
|
||||
// Do not encrypt ad-hoc groups.
|
||||
!self.grpid.is_empty()
|
||||
}
|
||||
Chattype::Mailinglist => false,
|
||||
Chattype::OutBroadcast | Chattype::InBroadcast => true,
|
||||
};
|
||||
Chattype::Group => {
|
||||
// Do not encrypt ad-hoc groups.
|
||||
!self.grpid.is_empty()
|
||||
}
|
||||
Chattype::Mailinglist => false,
|
||||
Chattype::OutBroadcast | Chattype::InBroadcast => true,
|
||||
};
|
||||
Ok(is_encrypted)
|
||||
}
|
||||
|
||||
@@ -3741,7 +3742,11 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
context.emit_event(EventType::ErrorSelfNotInGroup(
|
||||
"Cannot add contact to group; self not in group.".into(),
|
||||
));
|
||||
bail!("can not add contact because the account is not part of the group/broadcast");
|
||||
warn!(
|
||||
context,
|
||||
"Can not add contact because the account is not part of the group/broadcast."
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let sync_qr_code_tokens;
|
||||
|
||||
@@ -801,6 +801,7 @@ async fn test_self_talk() -> Result<()> {
|
||||
let chat = &t.get_self_chat().await;
|
||||
assert!(!chat.id.is_special());
|
||||
assert!(chat.is_self_talk());
|
||||
assert!(chat.is_encrypted(&t).await?);
|
||||
assert!(chat.visibility == ChatVisibility::Normal);
|
||||
assert!(!chat.is_device_talk());
|
||||
assert!(chat.can_send(&t).await?);
|
||||
@@ -5085,6 +5086,28 @@ async fn test_send_edit_request() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_edit_saved_messages() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice1 = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await;
|
||||
|
||||
alice1.set_config_bool(Config::BccSelf, true).await?;
|
||||
alice2.set_config_bool(Config::BccSelf, true).await?;
|
||||
|
||||
let alice1_chat_id = ChatId::create_for_contact(alice1, ContactId::SELF).await?;
|
||||
let alice1_sent_msg = alice1.send_text(alice1_chat_id, "Original message").await;
|
||||
let alice1_msg_id = alice1_sent_msg.sender_msg_id;
|
||||
let received_msg = alice2.recv_msg(&alice1_sent_msg).await;
|
||||
assert_eq!(received_msg.text, "Original message");
|
||||
|
||||
send_edit_request(alice1, alice1_msg_id, "Edited message".to_string()).await?;
|
||||
alice2.recv_msg_trash(&alice1.pop_sent_msg().await).await;
|
||||
let received_msg = Message::load_from_db(alice2, received_msg.id).await?;
|
||||
assert_eq!(received_msg.text, "Edited message");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_receive_edit_request_after_removal() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::blob::BlobObject;
|
||||
use crate::configure::EnteredLoginParam;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::{LogExt, info};
|
||||
use crate::log::LogExt;
|
||||
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
|
||||
use crate::provider::{Provider, get_provider_by_id};
|
||||
use crate::sync::{self, Sync::*, SyncData};
|
||||
|
||||
@@ -27,7 +27,7 @@ use crate::config::{self, Config};
|
||||
use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT;
|
||||
use crate::context::Context;
|
||||
use crate::imap::Imap;
|
||||
use crate::log::{LogExt, info, warn};
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::login_param::EnteredCertificateChecks;
|
||||
pub use crate::login_param::EnteredLoginParam;
|
||||
use crate::message::Message;
|
||||
@@ -565,14 +565,6 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
|
||||
|
||||
progress!(ctx, 910);
|
||||
|
||||
if let Some(configured_addr) = ctx.get_config(Config::ConfiguredAddr).await? {
|
||||
if configured_addr != param.addr {
|
||||
// Switched account, all server UIDs we know are invalid
|
||||
info!(ctx, "Scheduling resync because the address has changed.");
|
||||
ctx.schedule_resync().await?;
|
||||
}
|
||||
}
|
||||
|
||||
let provider = configured_param.provider;
|
||||
configured_param
|
||||
.save_to_transports_table(ctx, param)
|
||||
|
||||
@@ -31,7 +31,7 @@ use crate::key::{
|
||||
DcKey, Fingerprint, SignedPublicKey, load_self_public_key, self_fingerprint,
|
||||
self_fingerprint_opt,
|
||||
};
|
||||
use crate::log::{LogExt, info, warn};
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::message::MessageState;
|
||||
use crate::mimeparser::AvatarAction;
|
||||
use crate::param::{Param, Params};
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::collections::{BTreeMap, HashMap};
|
||||
use std::ffi::OsString;
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -21,7 +21,7 @@ use crate::debug_logging::DebugLogging;
|
||||
use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::imap::{FolderMeaning, Imap, ServerMetadata};
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::log::{info, warn};
|
||||
use crate::log::warn;
|
||||
use crate::logged_debug_assert;
|
||||
use crate::login_param::EnteredLoginParam;
|
||||
use crate::message::{self, MessageState, MsgId};
|
||||
@@ -138,7 +138,7 @@ impl ContextBuilder {
|
||||
///
|
||||
/// This is useful in order to share the same translation strings in all [`Context`]s.
|
||||
/// The mapping may be empty when set, it will be populated by
|
||||
/// [`Context::set_stock-translation`] or [`Accounts::set_stock_translation`] calls.
|
||||
/// [`Context::set_stock_translation`] or [`Accounts::set_stock_translation`] calls.
|
||||
///
|
||||
/// Note that the [account manager](crate::accounts::Accounts) is designed to handle the
|
||||
/// common case for using multiple [`Context`] instances.
|
||||
@@ -243,9 +243,6 @@ pub struct InnerContext {
|
||||
/// Set to `None` if quota was never tried to load.
|
||||
pub(crate) quota: RwLock<Option<QuotaInfo>>,
|
||||
|
||||
/// IMAP UID resync request.
|
||||
pub(crate) resync_request: AtomicBool,
|
||||
|
||||
/// Notify about new messages.
|
||||
///
|
||||
/// This causes [`Context::wait_next_msgs`] to wake up.
|
||||
@@ -457,7 +454,6 @@ impl Context {
|
||||
scheduler: SchedulerState::new(),
|
||||
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow at least 1 message every 10 seconds + a burst of 6.
|
||||
quota: RwLock::new(None),
|
||||
resync_request: AtomicBool::new(false),
|
||||
new_msgs_notify,
|
||||
server_id: RwLock::new(None),
|
||||
metadata: RwLock::new(None),
|
||||
@@ -616,12 +612,6 @@ impl Context {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn schedule_resync(&self) -> Result<()> {
|
||||
self.resync_request.store(true, Ordering::Relaxed);
|
||||
self.scheduler.interrupt_inbox().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a reference to the underlying SQL instance.
|
||||
///
|
||||
/// Warning: this is only here for testing, not part of the public API.
|
||||
|
||||
@@ -3,7 +3,6 @@ use crate::chat::ChatId;
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::{error, info};
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::param::Param;
|
||||
use crate::tools::time;
|
||||
|
||||
@@ -10,7 +10,6 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::imap::session::Session;
|
||||
use crate::log::info;
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, Part};
|
||||
use crate::tools::time;
|
||||
|
||||
@@ -80,7 +80,7 @@ use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::download::MIN_DELETE_SERVER_AFTER;
|
||||
use crate::events::EventType;
|
||||
use crate::log::{LogExt, error, info, warn};
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::message::{Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::stock_str;
|
||||
|
||||
143
src/imap.rs
143
src/imap.rs
@@ -27,12 +27,12 @@ use crate::calls::{create_fallback_ice_servers, create_ice_servers_from_metadata
|
||||
use crate::chat::{self, ChatId, ChatIdBlocked, add_device_msg};
|
||||
use crate::chatlist_events;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{self, Blocked, Chattype, ShowEmails};
|
||||
use crate::constants::{self, Blocked, ShowEmails};
|
||||
use crate::contact::{Contact, ContactId, Modifier, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::log::{LogExt, error, info, warn};
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::message::{self, Message, MessageState, MessengerMessage, MsgId};
|
||||
use crate::mimeparser;
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
@@ -104,6 +104,12 @@ pub(crate) struct Imap {
|
||||
/// immediately after logging in or returning an error in response to LOGIN command
|
||||
/// due to internal server error.
|
||||
ratelimit: Ratelimit,
|
||||
|
||||
/// IMAP UID resync request sender.
|
||||
pub(crate) resync_request_sender: async_channel::Sender<()>,
|
||||
|
||||
/// IMAP UID resync request receiver.
|
||||
pub(crate) resync_request_receiver: async_channel::Receiver<()>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -254,6 +260,7 @@ impl Imap {
|
||||
oauth2: bool,
|
||||
idle_interrupt_receiver: Receiver<()>,
|
||||
) -> Self {
|
||||
let (resync_request_sender, resync_request_receiver) = async_channel::bounded(1);
|
||||
Imap {
|
||||
idle_interrupt_receiver,
|
||||
addr: addr.to_string(),
|
||||
@@ -268,6 +275,8 @@ impl Imap {
|
||||
conn_backoff_ms: 0,
|
||||
// 1 connection per minute + a burst of 2.
|
||||
ratelimit: Ratelimit::new(Duration::new(120, 0), 2.0),
|
||||
resync_request_sender,
|
||||
resync_request_receiver,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -392,6 +401,7 @@ impl Imap {
|
||||
match login_res {
|
||||
Ok(mut session) => {
|
||||
let capabilities = determine_capabilities(&mut session).await?;
|
||||
let resync_request_sender = self.resync_request_sender.clone();
|
||||
|
||||
let session = if capabilities.can_compress {
|
||||
info!(context, "Enabling IMAP compression.");
|
||||
@@ -402,9 +412,9 @@ impl Imap {
|
||||
})
|
||||
.await
|
||||
.context("Failed to enable IMAP compression")?;
|
||||
Session::new(compressed_session, capabilities)
|
||||
Session::new(compressed_session, capabilities, resync_request_sender)
|
||||
} else {
|
||||
Session::new(session, capabilities)
|
||||
Session::new(session, capabilities, resync_request_sender)
|
||||
};
|
||||
|
||||
// Store server ID in the context to display in account info.
|
||||
@@ -1954,21 +1964,24 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_encrypted(headers: &[mailparse::MailHeader<'_>]) -> bool {
|
||||
let content_type = headers.get_header_value(HeaderDef::ContentType);
|
||||
let content_type = content_type.as_ref();
|
||||
let res = content_type.is_some_and(|v| v.contains("multipart/encrypted"));
|
||||
// Some MUAs use "multipart/mixed", look also at Subject in this case. We can't only look at
|
||||
// Subject as it's not mandatory (<https://datatracker.ietf.org/doc/html/rfc5322#section-3.6>)
|
||||
// and may be user-formed.
|
||||
res || content_type.is_some_and(|v| v.contains("multipart/mixed"))
|
||||
&& headers
|
||||
.get_header_value(HeaderDef::Subject)
|
||||
.is_some_and(|v| v == "..." || v == "[...]")
|
||||
}
|
||||
|
||||
async fn should_move_out_of_spam(
|
||||
context: &Context,
|
||||
headers: &[mailparse::MailHeader<'_>],
|
||||
) -> Result<bool> {
|
||||
if headers.get_header_value(HeaderDef::ChatVersion).is_some() {
|
||||
// If this is a chat message (i.e. has a ChatVersion header), then this might be
|
||||
// a securejoin message. We can't find out at this point as we didn't prefetch
|
||||
// the SecureJoin header. So, we always move chat messages out of Spam.
|
||||
// Two possibilities to change this would be:
|
||||
// 1. Remove the `&& !context.is_spam_folder(folder).await?` check from
|
||||
// `fetch_new_messages()`, and then let `receive_imf()` check
|
||||
// if it's a spam message and should be hidden.
|
||||
// 2. Or add a flag to the ChatVersion header that this is a securejoin
|
||||
// request, and return `true` here only if the message has this flag.
|
||||
// `receive_imf()` can then check if the securejoin request is valid.
|
||||
if headers.get_header_value(HeaderDef::SecureJoin).is_some() || is_encrypted(headers) {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
@@ -2027,7 +2040,8 @@ async fn spam_target_folder_cfg(
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if needs_move_to_mvbox(context, headers).await?
|
||||
if is_encrypted(headers) && context.get_config_bool(Config::MvboxMove).await?
|
||||
|| needs_move_to_mvbox(context, headers).await?
|
||||
// If OnlyFetchMvbox is set, we don't want to move the message to
|
||||
// the inbox where we wouldn't fetch it again:
|
||||
|| context.get_config_bool(Config::OnlyFetchMvbox).await?
|
||||
@@ -2080,20 +2094,6 @@ async fn needs_move_to_mvbox(
|
||||
context: &Context,
|
||||
headers: &[mailparse::MailHeader<'_>],
|
||||
) -> Result<bool> {
|
||||
let has_chat_version = headers.get_header_value(HeaderDef::ChatVersion).is_some();
|
||||
if !context.get_config_bool(Config::IsChatmail).await?
|
||||
&& has_chat_version
|
||||
&& headers
|
||||
.get_header_value(HeaderDef::AutoSubmitted)
|
||||
.filter(|val| val.eq_ignore_ascii_case("auto-generated"))
|
||||
.is_some()
|
||||
{
|
||||
if let Some(from) = mimeparser::get_from(headers) {
|
||||
if context.is_self_addr(&from.addr).await? {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !context.get_config_bool(Config::MvboxMove).await? {
|
||||
return Ok(false);
|
||||
}
|
||||
@@ -2106,17 +2106,7 @@ async fn needs_move_to_mvbox(
|
||||
// there may be a non-delta device that wants to handle it
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if has_chat_version {
|
||||
Ok(true)
|
||||
} else if let Some(parent) = get_prefetch_parent_message(context, headers).await? {
|
||||
match parent.is_dc_message {
|
||||
MessengerMessage::No => Ok(false),
|
||||
MessengerMessage::Yes | MessengerMessage::Reply => Ok(true),
|
||||
}
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
Ok(headers.get_header_value(HeaderDef::SecureJoin).is_some() || is_encrypted(headers))
|
||||
}
|
||||
|
||||
/// Try to get the folder meaning by the name of the folder only used if the server does not support XLIST.
|
||||
@@ -2229,21 +2219,6 @@ pub(crate) fn create_message_id() -> String {
|
||||
format!("{}{}", GENERATED_PREFIX, create_id())
|
||||
}
|
||||
|
||||
/// Returns chat by prefetched headers.
|
||||
async fn prefetch_get_chat(
|
||||
context: &Context,
|
||||
headers: &[mailparse::MailHeader<'_>],
|
||||
) -> Result<Option<chat::Chat>> {
|
||||
let parent = get_prefetch_parent_message(context, headers).await?;
|
||||
if let Some(parent) = &parent {
|
||||
return Ok(Some(
|
||||
chat::Chat::load_from_db(context, parent.get_chat_id()).await?,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Determines whether the message should be downloaded based on prefetched headers.
|
||||
pub(crate) async fn prefetch_should_download(
|
||||
context: &Context,
|
||||
@@ -2262,14 +2237,6 @@ pub(crate) async fn prefetch_should_download(
|
||||
// We do not know the Message-ID or the Message-ID is missing (in this case, we create one in
|
||||
// the further process).
|
||||
|
||||
if let Some(chat) = prefetch_get_chat(context, headers).await? {
|
||||
if chat.typ == Chattype::Group && !chat.id.is_special() {
|
||||
// This might be a group command, like removing a group member.
|
||||
// We really need to fetch this to avoid inconsistent group state.
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
let maybe_ndn = if let Some(from) = headers.get_header_value(HeaderDef::From_) {
|
||||
let from = from.to_ascii_lowercase();
|
||||
from.contains("mailer-daemon") || from.contains("mail-daemon")
|
||||
@@ -2299,27 +2266,24 @@ pub(crate) async fn prefetch_should_download(
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion).is_some();
|
||||
let accepted_contact = origin.is_known();
|
||||
let is_reply_to_chat_message = get_prefetch_parent_message(context, headers)
|
||||
.await?
|
||||
.map(|parent| match parent.is_dc_message {
|
||||
MessengerMessage::No => false,
|
||||
MessengerMessage::Yes | MessengerMessage::Reply => true,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let show_emails =
|
||||
ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default();
|
||||
|
||||
let show = is_autocrypt_setup_message
|
||||
|| match show_emails {
|
||||
ShowEmails::Off => is_chat_message || is_reply_to_chat_message,
|
||||
ShowEmails::AcceptedContacts => {
|
||||
is_chat_message || is_reply_to_chat_message || accepted_contact
|
||||
}
|
||||
|| headers.get_header_value(HeaderDef::SecureJoin).is_some()
|
||||
|| is_encrypted(headers)
|
||||
|| match ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
ShowEmails::Off => false,
|
||||
ShowEmails::AcceptedContacts => accepted_contact,
|
||||
ShowEmails::All => true,
|
||||
};
|
||||
}
|
||||
|| get_prefetch_parent_message(context, headers)
|
||||
.await?
|
||||
.map(|parent| match parent.is_dc_message {
|
||||
MessengerMessage::No => false,
|
||||
MessengerMessage::Yes | MessengerMessage::Reply => true,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let should_download = (show && !blocked_contact) || maybe_ndn;
|
||||
Ok(should_download)
|
||||
@@ -2491,21 +2455,6 @@ pub(crate) async fn get_imap_self_sent_search_command(context: &Context) -> Resu
|
||||
Ok(search_command)
|
||||
}
|
||||
|
||||
/// Deprecated, use get_uid_next() and get_uidvalidity()
|
||||
pub async fn get_config_last_seen_uid(context: &Context, folder: &str) -> Result<(u32, u32)> {
|
||||
let key = format!("imap.mailbox.{folder}");
|
||||
if let Some(entry) = context.sql.get_raw_config(&key).await? {
|
||||
// the entry has the format `imap.mailbox.<folder>=<uidvalidity>:<lastseenuid>`
|
||||
let mut parts = entry.split(':');
|
||||
Ok((
|
||||
parts.next().unwrap_or_default().parse().unwrap_or(0),
|
||||
parts.next().unwrap_or_default().parse().unwrap_or(0),
|
||||
))
|
||||
} else {
|
||||
Ok((0, 0))
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether to ignore fetching messages from a folder.
|
||||
///
|
||||
/// This caters for the [`Config::OnlyFetchMvbox`] setting which means mails from folders
|
||||
|
||||
@@ -8,7 +8,7 @@ use tokio::io::BufWriter;
|
||||
|
||||
use super::capabilities::Capabilities;
|
||||
use crate::context::Context;
|
||||
use crate::log::{LoggingStream, info, warn};
|
||||
use crate::log::{LoggingStream, warn};
|
||||
use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp};
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::net::session::SessionStream;
|
||||
|
||||
@@ -8,7 +8,7 @@ use tokio::time::timeout;
|
||||
use super::Imap;
|
||||
use super::session::Session;
|
||||
use crate::context::Context;
|
||||
use crate::log::{info, warn};
|
||||
use crate::log::warn;
|
||||
use crate::net::TIMEOUT;
|
||||
use crate::tools::{self, time_elapsed};
|
||||
|
||||
|
||||
@@ -94,14 +94,14 @@ fn test_build_sequence_sets() {
|
||||
async fn check_target_folder_combination(
|
||||
folder: &str,
|
||||
mvbox_move: bool,
|
||||
chat_msg: bool,
|
||||
is_encrypted: bool,
|
||||
expected_destination: &str,
|
||||
accepted_chat: bool,
|
||||
outgoing: bool,
|
||||
setupmessage: bool,
|
||||
) -> Result<()> {
|
||||
println!(
|
||||
"Testing: For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}"
|
||||
"Testing: For folder {folder}, mvbox_move {mvbox_move}, is_encrypted {is_encrypted}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}"
|
||||
);
|
||||
|
||||
let t = TestContext::new_alice().await;
|
||||
@@ -124,7 +124,6 @@ async fn check_target_folder_combination(
|
||||
temp = format!(
|
||||
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
|
||||
{}\
|
||||
Subject: foo\n\
|
||||
Message-ID: <abc@example.com>\n\
|
||||
{}\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
@@ -135,7 +134,12 @@ async fn check_target_folder_combination(
|
||||
} else {
|
||||
"From: bob@example.net\nTo: alice@example.org\n"
|
||||
},
|
||||
if chat_msg { "Chat-Version: 1.0\n" } else { "" },
|
||||
if is_encrypted {
|
||||
"Subject: [...]\n\
|
||||
Content-Type: multipart/mixed; boundary=\"someboundary\"\n"
|
||||
} else {
|
||||
"Subject: foo\n"
|
||||
},
|
||||
);
|
||||
temp.as_bytes()
|
||||
};
|
||||
@@ -157,25 +161,26 @@ async fn check_target_folder_combination(
|
||||
assert_eq!(
|
||||
expected,
|
||||
actual.as_deref(),
|
||||
"For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}: expected {expected:?}, got {actual:?}"
|
||||
"For folder {folder}, mvbox_move {mvbox_move}, is_encrypted {is_encrypted}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}: expected {expected:?}, got {actual:?}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// chat_msg means that the message was sent by Delta Chat
|
||||
// The tuples are (folder, mvbox_move, chat_msg, expected_destination)
|
||||
// The tuples are (folder, mvbox_move, is_encrypted, expected_destination)
|
||||
const COMBINATIONS_ACCEPTED_CHAT: &[(&str, bool, bool, &str)] = &[
|
||||
("INBOX", false, false, "INBOX"),
|
||||
("INBOX", false, true, "INBOX"),
|
||||
("INBOX", true, false, "INBOX"),
|
||||
("INBOX", true, true, "DeltaChat"),
|
||||
("Spam", false, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs
|
||||
("Spam", false, false, "INBOX"),
|
||||
("Spam", false, true, "INBOX"),
|
||||
("Spam", true, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs
|
||||
// Move unencrypted emails in accepted chats from Spam to INBOX, not 100% sure on this, we could
|
||||
// also not move unencrypted emails or, if mvbox_move=1, move them to DeltaChat.
|
||||
("Spam", true, false, "INBOX"),
|
||||
("Spam", true, true, "DeltaChat"),
|
||||
];
|
||||
|
||||
// These are the same as above, but non-chat messages in Spam stay in Spam
|
||||
// These are the same as above, but unencrypted messages in Spam stay in Spam.
|
||||
const COMBINATIONS_REQUEST: &[(&str, bool, bool, &str)] = &[
|
||||
("INBOX", false, false, "INBOX"),
|
||||
("INBOX", false, true, "INBOX"),
|
||||
@@ -189,11 +194,11 @@ const COMBINATIONS_REQUEST: &[(&str, bool, bool, &str)] = &[
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_target_folder_incoming_accepted() -> Result<()> {
|
||||
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||
for (folder, mvbox_move, is_encrypted, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||
check_target_folder_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
*is_encrypted,
|
||||
expected_destination,
|
||||
true,
|
||||
false,
|
||||
@@ -206,11 +211,11 @@ async fn test_target_folder_incoming_accepted() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_target_folder_incoming_request() -> Result<()> {
|
||||
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_REQUEST {
|
||||
for (folder, mvbox_move, is_encrypted, expected_destination) in COMBINATIONS_REQUEST {
|
||||
check_target_folder_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
*is_encrypted,
|
||||
expected_destination,
|
||||
false,
|
||||
false,
|
||||
@@ -224,11 +229,11 @@ async fn test_target_folder_incoming_request() -> Result<()> {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_target_folder_outgoing() -> Result<()> {
|
||||
// Test outgoing emails
|
||||
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||
for (folder, mvbox_move, is_encrypted, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||
check_target_folder_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
*is_encrypted,
|
||||
expected_destination,
|
||||
true,
|
||||
true,
|
||||
@@ -242,11 +247,11 @@ async fn test_target_folder_outgoing() -> Result<()> {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_target_folder_setupmsg() -> Result<()> {
|
||||
// Test setupmessages
|
||||
for (folder, mvbox_move, chat_msg, _expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||
for (folder, mvbox_move, is_encrypted, _expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||
check_target_folder_combination(
|
||||
folder,
|
||||
*mvbox_move,
|
||||
*chat_msg,
|
||||
*is_encrypted,
|
||||
if folder == &"Spam" { "INBOX" } else { folder }, // Never move setup messages, except if they are in "Spam"
|
||||
false,
|
||||
true,
|
||||
|
||||
@@ -5,7 +5,7 @@ use anyhow::{Context as _, Result};
|
||||
use super::{get_folder_meaning_by_attrs, get_folder_meaning_by_name};
|
||||
use crate::config::Config;
|
||||
use crate::imap::{Imap, session::Session};
|
||||
use crate::log::{LogExt, info};
|
||||
use crate::log::LogExt;
|
||||
use crate::tools::{self, time_elapsed};
|
||||
use crate::{context::Context, imap::FolderMeaning};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use anyhow::Context as _;
|
||||
use super::session::Session as ImapSession;
|
||||
use super::{get_uid_next, get_uidvalidity, set_modseq, set_uid_next, set_uidvalidity};
|
||||
use crate::context::Context;
|
||||
use crate::log::{info, warn};
|
||||
use crate::log::warn;
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
@@ -206,7 +206,7 @@ impl ImapSession {
|
||||
"The server illegally decreased the uid_next of folder {folder:?} from {old_uid_next} to {new_uid_next} without changing validity ({new_uid_validity}), resyncing UIDs...",
|
||||
);
|
||||
set_uid_next(context, folder, new_uid_next).await?;
|
||||
context.schedule_resync().await?;
|
||||
self.resync_request_sender.try_send(()).ok();
|
||||
}
|
||||
|
||||
// If UIDNEXT changed, there are new emails.
|
||||
@@ -243,7 +243,7 @@ impl ImapSession {
|
||||
.await?;
|
||||
|
||||
if old_uid_validity != 0 || old_uid_next != 0 {
|
||||
context.schedule_resync().await?;
|
||||
self.resync_request_sender.try_send(()).ok();
|
||||
}
|
||||
info!(
|
||||
context,
|
||||
|
||||
@@ -14,17 +14,20 @@ use crate::tools;
|
||||
/// Prefetch:
|
||||
/// - Message-ID to check if we already have the message.
|
||||
/// - In-Reply-To and References to check if message is a reply to chat message.
|
||||
/// - Chat-Version to check if a message is a chat message
|
||||
/// - Autocrypt-Setup-Message to check if a message is an autocrypt setup message,
|
||||
/// not necessarily sent by Delta Chat.
|
||||
///
|
||||
/// NB: We don't look at Chat-Version as we don't want any "better" handling for unencrypted
|
||||
/// messages.
|
||||
const PREFETCH_FLAGS: &str = "(UID INTERNALDATE RFC822.SIZE BODY.PEEK[HEADER.FIELDS (\
|
||||
MESSAGE-ID \
|
||||
DATE \
|
||||
X-MICROSOFT-ORIGINAL-MESSAGE-ID \
|
||||
FROM \
|
||||
IN-REPLY-TO REFERENCES \
|
||||
CHAT-VERSION \
|
||||
AUTO-SUBMITTED \
|
||||
CONTENT-TYPE \
|
||||
SECURE-JOIN \
|
||||
SUBJECT \
|
||||
AUTOCRYPT-SETUP-MESSAGE\
|
||||
)])";
|
||||
|
||||
@@ -48,6 +51,8 @@ pub(crate) struct Session {
|
||||
///
|
||||
/// Should be false if no folder is currently selected.
|
||||
pub new_mail: bool,
|
||||
|
||||
pub resync_request_sender: async_channel::Sender<()>,
|
||||
}
|
||||
|
||||
impl Deref for Session {
|
||||
@@ -68,6 +73,7 @@ impl Session {
|
||||
pub(crate) fn new(
|
||||
inner: ImapSession<Box<dyn SessionStream>>,
|
||||
capabilities: Capabilities,
|
||||
resync_request_sender: async_channel::Sender<()>,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
@@ -77,6 +83,7 @@ impl Session {
|
||||
selected_folder_needs_expunge: false,
|
||||
last_full_folder_scan: Mutex::new(None),
|
||||
new_mail: false,
|
||||
resync_request_sender,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ use crate::context::Context;
|
||||
use crate::e2ee;
|
||||
use crate::events::EventType;
|
||||
use crate::key::{self, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey};
|
||||
use crate::log::{LogExt, error, info, warn};
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::pgp;
|
||||
use crate::qr::DCBACKUP_VERSION;
|
||||
use crate::sql;
|
||||
|
||||
@@ -41,7 +41,7 @@ use tokio_util::sync::CancellationToken;
|
||||
use crate::chat::add_device_msg;
|
||||
use crate::context::Context;
|
||||
use crate::imex::BlobDirContents;
|
||||
use crate::log::{info, warn};
|
||||
use crate::log::warn;
|
||||
use crate::message::Message;
|
||||
use crate::qr::Qr;
|
||||
use crate::stock_str::backup_transfer_msg_body;
|
||||
|
||||
@@ -15,7 +15,7 @@ use tokio::runtime::Handle;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::{LogExt, info};
|
||||
use crate::log::LogExt;
|
||||
use crate::pgp::KeyPair;
|
||||
use crate::tools::{self, time_elapsed};
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ use crate::constants::DC_CHAT_ID_TRASH;
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::{info, warn};
|
||||
use crate::log::warn;
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::tools::{duration_to_str, time};
|
||||
|
||||
@@ -23,8 +23,6 @@ macro_rules! info {
|
||||
}};
|
||||
}
|
||||
|
||||
pub(crate) use info;
|
||||
|
||||
// Workaround for <https://github.com/rust-lang/rust/issues/133708>.
|
||||
#[macro_use]
|
||||
mod warn_macro_mod {
|
||||
@@ -60,8 +58,6 @@ macro_rules! error {
|
||||
}};
|
||||
}
|
||||
|
||||
pub(crate) use error;
|
||||
|
||||
impl Context {
|
||||
/// Set last error string.
|
||||
/// Implemented as blocking as used from macros in different, not always async blocks.
|
||||
|
||||
@@ -24,7 +24,7 @@ use crate::ephemeral::{Timer as EphemeralTimer, start_ephemeral_timers_msgids};
|
||||
use crate::events::EventType;
|
||||
use crate::imap::markseen_on_imap_table;
|
||||
use crate::location::delete_poi_location;
|
||||
use crate::log::{error, info, warn};
|
||||
use crate::log::warn;
|
||||
use crate::mimeparser::{SystemMessage, parse_message_id};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::pgp::split_armored_data;
|
||||
|
||||
@@ -27,7 +27,7 @@ use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::key::{DcKey, SignedPublicKey, self_fingerprint};
|
||||
use crate::location;
|
||||
use crate::log::{info, warn};
|
||||
use crate::log::warn;
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::{SystemMessage, is_hidden};
|
||||
use crate::param::Param;
|
||||
@@ -947,8 +947,7 @@ impl MimeFactory {
|
||||
//
|
||||
// These are standard headers such as Date, In-Reply-To, References, which cannot be placed
|
||||
// anywhere else according to the standard. Placing headers here also allows them to be fetched
|
||||
// individually over IMAP without downloading the message body. This is why Chat-Version is
|
||||
// placed here.
|
||||
// individually over IMAP without downloading the message body.
|
||||
let mut unprotected_headers: Vec<(&'static str, HeaderType<'static>)> = Vec::new();
|
||||
|
||||
// Headers that MUST NOT (only) go into IMF header section:
|
||||
@@ -1063,11 +1062,7 @@ impl MimeFactory {
|
||||
mail_builder::headers::raw::Raw::new("[...]").into(),
|
||||
));
|
||||
}
|
||||
"in-reply-to"
|
||||
| "references"
|
||||
| "auto-submitted"
|
||||
| "chat-version"
|
||||
| "autocrypt-setup-message" => {
|
||||
"in-reply-to" | "references" | "autocrypt-setup-message" => {
|
||||
unprotected_headers.push(header.clone());
|
||||
}
|
||||
_ => {
|
||||
|
||||
@@ -26,7 +26,7 @@ use crate::dehtml::dehtml;
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::key::{self, DcKey, Fingerprint, SignedPublicKey, load_self_secret_keyring};
|
||||
use crate::log::{error, info, warn};
|
||||
use crate::log::warn;
|
||||
use crate::message::{self, Message, MsgId, Viewtype, get_vcard_summary, set_msg_failed};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::simplify::{SimplifiedText, simplify};
|
||||
|
||||
@@ -50,7 +50,7 @@ use tokio::time::timeout;
|
||||
|
||||
use super::load_connection_timestamp;
|
||||
use crate::context::Context;
|
||||
use crate::log::{info, warn};
|
||||
use crate::log::warn;
|
||||
use crate::tools::time;
|
||||
|
||||
/// Inserts entry into DNS cache
|
||||
|
||||
@@ -10,7 +10,7 @@ use tokio::fs;
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::context::Context;
|
||||
use crate::log::{info, warn};
|
||||
use crate::log::warn;
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::net::session::SessionStream;
|
||||
use crate::net::tls::wrap_rustls;
|
||||
|
||||
@@ -7,7 +7,7 @@ use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::log::{info, warn};
|
||||
use crate::log::warn;
|
||||
use crate::net::http::post_form;
|
||||
use crate::net::read_url_blob;
|
||||
use crate::provider;
|
||||
|
||||
@@ -40,7 +40,7 @@ use crate::EventType;
|
||||
use crate::chat::send_msg;
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::log::{info, warn};
|
||||
use crate::log::warn;
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ use crate::chatlist_events;
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::info;
|
||||
use crate::message::{Message, MsgId, rfc724_mid_exists};
|
||||
use crate::param::Param;
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ use crate::imap::{GENERATED_PREFIX, markseen_on_imap_table};
|
||||
use crate::key::{DcKey, Fingerprint};
|
||||
use crate::key::{self_fingerprint, self_fingerprint_opt};
|
||||
use crate::log::LogExt;
|
||||
use crate::log::{info, warn};
|
||||
use crate::log::warn;
|
||||
use crate::logged_debug_assert;
|
||||
use crate::message::{
|
||||
self, Message, MessageState, MessengerMessage, MsgId, Viewtype, rfc724_mid_exists,
|
||||
@@ -997,7 +997,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
if let Some(is_bot) = mime_parser.is_bot {
|
||||
// If the message is auto-generated and was generated by Delta Chat,
|
||||
// mark the contact as a bot.
|
||||
if mime_parser.get_header(HeaderDef::ChatVersion).is_some() {
|
||||
if mime_parser.has_chat_version() {
|
||||
from_id.mark_bot(context, is_bot).await?;
|
||||
}
|
||||
}
|
||||
@@ -2414,7 +2414,14 @@ async fn lookup_chat_by_reply(
|
||||
|
||||
// If this was a private message just to self, it was probably a private reply.
|
||||
// It should not go into the group then, but into the private chat.
|
||||
if is_probably_private_reply(context, mime_parser, parent_chat_id).await? {
|
||||
if is_probably_private_reply(
|
||||
context,
|
||||
mime_parser,
|
||||
is_partial_download.is_some(),
|
||||
parent_chat_id,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
@@ -2561,6 +2568,7 @@ async fn lookup_or_create_adhoc_group(
|
||||
async fn is_probably_private_reply(
|
||||
context: &Context,
|
||||
mime_parser: &MimeMessage,
|
||||
is_partial_download: bool,
|
||||
parent_chat_id: ChatId,
|
||||
) -> Result<bool> {
|
||||
// Message cannot be a private reply if it has an explicit Chat-Group-ID header.
|
||||
@@ -2579,7 +2587,7 @@ async fn is_probably_private_reply(
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if !mime_parser.has_chat_version() {
|
||||
if !is_partial_download && !mime_parser.has_chat_version() {
|
||||
let chat_contacts = chat::get_chat_contacts(context, parent_chat_id).await?;
|
||||
if chat_contacts.len() == 2 && chat_contacts.contains(&ContactId::SELF) {
|
||||
return Ok(false);
|
||||
@@ -2841,6 +2849,16 @@ async fn apply_group_changes(
|
||||
if !is_from_in_chat {
|
||||
better_msg = Some(String::new());
|
||||
} else if let Some(key) = mime_parser.gossiped_keys.get(added_addr) {
|
||||
if !chat_contacts.contains(&from_id) {
|
||||
chat::add_to_chat_contacts_table(
|
||||
context,
|
||||
mime_parser.timestamp_sent,
|
||||
chat.id,
|
||||
&[from_id],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// TODO: if gossiped keys contain the same address multiple times,
|
||||
// we may lookup the wrong contact.
|
||||
// This can be fixed by looking at ChatGroupMemberAddedFpr,
|
||||
@@ -2939,7 +2957,7 @@ async fn apply_group_changes(
|
||||
}
|
||||
|
||||
// Allow non-Delta Chat MUAs to add members.
|
||||
if mime_parser.get_header(HeaderDef::ChatVersion).is_none() {
|
||||
if !mime_parser.has_chat_version() {
|
||||
// Don't delete any members locally, but instead add absent ones to provide group
|
||||
// membership consistency for all members:
|
||||
new_members.extend(to_ids_flat.iter());
|
||||
|
||||
@@ -5099,37 +5099,6 @@ async fn test_rename_chat_after_creating_invite() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Regression test for the bug
|
||||
/// that resulted in an info message
|
||||
/// about Bob addition to the group on Fiona's device.
|
||||
///
|
||||
/// There should be no info messages about implicit
|
||||
/// member changes when we are added to the group.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_two_group_securejoins() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
|
||||
let group_id = chat::create_group(alice, "Group").await?;
|
||||
|
||||
let qr = get_securejoin_qr(alice, Some(group_id)).await?;
|
||||
|
||||
// Bob joins using QR code.
|
||||
tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||
|
||||
// Fiona joins using QR code.
|
||||
tcm.exec_securejoin_qr(fiona, alice, &qr).await;
|
||||
|
||||
let fiona_chat_id = fiona.get_last_msg().await.chat_id;
|
||||
fiona
|
||||
.golden_test_chat(fiona_chat_id, "two_group_securejoins")
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_unverified_member_msg() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::cmp;
|
||||
use std::iter::{self, once};
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use anyhow::{Context as _, Error, Result, bail};
|
||||
use async_channel::{self as channel, Receiver, Sender};
|
||||
@@ -21,7 +20,7 @@ use crate::ephemeral::{self, delete_expired_imap_messages};
|
||||
use crate::events::EventType;
|
||||
use crate::imap::{FolderMeaning, Imap, session::Session};
|
||||
use crate::location;
|
||||
use crate::log::{LogExt, error, info, warn};
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::message::MsgId;
|
||||
use crate::smtp::{Smtp, send_smtp_messages};
|
||||
use crate::sql;
|
||||
@@ -481,11 +480,10 @@ async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session)
|
||||
}
|
||||
}
|
||||
|
||||
let resync_requested = ctx.resync_request.swap(false, Ordering::Relaxed);
|
||||
if resync_requested {
|
||||
if let Ok(()) = imap.resync_request_receiver.try_recv() {
|
||||
if let Err(err) = session.resync_folders(ctx).await {
|
||||
warn!(ctx, "Failed to resync folders: {:#}.", err);
|
||||
ctx.resync_request.store(true, Ordering::Relaxed);
|
||||
imap.resync_request_sender.try_send(()).ok();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ use humansize::{BINARY, format_size};
|
||||
|
||||
use crate::events::EventType;
|
||||
use crate::imap::{FolderMeaning, scan_folders::get_watched_folder_configs};
|
||||
use crate::log::info;
|
||||
use crate::quota::{QUOTA_ERROR_THRESHOLD_PERCENTAGE, QUOTA_WARN_THRESHOLD_PERCENTAGE};
|
||||
use crate::stock_str;
|
||||
use crate::{context::Context, log::LogExt};
|
||||
|
||||
@@ -19,7 +19,7 @@ use crate::events::EventType;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::key::{DcKey, Fingerprint, load_self_public_key};
|
||||
use crate::log::LogExt as _;
|
||||
use crate::log::{error, info, warn};
|
||||
use crate::log::warn;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::param::Param;
|
||||
|
||||
@@ -10,7 +10,6 @@ use crate::contact::Origin;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::log::info;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::param::Param;
|
||||
@@ -127,17 +126,6 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
match invite {
|
||||
QrInvite::Group { .. } => {
|
||||
let joining_chat_id = joining_chat_id(context, &invite, private_chat_id).await?;
|
||||
// We created the group already, now we need to add Alice to the group.
|
||||
// The group will only become usable once the protocol is finished.
|
||||
if !is_contact_in_chat(context, joining_chat_id, invite.contact_id()).await? {
|
||||
chat::add_to_chat_contacts_table(
|
||||
context,
|
||||
time(),
|
||||
joining_chat_id,
|
||||
&[invite.contact_id()],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
let msg = stock_str::secure_join_started(context, invite.contact_id()).await;
|
||||
chat::add_info_msg(context, joining_chat_id, &msg, time()).await?;
|
||||
Ok(joining_chat_id)
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::time::Duration;
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{CantSendReason, remove_contact_from_chat};
|
||||
use crate::chat::{CantSendReason, add_contact_to_chat, remove_contact_from_chat};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::key::self_fingerprint;
|
||||
@@ -60,13 +60,13 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
bob.set_config(Config::Displayname, Some("Bob Examplenet"))
|
||||
.await
|
||||
.unwrap();
|
||||
let alice_auto_submitted_hdr;
|
||||
let alice_auto_submitted_val;
|
||||
match case {
|
||||
SetupContactCase::AliceIsBot => {
|
||||
alice.set_config_bool(Config::Bot, true).await.unwrap();
|
||||
alice_auto_submitted_hdr = "Auto-Submitted: auto-generated";
|
||||
alice_auto_submitted_val = "auto-generated";
|
||||
}
|
||||
_ => alice_auto_submitted_hdr = "Auto-Submitted: auto-replied",
|
||||
_ => alice_auto_submitted_val = "auto-replied",
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
@@ -121,7 +121,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
);
|
||||
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
assert!(sent.payload.contains(alice_auto_submitted_hdr));
|
||||
assert!(!sent.payload.contains("Auto-Submitted:"));
|
||||
assert!(!sent.payload.contains("Alice Exampleorg"));
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
@@ -129,6 +129,10 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||
"vc-auth-required"
|
||||
);
|
||||
assert_eq!(
|
||||
msg.get_header(HeaderDef::AutoSubmitted).unwrap(),
|
||||
alice_auto_submitted_val
|
||||
);
|
||||
|
||||
let bob_chat = bob.get_chat(&alice).await;
|
||||
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true);
|
||||
@@ -157,7 +161,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
|
||||
// Check Bob sent the right message.
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
assert!(sent.payload.contains("Auto-Submitted: auto-replied"));
|
||||
assert!(!sent.payload.contains("Auto-Submitted:"));
|
||||
assert!(!sent.payload.contains("Bob Examplenet"));
|
||||
let mut msg = alice.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
@@ -171,6 +175,10 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
|
||||
bob_fp
|
||||
);
|
||||
assert_eq!(
|
||||
msg.get_header(HeaderDef::AutoSubmitted).unwrap(),
|
||||
"auto-replied"
|
||||
);
|
||||
|
||||
if case == SetupContactCase::WrongAliceGossip {
|
||||
let wrong_pubkey = GossipedKey {
|
||||
@@ -248,7 +256,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
|
||||
// Check Alice sent the right message to Bob.
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
assert!(sent.payload.contains(alice_auto_submitted_hdr));
|
||||
assert!(!sent.payload.contains("Auto-Submitted:"));
|
||||
assert!(!sent.payload.contains("Alice Exampleorg"));
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
@@ -256,6 +264,10 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||
"vc-contact-confirm"
|
||||
);
|
||||
assert_eq!(
|
||||
msg.get_header(HeaderDef::AutoSubmitted).unwrap(),
|
||||
alice_auto_submitted_val
|
||||
);
|
||||
|
||||
// Bob has verified Alice already.
|
||||
//
|
||||
@@ -465,18 +477,29 @@ async fn test_secure_join() -> Result<()> {
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
assert!(sent.payload.contains("Auto-Submitted: auto-replied"));
|
||||
assert!(!sent.payload.contains("Auto-Submitted:"));
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||
"vg-auth-required"
|
||||
);
|
||||
assert_eq!(
|
||||
msg.get_header(HeaderDef::AutoSubmitted).unwrap(),
|
||||
"auto-replied"
|
||||
);
|
||||
|
||||
tcm.section("Step 4: Bob receives vg-auth-required, sends vg-request-with-auth");
|
||||
bob.recv_msg_trash(&sent).await;
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
|
||||
// At this point Alice is still not part of the chat.
|
||||
// The final step of Alice adding Bob to the chat
|
||||
// may not work out and we don't want Bob
|
||||
// to implicitly add Alice if he manages to join the group
|
||||
// much later via another member.
|
||||
assert_eq!(chat::get_chat_contacts(&bob, bob_chatid).await?.len(), 0);
|
||||
|
||||
let contact_alice_id = bob.add_or_lookup_contact_no_key(&alice).await.id;
|
||||
|
||||
// Check Bob emitted the JoinerProgress event.
|
||||
@@ -496,7 +519,7 @@ async fn test_secure_join() -> Result<()> {
|
||||
}
|
||||
|
||||
// Check Bob sent the right handshake message.
|
||||
assert!(sent.payload.contains("Auto-Submitted: auto-replied"));
|
||||
assert!(!sent.payload.contains("Auto-Submitted:"));
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
@@ -509,6 +532,10 @@ async fn test_secure_join() -> Result<()> {
|
||||
msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
|
||||
bob_fp
|
||||
);
|
||||
assert_eq!(
|
||||
msg.get_header(HeaderDef::AutoSubmitted).unwrap(),
|
||||
"auto-replied"
|
||||
);
|
||||
|
||||
// Alice should not yet have Bob verified
|
||||
let contact_bob = alice.add_or_lookup_contact_no_key(&bob).await;
|
||||
@@ -605,6 +632,10 @@ async fn test_secure_join() -> Result<()> {
|
||||
let bob_chat = Chat::load_from_db(&bob.ctx, bob_chatid).await?;
|
||||
assert!(bob_chat.typ == Chattype::Group);
|
||||
|
||||
// At the end of the protocol
|
||||
// Bob should have two members in the chat.
|
||||
assert_eq!(chat::get_chat_contacts(&bob, bob_chatid).await?.len(), 2);
|
||||
|
||||
// On this "happy path", Alice and Bob get only a group-chat where all information are added to.
|
||||
// The one-to-one chats are used internally for the hidden handshake messages,
|
||||
// however, should not be visible in the UIs.
|
||||
@@ -1113,3 +1144,96 @@ async fn test_get_securejoin_qr_name_is_truncated() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Regression test for the bug
|
||||
/// that resulted in an info message
|
||||
/// about Bob addition to the group on Fiona's device.
|
||||
///
|
||||
/// There should be no info messages about implicit
|
||||
/// member changes when we are added to the group.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_two_group_securejoins() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
|
||||
let group_id = chat::create_group(alice, "Group").await?;
|
||||
|
||||
let qr = get_securejoin_qr(alice, Some(group_id)).await?;
|
||||
|
||||
// Bob joins using QR code.
|
||||
tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||
|
||||
// Fiona joins using QR code.
|
||||
tcm.exec_securejoin_qr(fiona, alice, &qr).await;
|
||||
|
||||
let fiona_chat_id = fiona.get_last_msg().await.chat_id;
|
||||
fiona
|
||||
.golden_test_chat(fiona_chat_id, "two_group_securejoins")
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that scanning an outdated QR code does not add the removed inviter back to the group.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_qr_no_implicit_inviter_addition() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
|
||||
// Alice creates a group with Bob.
|
||||
let alice_chat_id = alice
|
||||
.create_group_with_members("Group with Bob", &[bob])
|
||||
.await;
|
||||
let alice_qr = get_securejoin_qr(alice, Some(alice_chat_id)).await?;
|
||||
|
||||
// Bob joins the group via QR code.
|
||||
let bob_chat_id = tcm.exec_securejoin_qr(bob, alice, &alice_qr).await;
|
||||
|
||||
// Bob creates a QR code for joining the group.
|
||||
let bob_qr = get_securejoin_qr(bob, Some(bob_chat_id)).await?;
|
||||
|
||||
// Alice removes Bob from the group.
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
remove_contact_from_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
|
||||
// Deliver the removal message to Bob.
|
||||
let removal_msg = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&removal_msg).await;
|
||||
|
||||
// Charlie scans Bob's outdated QR code.
|
||||
let charlie_chat_id = join_securejoin(charlie, &bob_qr).await?;
|
||||
|
||||
// Charlie sends vg-request to Bob.
|
||||
let sent = charlie.pop_sent_msg().await;
|
||||
bob.recv_msg_trash(&sent).await;
|
||||
|
||||
// Bob sends vg-auth-required to Charlie.
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
charlie.recv_msg_trash(&sent).await;
|
||||
|
||||
// Bob receives vg-request-with-auth, but cannot add Charlie
|
||||
// because Bob himself is not in the group.
|
||||
let sent = charlie.pop_sent_msg().await;
|
||||
bob.recv_msg_trash(&sent).await;
|
||||
|
||||
// Charlie still has no contacts in the list.
|
||||
let charlie_chat_contacts = chat::get_chat_contacts(charlie, charlie_chat_id).await?;
|
||||
assert_eq!(charlie_chat_contacts.len(), 0);
|
||||
|
||||
// Alice adds Charlie to the group
|
||||
let alice_charlie_contact_id = alice.add_or_lookup_contact_id(charlie).await;
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_charlie_contact_id).await?;
|
||||
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
assert_eq!(charlie.recv_msg(&sent).await.chat_id, charlie_chat_id);
|
||||
|
||||
// Charlie has two contacts in the list: Alice and self.
|
||||
let charlie_chat_contacts = chat::get_chat_contacts(charlie, charlie_chat_id).await?;
|
||||
assert_eq!(charlie_chat_contacts.len(), 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::config::Config;
|
||||
use crate::contact::{Contact, ContactId};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::{error, info, warn};
|
||||
use crate::log::warn;
|
||||
use crate::message::Message;
|
||||
use crate::message::{self, MsgId};
|
||||
use crate::mimefactory::MimeFactory;
|
||||
|
||||
@@ -7,7 +7,7 @@ use async_smtp::{SmtpClient, SmtpTransport};
|
||||
use tokio::io::{AsyncBufRead, AsyncWrite, BufStream};
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::log::{info, warn};
|
||||
use crate::log::warn;
|
||||
use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp};
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::net::session::SessionBufStream;
|
||||
|
||||
@@ -6,7 +6,7 @@ use super::Smtp;
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::{info, warn};
|
||||
use crate::log::warn;
|
||||
use crate::tools;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::debug_logging::set_debug_logging_xdc;
|
||||
use crate::ephemeral::start_ephemeral_timers;
|
||||
use crate::imex::BLOBS_BACKUP_NAME;
|
||||
use crate::location::delete_orphaned_poi_locations;
|
||||
use crate::log::{LogExt, error, info, warn};
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::message::{Message, MsgId};
|
||||
use crate::net::dns::prune_dns_cache;
|
||||
use crate::net::http::http_cache_cleanup;
|
||||
|
||||
@@ -14,9 +14,8 @@ use crate::config::Config;
|
||||
use crate::configure::EnteredLoginParam;
|
||||
use crate::constants::ShowEmails;
|
||||
use crate::context::Context;
|
||||
use crate::imap;
|
||||
use crate::key::DcKey;
|
||||
use crate::log::{info, warn};
|
||||
use crate::log::warn;
|
||||
use crate::message::MsgId;
|
||||
use crate::provider::get_provider_info;
|
||||
use crate::sql::Sql;
|
||||
@@ -413,11 +412,36 @@ CREATE TABLE imap_sync (folder TEXT PRIMARY KEY, uidvalidity INTEGER DEFAULT 0,
|
||||
"configured_mvbox_folder",
|
||||
] {
|
||||
if let Some(folder) = context.sql.get_raw_config(c).await? {
|
||||
let key = format!("imap.mailbox.{folder}");
|
||||
|
||||
let (uid_validity, last_seen_uid) =
|
||||
imap::get_config_last_seen_uid(context, &folder).await?;
|
||||
if let Some(entry) = context.sql.get_raw_config(&key).await? {
|
||||
// the entry has the format `imap.mailbox.<folder>=<uidvalidity>:<lastseenuid>`
|
||||
let mut parts = entry.split(':');
|
||||
(
|
||||
parts.next().unwrap_or_default().parse().unwrap_or(0),
|
||||
parts.next().unwrap_or_default().parse().unwrap_or(0),
|
||||
)
|
||||
} else {
|
||||
(0, 0)
|
||||
};
|
||||
if last_seen_uid > 0 {
|
||||
imap::set_uid_next(context, &folder, last_seen_uid + 1).await?;
|
||||
imap::set_uidvalidity(context, &folder, uid_validity).await?;
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO imap_sync (folder, uid_next) VALUES (?,?)
|
||||
ON CONFLICT(folder) DO UPDATE SET uid_next=excluded.uid_next",
|
||||
(&folder, last_seen_uid + 1),
|
||||
)
|
||||
.await?;
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO imap_sync (folder, uidvalidity) VALUES (?,?)
|
||||
ON CONFLICT(folder) DO UPDATE SET uidvalidity=excluded.uidvalidity",
|
||||
(&folder, uid_validity),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::constants::Blocked;
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::log::LogExt;
|
||||
use crate::log::{info, warn};
|
||||
use crate::log::warn;
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Param;
|
||||
|
||||
@@ -40,7 +40,7 @@ use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::log::{info, warn};
|
||||
use crate::log::warn;
|
||||
use crate::message::{Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
|
||||
Reference in New Issue
Block a user