Compare commits

..

11 Commits

Author SHA1 Message Date
link2xt
48eb400a69 imap: skip sync flags update if highest modseq has not increased 2022-01-30 22:44:19 +00:00
link2xt
8aa6decbf9 imap: call delete_expired_imap_messages() less often
This operation takes roughly 0.3 s on a moderate size database.
Calling it once before scanning all folders and scanning
the watched folder instead of each time after downloading
a message from a folder speeds up IMAP loop.
2022-01-30 20:49:32 +00:00
link2xt
7cf4bcaca2 imap: call delete_expired_imap_messages() less often
This operation takes roughly 0.3 s on a moderate size database.
Calling it once before scanning all folders and scanning
the watched folder instead of each time after downloading
a message from a folder speeds up IMAP loop.
2022-01-30 20:47:32 +00:00
dependabot[bot]
9ccd9c3e0e Merge pull request #3011 from deltachat/dependabot/cargo/serde-1.0.136 2022-01-30 20:13:39 +00:00
dependabot[bot]
c6773a6303 Merge pull request #3020 from deltachat/dependabot/cargo/backtrace-0.3.64 2022-01-30 20:13:20 +00:00
link2xt
e858a32aa1 smtp: cancel message sending by removing the message
This restores the logic removed in
afd8c0d879
2022-01-30 10:59:10 +00:00
B. Petersen
99f2680e2c fix splitting off text from webxdc messages
moreover, make the split check exhaustive
to avoid the same error on the next added Viewtype.
2022-01-30 11:49:09 +01:00
B. Petersen
7a9a323bac test sending webxdc+text 2022-01-30 11:49:09 +01:00
dependabot[bot]
62aa234352 cargo: bump backtrace from 0.3.63 to 0.3.64
Bumps [backtrace](https://github.com/rust-lang/backtrace-rs) from 0.3.63 to 0.3.64.
- [Release notes](https://github.com/rust-lang/backtrace-rs/releases)
- [Commits](https://github.com/rust-lang/backtrace-rs/compare/0.3.63...0.3.64)

---
updated-dependencies:
- dependency-name: backtrace
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-30 09:54:28 +00:00
link2xt
0cb9e7922a Remove direct dependency on byteorder crate 2022-01-29 23:24:25 +00:00
dependabot[bot]
7fc9bacf54 cargo: bump serde from 1.0.135 to 1.0.136
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.135 to 1.0.136.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.135...v1.0.136)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-26 21:14:08 +00:00
15 changed files with 249 additions and 266 deletions

View File

@@ -2,14 +2,20 @@
## Unreleased
### API Changes
- added APIs to check if database is encrypted and to change the passphrase:
`dc_context_is_encrypted()` and `dc_context_change_passphrase()` #3029.
### Changes
- don't watch Sent folder by default #3025
- use webxdc app name in chatlist/quotes/replies etc. #3027
- refactorings #3023
- remove direct dependency on `byteorder` crate #3031
- make it possible to cancel message sending by removing the message #3034,
this was previosuly removed in 1.71.0 #2939
- always skip Seen flag synchronization when there are no updates #3039
### Fixes
- fix splitting off text from webxdc messages #3032
- call slow `delete_expired_imap_messages()` less often #3037
- make synchronization of Seen status more robust in case unsolicited FETCH
result without UID is returned #3022
## 1.72.0

14
Cargo.lock generated
View File

@@ -420,9 +420,9 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "backtrace"
version = "0.3.63"
version = "0.3.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "321629d8ba6513061f26707241fa9bc89524ff1cd7a915a97ef0c62c666ce1b6"
checksum = "5e121dee8023ce33ab248d9ce1493df03c3b38a659b240096fcbd7048ff9c31f"
dependencies = [
"addr2line",
"cc",
@@ -1077,7 +1077,6 @@ dependencies = [
"backtrace",
"base64 0.13.0",
"bitflags",
"byteorder",
"chrono",
"criterion",
"deltachat_derive",
@@ -1091,6 +1090,7 @@ dependencies = [
"hex",
"humansize",
"image",
"imap-proto",
"kamadak-exif",
"lettre_email",
"libc",
@@ -3301,9 +3301,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "serde"
version = "1.0.135"
version = "1.0.136"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cf9235533494ea2ddcdb794665461814781c53f19d87b76e571a1c35acbad2b"
checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
dependencies = [
"serde_derive",
]
@@ -3320,9 +3320,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.135"
version = "1.0.136"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8dcde03d87d4c973c04be249e7d8f0b35db1c848c487bd43032808e59dd8328d"
checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -30,7 +30,6 @@ async-trait = "0.1"
backtrace = "0.3"
base64 = "0.13"
bitflags = "1.3"
byteorder = "1.3"
chrono = "0.4"
dirs = { version = "4", optional=true }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
@@ -39,6 +38,7 @@ escaper = "0.1"
futures = "0.3"
hex = "0.4.0"
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
imap-proto = "0.14.3"
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = "0.2"

View File

@@ -241,32 +241,6 @@ int dc_context_open (dc_context_t *context, const char*
int dc_context_is_open (dc_context_t *context);
/**
* Return 1 if database is encrypted. Can only be checked on open database.
* Use this method to decide whether to present an option to change passphrase
* to the user.
*
* @member dc_context_t
* @param context The context object.
* @return 1 if database is encrypted, 0 if database is not encrypted or on
* error.
*/
int dc_context_is_encrypted (dc_context_t *context);
/**
* Changes passphrase for the open database. The database must be encrypted
* already, i.e. have a non-empty password. Unencrypted databases can only be
* encrypted during import/export.
* @memberof dc_context_t
* @param context The context object.
* @param passpharse New passphrase.
* @return 1 if database was reencrypted with the new passphrase, 0 on error
* (database is closed, database is not encrypted, other SQLCipher error).
*/
int dc_context_change_passphrase (dc_context_t *context, char *passphrase);
/**
* Free a context object.
*

View File

@@ -144,34 +144,6 @@ pub unsafe extern "C" fn dc_context_is_open(context: *mut dc_context_t) -> libc:
block_on(ctx.is_open()) as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_context_is_encrypted(context: *mut dc_context_t) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_context_is_encrypted()");
return 0;
}
let ctx = &*context;
block_on(ctx.is_encrypted()) as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_context_change_passphrase(
context: *mut dc_context_t,
passphrase: *const libc::c_char,
) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_context_change_passphrase()");
return 0;
}
let ctx = &*context;
let passphrase = to_string_lossy(passphrase);
block_on(ctx.change_passphrase(passphrase))
.log_err(ctx, "change_passphrase failed")
.is_ok() as libc::c_int
}
/// Release the context structure.
///
/// This function releases the memory of the `dc_context_t` structure.

View File

@@ -146,12 +146,6 @@ impl Context {
self.sql.is_open().await
}
/// Returns true if database is encrypted. Returns false if database is not open yet or not
/// encrypted.
pub async fn is_encrypted(&self) -> bool {
self.sql.is_encrypted().await.unwrap_or(false)
}
/// Tests the database passphrase.
///
/// Returns true if passphrase is correct.
@@ -161,14 +155,6 @@ impl Context {
self.sql.check_passphrase(passphrase).await
}
/// Changes the database passphrase.
///
/// Works only for encrypted databases. Encrypted database can only be converted to unencrypted
/// one and backwards via import/export.
pub async fn change_passphrase(&self, passphrase: String) -> Result<()> {
self.sql.change_passphrase(self, passphrase).await
}
pub(crate) async fn with_blobdir(
dbfile: PathBuf,
blobdir: PathBuf,
@@ -1083,34 +1069,4 @@ mod tests {
Ok(())
}
#[async_std::test]
async fn test_change_passphrase() -> Result<()> {
let dir = tempdir()?;
let dbfile = dir.path().join("db.sqlite");
let id = 1;
let context = Context::new_closed(dbfile.clone().into(), id)
.await
.context("failed to create context")?;
assert_eq!(context.open("foo".to_string()).await?, true);
assert_eq!(context.is_open().await, true);
assert_eq!(context.is_encrypted().await, true);
context.change_passphrase("bar".to_string()).await?;
drop(context);
let id = 2;
let context = Context::new(dbfile.into(), id)
.await
.context("failed to create context")?;
assert_eq!(context.is_open().await, false);
assert_eq!(context.check_passphrase("foo".to_string()).await?, false);
assert_eq!(context.check_passphrase("bar".to_string()).await?, true);
assert_eq!(context.open("foo".to_string()).await?, false);
assert_eq!(context.open("bar".to_string()).await?, true);
assert_eq!(context.is_encrypted().await, true);
Ok(())
}
}

View File

@@ -195,44 +195,26 @@ async fn maybe_warn_on_outdated(context: &Context, now: i64, approx_compile_time
}
/* Message-ID tools */
/// Generate an ID. The generated ID should be as short and as unique as possible:
/// - short, because it may also used as part of Message-ID headers or in QR codes
/// - unique as two IDs generated on two devices should not be the same. However, collisions are not world-wide but only by the few contacts.
/// IDs generated by this function are 66 bit wide and are returned as 11 base64 characters.
///
/// Additional information when used as a message-id or group-id:
/// - for OUTGOING messages this ID is written to the header as `Chat-Group-ID:` and is added to the message ID as Gr.<grpid>.<random>@<random>
/// - for INCOMING messages, the ID is taken from the Chat-Group-ID-header or from the Message-ID in the In-Reply-To: or References:-Header
/// - the group-id should be a string with the characters [a-zA-Z0-9\-_]
pub(crate) fn dc_create_id() -> String {
/* generate an id. the generated ID should be as short and as unique as possible:
- short, because it may also used as part of Message-ID headers or in QR codes
- unique as two IDs generated on two devices should not be the same. However, collisions are not world-wide but only by the few contacts.
IDs generated by this function are 66 bit wide and are returned as 11 base64 characters.
If possible, RNG of OpenSSL is used.
Additional information when used as a message-id or group-id:
- for OUTGOING messages this ID is written to the header as `Chat-Group-ID:` and is added to the message ID as Gr.<grpid>.<random>@<random>
- for INCOMING messages, the ID is taken from the Chat-Group-ID-header or from the Message-ID in the In-Reply-To: or References:-Header
- the group-id should be a string with the characters [a-zA-Z0-9\-_] */
// ThreadRng implements CryptoRng trait and is supposed to be cryptographically secure.
let mut rng = thread_rng();
let buf: [u32; 3] = [rng.gen(), rng.gen(), rng.gen()];
encode_66bits_as_base64(buf[0usize], buf[1usize], buf[2usize])
}
// Generate 72 random bits.
let mut arr = [0u8; 9];
rng.fill(&mut arr[..]);
/// Encode 66 bits as a base64 string.
/// This is useful for ID generating with short strings as we save 5 character
/// in each id compared to 64 bit hex encoding. For a typical group ID, these
/// are 10 characters (grpid+msgid):
/// hex: 64 bit, 4 bits/character, length = 64/4 = 16 characters
/// base64: 64 bit, 6 bits/character, length = 64/6 = 11 characters (plus 2 additional bits)
/// Only the lower 2 bits of `fill` are used.
fn encode_66bits_as_base64(v1: u32, v2: u32, fill: u32) -> String {
use byteorder::{BigEndian, WriteBytesExt};
let mut wrapped_writer = Vec::new();
{
let mut enc = base64::write::EncoderWriter::new(&mut wrapped_writer, base64::URL_SAFE);
enc.write_u32::<BigEndian>(v1).unwrap();
enc.write_u32::<BigEndian>(v2).unwrap();
enc.write_u8(((fill & 0x3) as u8) << 6).unwrap();
enc.finish().unwrap();
}
assert_eq!(wrapped_writer.pop(), Some(b'A')); // Remove last "A"
String::from_utf8(wrapped_writer).unwrap()
// Take 11 base64 characters containing 66 random bits.
base64::encode(&arr).chars().take(11).collect()
}
/// Function generates a Message-ID that can be used for a new outgoing message.
@@ -780,26 +762,6 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
assert_eq!(buf.len(), 11);
}
#[test]
fn test_encode_66bits_as_base64() {
assert_eq!(
encode_66bits_as_base64(0x01234567, 0x89abcdef, 0),
"ASNFZ4mrze8"
);
assert_eq!(
encode_66bits_as_base64(0x01234567, 0x89abcdef, 1),
"ASNFZ4mrze9"
);
assert_eq!(
encode_66bits_as_base64(0x01234567, 0x89abcdef, 2),
"ASNFZ4mrze-"
);
assert_eq!(
encode_66bits_as_base64(0x01234567, 0x89abcdef, 3),
"ASNFZ4mrze_"
);
}
#[test]
fn test_dc_extract_grpid_from_rfc724_mid() {
// Should return None if we pass invalid mid

View File

@@ -28,7 +28,6 @@ use crate::context::Context;
use crate::dc_receive_imf::{
dc_receive_imf_inner, from_field_to_contact_id, get_prefetch_parent_message, ReceivedMsg,
};
use crate::ephemeral::delete_expired_imap_messages;
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::job::{self, Action};
@@ -459,9 +458,6 @@ impl Imap {
.await
.context("fetch_new_messages")?;
// Mark expired messages for deletion.
delete_expired_imap_messages(context).await?;
self.move_messages(context, watch_folder)
.await
.context("move_messages")?;
@@ -1013,7 +1009,9 @@ impl Imap {
return Ok(());
}
self.select_folder(context, Some(folder)).await?;
self.select_folder(context, Some(folder))
.await
.context("failed to select folder")?;
let session = self
.session
.as_mut()
@@ -1025,77 +1023,74 @@ impl Imap {
.as_ref()
.with_context(|| format!("No mailbox selected, folder: {}", folder))?;
// Check if the mailbox supports MODSEQ.
// We are not interested in actual value of HIGHESTMODSEQ.
if mailbox.highest_modseq.is_none() {
let remote_highest_modseq = if let Some(remote_highest_modseq) = mailbox.highest_modseq {
remote_highest_modseq
} else {
info!(
context,
"Mailbox {} does not support mod-sequences, skipping flag synchronization.", folder
);
return Ok(());
};
let mut highest_modseq = get_modseq(context, folder)
.await
.with_context(|| format!("failed to get MODSEQ for folder {}", folder))?;
if highest_modseq >= remote_highest_modseq {
info!(
context,
"MODSEQ {} is already new, HIGHESTMODSEQ={}, skipping seen flag update",
highest_modseq,
remote_highest_modseq
);
return Ok(());
}
let mut updated_chat_ids = BTreeSet::new();
let uid_validity = get_uidvalidity(context, folder).await?;
let mut highest_modseq = get_modseq(context, folder).await?;
let uid_validity = get_uidvalidity(context, folder)
.await
.with_context(|| format!("failed to get UID validity for folder {}", folder))?;
let mut list = session
.uid_fetch("1:*", format!("(FLAGS) (CHANGEDSINCE {})", highest_modseq))
.await
.context("failed to fetch flags")?;
while let Some(fetch) = list.next().await {
let msg = fetch?;
let is_seen = msg.flags().any(|flag| flag == Flag::Seen);
let fetch = fetch.context("failed to get FETCH result")?;
let uid = if let Some(uid) = fetch.uid {
uid
} else {
info!(context, "FETCH result contains no UID, skipping");
continue;
};
let is_seen = fetch.flags().any(|flag| flag == Flag::Seen);
if is_seen {
if let Some((msg_id, chat_id)) = context
.sql
.query_row_optional(
"SELECT id, chat_id FROM msgs
WHERE rfc724_mid IN (
SELECT rfc724_mid FROM imap
WHERE folder=?1
AND uidvalidity=?2
AND uid=?3
LIMIT 1
)",
paramsv![&folder, uid_validity, msg.uid],
|row| {
let msg_id: MsgId = row.get(0)?;
let chat_id: ChatId = row.get(1)?;
Ok((msg_id, chat_id))
},
)
.await?
if let Some(chat_id) = mark_seen_by_uid(context, folder, uid_validity, uid)
.await
.with_context(|| {
format!("failed to update seen status for msg {}/{}", folder, uid)
})?
{
let updated = context
.sql
.execute(
"UPDATE msgs SET state=?1
WHERE (state=?2 OR state=?3)
AND id=?4",
paramsv![
MessageState::InSeen,
MessageState::InFresh,
MessageState::InNoticed,
msg_id
],
)
.await?
> 0;
if updated {
updated_chat_ids.insert(chat_id);
let modseq = msg.modseq.unwrap_or_default();
if modseq > highest_modseq {
highest_modseq = modseq;
}
}
updated_chat_ids.insert(chat_id);
}
}
if let Some(modseq) = fetch.modseq {
if modseq > highest_modseq {
highest_modseq = modseq;
}
} else {
warn!(context, "FETCH result contains no MODSEQ");
}
}
set_modseq(context, folder, highest_modseq).await?;
if remote_highest_modseq > highest_modseq {
// We haven't seen the message with the highest MODSEQ, maybe it was deleted already.
highest_modseq = remote_highest_modseq;
}
set_modseq(context, folder, highest_modseq)
.await
.with_context(|| format!("failed to set MODSEQ for folder {}", folder))?;
for updated_chat_id in updated_chat_ids {
context.emit_event(EventType::MsgsNoticed(updated_chat_id));
}
@@ -1589,6 +1584,23 @@ impl Imap {
Ok(())
}
/// Update HIGHESTMODSEQ on selected mailbox.
///
/// Should be called when MODSEQ is seen on the response, such as IDLE response.
pub(crate) fn update_modseq(&mut self, modseq: u64) {
self.config.selected_mailbox =
self.config
.selected_mailbox
.as_ref()
.map(|mailbox| Mailbox {
highest_modseq: Some(std::cmp::max(
mailbox.highest_modseq.unwrap_or_default(),
modseq,
)),
..mailbox.clone()
});
}
/// Return whether the server sent an unsolicited EXISTS response.
/// Drains all responses from `session.unsolicited_responses` in the process.
/// If this returns `true`, this means that new emails arrived and you should
@@ -1980,6 +1992,70 @@ fn get_fallback_folder(delimiter: &str) -> String {
format!("INBOX{}DeltaChat", delimiter)
}
/// Marks messages in `msgs` table as seen, searching for them by UID.
///
/// Returns updated chat ID if any message was marked as seen.
async fn mark_seen_by_uid(
context: &Context,
folder: &str,
uid_validity: u32,
uid: u32,
) -> Result<Option<ChatId>> {
if let Some((msg_id, chat_id)) = context
.sql
.query_row_optional(
"SELECT id, chat_id FROM msgs
WHERE rfc724_mid IN (
SELECT rfc724_mid FROM imap
WHERE folder=?1
AND uidvalidity=?2
AND uid=?3
LIMIT 1
)",
paramsv![&folder, uid_validity, uid],
|row| {
let msg_id: MsgId = row.get(0)?;
let chat_id: ChatId = row.get(1)?;
Ok((msg_id, chat_id))
},
)
.await
.with_context(|| {
format!(
"failed to get msg and chat ID for IMAP message {}/{}",
folder, uid
)
})?
{
let updated = context
.sql
.execute(
"UPDATE msgs SET state=?1
WHERE (state=?2 OR state=?3)
AND id=?4",
paramsv![
MessageState::InSeen,
MessageState::InFresh,
MessageState::InNoticed,
msg_id
],
)
.await
.with_context(|| format!("failed to update msg {} state", msg_id))?
> 0;
if updated {
Ok(Some(chat_id))
} else {
// Message state has not chnaged.
Ok(None)
}
} else {
// There is no message is `msgs` table matchng the given UID.
Ok(None)
}
}
/// uid_next is the next unique identifier value from the last time we fetched a folder
/// See <https://tools.ietf.org/html/rfc3501#section-2.3.1.1>
/// This function is used to update our uid_next after fetching messages.

View File

@@ -3,6 +3,7 @@ use super::Imap;
use anyhow::{bail, Context as _, Result};
use async_imap::extensions::idle::IdleResponse;
use async_std::prelude::*;
use imap_proto::types::{AttributeValue, Response};
use std::time::{Duration, SystemTime};
use crate::{context::Context, scheduler::InterruptInfo};
@@ -71,6 +72,13 @@ impl Imap {
match fut.await {
Ok(Event::IdleResponse(IdleResponse::NewData(x))) => {
info!(context, "Idle has NewData {:?}", x);
if let Response::Fetch(_message, attrs) = x.parsed() {
for attr in attrs {
if let AttributeValue::ModSeq(modseq) = attr {
self.update_modseq(*modseq);
}
}
}
}
Ok(Event::IdleResponse(IdleResponse::Timeout)) => {
info!(context, "Idle-wait timeout or interruption");

View File

@@ -180,7 +180,7 @@ impl rusqlite::types::ToSql for MsgId {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
if self.0 <= DC_MSG_ID_LAST_SPECIAL {
return Err(rusqlite::Error::ToSqlConversionFailure(
format_err!("Invalid MsgId").into(),
format_err!("Invalid MsgId {}", self.0).into(),
));
}
let val = rusqlite::types::Value::Integer(self.0 as i64);

View File

@@ -403,16 +403,18 @@ impl MimeMessage {
#[allow(clippy::indexing_slicing)]
fn squash_attachment_parts(&mut self) {
if let [textpart, filepart] = &self.parts[..] {
let need_drop = {
textpart.typ == Viewtype::Text
&& (filepart.typ == Viewtype::Image
|| filepart.typ == Viewtype::Gif
|| filepart.typ == Viewtype::Sticker
|| filepart.typ == Viewtype::Audio
|| filepart.typ == Viewtype::Voice
|| filepart.typ == Viewtype::Video
|| filepart.typ == Viewtype::File)
};
let need_drop = textpart.typ == Viewtype::Text
&& match filepart.typ {
Viewtype::Image
| Viewtype::Gif
| Viewtype::Sticker
| Viewtype::Audio
| Viewtype::Voice
| Viewtype::Video
| Viewtype::File
| Viewtype::Webxdc => true,
Viewtype::Unknown | Viewtype::Text | Viewtype::VideochatInvitation => false,
};
if need_drop {
let mut filepart = self.parts.swap_remove(1);

View File

@@ -1,4 +1,4 @@
use anyhow::{bail, Result};
use anyhow::{bail, Context as _, Result};
use async_std::prelude::*;
use async_std::{
channel::{self, Receiver, Sender},
@@ -8,6 +8,7 @@ use async_std::{
use crate::config::Config;
use crate::context::Context;
use crate::dc_tools::maybe_add_time_based_warnings;
use crate::ephemeral::delete_expired_imap_messages;
use crate::imap::Imap;
use crate::job::{self, Thread};
use crate::smtp::{send_smtp_messages, Smtp};
@@ -160,6 +161,14 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
return connection.fake_idle(ctx, Some(watch_folder)).await;
}
// Mark expired messages for deletion.
if let Err(err) = delete_expired_imap_messages(ctx)
.await
.context("delete_expired_imap_messages failed")
{
warn!(ctx, "{:#}", err);
}
// Scan other folders before fetching from watched folder. This may result in the
// messages being moved into the watched folder, for example from the Spam folder to
// the Inbox folder.

View File

@@ -369,6 +369,19 @@ pub(crate) async fn send_msg_to_smtp(
)
.collect::<Vec<_>>();
// If there is a msg-id and it does not exist in the db, cancel sending. this happens if
// dc_delete_msgs() was called before the generated mime was sent out.
if !message::exists(context, msg_id)
.await
.with_context(|| format!("failed to check message {} existence", msg_id))?
{
info!(
context,
"Sending of message {} was cancelled by the user.", msg_id
);
return Ok(());
}
let status = match smtp_send(
context,
&recipients_list,

View File

@@ -97,40 +97,6 @@ impl Sql {
*self.is_encrypted.read().await
}
/// Changes the database passpharse.
///
/// The database must be open and encrypted already.
pub(crate) async fn change_passphrase(
&self,
context: &Context,
passphrase: String,
) -> Result<()> {
// Take the whole pool so nobody opens another connection in parallel.
let pool = self
.pool
.write()
.await
.take()
.context("the database must be open before rekeying")?;
// Get one connection and rekey the database.
// All other connections will stop working after that.
let connection = pool
.get()
.context("failed to get connection from the pool")?;
connection
.pragma_update(None, "rekey", &passphrase)
.context("failed to set PRAGMA rekey")?;
drop(pool);
// Reopen the database with new passphrase.
self.open(context, passphrase)
.await
.context("failed to reopen the database after rekeying")?;
Ok(())
}
/// Closes all underlying Sqlite connections.
async fn close(&self) {
let _ = self.pool.write().await.take();

View File

@@ -1535,4 +1535,43 @@ sth_for_the = "future""#
Ok(())
}
#[async_std::test]
async fn test_webxdc_and_text() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
// Alice sends instance and adds some text
let alice_chat = alice.create_chat(&bob).await;
let mut alice_instance = create_webxdc_instance(
&alice,
"minimal.xdc",
include_bytes!("../test-data/webxdc/minimal.xdc"),
)
.await?;
alice_instance.set_text(Some("user added text".to_string()));
send_msg(&alice, alice_chat.id, &mut alice_instance).await?;
let alice_instance = alice.get_last_msg().await;
assert_eq!(
alice_instance.get_text(),
Some("user added text".to_string())
);
// Bob receives that instance
let sent1 = alice.pop_sent_msg().await;
bob.recv_msg(&sent1).await;
let bob_instance = bob.get_last_msg().await;
assert_eq!(bob_instance.get_text(), Some("user added text".to_string()));
// Alice's second device receives the instance as well
let alice2 = TestContext::new_alice().await;
alice2.recv_msg(&sent1).await;
let alice2_instance = alice2.get_last_msg().await;
assert_eq!(
alice2_instance.get_text(),
Some("user added text".to_string())
);
Ok(())
}
}