Compare commits

..

21 Commits

Author SHA1 Message Date
iequidoo
267026b44e feat: Do not copy Auto-Submitted header into outer part
Before, copying Auto-Submitted to the outer headers was needed for moving such messages,
e.g. multi-device sync messages, to the DeltaChat folder. Now all encrypted messages are moved.
2025-11-10 06:08:52 -03:00
iequidoo
22da92c563 feat: Do not copy Chat-Version header into outer part
Chat-Version is used sometimes by Sieve filters to move messages to DeltaChat folder:
37beed6ad9/data/conf/dovecot/global_sieve_before
This probably prevents notifications to MUAs that don't watch DeltaChat but watch INBOX.

There are however disadvantages to exposing Chat-Version:
1. Spam filters may not like this header and it is difficult or impossible to tell if `Chat-Version`
   plays role in rejecting the message or delivering it into Spam folder. If there is no such header
   visible to the spam filter, this possibility can be ruled out.
2. Replies to chat messages may have no `Chat-Version` but have to be moved anyway.
3. The user may have no control over the Sieve filter, but it comes preconfigured in mailcow, so it
   is not possible to disable it on the client.

Thanks to link2xt for providing this motivation.

NOTE: Old Delta Chat will assign partially downloaded replies to an ad-hoc group with the sender
instead of the 1:1 chat, but we're removing partial downloads anyway.
2025-11-10 05:34:31 -03:00
iequidoo
357f7107a6 feat: Move all encrypted messages to mvbox if MvboxMove is on
Before, only replies to chat messages were moved to the mvbox because we're removing Chat-Version
from outer headers, but there's no much sense in moving only replies and not moving original
messages and MDNs. Instead, move all encrypted messages. Users should be informed about this in UIs,
so if a user has another PGP-capable MUA, probably they should disable MvboxMove. Moreover, untying
this logic from References and In-Reply-To allows to remove them from outer headers too, the "Header
Protection for Cryptographically Protected Email" RFC even suggests such a behavior:
https://datatracker.ietf.org/doc/html/rfc9788#name-offering-more-ambitious-hea.
2025-11-10 05:34:31 -03:00
iequidoo
d96b4beff1 feat: Don't download group messages unconditionally
There was a comment that group messages should always be downloaded to avoid inconsistent group
state, but this is solved by the group consistency algo nowadays in the sense that inconsistent
group state won't spread to other members if we send to the group. Moreover, encrypted messages are
now always downloaded, and unencrypted chat replies too, and as for ad-hoc groups,
`Config::ShowEmails` controls everything.
2025-11-10 05:34:31 -03:00
iequidoo
56c605fa0a feat: imap: Don't prefetch Chat-Version; try to find out message encryption state instead
Instead, prefetch Secure-Join, Content-Type and Subject headers, try to find out if the message is encrypted, i.e.:
- if its Content-Type is "multipart/encrypted"
- or Subject is "..." or "[...]" as some MUAs use "multipart/mixed"; we can't only look at Subject
  as it's not mandatory;
and depending on this decide on the target folder and whether the message should be
downloaded. There's no much sense in downloading unencrypted "Chat-Version"-containing messages if
`ShowEmails` is `Off` or `AcceptedContacts`, unencrypted Delta Chat messages should be considered as
usual emails, there's even the "New E-Mail" feature in UIs nowadays which sends such messages.

Don't prefetch Auto-Submitted as well, this becomes unnecessary.

Changed behavior: before, "Chat-Version"-containing messages were moved from INBOX to DeltaChat, now
such encrypted messages may remain in INBOX -- if there's no parent message or it's not
`MessengerMessage`. Don't unconditionally move encrypted messages yet because the account may be
shared with other software which doesn't and shouldn't look into the DeltaChat folder.
2025-11-10 05:32:40 -03:00
link2xt
2e9fd1c25d test: do not add QR inviter to groups right after scanning the code
The inviter may be not part of the group
by the time we scan the QR code.
2025-11-08 03:26:23 +00:00
link2xt
1b1a5f170e test: Bob has 0 members in the chat until securejoin finishes 2025-11-08 03:26:23 +00:00
link2xt
1946603be6 test: at the end of securejoin Bob has two members in a group chat 2025-11-08 03:26:23 +00:00
link2xt
c43b622c23 test: move test_two_group_securejoins from receive_imf to securejoin module 2025-11-08 03:26:23 +00:00
link2xt
73bf6983b9 fix: do not add QR inviter to groups immediately
By the time you scan the QR code,
inviter may not be in the group already.
In this case securejoin protocol will never complete.
If you then join the group in some other way,
this results in you implicitly adding that inviter
to the group.
2025-11-08 03:26:23 +00:00
link2xt
aaa0f8e245 fix: do not return an error from receive_imf if we fail to add a member because we are not in chat
This happens when we receive a vg-request-with-auth message
for a chat from which we have been removed already.
2025-11-08 03:26:23 +00:00
link2xt
5a1e0e8824 chore: rustfmt 2025-11-08 03:26:23 +00:00
link2xt
cf5b145ce0 refactor: remove unused imports 2025-11-07 17:31:34 +00:00
link2xt
dd11a0e29a refactor: replace imap:: calls in migration 73 with SQL queries 2025-11-07 07:12:08 +00:00
link2xt
3d86cb5953 test: remove ThreadPoolExecutor from test_wait_next_messages 2025-11-07 07:09:35 +00:00
link2xt
75eb94e44f docs: fix Context::set_stock_translation reference 2025-11-07 06:56:10 +00:00
link2xt
7fef812b1e refactor(imap): move resync request from Context to Imap
For multiple transports we will need to run
multiple IMAP clients in parallel.
UID validity change detected by one IMAP client
should not result in UID resync
for another IMAP client.
2025-11-06 19:16:30 +00:00
link2xt
5f174ceaf2 test: test editing saved messages 2025-11-06 18:38:11 +00:00
link2xt
06b038ab5d fix: is_encrypted() should be true for Saved Messages chat
Otherwise UIs don't allow to edit messages sent to self.
This was likely broken in b417ba86bc
2025-11-06 18:38:11 +00:00
Simon Laux
b20da3cb0e docs: readme: update language binding section to avoid usage of cffi in new projects (#7380)
Updated language bindings section to reflect deprecation of
`libdeltachat and removed outdated entries.
2025-11-06 13:04:56 +00:00
Simon Laux
a3328ea2de api!(jsonrpc): chat_type now contains a variant of a string enum/union. Affected places: FullChat.chat_type, BasicChat.chat_type, ChatListItemFetchResult::ChatListItem.chat_type, Event:: SecurejoinInviterProgress.chat_type and MessageSearchResult.chat_type (#7285)
Actually it will be not as breaking if you used the constants, because
this pr also changes the constants.

closes #7029 

Note that I had to change the constants from enum to namespace, this has
the side effect, that you can no longer also use the constants as types,
you need to instead prefix them with `typeof ` now.
2025-11-06 12:53:48 +00:00
55 changed files with 446 additions and 322 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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`,
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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());
}
_ => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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());

View File

@@ -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();

View File

@@ -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();
}
}

View File

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

View File

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

View File

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

View File

@@ -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(())
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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?;
}
}
}

View File

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

View File

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