mirror of
https://github.com/chatmail/core.git
synced 2026-06-29 02:56:36 +03:00
Compare commits
45 Commits
v1.119.1
...
link2xt/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22aa1b7b12 | ||
|
|
087f6edd0c | ||
|
|
d6b7ee04a0 | ||
|
|
d5c5ff8b3f | ||
|
|
dc4396a699 | ||
|
|
a74b00c3f9 | ||
|
|
2fdb9f8b7e | ||
|
|
80fac3f1b8 | ||
|
|
17a6c88cc7 | ||
|
|
1ba69dbb9b | ||
|
|
ab1c7ebbe2 | ||
|
|
ee715da078 | ||
|
|
27e177dc05 | ||
|
|
7aac4bfc83 | ||
|
|
7b24f9b7a4 | ||
|
|
b36b902eeb | ||
|
|
30024abb6c | ||
|
|
1d9702e9e7 | ||
|
|
ee2eae63d6 | ||
|
|
cd477936b5 | ||
|
|
dbe9d7e34e | ||
|
|
49f143e0d5 | ||
|
|
9d7bdf369d | ||
|
|
a270db1d87 | ||
|
|
7c7cd9cc80 | ||
|
|
47d465e6e4 | ||
|
|
03d3e0578f | ||
|
|
440a442f30 | ||
|
|
1da52d7d1d | ||
|
|
4d74f625d3 | ||
|
|
0a94fbc735 | ||
|
|
9ef34890fa | ||
|
|
3e07f2c173 | ||
|
|
ee28298d7f | ||
|
|
62aed13880 | ||
|
|
bffe934acc | ||
|
|
87ffcaf03e | ||
|
|
2635146328 | ||
|
|
d727d85f6d | ||
|
|
81a7af10c7 | ||
|
|
4a6e94f8ab | ||
|
|
146fe50e20 | ||
|
|
9bf2850fb1 | ||
|
|
ba2c36548e | ||
|
|
d07c743cdc |
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -15,6 +15,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- stable
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
@@ -24,7 +25,7 @@ jobs:
|
||||
name: Lint Rust
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: 1.70.0
|
||||
RUSTUP_TOOLCHAIN: 1.72.0
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install rustfmt and clippy
|
||||
|
||||
4
.github/workflows/node-package.yml
vendored
4
.github/workflows/node-package.yml
vendored
@@ -67,7 +67,9 @@ jobs:
|
||||
|
||||
# Build Linux prebuilds inside a container with old glibc for backwards compatibility.
|
||||
# Debian 10 contained glibc 2.28 at the time of the writing (2023-06-04): https://packages.debian.org/buster/libc6
|
||||
container: debian:10
|
||||
# Ubuntu 18.04 is at the End of Standard Support since June 2023, but it contains glibc 2.27,
|
||||
# so we are using it to support Ubuntu 18.04 setups that are still not upgraded.
|
||||
container: ubuntu:18.04
|
||||
steps:
|
||||
# Working directory is owned by 1001:1001 by default.
|
||||
# Change it to our user.
|
||||
|
||||
34
CHANGELOG.md
34
CHANGELOG.md
@@ -1,5 +1,38 @@
|
||||
# Changelog
|
||||
|
||||
## [1.120.0] - 2023-08-28
|
||||
|
||||
### API-Changes
|
||||
|
||||
- jsonrpc: Add `resend_messages`.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Update async-imap to 0.9.1 to fix memory leak.
|
||||
- Delete messages from SMTP queue only on user demand ([#4579](https://github.com/deltachat/deltachat-core-rust/pull/4579)).
|
||||
- Do not send images without transparency as stickers ([#4611](https://github.com/deltachat/deltachat-core-rust/pull/4611)).
|
||||
- `prepare_msg_blob()`: do not use the image if it has Exif metadata but the image cannot be recoded.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Hide accounts.rs constants from public API.
|
||||
- Hide pgp module from public API.
|
||||
|
||||
### Build system
|
||||
|
||||
- Update to Zig 0.11.0.
|
||||
- Update to Rust 1.72.0.
|
||||
|
||||
### CI
|
||||
|
||||
- Run on push to stable branch.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- python: Fix lint errors.
|
||||
- python: Fix `ruff` 0.0.286 warnings.
|
||||
- Fix beta clippy warnings.
|
||||
|
||||
## [1.119.1] - 2023-08-06
|
||||
|
||||
Bugfix release attempting to fix the [iOS build error](https://github.com/deltachat/deltachat-core-rust/issues/4610).
|
||||
@@ -2730,3 +2763,4 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.118.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.117.0...v1.118.0
|
||||
[1.119.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.118.0...v1.119.0
|
||||
[1.119.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.0...v1.119.1
|
||||
[1.120.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.1...v1.120.0
|
||||
|
||||
42
Cargo.lock
generated
42
Cargo.lock
generated
@@ -221,13 +221,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-imap"
|
||||
version = "0.9.0"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da93622739d458dd9a6abc1abf0e38e81965a5824a3b37f9500437c82a8bb572"
|
||||
checksum = "b538b767cbf9c162a6c5795d4b932bd2c20ba10b5a91a94d2b2b6886c1dce6a8"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"base64 0.21.2",
|
||||
"byte-pool",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"futures",
|
||||
"imap-proto",
|
||||
@@ -544,16 +544,6 @@ version = "3.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
|
||||
|
||||
[[package]]
|
||||
name = "byte-pool"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2f1b21189f50b5625efa6227cf45e9d4cfdc2e73582df2b879e9689e78a7158"
|
||||
dependencies = [
|
||||
"crossbeam-queue",
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.13.1"
|
||||
@@ -924,16 +914,6 @@ dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.16"
|
||||
@@ -1123,7 +1103,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.119.1"
|
||||
version = "1.120.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -1199,7 +1179,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.119.1"
|
||||
version = "1.120.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel",
|
||||
@@ -1223,7 +1203,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "1.119.1"
|
||||
version = "1.120.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -1238,7 +1218,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.119.1"
|
||||
version = "1.120.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1263,7 +1243,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.119.1"
|
||||
version = "1.120.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -4596,12 +4576,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.119.1"
|
||||
version = "1.120.0"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.65"
|
||||
@@ -36,7 +36,7 @@ ratelimit = { path = "./deltachat-ratelimit" }
|
||||
|
||||
anyhow = "1"
|
||||
async-channel = "1.8.0"
|
||||
async-imap = { version = "0.9.0", default-features = false, features = ["runtime-tokio"] }
|
||||
async-imap = { version = "0.9.1", default-features = false, features = ["runtime-tokio"] }
|
||||
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
|
||||
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.119.1"
|
||||
version = "1.120.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -301,6 +301,19 @@ dc_context_t* dc_context_new_closed (const char* dbfile);
|
||||
int dc_context_open (dc_context_t *context, const char* passphrase);
|
||||
|
||||
|
||||
/**
|
||||
* Changes the passphrase on the open database.
|
||||
* Existing database must already be encrypted and the passphrase cannot be NULL or empty.
|
||||
* It is impossible to encrypt unencrypted database with this method and vice versa.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param passphrase The new passphrase.
|
||||
* @return 1 on success, 0 on error.
|
||||
*/
|
||||
int dc_context_change_passphrase (dc_context_t* context, const char* passphrase);
|
||||
|
||||
|
||||
/**
|
||||
* Returns 1 if database is open.
|
||||
*
|
||||
@@ -1321,6 +1334,20 @@ int dc_get_msg_cnt (dc_context_t* context, uint32_t ch
|
||||
int dc_get_fresh_msg_cnt (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* Returns a list of similar chats.
|
||||
*
|
||||
* @warning This is an experimental API which may change or be removed in the future.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param chat_id The ID of the chat for which to find similar chats.
|
||||
* @return The list of similar chats.
|
||||
* On errors, NULL is returned.
|
||||
* Must be freed using dc_chatlist_unref() when no longer used.
|
||||
*/
|
||||
dc_chatlist_t* dc_get_similar_chatlist (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* Estimate the number of messages that will be deleted
|
||||
|
||||
@@ -167,6 +167,24 @@ pub unsafe extern "C" fn dc_context_open(
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[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))
|
||||
.context("dc_context_change_passphrase() failed")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_context_is_open(context: *mut dc_context_t) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
@@ -1242,6 +1260,30 @@ pub unsafe extern "C" fn dc_get_fresh_msg_cnt(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_similar_chatlist(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
) -> *mut dc_chatlist_t {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_similar_chatlist()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
let chat_id = ChatId::new(chat_id);
|
||||
match block_on(chat_id.get_similar_chatlist(ctx))
|
||||
.context("failed to get similar chatlist")
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(list) => {
|
||||
let ffi_list = ChatlistWrapper { context, list };
|
||||
Box::into_raw(Box::new(ffi_list))
|
||||
}
|
||||
Err(_) => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_estimate_deletion_cnt(
|
||||
context: *mut dc_context_t,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.119.1"
|
||||
version = "1.120.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
|
||||
@@ -39,6 +39,7 @@ pub mod types;
|
||||
use num_traits::FromPrimitive;
|
||||
use types::account::Account;
|
||||
use types::chat::FullChat;
|
||||
use types::chat_list::ChatListEntry;
|
||||
use types::contact::ContactObject;
|
||||
use types::events::Event;
|
||||
use types::http::HttpResponse;
|
||||
@@ -566,6 +567,25 @@ impl CommandApi {
|
||||
Ok(l)
|
||||
}
|
||||
|
||||
/// Returns chats similar to the given one.
|
||||
async fn get_similar_chatlist_entries(
|
||||
&self,
|
||||
account_id: u32,
|
||||
chat_id: u32,
|
||||
) -> Result<Vec<ChatListEntry>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let chat_id = ChatId::new(chat_id);
|
||||
let list = chat_id.get_similar_chatlist(&ctx).await?;
|
||||
let mut l: Vec<ChatListEntry> = Vec::with_capacity(list.len());
|
||||
for i in 0..list.len() {
|
||||
l.push(ChatListEntry(
|
||||
list.get_chat_id(i)?.to_u32(),
|
||||
list.get_msg_id(i)?.unwrap_or_default().to_u32(),
|
||||
));
|
||||
}
|
||||
Ok(l)
|
||||
}
|
||||
|
||||
async fn get_chatlist_items_by_entries(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -1709,6 +1729,20 @@ impl CommandApi {
|
||||
forward_msgs(&ctx, &message_ids, ChatId::new(chat_id)).await
|
||||
}
|
||||
|
||||
/// Resend messages and make information available for newly added chat members.
|
||||
/// Resending sends out the original message, however, recipients and webxdc-status may differ.
|
||||
/// Clients that already have the original message can still ignore the resent message as
|
||||
/// they have tracked the state by dedicated updates.
|
||||
///
|
||||
/// Some messages cannot be resent, eg. info-messages, drafts, already pending messages or messages that are not sent by SELF.
|
||||
///
|
||||
/// message_ids all message IDs that should be resend. All messages must belong to the same chat.
|
||||
async fn resend_messages(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let message_ids: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
|
||||
chat::resend_msgs(&ctx, &message_ids).await
|
||||
}
|
||||
|
||||
async fn send_sticker(
|
||||
&self,
|
||||
account_id: u32,
|
||||
|
||||
@@ -55,5 +55,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.119.1"
|
||||
"version": "1.120.0"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.119.1"
|
||||
version = "1.120.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
|
||||
|
||||
@@ -805,15 +805,30 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
"chatinfo" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
let sel_chat_id = sel_chat.as_ref().unwrap().get_id();
|
||||
|
||||
let contacts =
|
||||
chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await?;
|
||||
let contacts = chat::get_chat_contacts(&context, sel_chat_id).await?;
|
||||
println!("Memberlist:");
|
||||
|
||||
log_contactlist(&context, &contacts).await?;
|
||||
println!("{} contacts", contacts.len());
|
||||
|
||||
let similar_chats = sel_chat_id.get_similar_chat_ids(&context).await?;
|
||||
if !similar_chats.is_empty() {
|
||||
println!("Similar chats: ");
|
||||
for (similar_chat_id, metric) in similar_chats {
|
||||
let similar_chat = Chat::load_from_db(&context, similar_chat_id).await?;
|
||||
println!(
|
||||
"{} (#{}) {:.1}",
|
||||
similar_chat.name,
|
||||
similar_chat_id,
|
||||
100.0 * metric
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
"{} contacts\nLocation streaming: {}",
|
||||
contacts.len(),
|
||||
"Location streaming: {}",
|
||||
location::is_sending_locations_to_chat(
|
||||
&context,
|
||||
Some(sel_chat.as_ref().unwrap().get_id())
|
||||
|
||||
@@ -6,8 +6,7 @@ build-backend = "setuptools.build_meta"
|
||||
name = "deltachat-rpc-client"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
dependencies = [
|
||||
"aiohttp",
|
||||
"aiodns"
|
||||
"aiohttp"
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
@@ -89,16 +89,14 @@ class Rpc:
|
||||
return await queue.get()
|
||||
|
||||
def __getattr__(self, attr: str):
|
||||
async def method(*args, **kwargs) -> Any:
|
||||
async def method(*args) -> Any:
|
||||
self.id += 1
|
||||
request_id = self.id
|
||||
|
||||
assert not (args and kwargs), "Mixing positional and keyword arguments"
|
||||
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": attr,
|
||||
"params": kwargs or args,
|
||||
"params": args,
|
||||
"id": self.id,
|
||||
}
|
||||
data = (json.dumps(request) + "\n").encode()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.119.1"
|
||||
version = "1.120.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
unmaintained = "allow"
|
||||
ignore = [
|
||||
"RUSTSEC-2020-0071",
|
||||
"RUSTSEC-2022-0093",
|
||||
|
||||
# Exponential CPU time usage for TLS certificate processing in webpki.
|
||||
# It is only used for backup transfer, so does not affect IMAP and SMTP connections.
|
||||
# Waiting for `iroh` to update dependencies.
|
||||
"RUSTSEC-2023-0052",
|
||||
]
|
||||
|
||||
[bans]
|
||||
|
||||
@@ -60,5 +60,5 @@
|
||||
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.119.1"
|
||||
"version": "1.120.0"
|
||||
}
|
||||
|
||||
@@ -427,7 +427,7 @@ class Account:
|
||||
|
||||
assert dc_chatlist != ffi.NULL
|
||||
chatlist = []
|
||||
for i in range(0, lib.dc_chatlist_get_cnt(dc_chatlist)):
|
||||
for i in range(lib.dc_chatlist_get_cnt(dc_chatlist)):
|
||||
chat_id = lib.dc_chatlist_get_chat_id(dc_chatlist, i)
|
||||
chatlist.append(Chat(self, chat_id))
|
||||
return chatlist
|
||||
|
||||
@@ -15,7 +15,7 @@ def as_dc_charpointer(obj):
|
||||
|
||||
|
||||
def iter_array(dc_array_t, constructor: Callable[[int], T]) -> Generator[T, None, None]:
|
||||
for i in range(0, lib.dc_array_get_cnt(dc_array_t)):
|
||||
for i in range(lib.dc_array_get_cnt(dc_array_t)):
|
||||
yield constructor(lib.dc_array_get_id(dc_array_t, i))
|
||||
|
||||
|
||||
|
||||
@@ -510,8 +510,7 @@ def get_viewtype_code_from_name(view_type_name):
|
||||
if code is not None:
|
||||
return code
|
||||
raise ValueError(
|
||||
"message typecode not found for {!r}, "
|
||||
"available {!r}".format(view_type_name, list(_view_type_mapping.keys())),
|
||||
f"message typecode not found for {view_type_name!r}, available {list(_view_type_mapping.keys())!r}",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -15,6 +15,6 @@ class TestEmpty:
|
||||
def test_prepare_setup_measurings(self, acfactory):
|
||||
acfactory.get_online_accounts(BENCH_NUM)
|
||||
|
||||
@pytest.mark.parametrize("num", range(0, BENCH_NUM + 1))
|
||||
@pytest.mark.parametrize("num", range(BENCH_NUM + 1))
|
||||
def test_setup_online_accounts(self, acfactory, num):
|
||||
acfactory.get_online_accounts(num)
|
||||
|
||||
@@ -1 +1 @@
|
||||
2023-08-06
|
||||
2023-08-28
|
||||
@@ -7,7 +7,7 @@ set -euo pipefail
|
||||
#
|
||||
# Avoid using rustup here as it depends on reading /proc/self/exe and
|
||||
# has problems running under QEMU.
|
||||
RUST_VERSION=1.68.0
|
||||
RUST_VERSION=1.72.0
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
@@ -44,7 +44,7 @@ def file2url(f):
|
||||
|
||||
def process_opt(data):
|
||||
if not "opt" in data:
|
||||
return "Default::default()"
|
||||
return "ProviderOptions::new()"
|
||||
opt = "ProviderOptions {\n"
|
||||
opt_data = data.get("opt", "")
|
||||
for key in opt_data:
|
||||
@@ -54,7 +54,7 @@ def process_opt(data):
|
||||
if value in {"True", "False"}:
|
||||
value = value.lower()
|
||||
opt += " " + key + ": " + value + ",\n"
|
||||
opt += " ..Default::default()\n"
|
||||
opt += " ..ProviderOptions::new()\n"
|
||||
opt += " }"
|
||||
return opt
|
||||
|
||||
@@ -62,7 +62,7 @@ def process_opt(data):
|
||||
def process_config_defaults(data):
|
||||
if not "config_defaults" in data:
|
||||
return "None"
|
||||
defaults = "Some(vec![\n"
|
||||
defaults = "Some(&[\n"
|
||||
config_defaults = data.get("config_defaults", "")
|
||||
for key in config_defaults:
|
||||
value = str(config_defaults[key])
|
||||
@@ -96,11 +96,11 @@ def process_data(data, file):
|
||||
raise TypeError("domain used twice: " + domain)
|
||||
domains_set.add(domain)
|
||||
|
||||
domains += ' ("' + domain + '", &*' + file2varname(file) + "),\n"
|
||||
domains += ' ("' + domain + '", &' + file2varname(file) + "),\n"
|
||||
comment += domain + ", "
|
||||
|
||||
ids = ""
|
||||
ids += ' ("' + file2id(file) + '", &*' + file2varname(file) + "),\n"
|
||||
ids += ' ("' + file2id(file) + '", &' + file2varname(file) + "),\n"
|
||||
|
||||
server = ""
|
||||
has_imap = False
|
||||
@@ -155,18 +155,18 @@ def process_data(data, file):
|
||||
provider += (
|
||||
"static "
|
||||
+ file2varname(file)
|
||||
+ ": Lazy<Provider> = Lazy::new(|| Provider {\n"
|
||||
+ ": Provider = Provider {\n"
|
||||
)
|
||||
provider += ' id: "' + file2id(file) + '",\n'
|
||||
provider += " status: Status::" + status.capitalize() + ",\n"
|
||||
provider += ' before_login_hint: "' + before_login_hint + '",\n'
|
||||
provider += ' after_login_hint: "' + after_login_hint + '",\n'
|
||||
provider += ' overview_page: "' + file2url(file) + '",\n'
|
||||
provider += " server: vec![\n" + server + " ],\n"
|
||||
provider += " server: &[\n" + server + " ],\n"
|
||||
provider += " opt: " + opt + ",\n"
|
||||
provider += " config_defaults: " + config_defaults + ",\n"
|
||||
provider += " oauth2_authorizer: " + oauth2 + ",\n"
|
||||
provider += "});\n\n"
|
||||
provider += "};\n\n"
|
||||
else:
|
||||
raise TypeError("SMTP and IMAP must be specified together or left out both")
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ set -e
|
||||
unset RUSTFLAGS
|
||||
|
||||
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
|
||||
export RUSTUP_TOOLCHAIN=1.70.0
|
||||
export RUSTUP_TOOLCHAIN=1.72.0
|
||||
|
||||
ZIG_VERSION=0.11.0-dev.2213+515e1c93e
|
||||
ZIG_VERSION=0.11.0
|
||||
|
||||
# Download Zig
|
||||
rm -fr "$ZIG_VERSION" "zig-linux-x86_64-$ZIG_VERSION.tar.xz"
|
||||
|
||||
@@ -8,9 +8,9 @@ set -e
|
||||
unset RUSTFLAGS
|
||||
|
||||
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
|
||||
export RUSTUP_TOOLCHAIN=1.70.0
|
||||
export RUSTUP_TOOLCHAIN=1.72.0
|
||||
|
||||
ZIG_VERSION=0.11.0-dev.2213+515e1c93e
|
||||
ZIG_VERSION=0.11.0
|
||||
|
||||
# Download Zig
|
||||
rm -fr "$ZIG_VERSION" "zig-linux-x86_64-$ZIG_VERSION.tar.xz"
|
||||
|
||||
@@ -296,10 +296,10 @@ impl Accounts {
|
||||
}
|
||||
|
||||
/// Configuration file name.
|
||||
pub const CONFIG_NAME: &str = "accounts.toml";
|
||||
const CONFIG_NAME: &str = "accounts.toml";
|
||||
|
||||
/// Database file name.
|
||||
pub const DB_NAME: &str = "dc.db";
|
||||
const DB_NAME: &str = "dc.db";
|
||||
|
||||
/// Account manager configuration file.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
|
||||
118
src/blob.rs
118
src/blob.rs
@@ -9,7 +9,7 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{format_err, Context as _, Result};
|
||||
use futures::StreamExt;
|
||||
use image::{DynamicImage, ImageFormat, ImageOutputFormat};
|
||||
use image::{DynamicImage, GenericImageView, ImageFormat, ImageOutputFormat};
|
||||
use num_traits::FromPrimitive;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::{fs, io};
|
||||
@@ -323,18 +323,35 @@ impl<'a> BlobObject<'a> {
|
||||
MediaQuality::Worse => constants::WORSE_AVATAR_SIZE,
|
||||
};
|
||||
|
||||
let maybe_sticker = &mut false;
|
||||
let strict_limits = true;
|
||||
// max_bytes is 20_000 bytes: Outlook servers don't allow headers larger than 32k.
|
||||
// 32 / 4 * 3 = 24k if you account for base64 encoding. To be safe, we reduced this to 20k.
|
||||
if let Some(new_name) =
|
||||
self.recode_to_size(context, blob_abs, img_wh, 20_000, strict_limits)?
|
||||
{
|
||||
if let Some(new_name) = self.recode_to_size(
|
||||
context,
|
||||
blob_abs,
|
||||
maybe_sticker,
|
||||
img_wh,
|
||||
20_000,
|
||||
strict_limits,
|
||||
)? {
|
||||
self.name = new_name;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn recode_to_image_size(&mut self, context: &Context) -> Result<()> {
|
||||
/// Recodes an image pointed by a [BlobObject] so that it fits into limits on the image width,
|
||||
/// height and file size specified by the config.
|
||||
///
|
||||
/// On some platforms images are passed to the core as [`crate::message::Viewtype::Sticker`] in
|
||||
/// which case `maybe_sticker` flag should be set. We recheck if an image is a true sticker
|
||||
/// assuming that it must have at least one fully transparent corner, otherwise this flag is
|
||||
/// reset.
|
||||
pub async fn recode_to_image_size(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
maybe_sticker: &mut bool,
|
||||
) -> Result<()> {
|
||||
let blob_abs = self.to_abs_path();
|
||||
let (img_wh, max_bytes) =
|
||||
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
|
||||
@@ -347,9 +364,14 @@ impl<'a> BlobObject<'a> {
|
||||
MediaQuality::Worse => (constants::WORSE_IMAGE_SIZE, constants::WORSE_IMAGE_BYTES),
|
||||
};
|
||||
let strict_limits = false;
|
||||
if let Some(new_name) =
|
||||
self.recode_to_size(context, blob_abs, img_wh, max_bytes, strict_limits)?
|
||||
{
|
||||
if let Some(new_name) = self.recode_to_size(
|
||||
context,
|
||||
blob_abs,
|
||||
maybe_sticker,
|
||||
img_wh,
|
||||
max_bytes,
|
||||
strict_limits,
|
||||
)? {
|
||||
self.name = new_name;
|
||||
}
|
||||
Ok(())
|
||||
@@ -358,20 +380,37 @@ impl<'a> BlobObject<'a> {
|
||||
/// If `!strict_limits`, then if `max_bytes` is exceeded, reduce the image to `img_wh` and just
|
||||
/// proceed with the result.
|
||||
fn recode_to_size(
|
||||
&self,
|
||||
&mut self,
|
||||
context: &Context,
|
||||
mut blob_abs: PathBuf,
|
||||
maybe_sticker: &mut bool,
|
||||
mut img_wh: u32,
|
||||
max_bytes: usize,
|
||||
strict_limits: bool,
|
||||
) -> Result<Option<String>> {
|
||||
tokio::task::block_in_place(move || {
|
||||
let mut img = image::open(&blob_abs).context("image decode failure")?;
|
||||
let mut no_exif = false;
|
||||
let no_exif_ref = &mut no_exif;
|
||||
let res = tokio::task::block_in_place(move || {
|
||||
let (nr_bytes, exif) = self.metadata()?;
|
||||
*no_exif_ref = exif.is_none();
|
||||
let mut img = image::open(&blob_abs).context("image decode failure")?;
|
||||
let orientation = exif.as_ref().map(|exif| exif_orientation(exif, context));
|
||||
let mut encoded = Vec::new();
|
||||
let mut changed_name = None;
|
||||
|
||||
if *maybe_sticker {
|
||||
let x_max = img.width().saturating_sub(1);
|
||||
let y_max = img.height().saturating_sub(1);
|
||||
*maybe_sticker = img.in_bounds(x_max, y_max)
|
||||
&& (img.get_pixel(0, 0).0[3] == 0
|
||||
|| img.get_pixel(x_max, 0).0[3] == 0
|
||||
|| img.get_pixel(0, y_max).0[3] == 0
|
||||
|| img.get_pixel(x_max, y_max).0[3] == 0);
|
||||
}
|
||||
if *maybe_sticker && exif.is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
img = match orientation {
|
||||
Some(90) => img.rotate90(),
|
||||
Some(180) => img.rotate180(),
|
||||
@@ -469,7 +508,21 @@ impl<'a> BlobObject<'a> {
|
||||
}
|
||||
|
||||
Ok(changed_name)
|
||||
})
|
||||
});
|
||||
match res {
|
||||
Ok(_) => res,
|
||||
Err(err) => {
|
||||
if !strict_limits && no_exif {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot recode image, using original data: {err:#}.",
|
||||
);
|
||||
Ok(None)
|
||||
} else {
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns image file size and Exif.
|
||||
@@ -860,10 +913,18 @@ mod tests {
|
||||
file.metadata().await.unwrap().len()
|
||||
}
|
||||
|
||||
let blob = BlobObject::new_from_path(&t, &avatar_blob).await.unwrap();
|
||||
let mut blob = BlobObject::new_from_path(&t, &avatar_blob).await.unwrap();
|
||||
let maybe_sticker = &mut false;
|
||||
let strict_limits = true;
|
||||
blob.recode_to_size(&t, blob.to_abs_path(), 1000, 3000, strict_limits)
|
||||
.unwrap();
|
||||
blob.recode_to_size(
|
||||
&t,
|
||||
blob.to_abs_path(),
|
||||
maybe_sticker,
|
||||
1000,
|
||||
3000,
|
||||
strict_limits,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(file_size(&avatar_blob).await <= 3000);
|
||||
assert!(file_size(&avatar_blob).await > 2000);
|
||||
tokio::task::block_in_place(move || {
|
||||
@@ -923,6 +984,7 @@ mod tests {
|
||||
async fn test_recode_image_1() {
|
||||
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("0"),
|
||||
bytes,
|
||||
"jpg",
|
||||
@@ -936,6 +998,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("1"),
|
||||
bytes,
|
||||
"jpg",
|
||||
@@ -955,6 +1018,7 @@ mod tests {
|
||||
// The "-rotated" files are rotated by 270 degrees using the Exif metadata
|
||||
let bytes = include_bytes!("../test-data/image/rectangle2000x1800-rotated.jpg");
|
||||
let img_rotated = send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("0"),
|
||||
bytes,
|
||||
"jpg",
|
||||
@@ -974,6 +1038,7 @@ mod tests {
|
||||
let bytes = buf.into_inner();
|
||||
|
||||
let img_rotated = send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("1"),
|
||||
&bytes,
|
||||
"jpg",
|
||||
@@ -994,6 +1059,7 @@ mod tests {
|
||||
let bytes = include_bytes!("../test-data/image/screenshot.png");
|
||||
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("0"),
|
||||
bytes,
|
||||
"png",
|
||||
@@ -1008,6 +1074,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("1"),
|
||||
bytes,
|
||||
"png",
|
||||
@@ -1020,12 +1087,29 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// This will be sent as Image, see [`BlobObject::maybe_sticker`] for explanation.
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::Sticker,
|
||||
Some("0"),
|
||||
bytes,
|
||||
"png",
|
||||
false, // no Exif
|
||||
1920,
|
||||
1080,
|
||||
0,
|
||||
1920,
|
||||
1080,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_image_huge_jpg() {
|
||||
let bytes = include_bytes!("../test-data/image/screenshot.jpg");
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("0"),
|
||||
bytes,
|
||||
"jpg",
|
||||
@@ -1059,6 +1143,7 @@ mod tests {
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn send_image_check_mediaquality(
|
||||
viewtype: Viewtype,
|
||||
media_quality_config: Option<&str>,
|
||||
bytes: &[u8],
|
||||
extension: &str,
|
||||
@@ -1090,7 +1175,7 @@ mod tests {
|
||||
assert!(exif.is_none());
|
||||
}
|
||||
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
let mut msg = Message::new(viewtype);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let sent = alice.send_msg(chat.id, &mut msg).await;
|
||||
@@ -1104,6 +1189,7 @@ mod tests {
|
||||
);
|
||||
|
||||
let bob_msg = bob.recv_msg(&sent).await;
|
||||
assert_eq!(bob_msg.get_viewtype(), Viewtype::Image);
|
||||
assert_eq!(bob_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(bob_msg.get_height() as u32, compressed_height);
|
||||
let file = bob_msg.get_file(&bob).unwrap();
|
||||
|
||||
441
src/chat.rs
441
src/chat.rs
@@ -13,6 +13,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::color::str_to_color;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{
|
||||
@@ -871,6 +872,133 @@ impl ChatId {
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// Returns timestamp of the latest message in the chat.
|
||||
pub(crate) async fn get_timestamp(self, context: &Context) -> Result<Option<i64>> {
|
||||
let timestamp = context
|
||||
.sql
|
||||
.query_get_value("SELECT MAX(timestamp) FROM msgs WHERE chat_id=?", (self,))
|
||||
.await?;
|
||||
Ok(timestamp)
|
||||
}
|
||||
|
||||
/// Returns a list of active similar chat IDs sorted by similarity metric.
|
||||
///
|
||||
/// Jaccard similarity coefficient is used to estimate similarity of chat member sets.
|
||||
///
|
||||
/// Chat is considered active if something was posted there within the last 42 days.
|
||||
pub async fn get_similar_chat_ids(self, context: &Context) -> Result<Vec<(ChatId, f64)>> {
|
||||
// Count number of common members in this and other chats.
|
||||
let intersection: Vec<(ChatId, f64)> = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT y.chat_id, SUM(x.contact_id = y.contact_id)
|
||||
FROM chats_contacts as x
|
||||
JOIN chats_contacts as y
|
||||
WHERE x.contact_id > 9
|
||||
AND y.contact_id > 9
|
||||
AND x.chat_id=?
|
||||
AND y.chat_id<>x.chat_id
|
||||
GROUP BY y.chat_id",
|
||||
(self,),
|
||||
|row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
let intersection: f64 = row.get(1)?;
|
||||
Ok((chat_id, intersection))
|
||||
},
|
||||
|rows| {
|
||||
rows.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("failed to calculate member set intersections")?;
|
||||
|
||||
let chat_size: HashMap<ChatId, f64> = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT chat_id, count(*) AS n
|
||||
FROM chats_contacts where contact_id > 9
|
||||
GROUP BY chat_id",
|
||||
(),
|
||||
|row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
let size: f64 = row.get(1)?;
|
||||
Ok((chat_id, size))
|
||||
},
|
||||
|rows| {
|
||||
rows.collect::<std::result::Result<HashMap<ChatId, f64>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("failed to count chat member sizes")?;
|
||||
|
||||
let our_chat_size = chat_size.get(&self).copied().unwrap_or_default();
|
||||
let mut chats_with_metrics = Vec::new();
|
||||
for (chat_id, intersection_size) in intersection {
|
||||
if intersection_size > 0.0 {
|
||||
let other_chat_size = chat_size.get(&chat_id).copied().unwrap_or_default();
|
||||
let union_size = our_chat_size + other_chat_size - intersection_size;
|
||||
let metric = intersection_size / union_size;
|
||||
chats_with_metrics.push((chat_id, metric))
|
||||
}
|
||||
}
|
||||
chats_with_metrics.sort_unstable_by(|(chat_id1, metric1), (chat_id2, metric2)| {
|
||||
metric2
|
||||
.partial_cmp(metric1)
|
||||
.unwrap_or(chat_id2.cmp(chat_id1))
|
||||
});
|
||||
|
||||
// Select up to five similar active chats.
|
||||
let mut res = Vec::new();
|
||||
let now = time();
|
||||
for (chat_id, metric) in chats_with_metrics {
|
||||
if let Some(chat_timestamp) = chat_id.get_timestamp(context).await? {
|
||||
if now > chat_timestamp + 42 * 24 * 3600 {
|
||||
// Chat was inactive for 42 days, skip.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if metric < 0.1 {
|
||||
// Chat is unrelated.
|
||||
break;
|
||||
}
|
||||
|
||||
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||
if chat.typ != Chattype::Group {
|
||||
continue;
|
||||
}
|
||||
|
||||
match chat.visibility {
|
||||
ChatVisibility::Normal | ChatVisibility::Pinned => {}
|
||||
ChatVisibility::Archived => continue,
|
||||
}
|
||||
|
||||
res.push((chat_id, metric));
|
||||
if res.len() >= 5 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Returns similar chats as a [`Chatlist`].
|
||||
///
|
||||
/// [`Chatlist`]: crate::chatlist::Chatlist
|
||||
pub async fn get_similar_chatlist(self, context: &Context) -> Result<Chatlist> {
|
||||
let chat_ids: Vec<ChatId> = self
|
||||
.get_similar_chat_ids(context)
|
||||
.await
|
||||
.context("failed to get similar chat IDs")?
|
||||
.into_iter()
|
||||
.map(|(chat_id, _metric)| chat_id)
|
||||
.collect();
|
||||
let chatlist = Chatlist::from_chat_ids(context, &chat_ids).await?;
|
||||
Ok(chatlist)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_param(self, context: &Context) -> Result<Params> {
|
||||
let res: Option<String> = context
|
||||
.sql
|
||||
@@ -1241,13 +1369,14 @@ impl Chat {
|
||||
pub(crate) async fn why_cant_send(&self, context: &Context) -> Result<Option<CantSendReason>> {
|
||||
use CantSendReason::*;
|
||||
|
||||
// NB: Don't forget to update Chatlist::try_load() when changing this function!
|
||||
let reason = if self.id.is_special() {
|
||||
Some(SpecialChat)
|
||||
} else if self.is_device_talk() {
|
||||
Some(DeviceChat)
|
||||
} else if self.is_contact_request() {
|
||||
Some(ContactRequest)
|
||||
} else if self.is_mailing_list() && self.param.get(Param::ListPost).is_none_or_empty() {
|
||||
} else if self.is_mailing_list() && self.get_mailinglist_addr().is_none_or_empty() {
|
||||
Some(ReadOnlyMailingList)
|
||||
} else if !self.is_self_in_chat(context).await? {
|
||||
Some(NotAMember)
|
||||
@@ -1310,11 +1439,11 @@ impl Chat {
|
||||
pub async fn get_profile_image(&self, context: &Context) -> Result<Option<PathBuf>> {
|
||||
if let Some(image_rel) = self.param.get(Param::ProfileImage) {
|
||||
if !image_rel.is_empty() {
|
||||
return Ok(Some(get_abs_path(context, image_rel)));
|
||||
return Ok(Some(get_abs_path(context, Path::new(&image_rel))));
|
||||
}
|
||||
} else if self.id.is_archived_link() {
|
||||
if let Ok(image_rel) = get_archive_icon(context).await {
|
||||
return Ok(Some(get_abs_path(context, image_rel)));
|
||||
return Ok(Some(get_abs_path(context, Path::new(&image_rel))));
|
||||
}
|
||||
} else if self.typ == Chattype::Single {
|
||||
let contacts = get_chat_contacts(context, self.id).await?;
|
||||
@@ -1325,7 +1454,7 @@ impl Chat {
|
||||
}
|
||||
} else if self.typ == Chattype::Broadcast {
|
||||
if let Ok(image_rel) = get_broadcast_icon(context).await {
|
||||
return Ok(Some(get_abs_path(context, image_rel)));
|
||||
return Ok(Some(get_abs_path(context, Path::new(&image_rel))));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
@@ -1584,6 +1713,11 @@ impl Chat {
|
||||
None
|
||||
};
|
||||
|
||||
msg.chat_id = self.id;
|
||||
msg.from_id = ContactId::SELF;
|
||||
msg.rfc724_mid = new_rfc724_mid;
|
||||
msg.timestamp_sort = timestamp;
|
||||
|
||||
// add message to the database
|
||||
if let Some(update_msg_id) = update_msg_id {
|
||||
context
|
||||
@@ -1597,11 +1731,11 @@ impl Chat {
|
||||
ephemeral_timestamp=?
|
||||
WHERE id=?;",
|
||||
params_slice![
|
||||
new_rfc724_mid,
|
||||
self.id,
|
||||
ContactId::SELF,
|
||||
msg.rfc724_mid,
|
||||
msg.chat_id,
|
||||
msg.from_id,
|
||||
to_id,
|
||||
timestamp,
|
||||
msg.timestamp_sort,
|
||||
msg.viewtype,
|
||||
msg.state,
|
||||
msg.text,
|
||||
@@ -1646,11 +1780,11 @@ impl Chat {
|
||||
ephemeral_timestamp)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?,?);",
|
||||
params_slice![
|
||||
new_rfc724_mid,
|
||||
self.id,
|
||||
ContactId::SELF,
|
||||
msg.rfc724_mid,
|
||||
msg.chat_id,
|
||||
msg.from_id,
|
||||
to_id,
|
||||
timestamp,
|
||||
msg.timestamp_sort,
|
||||
msg.viewtype,
|
||||
msg.state,
|
||||
msg.text,
|
||||
@@ -2033,12 +2167,12 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
.await?
|
||||
.with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
|
||||
|
||||
if msg.viewtype == Viewtype::Image {
|
||||
if let Err(err) = blob.recode_to_image_size(context).await {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot recode image, using original data: {err:#}."
|
||||
);
|
||||
let mut maybe_sticker = msg.viewtype == Viewtype::Sticker;
|
||||
if msg.viewtype == Viewtype::Image || maybe_sticker {
|
||||
blob.recode_to_image_size(context, &mut maybe_sticker)
|
||||
.await?;
|
||||
if !maybe_sticker {
|
||||
msg.viewtype = Viewtype::Image;
|
||||
}
|
||||
}
|
||||
msg.param.set(Param::File, blob.as_name());
|
||||
@@ -2085,6 +2219,8 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
msg.try_calc_and_set_dimensions(context).await?;
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Attaching \"{}\" for message type #{}.",
|
||||
@@ -2252,7 +2388,7 @@ async fn prepare_send_msg(
|
||||
);
|
||||
message::update_msg_state(context, msg.id, MessageState::OutPending).await?;
|
||||
}
|
||||
let row_id = create_send_msg_job(context, msg.id).await?;
|
||||
let row_id = create_send_msg_job(context, msg).await?;
|
||||
Ok(row_id)
|
||||
}
|
||||
|
||||
@@ -2262,13 +2398,10 @@ async fn prepare_send_msg(
|
||||
/// group with only self and no BCC-to-self configured.
|
||||
///
|
||||
/// The caller has to interrupt SMTP loop or otherwise process a new row.
|
||||
async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<i64>> {
|
||||
let mut msg = Message::load_from_db(context, msg_id).await?;
|
||||
msg.try_calc_and_set_dimensions(context)
|
||||
.await
|
||||
.context("failed to calculate media dimensions")?;
|
||||
|
||||
/* create message */
|
||||
pub(crate) async fn create_send_msg_job(
|
||||
context: &Context,
|
||||
msg: &mut Message,
|
||||
) -> Result<Option<i64>> {
|
||||
let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default();
|
||||
|
||||
let attach_selfavatar = match shall_attach_selfavatar(context, msg.chat_id).await {
|
||||
@@ -2279,7 +2412,7 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
|
||||
}
|
||||
};
|
||||
|
||||
let mimefactory = MimeFactory::from_msg(context, &msg, attach_selfavatar).await?;
|
||||
let mimefactory = MimeFactory::from_msg(context, msg, attach_selfavatar).await?;
|
||||
|
||||
let mut recipients = mimefactory.recipients();
|
||||
|
||||
@@ -2301,16 +2434,17 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
|
||||
// may happen eg. for groups with only SELF and bcc_self disabled
|
||||
info!(
|
||||
context,
|
||||
"Message {msg_id} has no recipient, skipping smtp-send."
|
||||
"Message {} has no recipient, skipping smtp-send.", msg.id
|
||||
);
|
||||
msg_id.set_delivered(context).await?;
|
||||
msg.id.set_delivered(context).await?;
|
||||
msg.state = MessageState::OutDelivered;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let rendered_msg = match mimefactory.render(context).await {
|
||||
Ok(res) => Ok(res),
|
||||
Err(err) => {
|
||||
message::set_msg_failed(context, msg_id, &err.to_string()).await;
|
||||
message::set_msg_failed(context, msg, &err.to_string()).await?;
|
||||
Err(err)
|
||||
}
|
||||
}?;
|
||||
@@ -2319,13 +2453,13 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
|
||||
/* unrecoverable */
|
||||
message::set_msg_failed(
|
||||
context,
|
||||
msg_id,
|
||||
msg,
|
||||
"End-to-end-encryption unavailable unexpectedly.",
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
bail!(
|
||||
"e2e encryption unavailable {} - {:?}",
|
||||
msg_id,
|
||||
msg.id,
|
||||
needs_encryption
|
||||
);
|
||||
}
|
||||
@@ -2381,7 +2515,7 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
|
||||
&rendered_msg.rfc724_mid,
|
||||
recipients,
|
||||
&rendered_msg.message,
|
||||
msg_id,
|
||||
msg.id,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
@@ -2574,14 +2708,7 @@ pub(crate) async fn marknoticed_chat_if_older_than(
|
||||
chat_id: ChatId,
|
||||
timestamp: i64,
|
||||
) -> Result<()> {
|
||||
if let Some(chat_timestamp) = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT MAX(timestamp) FROM msgs WHERE chat_id=?",
|
||||
(chat_id,),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
if let Some(chat_timestamp) = chat_id.get_timestamp(context).await? {
|
||||
if timestamp > chat_timestamp {
|
||||
marknoticed_chat(context, chat_id).await?;
|
||||
}
|
||||
@@ -3371,89 +3498,86 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
chat_id
|
||||
.unarchive_if_not_muted(context, MessageState::Undefined)
|
||||
.await?;
|
||||
if let Ok(mut chat) = Chat::load_from_db(context, chat_id).await {
|
||||
if let Some(reason) = chat.why_cant_send(context).await? {
|
||||
bail!("cannot send to {}: {}", chat_id, reason);
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
if let Some(reason) = chat.why_cant_send(context).await? {
|
||||
bail!("cannot send to {}: {}", chat_id, reason);
|
||||
}
|
||||
curr_timestamp = create_smeared_timestamps(context, msg_ids.len());
|
||||
let ids = context
|
||||
.sql
|
||||
.query_map(
|
||||
&format!(
|
||||
"SELECT id FROM msgs WHERE id IN({}) ORDER BY timestamp,id",
|
||||
sql::repeat_vars(msg_ids.len())
|
||||
),
|
||||
rusqlite::params_from_iter(msg_ids),
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
|ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await?;
|
||||
|
||||
for id in ids {
|
||||
let src_msg_id: MsgId = id;
|
||||
let mut msg = Message::load_from_db(context, src_msg_id).await?;
|
||||
if msg.state == MessageState::OutDraft {
|
||||
bail!("cannot forward drafts.");
|
||||
}
|
||||
curr_timestamp = create_smeared_timestamps(context, msg_ids.len());
|
||||
let ids = context
|
||||
.sql
|
||||
.query_map(
|
||||
&format!(
|
||||
"SELECT id FROM msgs WHERE id IN({}) ORDER BY timestamp,id",
|
||||
sql::repeat_vars(msg_ids.len())
|
||||
),
|
||||
rusqlite::params_from_iter(msg_ids),
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
|ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await?;
|
||||
|
||||
for id in ids {
|
||||
let src_msg_id: MsgId = id;
|
||||
let mut msg = Message::load_from_db(context, src_msg_id).await?;
|
||||
if msg.state == MessageState::OutDraft {
|
||||
bail!("cannot forward drafts.");
|
||||
}
|
||||
let original_param = msg.param.clone();
|
||||
|
||||
let original_param = msg.param.clone();
|
||||
// we tested a sort of broadcast
|
||||
// by not marking own forwarded messages as such,
|
||||
// however, this turned out to be to confusing and unclear.
|
||||
|
||||
// we tested a sort of broadcast
|
||||
// by not marking own forwarded messages as such,
|
||||
// however, this turned out to be to confusing and unclear.
|
||||
if msg.get_viewtype() != Viewtype::Sticker {
|
||||
msg.param
|
||||
.set_int(Param::Forwarded, src_msg_id.to_u32() as i32);
|
||||
}
|
||||
|
||||
if msg.get_viewtype() != Viewtype::Sticker {
|
||||
msg.param
|
||||
.set_int(Param::Forwarded, src_msg_id.to_u32() as i32);
|
||||
}
|
||||
msg.param.remove(Param::GuaranteeE2ee);
|
||||
msg.param.remove(Param::ForcePlaintext);
|
||||
msg.param.remove(Param::Cmd);
|
||||
msg.param.remove(Param::OverrideSenderDisplayname);
|
||||
msg.param.remove(Param::WebxdcSummary);
|
||||
msg.param.remove(Param::WebxdcSummaryTimestamp);
|
||||
msg.in_reply_to = None;
|
||||
|
||||
msg.param.remove(Param::GuaranteeE2ee);
|
||||
msg.param.remove(Param::ForcePlaintext);
|
||||
msg.param.remove(Param::Cmd);
|
||||
msg.param.remove(Param::OverrideSenderDisplayname);
|
||||
msg.param.remove(Param::WebxdcSummary);
|
||||
msg.param.remove(Param::WebxdcSummaryTimestamp);
|
||||
msg.in_reply_to = None;
|
||||
// do not leak data as group names; a default subject is generated by mimefactory
|
||||
msg.subject = "".to_string();
|
||||
|
||||
// do not leak data as group names; a default subject is generated by mimefactory
|
||||
msg.subject = "".to_string();
|
||||
let new_msg_id: MsgId;
|
||||
if msg.state == MessageState::OutPreparing {
|
||||
new_msg_id = chat
|
||||
.prepare_msg_raw(context, &mut msg, None, curr_timestamp)
|
||||
.await?;
|
||||
curr_timestamp += 1;
|
||||
msg.param = original_param;
|
||||
msg.id = src_msg_id;
|
||||
|
||||
let new_msg_id: MsgId;
|
||||
if msg.state == MessageState::OutPreparing {
|
||||
new_msg_id = chat
|
||||
.prepare_msg_raw(context, &mut msg, None, curr_timestamp)
|
||||
.await?;
|
||||
curr_timestamp += 1;
|
||||
let save_param = msg.param.clone();
|
||||
msg.param = original_param;
|
||||
msg.id = src_msg_id;
|
||||
|
||||
if let Some(old_fwd) = msg.param.get(Param::PrepForwards) {
|
||||
let new_fwd = format!("{} {}", old_fwd, new_msg_id.to_u32());
|
||||
msg.param.set(Param::PrepForwards, new_fwd);
|
||||
} else {
|
||||
msg.param
|
||||
.set(Param::PrepForwards, new_msg_id.to_u32().to_string());
|
||||
}
|
||||
|
||||
msg.update_param(context).await?;
|
||||
msg.param = save_param;
|
||||
if let Some(old_fwd) = msg.param.get(Param::PrepForwards) {
|
||||
let new_fwd = format!("{} {}", old_fwd, new_msg_id.to_u32());
|
||||
msg.param.set(Param::PrepForwards, new_fwd);
|
||||
} else {
|
||||
msg.state = MessageState::OutPending;
|
||||
new_msg_id = chat
|
||||
.prepare_msg_raw(context, &mut msg, None, curr_timestamp)
|
||||
.await?;
|
||||
curr_timestamp += 1;
|
||||
if create_send_msg_job(context, new_msg_id).await?.is_some() {
|
||||
context
|
||||
.scheduler
|
||||
.interrupt_smtp(InterruptInfo::new(false))
|
||||
.await;
|
||||
}
|
||||
msg.param
|
||||
.set(Param::PrepForwards, new_msg_id.to_u32().to_string());
|
||||
}
|
||||
|
||||
msg.update_param(context).await?;
|
||||
} else {
|
||||
msg.state = MessageState::OutPending;
|
||||
new_msg_id = chat
|
||||
.prepare_msg_raw(context, &mut msg, None, curr_timestamp)
|
||||
.await?;
|
||||
curr_timestamp += 1;
|
||||
if create_send_msg_job(context, &mut msg).await?.is_some() {
|
||||
context
|
||||
.scheduler
|
||||
.interrupt_smtp(InterruptInfo::new(false))
|
||||
.await;
|
||||
}
|
||||
created_chats.push(chat_id);
|
||||
created_msgs.push(new_msg_id);
|
||||
}
|
||||
created_chats.push(chat_id);
|
||||
created_msgs.push(new_msg_id);
|
||||
}
|
||||
for (chat_id, msg_id) in created_chats.iter().zip(created_msgs.iter()) {
|
||||
context.emit_msgs_changed(*chat_id, *msg_id);
|
||||
@@ -3485,29 +3609,31 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
msgs.push(msg)
|
||||
}
|
||||
|
||||
if let Some(chat_id) = chat_id {
|
||||
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||
for mut msg in msgs {
|
||||
if msg.get_showpadlock() && !chat.is_protected() {
|
||||
msg.param.remove(Param::GuaranteeE2ee);
|
||||
msg.update_param(context).await?;
|
||||
}
|
||||
match msg.get_state() {
|
||||
MessageState::OutFailed | MessageState::OutDelivered | MessageState::OutMdnRcvd => {
|
||||
message::update_msg_state(context, msg.id, MessageState::OutPending).await?
|
||||
}
|
||||
_ => bail!("unexpected message state"),
|
||||
}
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id: msg.chat_id,
|
||||
msg_id: msg.id,
|
||||
});
|
||||
if create_send_msg_job(context, msg.id).await?.is_some() {
|
||||
context
|
||||
.scheduler
|
||||
.interrupt_smtp(InterruptInfo::new(false))
|
||||
.await;
|
||||
let Some(chat_id) = chat_id else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||
for mut msg in msgs {
|
||||
if msg.get_showpadlock() && !chat.is_protected() {
|
||||
msg.param.remove(Param::GuaranteeE2ee);
|
||||
msg.update_param(context).await?;
|
||||
}
|
||||
match msg.get_state() {
|
||||
MessageState::OutFailed | MessageState::OutDelivered | MessageState::OutMdnRcvd => {
|
||||
message::update_msg_state(context, msg.id, MessageState::OutPending).await?
|
||||
}
|
||||
_ => bail!("unexpected message state"),
|
||||
}
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id: msg.chat_id,
|
||||
msg_id: msg.id,
|
||||
});
|
||||
if create_send_msg_job(context, &mut msg).await?.is_some() {
|
||||
context
|
||||
.scheduler
|
||||
.interrupt_smtp(InterruptInfo::new(false))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -3577,7 +3703,6 @@ pub async fn add_device_msg_with_importance(
|
||||
chat_id = ChatId::get_for_contact(context, ContactId::DEVICE).await?;
|
||||
|
||||
let rfc724_mid = create_outgoing_rfc724_mid(None, "@device");
|
||||
msg.try_calc_and_set_dimensions(context).await.ok();
|
||||
prepare_msg_blob(context, msg).await?;
|
||||
|
||||
let timestamp_sent = create_smeared_timestamp(context);
|
||||
@@ -5510,7 +5635,13 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_sticker(filename: &str, bytes: &[u8], w: i32, h: i32) -> Result<()> {
|
||||
async fn test_sticker(
|
||||
filename: &str,
|
||||
bytes: &[u8],
|
||||
res_viewtype: Viewtype,
|
||||
w: i32,
|
||||
h: i32,
|
||||
) -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
@@ -5524,12 +5655,19 @@ mod tests {
|
||||
|
||||
let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||
let mime = sent_msg.payload();
|
||||
assert_eq!(mime.match_indices("Chat-Content: sticker").count(), 1);
|
||||
if res_viewtype == Viewtype::Sticker {
|
||||
assert_eq!(mime.match_indices("Chat-Content: sticker").count(), 1);
|
||||
}
|
||||
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.chat_id, bob_chat.id);
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
|
||||
assert_eq!(msg.get_filename().unwrap(), filename);
|
||||
assert_eq!(msg.get_viewtype(), res_viewtype);
|
||||
let msg_filename = msg.get_filename().unwrap();
|
||||
match res_viewtype {
|
||||
Viewtype::Sticker => assert_eq!(msg_filename, filename),
|
||||
Viewtype::Image => assert!(msg_filename.starts_with("image_")),
|
||||
_ => panic!("Not implemented"),
|
||||
}
|
||||
assert_eq!(msg.get_width(), w);
|
||||
assert_eq!(msg.get_height(), h);
|
||||
assert!(msg.get_filebytes(&bob).await?.unwrap() > 250);
|
||||
@@ -5541,9 +5679,10 @@ mod tests {
|
||||
async fn test_sticker_png() -> Result<()> {
|
||||
test_sticker(
|
||||
"sticker.png",
|
||||
include_bytes!("../test-data/image/avatar64x64.png"),
|
||||
64,
|
||||
64,
|
||||
include_bytes!("../test-data/image/logo.png"),
|
||||
Viewtype::Sticker,
|
||||
135,
|
||||
135,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -5553,6 +5692,7 @@ mod tests {
|
||||
test_sticker(
|
||||
"sticker.jpg",
|
||||
include_bytes!("../test-data/image/avatar1000x1000.jpg"),
|
||||
Viewtype::Image,
|
||||
1000,
|
||||
1000,
|
||||
)
|
||||
@@ -5563,9 +5703,10 @@ mod tests {
|
||||
async fn test_sticker_gif() -> Result<()> {
|
||||
test_sticker(
|
||||
"sticker.gif",
|
||||
include_bytes!("../test-data/image/image100x50.gif"),
|
||||
100,
|
||||
50,
|
||||
include_bytes!("../test-data/image/logo.gif"),
|
||||
Viewtype::Sticker,
|
||||
135,
|
||||
135,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -5579,8 +5720,8 @@ mod tests {
|
||||
let bob_chat = bob.create_chat(&alice).await;
|
||||
|
||||
// create sticker
|
||||
let file_name = "sticker.jpg";
|
||||
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
|
||||
let file_name = "sticker.png";
|
||||
let bytes = include_bytes!("../test-data/image/logo.png");
|
||||
let file = alice.get_blobdir().join(file_name);
|
||||
tokio::fs::write(&file, bytes).await?;
|
||||
let mut msg = Message::new(Viewtype::Sticker);
|
||||
@@ -6117,7 +6258,7 @@ mod tests {
|
||||
chat_id1,
|
||||
Viewtype::Sticker,
|
||||
"b.png",
|
||||
include_bytes!("../test-data/image/avatar64x64.png"),
|
||||
include_bytes!("../test-data/image/logo.png"),
|
||||
)
|
||||
.await?;
|
||||
let second_image_msg_id = send_media(
|
||||
|
||||
135
src/chatlist.rs
135
src/chatlist.rs
@@ -10,8 +10,10 @@ use crate::constants::{
|
||||
use crate::contact::{Contact, ContactId};
|
||||
use crate::context::Context;
|
||||
use crate::message::{Message, MessageState, MsgId};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::stock_str;
|
||||
use crate::summary::Summary;
|
||||
use crate::tools::IsNoneOrEmpty;
|
||||
|
||||
/// An object representing a single chatlist in memory.
|
||||
///
|
||||
@@ -204,34 +206,84 @@ impl Chatlist {
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
// show normal chatlist
|
||||
let sort_id_up = if flag_for_forwarding {
|
||||
ChatId::lookup_by_contact(context, ContactId::SELF)
|
||||
let mut ids = if flag_for_forwarding {
|
||||
let sort_id_up = ChatId::lookup_by_contact(context, ContactId::SELF)
|
||||
.await?
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default();
|
||||
let process_row = |row: &rusqlite::Row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
let typ: Chattype = row.get(1)?;
|
||||
let param: Params = row.get::<_, String>(2)?.parse().unwrap_or_default();
|
||||
let msg_id: Option<MsgId> = row.get(3)?;
|
||||
Ok((chat_id, typ, param, msg_id))
|
||||
};
|
||||
let process_rows = |rows: rusqlite::MappedRows<_>| {
|
||||
rows.filter_map(|row: std::result::Result<(_, _, Params, _), _>| match row {
|
||||
Ok((chat_id, typ, param, msg_id)) => {
|
||||
if typ == Chattype::Mailinglist
|
||||
&& param.get(Param::ListPost).is_none_or_empty()
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(Ok((chat_id, msg_id)))
|
||||
}
|
||||
}
|
||||
Err(e) => Some(Err(e)),
|
||||
})
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
};
|
||||
// Return ProtectionBroken chats also, as that may happen to a verified chat at any
|
||||
// time. It may be confusing if a chat that is normally in the list disappears
|
||||
// suddenly. The UI need to deal with that case anyway.
|
||||
context.sql.query_map(
|
||||
"SELECT c.id, c.type, c.param, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
ON c.id=m.chat_id
|
||||
AND m.id=(
|
||||
SELECT id
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?)
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||
WHERE c.id>9 AND c.id!=?
|
||||
AND c.blocked=0
|
||||
AND NOT c.archived=?
|
||||
AND (c.type!=? OR c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?))
|
||||
GROUP BY c.id
|
||||
ORDER BY c.id=? DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
(
|
||||
MessageState::OutDraft, skip_id, ChatVisibility::Archived,
|
||||
Chattype::Group, ContactId::SELF,
|
||||
sort_id_up, ChatVisibility::Pinned,
|
||||
),
|
||||
process_row,
|
||||
process_rows,
|
||||
).await?
|
||||
} else {
|
||||
ChatId::new(0)
|
||||
// show normal chatlist
|
||||
context.sql.query_map(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
ON c.id=m.chat_id
|
||||
AND m.id=(
|
||||
SELECT id
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?)
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||
WHERE c.id>9 AND c.id!=?
|
||||
AND (c.blocked=0 OR c.blocked=2)
|
||||
AND NOT c.archived=?
|
||||
GROUP BY c.id
|
||||
ORDER BY c.id=0 DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
(MessageState::OutDraft, skip_id, ChatVisibility::Archived, ChatVisibility::Pinned),
|
||||
process_row,
|
||||
process_rows,
|
||||
).await?
|
||||
};
|
||||
let mut ids = context.sql.query_map(
|
||||
"SELECT c.id, m.id
|
||||
FROM chats c
|
||||
LEFT JOIN msgs m
|
||||
ON c.id=m.chat_id
|
||||
AND m.id=(
|
||||
SELECT id
|
||||
FROM msgs
|
||||
WHERE chat_id=c.id
|
||||
AND (hidden=0 OR state=?1)
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1)
|
||||
WHERE c.id>9 AND c.id!=?2
|
||||
AND (c.blocked=0 OR (c.blocked=2 AND NOT ?3))
|
||||
AND NOT c.archived=?4
|
||||
GROUP BY c.id
|
||||
ORDER BY c.id=?5 DESC, c.archived=?6 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
|
||||
(MessageState::OutDraft, skip_id, flag_for_forwarding, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned),
|
||||
process_row,
|
||||
process_rows,
|
||||
).await?;
|
||||
if !flag_no_specials && get_archived_cnt(context).await? > 0 {
|
||||
if ids.is_empty() && flag_add_alldone_hint {
|
||||
ids.push((DC_CHAT_ID_ALLDONE_HINT, None));
|
||||
@@ -244,6 +296,27 @@ impl Chatlist {
|
||||
Ok(Chatlist { ids })
|
||||
}
|
||||
|
||||
/// Converts list of chat IDs to a chatlist.
|
||||
pub(crate) async fn from_chat_ids(context: &Context, chat_ids: &[ChatId]) -> Result<Self> {
|
||||
let mut ids = Vec::new();
|
||||
for &chat_id in chat_ids {
|
||||
let msg_id: Option<MsgId> = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT id
|
||||
FROM msgs
|
||||
WHERE chat_id=?1
|
||||
AND (hidden=0 OR state=?2)
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1",
|
||||
(chat_id, MessageState::OutDraft),
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("failed to get msg ID for chat {}", chat_id))?;
|
||||
ids.push((chat_id, msg_id));
|
||||
}
|
||||
Ok(Chatlist { ids })
|
||||
}
|
||||
|
||||
/// Find out the number of chats.
|
||||
pub fn len(&self) -> usize {
|
||||
self.ids.len()
|
||||
@@ -388,7 +461,9 @@ pub async fn get_last_message_for_chat(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat::{create_group_chat, get_chat_contacts, ProtectionStatus};
|
||||
use crate::chat::{
|
||||
create_group_chat, get_chat_contacts, remove_contact_from_chat, ProtectionStatus,
|
||||
};
|
||||
use crate::message::Viewtype;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::stock_str::StockMessage;
|
||||
@@ -473,6 +548,14 @@ mod tests {
|
||||
.await
|
||||
.unwrap()
|
||||
.is_self_talk());
|
||||
|
||||
remove_contact_from_chat(&t, chats.get_chat_id(1).unwrap(), ContactId::SELF)
|
||||
.await
|
||||
.unwrap();
|
||||
let chats = Chatlist::try_load(&t, DC_GCL_FOR_FORWARDING, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(chats.len() == 1);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! # Key-value configuration management.
|
||||
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
@@ -329,7 +330,11 @@ impl Context {
|
||||
let value = match key {
|
||||
Config::Selfavatar => {
|
||||
let rel_path = self.sql.get_raw_config(key.as_ref()).await?;
|
||||
rel_path.map(|p| get_abs_path(self, p).to_string_lossy().into_owned())
|
||||
rel_path.map(|p| {
|
||||
get_abs_path(self, Path::new(&p))
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
})
|
||||
}
|
||||
Config::SysVersion => Some((*DC_VERSION_STR).clone()),
|
||||
Config::SysMsgsizeMaxRecommended => Some(format!("{RECOMMENDED_FILE_SIZE}")),
|
||||
|
||||
@@ -130,8 +130,8 @@ async fn on_configure_completed(
|
||||
old_addr: Option<String>,
|
||||
) -> Result<()> {
|
||||
if let Some(provider) = param.provider {
|
||||
if let Some(config_defaults) = &provider.config_defaults {
|
||||
for def in config_defaults.iter() {
|
||||
if let Some(config_defaults) = provider.config_defaults {
|
||||
for def in config_defaults {
|
||||
if !context.config_exists(def.key).await? {
|
||||
info!(context, "apply config_defaults {}={}", def.key, def.value);
|
||||
context.set_config(def.key, Some(def.value)).await?;
|
||||
@@ -656,7 +656,7 @@ async fn try_smtp_one_param(
|
||||
})
|
||||
} else {
|
||||
info!(context, "success: {}", inf);
|
||||
smtp.disconnect().await;
|
||||
smtp.disconnect();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ fn parse_serverparams(in_emailaddr: &str, xml_raw: &str) -> Result<Vec<ServerPar
|
||||
let res = moz_ac
|
||||
.incoming_servers
|
||||
.into_iter()
|
||||
.chain(moz_ac.outgoing_servers.into_iter())
|
||||
.chain(moz_ac.outgoing_servers)
|
||||
.filter_map(|server| {
|
||||
let protocol = match server.typ.as_ref() {
|
||||
"imap" => Some(Protocol::Imap),
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::collections::BinaryHeap;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
@@ -1186,7 +1186,7 @@ impl Contact {
|
||||
}
|
||||
} else if let Some(image_rel) = self.param.get(Param::ProfileImage) {
|
||||
if !image_rel.is_empty() {
|
||||
return Ok(Some(get_abs_path(context, image_rel)));
|
||||
return Ok(Some(get_abs_path(context, Path::new(image_rel))));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
@@ -1732,7 +1732,7 @@ mod tests {
|
||||
assert_eq!(may_be_valid_addr("dd.tt"), false);
|
||||
assert_eq!(may_be_valid_addr("tt.dd@uu"), true);
|
||||
assert_eq!(may_be_valid_addr("u@d"), true);
|
||||
assert_eq!(may_be_valid_addr("u@d."), true);
|
||||
assert_eq!(may_be_valid_addr("u@d."), false);
|
||||
assert_eq!(may_be_valid_addr("u@d.t"), true);
|
||||
assert_eq!(may_be_valid_addr("u@d.tt"), true);
|
||||
assert_eq!(may_be_valid_addr("u@.tt"), true);
|
||||
@@ -1741,6 +1741,7 @@ mod tests {
|
||||
assert_eq!(may_be_valid_addr("sk <@d.tt>"), false);
|
||||
assert_eq!(may_be_valid_addr("as@sd.de>"), false);
|
||||
assert_eq!(may_be_valid_addr("ask dkl@dd.tt"), false);
|
||||
assert_eq!(may_be_valid_addr("user@domain.tld."), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -332,6 +332,12 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes encrypted database passphrase.
|
||||
pub async fn change_passphrase(&self, passphrase: String) -> Result<()> {
|
||||
self.sql.change_passphrase(passphrase).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns true if database is open.
|
||||
pub async fn is_open(&self) -> bool {
|
||||
self.sql.is_open().await
|
||||
|
||||
@@ -29,14 +29,30 @@ pub fn try_decrypt(
|
||||
private_keyring: &Keyring<SignedSecretKey>,
|
||||
public_keyring_for_validate: &Keyring<SignedPublicKey>,
|
||||
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
|
||||
let encrypted_data_part = match get_autocrypt_mime(mail)
|
||||
.or_else(|| get_mixed_up_mime(mail))
|
||||
.or_else(|| get_attachment_mime(mail))
|
||||
{
|
||||
let encrypted_data_part = match {
|
||||
let mime = get_autocrypt_mime(mail);
|
||||
if mime.is_some() {
|
||||
info!(context, "Detected Autocrypt-mime message.");
|
||||
}
|
||||
mime
|
||||
}
|
||||
.or_else(|| {
|
||||
let mime = get_mixed_up_mime(mail);
|
||||
if mime.is_some() {
|
||||
info!(context, "Detected mixed-up mime message.");
|
||||
}
|
||||
mime
|
||||
})
|
||||
.or_else(|| {
|
||||
let mime = get_attachment_mime(mail);
|
||||
if mime.is_some() {
|
||||
info!(context, "Detected attached Autocrypt-mime message.");
|
||||
}
|
||||
mime
|
||||
}) {
|
||||
None => return Ok(None),
|
||||
Some(res) => res,
|
||||
};
|
||||
info!(context, "Detected Autocrypt-mime message");
|
||||
|
||||
decrypt_part(
|
||||
encrypted_data_part,
|
||||
@@ -403,4 +419,18 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_mixed_up_mime_long() -> Result<()> {
|
||||
// Long "mixed-up" mail as received when sending an encrypted message using Delta Chat
|
||||
// Desktop via MS Exchange (actually made with TB though).
|
||||
let mixed_up_mime = include_bytes!("../test-data/message/mixed-up-long.eml");
|
||||
let bob = TestContext::new_bob().await;
|
||||
receive_imf(&bob, mixed_up_mime, false).await?;
|
||||
let msg = bob.get_last_msg().await;
|
||||
assert!(!msg.get_text().is_empty());
|
||||
assert!(msg.has_html());
|
||||
assert!(msg.id.get_html(&bob).await?.unwrap().len() > 40000);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1062,14 +1062,14 @@ mod tests {
|
||||
delete_expired_messages(t, not_deleted_at).await?;
|
||||
|
||||
let loaded = Message::load_from_db(t, msg_id).await?;
|
||||
assert_eq!(loaded.text, "Message text");
|
||||
assert!(!loaded.text.is_empty());
|
||||
assert_eq!(loaded.chat_id, chat.id);
|
||||
|
||||
assert!(next_expiration < deleted_at);
|
||||
delete_expired_messages(t, deleted_at).await?;
|
||||
t.evtracker
|
||||
.get_matching(|evt| {
|
||||
if let EventType::MsgsChanged {
|
||||
if let EventType::MsgDeleted {
|
||||
msg_id: event_msg_id,
|
||||
..
|
||||
} = evt
|
||||
@@ -1082,7 +1082,6 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let loaded = Message::load_from_db(t, msg_id).await?;
|
||||
assert_eq!(loaded.text, "");
|
||||
assert_eq!(loaded.chat_id, DC_CHAT_ID_TRASH);
|
||||
|
||||
// Check that the msg was deleted locally.
|
||||
@@ -1300,4 +1299,32 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Tests that if we are offline for a time longer than the ephemeral timer duration, the message
|
||||
// is deleted from the chat but is still in the "smtp" table, i.e. will be sent upon a
|
||||
// successful reconnection.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_msg_offline() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.org")
|
||||
.await;
|
||||
let duration = 60;
|
||||
chat.id
|
||||
.set_ephemeral_timer(&alice, Timer::Enabled { duration })
|
||||
.await?;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("hi".to_string());
|
||||
assert!(chat::send_msg_sync(&alice, chat.id, &mut msg)
|
||||
.await
|
||||
.is_err());
|
||||
let stmt = "SELECT COUNT(*) FROM smtp WHERE msg_id=?";
|
||||
assert!(alice.sql.exists(stmt, (msg.id,)).await?);
|
||||
let now = time();
|
||||
check_msg_will_be_deleted(&alice, msg.id, &chat, now, now + i64::from(duration) + 1)
|
||||
.await?;
|
||||
assert!(alice.sql.exists(stmt, (msg.id,)).await?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,9 @@ pub enum HeaderDef {
|
||||
SecureJoinAuth,
|
||||
Sender,
|
||||
|
||||
/// [`Supersedes`](https://www.rfc-editor.org/rfc/rfc4021.html#section-2.1.46) header.
|
||||
Supersedes,
|
||||
|
||||
/// Ephemeral message timer.
|
||||
EphemeralTimer,
|
||||
Received,
|
||||
|
||||
16
src/html.rs
16
src/html.rs
@@ -291,7 +291,7 @@ mod tests {
|
||||
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
assert_eq!(
|
||||
parser.html,
|
||||
r##"<!DOCTYPE html>
|
||||
r#"<!DOCTYPE html>
|
||||
<html><head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
@@ -299,7 +299,7 @@ mod tests {
|
||||
This message does not have Content-Type nor Subject.<br/>
|
||||
<br/>
|
||||
</body></html>
|
||||
"##
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
@@ -310,7 +310,7 @@ This message does not have Content-Type nor Subject.<br/>
|
||||
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
assert_eq!(
|
||||
parser.html,
|
||||
r##"<!DOCTYPE html>
|
||||
r#"<!DOCTYPE html>
|
||||
<html><head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
@@ -318,7 +318,7 @@ This message does not have Content-Type nor Subject.<br/>
|
||||
message with a non-UTF-8 encoding: äöüßÄÖÜ<br/>
|
||||
<br/>
|
||||
</body></html>
|
||||
"##
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
@@ -330,7 +330,7 @@ message with a non-UTF-8 encoding: äöüßÄÖÜ<br/>
|
||||
assert!(parser.plain.unwrap().flowed);
|
||||
assert_eq!(
|
||||
parser.html,
|
||||
r##"<!DOCTYPE html>
|
||||
r#"<!DOCTYPE html>
|
||||
<html><head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
@@ -341,7 +341,7 @@ This line does not end with a space<br/>
|
||||
and will be wrapped as usual.<br/>
|
||||
<br/>
|
||||
</body></html>
|
||||
"##
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
@@ -352,7 +352,7 @@ and will be wrapped as usual.<br/>
|
||||
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
|
||||
assert_eq!(
|
||||
parser.html,
|
||||
r##"<!DOCTYPE html>
|
||||
r#"<!DOCTYPE html>
|
||||
<html><head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
@@ -363,7 +363,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
<br/>
|
||||
<br/>
|
||||
</body></html>
|
||||
"##
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -155,8 +155,8 @@ impl<'a> Connection<'a> {
|
||||
pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_>, mut job: Job) {
|
||||
info!(context, "Job {} started...", &job);
|
||||
|
||||
let try_res = match perform_job_action(context, &mut job, &mut connection, 0).await {
|
||||
Status::RetryNow => perform_job_action(context, &mut job, &mut connection, 1).await,
|
||||
let try_res = match perform_job_action(context, &job, &mut connection, 0).await {
|
||||
Status::RetryNow => perform_job_action(context, &job, &mut connection, 1).await,
|
||||
x => x,
|
||||
};
|
||||
|
||||
@@ -205,7 +205,7 @@ pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_
|
||||
|
||||
async fn perform_job_action(
|
||||
context: &Context,
|
||||
job: &mut Job,
|
||||
job: &Job,
|
||||
connection: &mut Connection<'_>,
|
||||
tries: u32,
|
||||
) -> Status {
|
||||
|
||||
@@ -260,7 +260,7 @@ pub(crate) async fn load_keypair(
|
||||
})
|
||||
}
|
||||
|
||||
/// Use of a [KeyPair] for encryption or decryption.
|
||||
/// Use of a key pair for encryption or decryption.
|
||||
///
|
||||
/// This is used by [store_self_keypair] to know what kind of key is
|
||||
/// being saved.
|
||||
|
||||
@@ -44,10 +44,6 @@ where
|
||||
self.keys.push(key);
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.keys.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.keys.is_empty()
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ pub mod mimeparser;
|
||||
pub mod oauth2;
|
||||
mod param;
|
||||
pub mod peerstate;
|
||||
pub mod pgp;
|
||||
mod pgp;
|
||||
pub mod provider;
|
||||
pub mod qr;
|
||||
pub mod qr_code_generator;
|
||||
|
||||
159
src/message.rs
159
src/message.rs
@@ -7,6 +7,7 @@ use anyhow::{ensure, format_err, Context as _, Result};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{Chat, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{
|
||||
@@ -434,6 +435,18 @@ pub struct Message {
|
||||
|
||||
/// `In-Reply-To` header value.
|
||||
pub(crate) in_reply_to: Option<String>,
|
||||
|
||||
/// `Supersedes` header value.
|
||||
///
|
||||
/// It is only used for sending and not stored in the database.
|
||||
///
|
||||
/// The header contains `Message-ID` of the original message
|
||||
/// superseded by this one.
|
||||
///
|
||||
/// The header is specified
|
||||
/// in <https://www.rfc-editor.org/rfc/rfc4021.html#section-2.1.46>.
|
||||
pub(crate) supersedes: Option<String>,
|
||||
|
||||
pub(crate) is_dc_message: MessengerMessage,
|
||||
pub(crate) mime_modified: bool,
|
||||
pub(crate) chat_blocked: Blocked,
|
||||
@@ -529,6 +542,7 @@ impl Message {
|
||||
download_state: row.get("download_state")?,
|
||||
error: Some(row.get::<_, String>("error")?)
|
||||
.filter(|error| !error.is_empty()),
|
||||
supersedes: None, // `Supersedes` header is only used for sending and not stored in the database.
|
||||
is_dc_message: row.get("msgrmsg")?,
|
||||
mime_modified: row.get("mime_modified")?,
|
||||
text,
|
||||
@@ -582,14 +596,22 @@ impl Message {
|
||||
if (self.viewtype == Viewtype::Image || self.viewtype == Viewtype::Gif)
|
||||
&& !self.param.exists(Param::Width)
|
||||
{
|
||||
self.param.set_int(Param::Width, 0);
|
||||
self.param.set_int(Param::Height, 0);
|
||||
let buf = read_file(context, &path_and_filename).await?;
|
||||
|
||||
if let Ok(buf) = read_file(context, path_and_filename).await {
|
||||
if let Ok((width, height)) = get_filemeta(&buf) {
|
||||
match get_filemeta(&buf) {
|
||||
Ok((width, height)) => {
|
||||
self.param.set_int(Param::Width, width as i32);
|
||||
self.param.set_int(Param::Height, height as i32);
|
||||
}
|
||||
Err(err) => {
|
||||
self.param.set_int(Param::Width, 0);
|
||||
self.param.set_int(Param::Height, 0);
|
||||
warn!(
|
||||
context,
|
||||
"Failed to get width and height for {}: {err:#}.",
|
||||
path_and_filename.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !self.id.is_unset() {
|
||||
@@ -980,19 +1002,28 @@ impl Message {
|
||||
}
|
||||
}
|
||||
self.param.set(Param::File, file);
|
||||
if let Some(filemime) = filemime {
|
||||
self.param.set(Param::MimeType, filemime);
|
||||
}
|
||||
self.param.set_optional(Param::MimeType, filemime);
|
||||
}
|
||||
|
||||
/// Creates a new blob and sets it as a file associated with a message.
|
||||
pub async fn set_file_from_bytes(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
suggested_name: &str,
|
||||
data: &[u8],
|
||||
filemime: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let blob = BlobObject::create(context, suggested_name, data).await?;
|
||||
self.param.set(Param::File, blob.as_name());
|
||||
self.param.set_optional(Param::MimeType, filemime);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set different sender name for a message.
|
||||
/// This overrides the name set by the `set_config()`-option `displayname`.
|
||||
pub fn set_override_sender_name(&mut self, name: Option<String>) {
|
||||
if let Some(name) = name {
|
||||
self.param.set(Param::OverrideSenderDisplayname, name);
|
||||
} else {
|
||||
self.param.remove(Param::OverrideSenderDisplayname);
|
||||
}
|
||||
self.param
|
||||
.set_optional(Param::OverrideSenderDisplayname, name);
|
||||
}
|
||||
|
||||
/// Sets the dimensions of associated image or video file.
|
||||
@@ -1429,6 +1460,7 @@ pub async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result<Vec<u8
|
||||
/// and scheduling for deletion on IMAP.
|
||||
pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
let mut modified_chat_ids = BTreeSet::new();
|
||||
let mut res = Ok(());
|
||||
|
||||
for &msg_id in msg_ids {
|
||||
let msg = Message::load_from_db(context, msg_id).await?;
|
||||
@@ -1452,13 +1484,19 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
modified_chat_ids.insert(msg.chat_id);
|
||||
|
||||
let target = context.get_delete_msgs_target().await?;
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
let update_db = |conn: &mut rusqlite::Connection| {
|
||||
conn.execute(
|
||||
"UPDATE imap SET target=? WHERE rfc724_mid=?",
|
||||
(target, msg.rfc724_mid),
|
||||
)
|
||||
.await?;
|
||||
)?;
|
||||
conn.execute("DELETE FROM smtp WHERE msg_id=?", (msg_id,))?;
|
||||
Ok(())
|
||||
};
|
||||
if let Err(e) = context.sql.call_write(update_db).await {
|
||||
error!(context, "delete_msgs: failed to update db: {e:#}.");
|
||||
res = Err(e);
|
||||
continue;
|
||||
}
|
||||
|
||||
let logging_xdc_id = context
|
||||
.debug_logging
|
||||
@@ -1473,6 +1511,7 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
res?;
|
||||
|
||||
for modified_chat_id in modified_chat_ids {
|
||||
context.emit_msgs_changed(modified_chat_id, MsgId::new(0));
|
||||
@@ -1641,53 +1680,36 @@ pub(crate) async fn update_msg_state(
|
||||
|
||||
// Context functions to work with messages
|
||||
|
||||
/// Returns true if given message ID exists in the database and is not trashed.
|
||||
pub(crate) async fn exists(context: &Context, msg_id: MsgId) -> Result<bool> {
|
||||
if msg_id.is_special() {
|
||||
return Ok(false);
|
||||
pub(crate) async fn set_msg_failed(
|
||||
context: &Context,
|
||||
msg: &mut Message,
|
||||
error: &str,
|
||||
) -> Result<()> {
|
||||
if msg.state.can_fail() {
|
||||
msg.state = MessageState::OutFailed;
|
||||
warn!(context, "{} failed: {}", msg.id, error);
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"{} seems to have failed ({}), but state is {}", msg.id, error, msg.state
|
||||
)
|
||||
}
|
||||
msg.error = Some(error.to_string());
|
||||
|
||||
let chat_id: Option<ChatId> = context
|
||||
context
|
||||
.sql
|
||||
.query_get_value("SELECT chat_id FROM msgs WHERE id=?;", (msg_id,))
|
||||
.execute(
|
||||
"UPDATE msgs SET state=?, error=? WHERE id=?;",
|
||||
(msg.state, error, msg.id),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(chat_id) = chat_id {
|
||||
Ok(!chat_id.is_trash())
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
context.emit_event(EventType::MsgFailed {
|
||||
chat_id: msg.chat_id,
|
||||
msg_id: msg.id,
|
||||
});
|
||||
|
||||
pub(crate) async fn set_msg_failed(context: &Context, msg_id: MsgId, error: &str) {
|
||||
if let Ok(mut msg) = Message::load_from_db(context, msg_id).await {
|
||||
if msg.state.can_fail() {
|
||||
msg.state = MessageState::OutFailed;
|
||||
warn!(context, "{} failed: {}", msg_id, error);
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"{} seems to have failed ({}), but state is {}", msg_id, error, msg.state
|
||||
)
|
||||
}
|
||||
|
||||
match context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET state=?, error=? WHERE id=?;",
|
||||
(msg.state, error, msg_id),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => context.emit_event(EventType::MsgFailed {
|
||||
chat_id: msg.chat_id,
|
||||
msg_id,
|
||||
}),
|
||||
Err(e) => {
|
||||
warn!(context, "{:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The number of messages assigned to unblocked chats
|
||||
@@ -2294,7 +2316,7 @@ mod tests {
|
||||
update_msg_state(&alice, alice_msg.id, MessageState::OutMdnRcvd).await?;
|
||||
assert_state(&alice, alice_msg.id, MessageState::OutMdnRcvd).await;
|
||||
|
||||
set_msg_failed(&alice, alice_msg.id, "badly failed").await;
|
||||
set_msg_failed(&alice, &mut alice_msg, "badly failed").await?;
|
||||
assert_state(&alice, alice_msg.id, MessageState::OutFailed).await;
|
||||
|
||||
// check incoming message states on receiver side
|
||||
@@ -2430,4 +2452,23 @@ def hello():
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_delete_msgs_offline() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.org")
|
||||
.await;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("hi".to_string());
|
||||
assert!(chat::send_msg_sync(&alice, chat.id, &mut msg)
|
||||
.await
|
||||
.is_err());
|
||||
let stmt = "SELECT COUNT(*) FROM smtp WHERE msg_id=?";
|
||||
assert!(alice.sql.exists(stmt, (msg.id,)).await?);
|
||||
delete_msgs(&alice, &[msg.id]).await?;
|
||||
assert!(!alice.sql.exists(stmt, (msg.id,)).await?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -549,6 +549,11 @@ impl<'a> MimeFactory<'a> {
|
||||
Loaded::Mdn { .. } => create_outgoing_rfc724_mid(None, &self.from_addr),
|
||||
};
|
||||
let rfc724_mid_headervalue = render_rfc724_mid(&rfc724_mid);
|
||||
if let Some(supersedes) = &self.msg.supersedes {
|
||||
headers
|
||||
.protected
|
||||
.push(Header::new("Supersedes".into(), supersedes.to_string()));
|
||||
}
|
||||
|
||||
// Amazon's SMTP servers change the `Message-ID`, just as Outlook's SMTP servers do.
|
||||
// Outlook's servers add an `X-Microsoft-Original-Message-ID` header with the original `Message-ID`,
|
||||
@@ -1248,7 +1253,7 @@ impl<'a> MimeFactory<'a> {
|
||||
} else if command == SystemMessage::WebxdcStatusUpdate {
|
||||
let json = self.msg.param.get(Param::Arg).unwrap_or_default();
|
||||
parts.push(context.build_status_update_part(json));
|
||||
} else if self.msg.viewtype == Viewtype::Webxdc {
|
||||
} else if self.msg.viewtype == Viewtype::Webxdc && self.msg.supersedes.is_none() {
|
||||
if let Some(json) = context
|
||||
.render_webxdc_status_update_object(self.msg.id, None)
|
||||
.await?
|
||||
|
||||
@@ -12,7 +12,6 @@ use deltachat_derive::{FromSql, ToSql};
|
||||
use format_flowed::unformat_flowed;
|
||||
use lettre_email::mime::{self, Mime};
|
||||
use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::aheader::{Aheader, EncryptPreference};
|
||||
use crate::blob::BlobObject;
|
||||
@@ -30,7 +29,9 @@ use crate::events::EventType;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
|
||||
use crate::keyring::Keyring;
|
||||
use crate::message::{self, set_msg_failed, update_msg_state, MessageState, MsgId, Viewtype};
|
||||
use crate::message::{
|
||||
self, set_msg_failed, update_msg_state, Message, MessageState, MsgId, Viewtype,
|
||||
};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::simplify::{simplify, SimplifiedText};
|
||||
@@ -1617,25 +1618,21 @@ impl MimeMessage {
|
||||
false
|
||||
};
|
||||
if maybe_ndn && self.delivery_report.is_none() {
|
||||
static RE: Lazy<regex::Regex> =
|
||||
Lazy::new(|| regex::Regex::new(r"Message-ID:(.*)").unwrap());
|
||||
for captures in self
|
||||
for original_message_id in self
|
||||
.parts
|
||||
.iter()
|
||||
.filter_map(|part| part.msg_raw.as_ref())
|
||||
.flat_map(|part| part.lines())
|
||||
.filter_map(|line| RE.captures(line))
|
||||
.filter_map(|line| line.split_once("Message-ID:"))
|
||||
.filter_map(|(_, message_id)| parse_message_id(message_id).ok())
|
||||
{
|
||||
if let Ok(original_message_id) = parse_message_id(&captures[1]) {
|
||||
if let Ok(Some(_)) =
|
||||
message::rfc724_mid_exists(context, &original_message_id).await
|
||||
{
|
||||
self.delivery_report = Some(DeliveryReport {
|
||||
rfc724_mid: original_message_id,
|
||||
failed_recipient: None,
|
||||
failure: true,
|
||||
})
|
||||
}
|
||||
if let Ok(Some(_)) = message::rfc724_mid_exists(context, &original_message_id).await
|
||||
{
|
||||
self.delivery_report = Some(DeliveryReport {
|
||||
rfc724_mid: original_message_id,
|
||||
failed_recipient: None,
|
||||
failure: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2161,7 +2158,8 @@ async fn handle_ndn(
|
||||
let mut first = true;
|
||||
for msg in msgs {
|
||||
let (msg_id, chat_id, chat_type) = msg?;
|
||||
set_msg_failed(context, msg_id, &error).await;
|
||||
let mut message = Message::load_from_db(context, msg_id).await?;
|
||||
set_msg_failed(context, &mut message, &error).await?;
|
||||
if first {
|
||||
// Add only one info msg for all failed messages
|
||||
ndn_maybe_add_info_msg(context, failed, chat_id, chat_type).await?;
|
||||
@@ -3107,7 +3105,7 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn parse_outlook_html_embedded_image() {
|
||||
let context = TestContext::new_alice().await;
|
||||
let raw = br##"From: Anonymous <anonymous@example.org>
|
||||
let raw = br#"From: Anonymous <anonymous@example.org>
|
||||
To: Anonymous <anonymous@example.org>
|
||||
Subject: Delta Chat is great stuff!
|
||||
Date: Tue, 5 May 2020 01:23:45 +0000
|
||||
@@ -3162,7 +3160,7 @@ K+TuvC7qOah0WLFhcsXWn2+dDV1bXuAeC769TkqkpHhdXfUHnVgK3Pv7u3rVPT5AMeFUGxRB2dP4
|
||||
CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
|
||||
|
||||
------=_NextPart_000_0003_01D622B3.CA753E60--
|
||||
"##;
|
||||
"#;
|
||||
|
||||
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
.await
|
||||
@@ -3537,7 +3535,7 @@ On 2020-10-25, Bob wrote:
|
||||
|
||||
// A message with a long Message-ID.
|
||||
// Long message-IDs are generated by Mailjet.
|
||||
let raw = br###"Date: Thu, 28 Jan 2021 00:26:57 +0000
|
||||
let raw = br"Date: Thu, 28 Jan 2021 00:26:57 +0000
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <ABCDEFGH.1234567_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@mailjet.com>
|
||||
To: Bob <bob@example.org>
|
||||
@@ -3545,11 +3543,11 @@ From: Alice <alice@example.org>
|
||||
Subject: ...
|
||||
|
||||
Some quote.
|
||||
"###;
|
||||
";
|
||||
receive_imf(&t, raw, false).await?;
|
||||
|
||||
// Delta Chat generates In-Reply-To with a starting tab when Message-ID is too long.
|
||||
let raw = br###"In-Reply-To:
|
||||
let raw = br"In-Reply-To:
|
||||
<ABCDEFGH.1234567_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@mailjet.com>
|
||||
Date: Thu, 28 Jan 2021 00:26:57 +0000
|
||||
Chat-Version: 1.0\n\
|
||||
@@ -3561,7 +3559,7 @@ Subject: ...
|
||||
> Some quote.
|
||||
|
||||
Some reply
|
||||
"###;
|
||||
";
|
||||
|
||||
receive_imf(&t, raw, false).await?;
|
||||
|
||||
@@ -3579,7 +3577,7 @@ Some reply
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let raw = br###"Date: Thu, 28 Jan 2021 00:26:57 +0000
|
||||
let raw = br"Date: Thu, 28 Jan 2021 00:26:57 +0000
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <foobarbaz@example.org>
|
||||
To: Bob <bob@example.org>
|
||||
@@ -3588,7 +3586,7 @@ Subject: subject
|
||||
Chat-Disposition-Notification-To: alice@example.org
|
||||
|
||||
Message.
|
||||
"###;
|
||||
";
|
||||
|
||||
// Bob receives message.
|
||||
receive_imf(&bob, raw, false).await?;
|
||||
|
||||
10
src/param.rs
10
src/param.rs
@@ -281,6 +281,16 @@ impl Params {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the given key from an optional value.
|
||||
/// Removes the key if the value is `None`.
|
||||
pub fn set_optional(&mut self, key: Param, value: Option<impl ToString>) -> &mut Self {
|
||||
if let Some(value) = value {
|
||||
self.set(key, value)
|
||||
} else {
|
||||
self.remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if there are any values in this.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.inner.is_empty()
|
||||
|
||||
@@ -24,7 +24,8 @@ use crate::keyring::Keyring;
|
||||
use crate::tools::EmailAddress;
|
||||
|
||||
#[allow(missing_docs)]
|
||||
pub const HEADER_AUTOCRYPT: &str = "autocrypt-prefer-encrypt";
|
||||
#[cfg(test)]
|
||||
pub(crate) const HEADER_AUTOCRYPT: &str = "autocrypt-prefer-encrypt";
|
||||
|
||||
#[allow(missing_docs)]
|
||||
pub const HEADER_SETUPCODE: &str = "passphrase-begin";
|
||||
|
||||
@@ -26,10 +26,10 @@ impl PlainText {
|
||||
/// The function handles quotes, links, fixed and floating text paragraphs.
|
||||
pub fn to_html(&self) -> String {
|
||||
static LINKIFY_MAIL_RE: Lazy<regex::Regex> =
|
||||
Lazy::new(|| regex::Regex::new(r#"\b([\w.\-+]+@[\w.\-]+)\b"#).unwrap());
|
||||
Lazy::new(|| regex::Regex::new(r"\b([\w.\-+]+@[\w.\-]+)\b").unwrap());
|
||||
|
||||
static LINKIFY_URL_RE: Lazy<regex::Regex> = Lazy::new(|| {
|
||||
regex::Regex::new(r#"\b((http|https|ftp|ftps):[\w.,:;$/@!?&%\-~=#+]+)"#).unwrap()
|
||||
regex::Regex::new(r"\b((http|https|ftp|ftps):[\w.,:;$/@!?&%\-~=#+]+)").unwrap()
|
||||
});
|
||||
|
||||
let lines = split_lines(&self.text);
|
||||
@@ -127,7 +127,7 @@ http://link-at-start-of-line.org
|
||||
.to_html();
|
||||
assert_eq!(
|
||||
html,
|
||||
r##"<!DOCTYPE html>
|
||||
r#"<!DOCTYPE html>
|
||||
<html><head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
@@ -138,7 +138,7 @@ line with <a href="https://link-mid-of-line.org">https://link-mid-of-line.org</a
|
||||
<a href="http://link-at-start-of-line.org">http://link-at-start-of-line.org</a><br/>
|
||||
<br/>
|
||||
</body></html>
|
||||
"##
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -123,10 +123,10 @@ pub struct Provider {
|
||||
pub overview_page: &'static str,
|
||||
|
||||
/// List of provider servers.
|
||||
pub server: Vec<Server>,
|
||||
pub server: &'static [Server],
|
||||
|
||||
/// Default configuration values to set when provider is configured.
|
||||
pub config_defaults: Option<Vec<ConfigDefault>>,
|
||||
pub config_defaults: Option<&'static [ConfigDefault]>,
|
||||
|
||||
/// Type of OAuth 2 authorization if provider supports it.
|
||||
pub oauth2_authorizer: Option<Oauth2Authorizer>,
|
||||
@@ -149,8 +149,8 @@ pub struct ProviderOptions {
|
||||
pub delete_to_trash: bool,
|
||||
}
|
||||
|
||||
impl Default for ProviderOptions {
|
||||
fn default() -> Self {
|
||||
impl ProviderOptions {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
strict_tls: true,
|
||||
max_smtp_rcpt_to: None,
|
||||
@@ -223,7 +223,7 @@ pub async fn get_provider_by_mx(context: &Context, domain: &str) -> Option<&'sta
|
||||
}
|
||||
|
||||
if let Ok(mx_domains) = resolver.mx_lookup(fqdn).await {
|
||||
for (provider_domain, provider) in PROVIDER_DATA.iter() {
|
||||
for (provider_domain, provider) in &*PROVIDER_DATA {
|
||||
if provider.id != "gmail" {
|
||||
// MX lookup is limited to Gmail for security reasons
|
||||
continue;
|
||||
|
||||
1380
src/provider/data.rs
1380
src/provider/data.rs
File diff suppressed because it is too large
Load Diff
@@ -71,7 +71,7 @@ async fn get_unique_quota_roots_and_usage(
|
||||
// messages could be received and so the usage could have been changed
|
||||
*unique_quota_roots
|
||||
.entry(quota_root_name.clone())
|
||||
.or_insert_with(Vec::new) = quota.resources;
|
||||
.or_default() = quota.resources;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,10 @@ pub struct ReceivedMsg {
|
||||
|
||||
/// Whether IMAP messages should be immediately deleted.
|
||||
pub needs_delete_job: bool,
|
||||
|
||||
/// Message-ID saved into the database.
|
||||
/// For messages with `Supersedes` header this is the Message-ID of the original message.
|
||||
pub rfc724_mid: String,
|
||||
}
|
||||
|
||||
/// Emulates reception of a message from the network.
|
||||
@@ -134,6 +138,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
sort_timestamp: 0,
|
||||
msg_ids,
|
||||
needs_delete_job: false,
|
||||
rfc724_mid: rfc724_mid.to_string(),
|
||||
}));
|
||||
}
|
||||
Ok(mime_parser) => mime_parser,
|
||||
@@ -338,12 +343,12 @@ pub(crate) async fn receive_imf_inner(
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE imap SET target=? WHERE rfc724_mid=?",
|
||||
(target, rfc724_mid),
|
||||
(target, &received_msg.rfc724_mid),
|
||||
)
|
||||
.await?;
|
||||
} else if !mime_parser.mdn_reports.is_empty() && mime_parser.has_chat_version() {
|
||||
// This is a Delta Chat MDN. Mark as read.
|
||||
markseen_on_imap_table(context, rfc724_mid).await?;
|
||||
markseen_on_imap_table(context, &received_msg.rfc724_mid).await?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1050,6 +1055,43 @@ async fn add_parts(
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
let rfc724_mid = if let Some(supersedes) = mime_parser.get_header(HeaderDef::Supersedes) {
|
||||
supersedes.to_string()
|
||||
} else {
|
||||
rfc724_mid.to_string()
|
||||
};
|
||||
|
||||
let supersedes_msg_id = match mime_parser.get_header(HeaderDef::Supersedes) {
|
||||
Some(supersedes) => {
|
||||
if let Some(msg_id) = rfc724_mid_exists(context, supersedes).await? {
|
||||
if let Some(orig_from_id) = context
|
||||
.sql
|
||||
.query_row_optional("SELECT from_id FROM msgs WHERE id=?", (msg_id,), |row| {
|
||||
let from_id: ContactId = row.get(0)?;
|
||||
|
||||
Ok(from_id)
|
||||
})
|
||||
.await?
|
||||
{
|
||||
if from_id == orig_from_id {
|
||||
Some(msg_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
if supersedes_msg_id.is_some() {
|
||||
replace_msg_id = supersedes_msg_id;
|
||||
info!(context, "Superseding {supersedes_msg_id:?}");
|
||||
}
|
||||
|
||||
// fine, so far. now, split the message into simple parts usable as "short messages"
|
||||
// and add them to the database (mails sent by other messenger clients should result
|
||||
// into only one message; mails sent by other clients may result in several messages
|
||||
@@ -1068,7 +1110,7 @@ async fn add_parts(
|
||||
let mut save_mime_modified = mime_parser.is_mime_modified;
|
||||
|
||||
let mime_headers = if save_mime_headers || save_mime_modified {
|
||||
let headers = if mime_parser.was_encrypted() && !mime_parser.decoded_data.is_empty() {
|
||||
let headers = if !mime_parser.decoded_data.is_empty() {
|
||||
mime_parser.decoded_data.clone()
|
||||
} else {
|
||||
imf_raw.to_vec()
|
||||
@@ -1301,6 +1343,7 @@ RETURNING id
|
||||
sort_timestamp,
|
||||
msg_ids: created_db_entries,
|
||||
needs_delete_job,
|
||||
rfc724_mid,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1655,19 +1698,22 @@ async fn apply_group_changes(
|
||||
};
|
||||
|
||||
// Whether to allow any changes to the member list at all.
|
||||
let allow_member_list_changes =
|
||||
if chat::is_contact_in_chat(context, chat_id, ContactId::SELF).await? || self_added {
|
||||
// Reject old group changes.
|
||||
chat_id
|
||||
.update_timestamp(context, Param::MemberListTimestamp, sent_timestamp)
|
||||
.await?
|
||||
} else {
|
||||
// Member list changes are not allowed if we're not in the group
|
||||
// and are not explicitly added.
|
||||
// This message comes from a Delta Chat that restored an old backup
|
||||
// or the message is a MUA reply to an old message.
|
||||
false
|
||||
};
|
||||
let allow_member_list_changes = if chat::is_contact_in_chat(context, chat_id, ContactId::SELF)
|
||||
.await?
|
||||
|| self_added
|
||||
|| !mime_parser.has_chat_version()
|
||||
{
|
||||
// Reject old group changes.
|
||||
chat_id
|
||||
.update_timestamp(context, Param::MemberListTimestamp, sent_timestamp)
|
||||
.await?
|
||||
} else {
|
||||
// Member list changes are not allowed if we're not in the group
|
||||
// and are not explicitly added.
|
||||
// This message comes from a Delta Chat that restored an old backup
|
||||
// or the message is a MUA reply to an old message.
|
||||
false
|
||||
};
|
||||
|
||||
// Whether to rebuild the member list from scratch.
|
||||
let recreate_member_list = if allow_member_list_changes {
|
||||
@@ -2345,7 +2391,7 @@ async fn add_or_lookup_contacts_by_address_list(
|
||||
origin: Origin,
|
||||
) -> Result<Vec<ContactId>> {
|
||||
let mut contact_ids = HashSet::new();
|
||||
for info in address_list.iter() {
|
||||
for info in address_list {
|
||||
let addr = &info.addr;
|
||||
if !may_be_valid_addr(addr) {
|
||||
continue;
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::chat::{
|
||||
};
|
||||
use crate::chat::{get_chat_msgs, ChatItem, ChatVisibility};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::DC_GCL_NO_SPECIALS;
|
||||
use crate::constants::{DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS};
|
||||
use crate::imap::prefetch_should_download;
|
||||
use crate::message::Message;
|
||||
use crate::test_utils::{get_chat_msg, TestContext, TestContextManager};
|
||||
@@ -793,6 +793,8 @@ async fn test_github_mailing_list() -> Result<()> {
|
||||
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
let chats = Chatlist::try_load(&t.ctx, DC_GCL_FOR_FORWARDING, None, None).await?;
|
||||
assert_eq!(chats.len(), 0);
|
||||
let contacts = Contact::get_all(&t.ctx, 0, None).await?;
|
||||
assert_eq!(contacts.len(), 0); // mailing list recipients and senders do not count as "known contacts"
|
||||
|
||||
@@ -3560,3 +3562,52 @@ async fn test_mua_can_add() -> Result<()> {
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_mua_can_readd() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
// Alice creates chat with 3 contacts.
|
||||
let msg = receive_imf(
|
||||
&alice,
|
||||
b"Subject: =?utf-8?q?Message_from_alice=40example=2Eorg?=\r\n\
|
||||
From: alice@example.org\r\n\
|
||||
To: <bob@example.net>, <claire@example.org>, <fiona@example.org> \r\n\
|
||||
Date: Mon, 12 Dec 2022 14:30:39 +0000\r\n\
|
||||
Message-ID: <Mr.alices_original_mail@example.org>\r\n\
|
||||
Chat-Version: 1.0\r\n\
|
||||
\r\n\
|
||||
Hi!\r\n",
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
let alice_chat = Chat::load_from_db(&alice, msg.chat_id).await?;
|
||||
assert_eq!(alice_chat.typ, Chattype::Group);
|
||||
assert!(is_contact_in_chat(&alice, alice_chat.id, ContactId::SELF).await?);
|
||||
|
||||
// And leaves it.
|
||||
remove_contact_from_chat(&alice, alice_chat.id, ContactId::SELF).await?;
|
||||
let alice_chat = Chat::load_from_db(&alice, alice_chat.id).await?;
|
||||
assert!(!is_contact_in_chat(&alice, alice_chat.id, ContactId::SELF).await?);
|
||||
|
||||
// Bob uses a classical MUA to answer, adding Alice back.
|
||||
receive_imf(
|
||||
&alice,
|
||||
b"Subject: Re: Message from alice\r\n\
|
||||
From: <bob@example.net>\r\n\
|
||||
To: <alice@example.org>, <claire@example.org>, <fiona@example.org>\r\n\
|
||||
Date: Mon, 12 Dec 2022 14:32:39 +0000\r\n\
|
||||
Message-ID: <bobs_answer_to_two_recipients@example.net>\r\n\
|
||||
In-Reply-To: <Mr.alices_original_mail@example.org>\r\n\
|
||||
\r\n\
|
||||
Hi back!\r\n",
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
|
||||
let alice_chat = Chat::load_from_db(&alice, alice_chat.id).await?;
|
||||
assert!(is_contact_in_chat(&alice, alice_chat.id, ContactId::SELF).await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
46
src/smtp.rs
46
src/smtp.rs
@@ -55,7 +55,7 @@ impl Smtp {
|
||||
}
|
||||
|
||||
/// Disconnect the SMTP transport and drop it entirely.
|
||||
pub async fn disconnect(&mut self) {
|
||||
pub fn disconnect(&mut self) {
|
||||
if let Some(mut transport) = self.transport.take() {
|
||||
// Closing connection with a QUIT command may take some time, especially if it's a
|
||||
// stale connection and an attempt to send the command times out. Send a command in a
|
||||
@@ -88,7 +88,7 @@ impl Smtp {
|
||||
pub async fn connect_configured(&mut self, context: &Context) -> Result<()> {
|
||||
if self.has_maybe_stale_connection() {
|
||||
info!(context, "Closing stale connection");
|
||||
self.disconnect().await;
|
||||
self.disconnect();
|
||||
}
|
||||
|
||||
if self.is_connected() {
|
||||
@@ -465,13 +465,13 @@ pub(crate) async fn smtp_send(
|
||||
|
||||
// this clears last_success info
|
||||
info!(context, "Failed to send message over SMTP, disconnecting");
|
||||
smtp.disconnect().await;
|
||||
smtp.disconnect();
|
||||
|
||||
res
|
||||
}
|
||||
Err(crate::smtp::send::Error::Envelope(err)) => {
|
||||
// Local error, job is invalid, do not retry.
|
||||
smtp.disconnect().await;
|
||||
smtp.disconnect();
|
||||
warn!(context, "SMTP job is invalid: {}", err);
|
||||
SendResult::Failure(err)
|
||||
}
|
||||
@@ -483,7 +483,7 @@ pub(crate) async fn smtp_send(
|
||||
}
|
||||
Err(crate::smtp::send::Error::Other(err)) => {
|
||||
// Local error, job is invalid, do not retry.
|
||||
smtp.disconnect().await;
|
||||
smtp.disconnect();
|
||||
warn!(context, "unable to load job: {}", err);
|
||||
SendResult::Failure(err)
|
||||
}
|
||||
@@ -492,7 +492,20 @@ pub(crate) async fn smtp_send(
|
||||
|
||||
if let SendResult::Failure(err) = &status {
|
||||
// We couldn't send the message, so mark it as failed
|
||||
message::set_msg_failed(context, msg_id, &err.to_string()).await;
|
||||
match Message::load_from_db(context, msg_id).await {
|
||||
Ok(mut msg) => {
|
||||
if let Err(err) = message::set_msg_failed(context, &mut msg, &err.to_string()).await
|
||||
{
|
||||
error!(context, "Failed to mark {msg_id} as failed: {err:#}.");
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
context,
|
||||
"Failed to load {msg_id} to mark it as failed: {err:#}."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
status
|
||||
}
|
||||
@@ -539,7 +552,8 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
)
|
||||
.await?;
|
||||
if retries > 6 {
|
||||
message::set_msg_failed(context, msg_id, "Number of retries exceeded the limit.").await;
|
||||
let mut msg = Message::load_from_db(context, msg_id).await?;
|
||||
message::set_msg_failed(context, &mut msg, "Number of retries exceeded the limit.").await?;
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp WHERE id=?", (rowid,))
|
||||
@@ -565,24 +579,6 @@ 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
|
||||
// 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 {msg_id} existence"))?
|
||||
{
|
||||
info!(
|
||||
context,
|
||||
"Sending of message {msg_id} (entry {rowid}) was cancelled by the user."
|
||||
);
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp WHERE id=?", (rowid,))
|
||||
.await
|
||||
.context("failed to remove cancelled message from smtp table")?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let status = smtp_send(context, &recipients_list, body.as_str(), smtp, msg_id).await;
|
||||
|
||||
match status {
|
||||
|
||||
57
src/sql.rs
57
src/sql.rs
@@ -304,6 +304,20 @@ impl Sql {
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes the passphrase of encrypted database.
|
||||
///
|
||||
/// The database must already be encrypted and the passphrase cannot be empty.
|
||||
/// It is impossible to turn encrypted database into unencrypted
|
||||
/// and vice versa this way, use import/export for this.
|
||||
pub async fn change_passphrase(&self, passphrase: String) -> Result<()> {
|
||||
self.call_write(move |conn| {
|
||||
conn.pragma_update(None, "rekey", passphrase)
|
||||
.context("failed to set PRAGMA rekey")?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Locks the write transactions mutex in order to make sure that there never are
|
||||
/// multiple write transactions at once.
|
||||
///
|
||||
@@ -1246,6 +1260,49 @@ mod tests {
|
||||
sql.open(&t, "foo".to_string())
|
||||
.await
|
||||
.context("failed to open the database second time")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_change_passphrase() -> Result<()> {
|
||||
use tempfile::tempdir;
|
||||
|
||||
// The context is used only for logging.
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// Create a separate empty database for testing.
|
||||
let dir = tempdir()?;
|
||||
let dbfile = dir.path().join("testdb.sqlite");
|
||||
let sql = Sql::new(dbfile.clone());
|
||||
|
||||
sql.open(&t, "foo".to_string())
|
||||
.await
|
||||
.context("failed to open the database first time")?;
|
||||
sql.close().await;
|
||||
|
||||
// Change the passphrase from "foo" to "bar".
|
||||
let sql = Sql::new(dbfile.clone());
|
||||
sql.open(&t, "foo".to_string())
|
||||
.await
|
||||
.context("failed to open the database second time")?;
|
||||
sql.change_passphrase("bar".to_string())
|
||||
.await
|
||||
.context("failed to change passphrase")?;
|
||||
sql.close().await;
|
||||
|
||||
let sql = Sql::new(dbfile);
|
||||
|
||||
// Test that old passphrase is not working.
|
||||
assert!(sql.open(&t, "foo".to_string()).await.is_err());
|
||||
|
||||
// Open the database with the new passphrase.
|
||||
sql.check_passphrase("bar".to_string()).await?;
|
||||
sql.open(&t, "bar".to_string())
|
||||
.await
|
||||
.context("failed to open the database third time")?;
|
||||
sql.close().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,7 +516,8 @@ DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
msg_id INTEGER,
|
||||
update_item TEXT DEFAULT '',
|
||||
update_item_read INTEGER DEFAULT 0);
|
||||
update_item_read INTEGER DEFAULT 0 -- XXX unused
|
||||
);
|
||||
CREATE INDEX msgs_status_updates_index1 ON msgs_status_updates (msg_id);"#,
|
||||
84,
|
||||
)
|
||||
|
||||
30
src/tools.rs
30
src/tools.rs
@@ -323,17 +323,16 @@ pub fn get_filemeta(buf: &[u8]) -> Result<(u32, u32)> {
|
||||
///
|
||||
/// If `path` starts with "$BLOBDIR", replaces it with the blobdir path.
|
||||
/// Otherwise, returns path as is.
|
||||
pub(crate) fn get_abs_path(context: &Context, path: impl AsRef<Path>) -> PathBuf {
|
||||
let p: &Path = path.as_ref();
|
||||
if let Ok(p) = p.strip_prefix("$BLOBDIR") {
|
||||
pub(crate) fn get_abs_path(context: &Context, path: &Path) -> PathBuf {
|
||||
if let Ok(p) = path.strip_prefix("$BLOBDIR") {
|
||||
context.get_blobdir().join(p)
|
||||
} else {
|
||||
p.into()
|
||||
path.into()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_filebytes(context: &Context, path: impl AsRef<Path>) -> Result<u64> {
|
||||
let path_abs = get_abs_path(context, &path);
|
||||
let path_abs = get_abs_path(context, path.as_ref());
|
||||
let meta = fs::metadata(&path_abs).await?;
|
||||
Ok(meta.len())
|
||||
}
|
||||
@@ -377,7 +376,7 @@ pub(crate) async fn create_folder(
|
||||
context: &Context,
|
||||
path: impl AsRef<Path>,
|
||||
) -> Result<(), io::Error> {
|
||||
let path_abs = get_abs_path(context, &path);
|
||||
let path_abs = get_abs_path(context, path.as_ref());
|
||||
if !path_abs.exists() {
|
||||
match fs::create_dir_all(path_abs).await {
|
||||
Ok(_) => Ok(()),
|
||||
@@ -402,7 +401,7 @@ pub(crate) async fn write_file(
|
||||
path: impl AsRef<Path>,
|
||||
buf: &[u8],
|
||||
) -> Result<(), io::Error> {
|
||||
let path_abs = get_abs_path(context, &path);
|
||||
let path_abs = get_abs_path(context, path.as_ref());
|
||||
fs::write(&path_abs, buf).await.map_err(|err| {
|
||||
warn!(
|
||||
context,
|
||||
@@ -417,7 +416,7 @@ pub(crate) async fn write_file(
|
||||
|
||||
/// Reads the file and returns its context as a byte vector.
|
||||
pub async fn read_file(context: &Context, path: impl AsRef<Path>) -> Result<Vec<u8>> {
|
||||
let path_abs = get_abs_path(context, &path);
|
||||
let path_abs = get_abs_path(context, path.as_ref());
|
||||
|
||||
match fs::read(&path_abs).await {
|
||||
Ok(bytes) => Ok(bytes),
|
||||
@@ -434,7 +433,7 @@ pub async fn read_file(context: &Context, path: impl AsRef<Path>) -> Result<Vec<
|
||||
}
|
||||
|
||||
pub async fn open_file(context: &Context, path: impl AsRef<Path>) -> Result<fs::File> {
|
||||
let path_abs = get_abs_path(context, &path);
|
||||
let path_abs = get_abs_path(context, path.as_ref());
|
||||
|
||||
match fs::File::open(&path_abs).await {
|
||||
Ok(bytes) => Ok(bytes),
|
||||
@@ -450,12 +449,8 @@ pub async fn open_file(context: &Context, path: impl AsRef<Path>) -> Result<fs::
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_file_std<P: AsRef<std::path::Path>>(
|
||||
context: &Context,
|
||||
path: P,
|
||||
) -> Result<std::fs::File> {
|
||||
let p: PathBuf = path.as_ref().into();
|
||||
let path_abs = get_abs_path(context, p);
|
||||
pub fn open_file_std(context: &Context, path: impl AsRef<Path>) -> Result<std::fs::File> {
|
||||
let path_abs = get_abs_path(context, path.as_ref());
|
||||
|
||||
match std::fs::File::open(path_abs) {
|
||||
Ok(bytes) => Ok(bytes),
|
||||
@@ -540,6 +535,9 @@ impl EmailAddress {
|
||||
if domain.is_empty() {
|
||||
bail!("missing domain after '@' in {:?}", input);
|
||||
}
|
||||
if domain.ends_with('.') {
|
||||
bail!("Domain {domain:?} should not contain the dot in the end");
|
||||
}
|
||||
Ok(EmailAddress {
|
||||
local: (*local).to_string(),
|
||||
domain: (*domain).to_string(),
|
||||
@@ -1001,7 +999,7 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true";
|
||||
assert_eq!(EmailAddress::new("dd.tt").is_ok(), false);
|
||||
assert!(EmailAddress::new("tt.dd@uu").is_ok());
|
||||
assert!(EmailAddress::new("u@d").is_ok());
|
||||
assert!(EmailAddress::new("u@d.").is_ok());
|
||||
assert!(EmailAddress::new("u@d.").is_err());
|
||||
assert!(EmailAddress::new("u@d.t").is_ok());
|
||||
assert_eq!(
|
||||
EmailAddress::new("u@d.tt").unwrap(),
|
||||
|
||||
309
src/webxdc.rs
309
src/webxdc.rs
@@ -1,4 +1,18 @@
|
||||
//! # Handle webxdc messages.
|
||||
//!
|
||||
//! Internally status updates are stored in the `msgs_status_updates` SQL table.
|
||||
//! `msgs_status_updates` contains the following columns:
|
||||
//! - `id` - status update serial number
|
||||
//! - `msg_id` - ID of the message in the `msgs` table
|
||||
//! - `update_item` - JSON representation of the status update
|
||||
//!
|
||||
//! Status updates are scheduled for sending by adding a record
|
||||
//! to `smtp_status_updates_table` SQL table.
|
||||
//! `smtp_status_updates` contains the following columns:
|
||||
//! - `msg_id` - ID of the message in the `msgs` table
|
||||
//! - `first_serial` - serial number of the first status update to send
|
||||
//! - `last_serial` - serial number of the last status update to send
|
||||
//! - `descr` - text to send along with the updates
|
||||
|
||||
use std::convert::TryFrom;
|
||||
use std::path::Path;
|
||||
@@ -12,7 +26,8 @@ use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use crate::chat::Chat;
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{self, create_send_msg_job, Chat};
|
||||
use crate::constants::Chattype;
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
@@ -20,12 +35,12 @@ use crate::download::DownloadState;
|
||||
use crate::message::{Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimefactory::wrapped_base64_encode;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Param;
|
||||
use crate::param::Params;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::scheduler::InterruptInfo;
|
||||
use crate::tools::strip_rtlo_characters;
|
||||
use crate::tools::{create_smeared_timestamp, get_abs_path};
|
||||
use crate::{chat, EventType};
|
||||
use crate::tools::{
|
||||
create_outgoing_rfc724_mid, create_smeared_timestamp, get_abs_path, strip_rtlo_characters,
|
||||
};
|
||||
use crate::EventType;
|
||||
|
||||
/// The current API version.
|
||||
/// If `min_api` in manifest.toml is set to a larger value,
|
||||
@@ -665,6 +680,10 @@ impl Context {
|
||||
///
|
||||
/// Example: `{"updates": [{"payload":"any update data"},
|
||||
/// {"payload":"another update data"}]}`
|
||||
///
|
||||
/// `range` is an optional range of status update serials to send.
|
||||
/// If it is `None`, all updates are sent.
|
||||
/// This is used when a message is resent using [`crate::chat::resend_msgs`].
|
||||
pub(crate) async fn render_webxdc_status_update_object(
|
||||
&self,
|
||||
instance_msg_id: MsgId,
|
||||
@@ -707,7 +726,7 @@ fn parse_webxdc_manifest(bytes: &[u8]) -> Result<WebxdcManifest> {
|
||||
Ok(manifest)
|
||||
}
|
||||
|
||||
async fn get_blob(archive: &mut async_zip::read::fs::ZipFileReader, name: &str) -> Result<Vec<u8>> {
|
||||
async fn get_blob(archive: &async_zip::read::fs::ZipFileReader, name: &str) -> Result<Vec<u8>> {
|
||||
let (i, _) = find_zip_entry(archive.file(), name)
|
||||
.ok_or_else(|| anyhow!("no entry found for {}", name))?;
|
||||
let mut reader = archive.entry(i).await?;
|
||||
@@ -750,10 +769,10 @@ impl Message {
|
||||
name
|
||||
};
|
||||
|
||||
let mut archive = self.get_webxdc_archive(context).await?;
|
||||
let archive = self.get_webxdc_archive(context).await?;
|
||||
|
||||
if name == "index.html" {
|
||||
if let Ok(bytes) = get_blob(&mut archive, "manifest.toml").await {
|
||||
if let Ok(bytes) = get_blob(&archive, "manifest.toml").await {
|
||||
if let Ok(manifest) = parse_webxdc_manifest(&bytes) {
|
||||
if let Some(min_api) = manifest.min_api {
|
||||
if min_api > WEBXDC_API_VERSION {
|
||||
@@ -766,15 +785,15 @@ impl Message {
|
||||
}
|
||||
}
|
||||
|
||||
get_blob(&mut archive, name).await
|
||||
get_blob(&archive, name).await
|
||||
}
|
||||
|
||||
/// Return info from manifest.toml or from fallbacks.
|
||||
pub async fn get_webxdc_info(&self, context: &Context) -> Result<WebxdcInfo> {
|
||||
ensure!(self.viewtype == Viewtype::Webxdc, "No webxdc instance.");
|
||||
let mut archive = self.get_webxdc_archive(context).await?;
|
||||
let archive = self.get_webxdc_archive(context).await?;
|
||||
|
||||
let mut manifest = get_blob(&mut archive, "manifest.toml")
|
||||
let mut manifest = get_blob(&archive, "manifest.toml")
|
||||
.await
|
||||
.map(|bytes| parse_webxdc_manifest(&bytes).unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
@@ -827,6 +846,77 @@ impl Message {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a replacement for an own WebXDC message.
|
||||
pub async fn send_webxdc_replacement(
|
||||
context: &Context,
|
||||
msg_id: MsgId,
|
||||
filename: &str,
|
||||
) -> Result<()> {
|
||||
let mut msg = Message::load_from_db(context, msg_id).await?;
|
||||
|
||||
ensure!(
|
||||
msg.from_id == ContactId::SELF,
|
||||
"Can update WebXDC only in own messages"
|
||||
);
|
||||
ensure!(
|
||||
msg.get_viewtype() == Viewtype::Webxdc,
|
||||
"Message {msg_id} is not a WebXDC instance"
|
||||
);
|
||||
let state = msg.get_state();
|
||||
match state {
|
||||
MessageState::OutFailed | MessageState::OutDelivered | MessageState::OutMdnRcvd => {}
|
||||
MessageState::Undefined
|
||||
| MessageState::InFresh
|
||||
| MessageState::InNoticed
|
||||
| MessageState::InSeen
|
||||
| MessageState::OutPreparing
|
||||
| MessageState::OutPending
|
||||
| MessageState::OutDraft => bail!("Unexpected message state: {state}"),
|
||||
}
|
||||
|
||||
let chat = Chat::load_from_db(context, msg.chat_id).await?;
|
||||
|
||||
let mut param = msg.param.clone();
|
||||
if !chat.is_protected() {
|
||||
param.remove(Param::GuaranteeE2ee);
|
||||
}
|
||||
let blob = BlobObject::new_from_path(context, Path::new(filename))
|
||||
.await
|
||||
.context("Failed to create webxdc replacement blob")?;
|
||||
param.set(Param::File, blob.as_name());
|
||||
msg.param = param;
|
||||
|
||||
// Generate new Message-ID.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs
|
||||
SET state=?, param=?
|
||||
WHERE id=?",
|
||||
(MessageState::OutPending, msg.param.to_string(), msg_id),
|
||||
)
|
||||
.await?;
|
||||
|
||||
msg.supersedes = Some(msg.rfc724_mid);
|
||||
msg.rfc724_mid = {
|
||||
let grpid = match chat.typ {
|
||||
Chattype::Group => Some(chat.grpid.as_str()),
|
||||
_ => None,
|
||||
};
|
||||
let from = context.get_primary_self_addr().await?;
|
||||
create_outgoing_rfc724_mid(grpid, &from)
|
||||
};
|
||||
|
||||
if create_send_msg_job(context, &mut msg).await?.is_some() {
|
||||
context
|
||||
.scheduler
|
||||
.interrupt_smtp(InterruptInfo::new(false))
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
@@ -895,10 +985,8 @@ mod tests {
|
||||
}
|
||||
|
||||
async fn create_webxdc_instance(t: &TestContext, name: &str, bytes: &[u8]) -> Result<Message> {
|
||||
let file = t.get_blobdir().join(name);
|
||||
tokio::fs::write(&file, bytes).await?;
|
||||
let mut instance = Message::new(Viewtype::File);
|
||||
instance.set_file(file.to_str().unwrap(), None);
|
||||
instance.set_file_from_bytes(t, name, bytes, None).await?;
|
||||
Ok(instance)
|
||||
}
|
||||
|
||||
@@ -926,10 +1014,10 @@ mod tests {
|
||||
assert_eq!(instance.chat_id, chat_id);
|
||||
|
||||
// sending using bad extension is not working, even when setting Viewtype to webxdc
|
||||
let file = t.get_blobdir().join("index.html");
|
||||
tokio::fs::write(&file, b"<html>ola!</html>").await?;
|
||||
let mut instance = Message::new(Viewtype::Webxdc);
|
||||
instance.set_file(file.to_str().unwrap(), None);
|
||||
instance
|
||||
.set_file_from_bytes(&t, "index.html", b"<html>ola!</html>", None)
|
||||
.await?;
|
||||
assert!(send_msg(&t, chat_id, &mut instance).await.is_err());
|
||||
|
||||
Ok(())
|
||||
@@ -953,14 +1041,15 @@ mod tests {
|
||||
assert_eq!(test.viewtype, Viewtype::File);
|
||||
|
||||
// sending invalid .xdc as Viewtype::Webxdc should fail already on sending
|
||||
let file = t.get_blobdir().join("invalid2.xdc");
|
||||
tokio::fs::write(
|
||||
&file,
|
||||
include_bytes!("../test-data/webxdc/invalid-no-zip-but-7z.xdc"),
|
||||
)
|
||||
.await?;
|
||||
let mut instance = Message::new(Viewtype::Webxdc);
|
||||
instance.set_file(file.to_str().unwrap(), None);
|
||||
instance
|
||||
.set_file_from_bytes(
|
||||
&t,
|
||||
"invalid2.xdc",
|
||||
include_bytes!("../test-data/webxdc/invalid-no-zip-but-7z.xdc"),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
assert!(send_msg(&t, chat_id, &mut instance).await.is_err());
|
||||
|
||||
Ok(())
|
||||
@@ -2606,4 +2695,174 @@ sth_for_the = "future""#
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests sending webxdc and replacing it with a newer version.
|
||||
///
|
||||
/// Updates should be preserved after upgrading.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_replace_webxdc() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
// Alice sends WebXDC instance.
|
||||
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("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(), "user added text");
|
||||
let original_rfc724_mid = alice_instance.rfc724_mid;
|
||||
|
||||
// Bob receives that instance.
|
||||
let alice_sent_instance = alice.pop_sent_msg().await;
|
||||
let bob_received_instance = bob.recv_msg(&alice_sent_instance).await;
|
||||
assert_eq!(bob_received_instance.get_text(), "user added text");
|
||||
|
||||
// Alice sends WebXDC update.
|
||||
alice
|
||||
.send_webxdc_status_update(alice_instance.id, r#"{"payload": 1}"#, "Alice update")
|
||||
.await?;
|
||||
alice.flush_status_updates().await?;
|
||||
let alice_sent_update = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&alice_sent_update).await;
|
||||
assert_eq!(
|
||||
bob.get_webxdc_status_updates(bob_received_instance.id, StatusUpdateSerial(0))
|
||||
.await?,
|
||||
r#"[{"payload":1,"serial":1,"max_serial":1}]"#
|
||||
);
|
||||
|
||||
// Alice sends WebXDC instance replacement.
|
||||
send_webxdc_replacement(
|
||||
&alice,
|
||||
alice_instance.id,
|
||||
"test-data/webxdc/with-minimal-manifest.xdc",
|
||||
)
|
||||
.await
|
||||
.context("Failed to send WebXDC replacement")?;
|
||||
let alice_replacement_instance = alice.get_last_msg().await;
|
||||
let alice_replacement_info = alice_replacement_instance.get_webxdc_info(&alice).await?;
|
||||
assert_eq!(alice_replacement_info.name, "nice app!");
|
||||
assert_eq!(alice_instance.id, alice_replacement_instance.id);
|
||||
let alice_sent_replacement_instance = alice.pop_sent_msg().await;
|
||||
assert!(alice_sent_replacement_instance
|
||||
.payload
|
||||
.contains(&format!("Supersedes: {original_rfc724_mid}")));
|
||||
assert_eq!(alice_replacement_instance.rfc724_mid, original_rfc724_mid);
|
||||
|
||||
// Bob receives WebXDC instance replacement.
|
||||
let bob_received_replacement_instance =
|
||||
bob.recv_msg(&alice_sent_replacement_instance).await;
|
||||
assert_eq!(
|
||||
bob_received_instance.id,
|
||||
bob_received_replacement_instance.id
|
||||
);
|
||||
assert_eq!(
|
||||
bob_received_replacement_instance.rfc724_mid,
|
||||
original_rfc724_mid
|
||||
);
|
||||
let bob_received_replacement_info = bob_received_replacement_instance
|
||||
.get_webxdc_info(&bob)
|
||||
.await?;
|
||||
assert_eq!(bob_received_replacement_info.name, "nice app!");
|
||||
|
||||
// Updates are not modified.
|
||||
assert_eq!(
|
||||
bob.get_webxdc_status_updates(
|
||||
bob_received_replacement_instance.id,
|
||||
StatusUpdateSerial(0)
|
||||
)
|
||||
.await?,
|
||||
r#"[{"payload":1,"serial":1,"max_serial":1}]"#
|
||||
);
|
||||
|
||||
// Bob is not allowed to replace the instance.
|
||||
assert!(send_webxdc_replacement(
|
||||
&bob,
|
||||
bob_received_instance.id,
|
||||
"test-data/webxdc/minimal.xdc"
|
||||
)
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
// Alice sends a second WebXDC instance replacement.
|
||||
send_webxdc_replacement(&alice, alice_instance.id, "test-data/webxdc/minimal.xdc")
|
||||
.await
|
||||
.context("Failed to send second WebXDC replacement")?;
|
||||
let alice_second_sent_replacement_instance = alice.pop_sent_msg().await;
|
||||
let bob_received_second_replacement_instance =
|
||||
bob.recv_msg(&alice_second_sent_replacement_instance).await;
|
||||
assert_eq!(
|
||||
bob_received_instance.id,
|
||||
bob_received_second_replacement_instance.id
|
||||
);
|
||||
assert_eq!(
|
||||
bob_received_second_replacement_instance.rfc724_mid,
|
||||
original_rfc724_mid
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_replace_webxdc_missing_original() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
// Alice sends WebXDC instance.
|
||||
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("user added text".to_string());
|
||||
send_msg(&alice, alice_chat.id, &mut alice_instance).await?;
|
||||
alice.pop_sent_msg().await;
|
||||
let alice_instance = alice.get_last_msg().await;
|
||||
assert_eq!(alice_instance.get_text(), "user added text");
|
||||
let original_rfc724_mid = alice_instance.rfc724_mid;
|
||||
|
||||
// Bob missed the original instance message.
|
||||
|
||||
// Alice sends WebXDC instance replacement.
|
||||
send_webxdc_replacement(&alice, alice_instance.id, "test-data/webxdc/minimal.xdc")
|
||||
.await
|
||||
.context("Failed to send WebXDC replacement")?;
|
||||
let alice_sent_replacement_instance = alice.pop_sent_msg().await;
|
||||
assert!(alice_sent_replacement_instance
|
||||
.payload
|
||||
.contains(&format!("Supersedes: {original_rfc724_mid}")));
|
||||
|
||||
// Bob receives WebXDC instance replacement.
|
||||
let bob_received_replacement_instance =
|
||||
bob.recv_msg(&alice_sent_replacement_instance).await;
|
||||
assert_eq!(
|
||||
bob_received_replacement_instance.rfc724_mid,
|
||||
original_rfc724_mid
|
||||
);
|
||||
|
||||
// Alice sends a second WebXDC instance replacement.
|
||||
send_webxdc_replacement(&alice, alice_instance.id, "test-data/webxdc/minimal.xdc")
|
||||
.await
|
||||
.context("Failed to send second WebXDC replacement")?;
|
||||
let alice_second_sent_replacement_instance = alice.pop_sent_msg().await;
|
||||
let bob_received_second_replacement_instance =
|
||||
bob.recv_msg(&alice_second_sent_replacement_instance).await;
|
||||
assert_eq!(
|
||||
bob_received_replacement_instance.id,
|
||||
bob_received_second_replacement_instance.id
|
||||
);
|
||||
assert_eq!(
|
||||
bob_received_second_replacement_instance.rfc724_mid,
|
||||
original_rfc724_mid
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
BIN
test-data/image/logo.gif
Normal file
BIN
test-data/image/logo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
BIN
test-data/image/logo.png
Normal file
BIN
test-data/image/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
1436
test-data/message/mixed-up-long.eml
Normal file
1436
test-data/message/mixed-up-long.eml
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user