mirror of
https://github.com/chatmail/core.git
synced 2026-04-05 06:52:10 +03:00
Compare commits
54 Commits
feat-iroh-
...
v1.121.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8551510cd | ||
|
|
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 | ||
|
|
d70c1d48b5 | ||
|
|
a8e0cb9b5a | ||
|
|
6ea9a8988b | ||
|
|
45e35b3571 | ||
|
|
e43f9066d8 | ||
|
|
bba6c8f15a | ||
|
|
55aaec744a | ||
|
|
2f24eddb7d | ||
|
|
a33c91afa9 |
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.
|
||||
|
||||
115
CHANGELOG.md
115
CHANGELOG.md
@@ -1,5 +1,116 @@
|
||||
# Changelog
|
||||
|
||||
## [1.121.0] - 2023-09-06
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add `dc_context_change_passphrase()`.
|
||||
- Add `Message.set_file_from_bytes()` API.
|
||||
- Add experimental API to get similar chats.
|
||||
|
||||
### Build system
|
||||
|
||||
- Build node packages on Ubuntu 18.04 instead of Debian 10.
|
||||
This reduces the requirement for glibc version from 2.28 to 2.27.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Allow membership changes by a MUA if we're not in the group ([#4624](https://github.com/deltachat/deltachat-core-rust/pull/4624)).
|
||||
- Save mime headers for messages not signed with a known key ([#4557](https://github.com/deltachat/deltachat-core-rust/pull/4557)).
|
||||
- Return from `dc_get_chatlist(DC_GCL_FOR_FORWARDING)` only chats where we can send ([#4616](https://github.com/deltachat/deltachat-core-rust/pull/4616)).
|
||||
- Do not allow dots at the end of email addresses.
|
||||
- deltachat-rpc-client: Remove `aiodns` optional dependency from required dependencies.
|
||||
`aiodns` depends on `pycares` which [fails to install in Termux](https://github.com/saghul/aiodns/issues/98).
|
||||
|
||||
## [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).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Guess message viewtype from "application/octet-stream" attachment extension ([#4378](https://github.com/deltachat/deltachat-core-rust/pull/4378)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Update `xattr` from 1.0.0 to 1.0.1 to fix UnsupportedPlatformError import.
|
||||
|
||||
### Tests
|
||||
|
||||
- webxdc: Ensure unknown WebXDC update properties do not result in an error.
|
||||
|
||||
## [1.119.0] - 2023-08-03
|
||||
|
||||
### Fixes
|
||||
|
||||
- imap: Avoid IMAP move loops when DeltaChat folder is aliased.
|
||||
- imap: Do not resync IMAP after initial configuration.
|
||||
|
||||
- webxdc: Accept WebXDC updates in mailing lists.
|
||||
- webxdc: Base64-encode WebXDC updates to prevent corruption of large unencrypted WebXDC updates.
|
||||
- webxdc: Delete old webxdc status updates during housekeeping.
|
||||
|
||||
- Return valid MsgId from `receive_imf()` when the message is replaced.
|
||||
- Emit MsgsChanged event with correct chat id for replaced messages.
|
||||
|
||||
- deltachat-rpc-server: Update tokio-tar to fix backup import.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- deltachat-rpc-client: Add `MSG_DELETED` constant.
|
||||
- Make `dc_msg_get_filename()` return the original attachment filename ([#4309](https://github.com/deltachat/deltachat-core-rust/pull/4309)).
|
||||
|
||||
### API-Changes
|
||||
|
||||
- deltachat-rpc-client: Add `Account.{import,export}_backup` methods.
|
||||
- deltachat-jsonrpc: Make `MessageObject.text` non-optional.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update default value for `show_emails` in `dc_set_config()` documentation.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Improve IMAP logs.
|
||||
|
||||
### Tests
|
||||
|
||||
- Add basic import/export test for async python.
|
||||
- Add `test_webxdc_download_on_demand`.
|
||||
- Add tests for deletion of webxdc status-updates.
|
||||
|
||||
## [1.118.0] - 2023-07-07
|
||||
|
||||
### API-Changes
|
||||
@@ -2672,3 +2783,7 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.116.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.115.0...v1.116.0
|
||||
[1.117.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.116.0...v1.117.0
|
||||
[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
|
||||
[1.121.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.120.0...v1.121.0
|
||||
|
||||
46
Cargo.lock
generated
46
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.118.0"
|
||||
version = "1.121.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -1199,7 +1179,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.118.0"
|
||||
version = "1.121.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel",
|
||||
@@ -1223,7 +1203,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "1.118.0"
|
||||
version = "1.121.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -1238,7 +1218,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.118.0"
|
||||
version = "1.121.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1263,7 +1243,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.118.0"
|
||||
version = "1.121.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"
|
||||
@@ -5778,9 +5752,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea263437ca03c1522846a4ddafbca2542d0ad5ed9b784909d4b27b76f62bc34a"
|
||||
checksum = "f4686009f71ff3e5c4dbcf1a282d0a44db3f021ba69350cd42086b3e5f1c6985"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.118.0"
|
||||
version = "1.121.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.118.0"
|
||||
version = "1.121.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
|
||||
@@ -3978,16 +4005,17 @@ char* dc_msg_get_text (const dc_msg_t* msg);
|
||||
*/
|
||||
char* dc_msg_get_subject (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Find out full path, file name and extension of the file associated with a
|
||||
* message.
|
||||
* Find out full path of the file associated with a message.
|
||||
*
|
||||
* Typically files are associated with images, videos, audios, documents.
|
||||
* Plain text messages do not have a file.
|
||||
* File name may be mangled. To obtain the original attachment filename use dc_msg_get_filename().
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return The full path, the file name, and the extension of the file associated with the message.
|
||||
* @return The full path (with file name and extension) of the file associated with the message.
|
||||
* If there is no file associated with the message, an empty string is returned.
|
||||
* NULL is never returned and the returned value must be released using dc_str_unref().
|
||||
*/
|
||||
@@ -3995,14 +4023,13 @@ char* dc_msg_get_file (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Get a base file name without the path. The base file name includes the extension; the path
|
||||
* is not returned. To get the full path, use dc_msg_get_file().
|
||||
* Get an original attachment filename, with extension but without the path. To get the full path,
|
||||
* use dc_msg_get_file().
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return The base file name plus the extension without part. If there is no file
|
||||
* associated with the message, an empty string is returned. The returned
|
||||
* value must be released using dc_str_unref().
|
||||
* @return The attachment filename. If there is no file associated with the message, an empty string
|
||||
* is returned. The returned value must be released using dc_str_unref().
|
||||
*/
|
||||
char* dc_msg_get_filename (const dc_msg_t* msg);
|
||||
|
||||
|
||||
@@ -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.118.0"
|
||||
version = "1.121.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.118.0"
|
||||
"version": "1.121.0"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.118.0"
|
||||
version = "1.121.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.118.0"
|
||||
version = "1.121.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]
|
||||
|
||||
@@ -446,7 +446,8 @@ describe('Offline Tests with unconfigured account', function () {
|
||||
context.setChatProfileImage(chatId, imagePath)
|
||||
const blobPath = context.getChat(chatId).getProfileImage()
|
||||
expect(blobPath.startsWith(blobs)).to.be.true
|
||||
expect(blobPath.endsWith(image)).to.be.true
|
||||
expect(blobPath.includes('image')).to.be.true
|
||||
expect(blobPath.endsWith('.jpeg')).to.be.true
|
||||
|
||||
context.setChatProfileImage(chatId, null)
|
||||
expect(context.getChat(chatId).getProfileImage()).to.be.equal(
|
||||
|
||||
@@ -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.118.0"
|
||||
"version": "1.121.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)
|
||||
|
||||
@@ -162,8 +162,9 @@ def test_send_file_twice_unicode_filename_mangling(tmp_path, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
basename = "somedäüta.html.zip"
|
||||
p = tmp_path / basename
|
||||
basename = "somedäüta"
|
||||
ext = ".html.zip"
|
||||
p = tmp_path / (basename + ext)
|
||||
p.write_text("some data")
|
||||
|
||||
def send_and_receive_message():
|
||||
@@ -181,12 +182,14 @@ def test_send_file_twice_unicode_filename_mangling(tmp_path, acfactory, lp):
|
||||
msg = send_and_receive_message()
|
||||
assert msg.text == "withfile"
|
||||
assert open(msg.filename).read() == "some data"
|
||||
assert msg.filename.endswith(basename)
|
||||
msg.filename.index(basename)
|
||||
assert msg.filename.endswith(ext)
|
||||
|
||||
msg2 = send_and_receive_message()
|
||||
assert msg2.text == "withfile"
|
||||
assert open(msg2.filename).read() == "some data"
|
||||
assert msg2.filename.endswith("html.zip")
|
||||
msg2.filename.index(basename)
|
||||
assert msg2.filename.endswith(ext)
|
||||
assert msg.filename != msg2.filename
|
||||
|
||||
|
||||
@@ -194,10 +197,11 @@ def test_send_file_html_attachment(tmp_path, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
basename = "test.html"
|
||||
basename = "test"
|
||||
ext = ".html"
|
||||
content = "<html><body>text</body>data"
|
||||
|
||||
p = tmp_path / basename
|
||||
p = tmp_path / (basename + ext)
|
||||
# write wrong html to see if core tries to parse it
|
||||
# (it shouldn't as it's a file attachment)
|
||||
p.write_text(content)
|
||||
@@ -211,7 +215,8 @@ def test_send_file_html_attachment(tmp_path, acfactory, lp):
|
||||
msg = ac2.get_message_by_id(ev.data2)
|
||||
|
||||
assert open(msg.filename).read() == content
|
||||
assert msg.filename.endswith(basename)
|
||||
msg.filename.index(basename)
|
||||
assert msg.filename.endswith(ext)
|
||||
|
||||
|
||||
def test_html_message(acfactory, lp):
|
||||
@@ -324,6 +329,27 @@ def test_webxdc_message(acfactory, data, lp):
|
||||
assert len(list(ac2.direct_imap.conn.fetch(AND(seen=True)))) == 1
|
||||
|
||||
|
||||
def test_webxdc_huge_update(acfactory, data, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = ac1.create_chat(ac2)
|
||||
|
||||
msg1 = Message.new_empty(ac1, "webxdc")
|
||||
msg1.set_text("message1")
|
||||
msg1.set_file(data.get_path("webxdc/minimal.xdc"))
|
||||
msg1 = chat.send_msg(msg1)
|
||||
assert msg1.is_webxdc()
|
||||
assert msg1.filename
|
||||
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.is_webxdc()
|
||||
|
||||
payload = "A" * 1000
|
||||
assert msg1.send_status_update({"payload": payload}, "some test data")
|
||||
ac2._evtracker.get_matching("DC_EVENT_WEBXDC_STATUS_UPDATE")
|
||||
update = msg2.get_status_updates()[0]
|
||||
assert update["payload"] == payload
|
||||
|
||||
|
||||
def test_webxdc_download_on_demand(acfactory, data, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
acfactory.introduce_each_other([ac1, ac2])
|
||||
@@ -350,6 +376,11 @@ def test_webxdc_download_on_demand(acfactory, data, lp):
|
||||
ac2._evtracker.get_matching("DC_EVENT_WEBXDC_STATUS_UPDATE")
|
||||
assert msg2.get_status_updates()
|
||||
|
||||
# Get a event notifying that the message disappeared from the chat.
|
||||
msgs_changed_event = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
assert msgs_changed_event.data1 == msg2.chat.id
|
||||
assert msgs_changed_event.data2 == 0
|
||||
|
||||
|
||||
def test_mvbox_sentbox_threads(acfactory, lp):
|
||||
lp.sec("ac1: start with mvbox thread")
|
||||
|
||||
@@ -49,10 +49,9 @@ class TestOnlineInCreation:
|
||||
assert str(tmp_path) != ac1.get_blobdir()
|
||||
src = tmp_path / "file.txt"
|
||||
src.write_text("hello there\n")
|
||||
chat.send_file(str(src))
|
||||
|
||||
blob_src = os.path.join(ac1.get_blobdir(), "file.txt")
|
||||
assert os.path.exists(blob_src), "file.txt not copied to blobdir"
|
||||
msg = chat.send_file(str(src))
|
||||
assert msg.filename.startswith(os.path.join(ac1.get_blobdir(), "file"))
|
||||
assert msg.filename.endswith(".txt")
|
||||
|
||||
def test_forward_increation(self, acfactory, data, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -1 +1 @@
|
||||
2023-07-07
|
||||
2023-09-06
|
||||
@@ -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();
|
||||
|
||||
449
src/chat.rs
449
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,15 +2167,23 @@ 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());
|
||||
if let (Some(filename), Some(blob_ext)) = (msg.param.get(Param::Filename), blob.suffix()) {
|
||||
let stem = match filename.rsplit_once('.') {
|
||||
Some((stem, _)) => stem,
|
||||
None => filename,
|
||||
};
|
||||
msg.param
|
||||
.set(Param::Filename, stem.to_string() + "." + blob_ext);
|
||||
}
|
||||
|
||||
if msg.viewtype == Viewtype::File || msg.viewtype == Viewtype::Image {
|
||||
// Correct the type, take care not to correct already very special
|
||||
@@ -2077,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 #{}.",
|
||||
@@ -2244,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)
|
||||
}
|
||||
|
||||
@@ -2254,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 {
|
||||
@@ -2271,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();
|
||||
|
||||
@@ -2293,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)
|
||||
}
|
||||
}?;
|
||||
@@ -2311,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
|
||||
);
|
||||
}
|
||||
@@ -2373,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?;
|
||||
@@ -2566,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?;
|
||||
}
|
||||
@@ -3363,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);
|
||||
@@ -3477,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(())
|
||||
@@ -3569,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);
|
||||
@@ -5502,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;
|
||||
@@ -5516,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(), Some(filename.to_string()));
|
||||
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);
|
||||
@@ -5533,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
|
||||
}
|
||||
@@ -5545,6 +5692,7 @@ mod tests {
|
||||
test_sticker(
|
||||
"sticker.jpg",
|
||||
include_bytes!("../test-data/image/avatar1000x1000.jpg"),
|
||||
Viewtype::Image,
|
||||
1000,
|
||||
1000,
|
||||
)
|
||||
@@ -5555,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
|
||||
}
|
||||
@@ -5571,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);
|
||||
@@ -6109,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(())
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
"##
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -650,7 +650,9 @@ mod tests {
|
||||
_ => panic!("wrong chat item"),
|
||||
};
|
||||
let msg = Message::load_from_db(&ctx1, *msgid).await.unwrap();
|
||||
|
||||
let path = msg.get_file(&ctx1).unwrap();
|
||||
assert_eq!(path.with_file_name("hello.txt"), path);
|
||||
let text = fs::read_to_string(&path).await.unwrap();
|
||||
assert_eq!(text, "i am attachment");
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
157
src/message.rs
157
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::{
|
||||
@@ -582,14 +583,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() {
|
||||
@@ -688,11 +697,13 @@ impl Message {
|
||||
&self.subject
|
||||
}
|
||||
|
||||
/// Returns base file name without the path.
|
||||
/// The base file name includes the extension.
|
||||
/// Returns original filename (as shown in chat).
|
||||
///
|
||||
/// To get the full path, use [`Self::get_file()`].
|
||||
pub fn get_filename(&self) -> Option<String> {
|
||||
if let Some(name) = self.param.get(Param::Filename) {
|
||||
return Some(name.to_string());
|
||||
}
|
||||
self.param
|
||||
.get(Param::File)
|
||||
.and_then(|file| Path::new(file).file_name())
|
||||
@@ -972,20 +983,34 @@ impl Message {
|
||||
/// the file will only be used when the message is prepared
|
||||
/// for sending.
|
||||
pub fn set_file(&mut self, file: impl ToString, filemime: Option<&str>) {
|
||||
self.param.set(Param::File, file);
|
||||
if let Some(filemime) = filemime {
|
||||
self.param.set(Param::MimeType, filemime);
|
||||
if let Some(name) = Path::new(&file.to_string()).file_name() {
|
||||
if let Some(name) = name.to_str() {
|
||||
self.param.set(Param::Filename, name);
|
||||
}
|
||||
}
|
||||
self.param.set(Param::File, file);
|
||||
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.
|
||||
@@ -1422,6 +1447,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?;
|
||||
@@ -1445,13 +1471,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
|
||||
@@ -1466,6 +1498,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));
|
||||
@@ -1634,53 +1667,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
|
||||
@@ -2287,7 +2303,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
|
||||
@@ -2423,4 +2439,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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1368,7 +1368,7 @@ impl<'a> MimeFactory<'a> {
|
||||
///
|
||||
/// This line length limit is an
|
||||
/// [RFC5322 requirement](https://tools.ietf.org/html/rfc5322#section-2.1.1).
|
||||
fn wrapped_base64_encode(buf: &[u8]) -> String {
|
||||
pub(crate) fn wrapped_base64_encode(buf: &[u8]) -> String {
|
||||
let base64 = base64::engine::general_purpose::STANDARD.encode(buf);
|
||||
let mut chars = base64.chars();
|
||||
std::iter::repeat_with(|| chars.by_ref().take(78).collect::<String>())
|
||||
@@ -1386,7 +1386,7 @@ async fn build_body_file(
|
||||
.param
|
||||
.get_blob(Param::File, context, true)
|
||||
.await?
|
||||
.context("msg has no filename")?;
|
||||
.context("msg has no file")?;
|
||||
let suffix = blob.suffix().unwrap_or("dat");
|
||||
|
||||
// Get file name to use for sending. For privacy purposes, we do
|
||||
@@ -1431,7 +1431,11 @@ async fn build_body_file(
|
||||
),
|
||||
&suffix
|
||||
),
|
||||
_ => blob.as_file_name().to_string(),
|
||||
_ => msg
|
||||
.param
|
||||
.get(Param::Filename)
|
||||
.unwrap_or_else(|| blob.as_file_name())
|
||||
.to_string(),
|
||||
};
|
||||
|
||||
/* check mimetype */
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::future::Future;
|
||||
use std::path::Path;
|
||||
use std::pin::Pin;
|
||||
use std::str;
|
||||
|
||||
@@ -11,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;
|
||||
@@ -29,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};
|
||||
@@ -861,7 +863,7 @@ impl MimeMessage {
|
||||
is_related: bool,
|
||||
) -> Result<bool> {
|
||||
let mut any_part_added = false;
|
||||
let mimetype = get_mime_type(mail)?.0;
|
||||
let mimetype = get_mime_type(mail, &get_attachment_filename(context, mail)?)?.0;
|
||||
match (mimetype.type_(), mimetype.subtype().as_str()) {
|
||||
/* Most times, multipart/alternative contains true alternatives
|
||||
as text/plain and text/html. If we find a multipart/mixed
|
||||
@@ -869,9 +871,9 @@ impl MimeMessage {
|
||||
apple mail: "plaintext" as an alternative to "html+PDF attachment") */
|
||||
(mime::MULTIPART, "alternative") => {
|
||||
for cur_data in &mail.subparts {
|
||||
if get_mime_type(cur_data)?.0 == "multipart/mixed"
|
||||
|| get_mime_type(cur_data)?.0 == "multipart/related"
|
||||
{
|
||||
let mime_type =
|
||||
get_mime_type(cur_data, &get_attachment_filename(context, cur_data)?)?.0;
|
||||
if mime_type == "multipart/mixed" || mime_type == "multipart/related" {
|
||||
any_part_added = self
|
||||
.parse_mime_recursive(context, cur_data, is_related)
|
||||
.await?;
|
||||
@@ -881,7 +883,11 @@ impl MimeMessage {
|
||||
if !any_part_added {
|
||||
/* search for text/plain and add this */
|
||||
for cur_data in &mail.subparts {
|
||||
if get_mime_type(cur_data)?.0.type_() == mime::TEXT {
|
||||
if get_mime_type(cur_data, &get_attachment_filename(context, cur_data)?)?
|
||||
.0
|
||||
.type_()
|
||||
== mime::TEXT
|
||||
{
|
||||
any_part_added = self
|
||||
.parse_mime_recursive(context, cur_data, is_related)
|
||||
.await?;
|
||||
@@ -1007,10 +1013,9 @@ impl MimeMessage {
|
||||
is_related: bool,
|
||||
) -> Result<bool> {
|
||||
// return true if a part was added
|
||||
let (mime_type, msg_type) = get_mime_type(mail)?;
|
||||
let raw_mime = mail.ctype.mimetype.to_lowercase();
|
||||
|
||||
let filename = get_attachment_filename(context, mail)?;
|
||||
let (mime_type, msg_type) = get_mime_type(mail, &filename)?;
|
||||
let raw_mime = mail.ctype.mimetype.to_lowercase();
|
||||
|
||||
let old_part_count = self.parts.len();
|
||||
|
||||
@@ -1268,6 +1273,7 @@ impl MimeMessage {
|
||||
part.mimetype = Some(mime_type);
|
||||
part.bytes = decoded_data.len();
|
||||
part.param.set(Param::File, blob.as_name());
|
||||
part.param.set(Param::Filename, filename);
|
||||
part.param.set(Param::MimeType, raw_mime);
|
||||
part.is_related = is_related;
|
||||
|
||||
@@ -1612,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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1864,7 +1866,10 @@ pub struct Part {
|
||||
}
|
||||
|
||||
/// return mimetype and viewtype for a parsed mail
|
||||
fn get_mime_type(mail: &mailparse::ParsedMail<'_>) -> Result<(Mime, Viewtype)> {
|
||||
fn get_mime_type(
|
||||
mail: &mailparse::ParsedMail<'_>,
|
||||
filename: &Option<String>,
|
||||
) -> Result<(Mime, Viewtype)> {
|
||||
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
|
||||
|
||||
let viewtype = match mimetype.type_() {
|
||||
@@ -1900,7 +1905,16 @@ fn get_mime_type(mail: &mailparse::ParsedMail<'_>) -> Result<(Mime, Viewtype)> {
|
||||
Viewtype::Unknown
|
||||
}
|
||||
}
|
||||
mime::APPLICATION => Viewtype::File,
|
||||
mime::APPLICATION => match mimetype.subtype() {
|
||||
mime::OCTET_STREAM => match filename {
|
||||
Some(filename) => match message::guess_msgtype_from_suffix(Path::new(&filename)) {
|
||||
Some((viewtype, _)) => viewtype,
|
||||
None => Viewtype::File,
|
||||
},
|
||||
None => Viewtype::File,
|
||||
},
|
||||
_ => Viewtype::File,
|
||||
},
|
||||
_ => Viewtype::Unknown,
|
||||
};
|
||||
|
||||
@@ -2144,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?;
|
||||
@@ -3090,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
|
||||
@@ -3145,7 +3160,7 @@ K+TuvC7qOah0WLFhcsXWn2+dDV1bXuAeC769TkqkpHhdXfUHnVgK3Pv7u3rVPT5AMeFUGxRB2dP4
|
||||
CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
|
||||
|
||||
------=_NextPart_000_0003_01D622B3.CA753E60--
|
||||
"##;
|
||||
"#;
|
||||
|
||||
let message = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
.await
|
||||
@@ -3520,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>
|
||||
@@ -3528,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\
|
||||
@@ -3544,7 +3559,7 @@ Subject: ...
|
||||
> Some quote.
|
||||
|
||||
Some reply
|
||||
"###;
|
||||
";
|
||||
|
||||
receive_imf(&t, raw, false).await?;
|
||||
|
||||
@@ -3562,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>
|
||||
@@ -3571,7 +3586,7 @@ Subject: subject
|
||||
Chat-Disposition-Notification-To: alice@example.org
|
||||
|
||||
Message.
|
||||
"###;
|
||||
";
|
||||
|
||||
// Bob receives message.
|
||||
receive_imf(&bob, raw, false).await?;
|
||||
@@ -3755,4 +3770,22 @@ Content-Disposition: reaction\n\
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_jpeg_as_application_octet_stream() -> Result<()> {
|
||||
let context = TestContext::new_alice().await;
|
||||
let raw = include_bytes!("../test-data/message/jpeg-as-application-octet-stream.eml");
|
||||
|
||||
let msg = MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msg.parts.len(), 1);
|
||||
assert_eq!(msg.parts[0].typ, Viewtype::Image);
|
||||
|
||||
receive_imf(&context, &raw[..], false).await?;
|
||||
let msg = context.get_last_msg().await;
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::Image);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
15
src/param.rs
15
src/param.rs
@@ -21,6 +21,9 @@ pub enum Param {
|
||||
/// For messages and jobs
|
||||
File = b'f',
|
||||
|
||||
/// For messages: original filename (as shown in chat)
|
||||
Filename = b'v',
|
||||
|
||||
/// For messages: This name should be shown instead of contact.get_display_name()
|
||||
/// (used if this is a mailinglist
|
||||
/// or explicitly set using set_override_sender_name(), eg. by bots)
|
||||
@@ -278,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()
|
||||
@@ -528,7 +541,7 @@ mod tests {
|
||||
|
||||
fs::write(fname, b"boo").await.unwrap();
|
||||
let blob = p.get_blob(Param::File, &t, true).await.unwrap().unwrap();
|
||||
assert_eq!(blob, BlobObject::from_name(&t, "foo".to_string()).unwrap());
|
||||
assert!(blob.as_file_name().starts_with("foo"));
|
||||
|
||||
// Blob in blobdir, expect blob.
|
||||
let bar_path = t.get_blobdir().join("bar");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
|
||||
// check, if the mail is already in our database.
|
||||
// make sure, this check is done eg. before securejoin-processing.
|
||||
let replace_partial_download =
|
||||
let (replace_partial_download, replace_chat_id) =
|
||||
if let Some(old_msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? {
|
||||
let msg = Message::load_from_db(context, old_msg_id).await?;
|
||||
if msg.download_state() != DownloadState::Done && is_partial_download.is_none() {
|
||||
@@ -152,14 +152,14 @@ pub(crate) async fn receive_imf_inner(
|
||||
context,
|
||||
"Message already partly in DB, replacing by full message."
|
||||
);
|
||||
Some(old_msg_id)
|
||||
(Some(old_msg_id), Some(msg.chat_id))
|
||||
} else {
|
||||
// the message was probably moved around.
|
||||
info!(context, "Message already in DB, doing nothing.");
|
||||
return Ok(None);
|
||||
}
|
||||
} else {
|
||||
None
|
||||
(None, None)
|
||||
};
|
||||
|
||||
let prevent_rename =
|
||||
@@ -347,8 +347,8 @@ pub(crate) async fn receive_imf_inner(
|
||||
}
|
||||
}
|
||||
|
||||
if replace_partial_download.is_some() {
|
||||
context.emit_msgs_changed(chat_id, MsgId::new(0));
|
||||
if let Some(replace_chat_id) = replace_chat_id {
|
||||
context.emit_msgs_changed(replace_chat_id, MsgId::new(0));
|
||||
} else if !chat_id.is_trash() {
|
||||
let fresh = received_msg.state == MessageState::InFresh;
|
||||
for msg_id in &received_msg.msg_ids {
|
||||
@@ -1068,7 +1068,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()
|
||||
@@ -1655,19 +1655,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 +2348,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"
|
||||
|
||||
@@ -1456,7 +1458,9 @@ async fn test_pdf_filename_simple() {
|
||||
.await;
|
||||
assert_eq!(msg.viewtype, Viewtype::File);
|
||||
assert_eq!(msg.text, "mail body");
|
||||
assert_eq!(msg.param.get(Param::File).unwrap(), "$BLOBDIR/simple.pdf");
|
||||
let file_path = msg.param.get(Param::File).unwrap();
|
||||
assert!(file_path.starts_with("$BLOBDIR/simple"));
|
||||
assert!(file_path.ends_with(".pdf"));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -1470,10 +1474,9 @@ async fn test_pdf_filename_continuation() {
|
||||
.await;
|
||||
assert_eq!(msg.viewtype, Viewtype::File);
|
||||
assert_eq!(msg.text, "mail body");
|
||||
assert_eq!(
|
||||
msg.param.get(Param::File).unwrap(),
|
||||
"$BLOBDIR/test pdf äöüß.pdf"
|
||||
);
|
||||
let file_path = msg.param.get(Param::File).unwrap();
|
||||
assert!(file_path.starts_with("$BLOBDIR/test pdf äöüß"));
|
||||
assert!(file_path.ends_with(".pdf"));
|
||||
}
|
||||
|
||||
/// HTML-images may come with many embedded images, eg. tiny icons, corners for formatting,
|
||||
@@ -2797,7 +2800,7 @@ Reply from different address
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_long_filenames() -> Result<()> {
|
||||
async fn test_long_and_duplicated_filenames() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
@@ -2809,6 +2812,7 @@ async fn test_long_filenames() -> Result<()> {
|
||||
"foo. .tar.gz",
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.tar.gz",
|
||||
"a.tar.gz",
|
||||
"a.tar.gz",
|
||||
"a.a..a.a.a.a.tar.gz",
|
||||
] {
|
||||
let attachment = alice.blobdir.join(filename_sent);
|
||||
@@ -2823,22 +2827,19 @@ async fn test_long_filenames() -> Result<()> {
|
||||
|
||||
let msg_bob = bob.recv_msg(&sent).await;
|
||||
|
||||
async fn check_message(msg: &Message, t: &TestContext, content: &str) {
|
||||
async fn check_message(msg: &Message, t: &TestContext, filename: &str, content: &str) {
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::File);
|
||||
let resulting_filename = msg.get_filename().unwrap();
|
||||
assert_eq!(resulting_filename, filename);
|
||||
let path = msg.get_file(t).unwrap();
|
||||
assert!(
|
||||
resulting_filename.ends_with(".tar.gz"),
|
||||
"{resulting_filename:?} doesn't end with .tar.gz, path: {path:?}"
|
||||
);
|
||||
assert!(
|
||||
path.to_str().unwrap().ends_with(".tar.gz"),
|
||||
"path {path:?} doesn't end with .tar.gz"
|
||||
);
|
||||
assert_eq!(fs::read_to_string(path).await.unwrap(), content);
|
||||
}
|
||||
check_message(&msg_alice, &alice, &content).await;
|
||||
check_message(&msg_bob, &bob, &content).await;
|
||||
check_message(&msg_alice, &alice, filename_sent, &content).await;
|
||||
check_message(&msg_bob, &bob, filename_sent, &content).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -3561,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,
|
||||
)
|
||||
|
||||
@@ -9,7 +9,6 @@ use crate::contact::{Contact, ContactId};
|
||||
use crate::context::Context;
|
||||
use crate::message::{Message, MessageState, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Param;
|
||||
use crate::stock_str;
|
||||
use crate::tools::truncate;
|
||||
|
||||
@@ -133,14 +132,8 @@ impl Message {
|
||||
append_text = false;
|
||||
stock_str::ac_setup_msg_subject(context).await
|
||||
} else {
|
||||
let file_name: String = self
|
||||
.param
|
||||
.get_path(Param::File, context)
|
||||
.unwrap_or(None)
|
||||
.and_then(|path| {
|
||||
path.file_name()
|
||||
.map(|fname| fname.to_string_lossy().into_owned())
|
||||
})
|
||||
let file_name = self
|
||||
.get_filename()
|
||||
.unwrap_or_else(|| String::from("ErrFileName"));
|
||||
let label = if self.viewtype == Viewtype::Audio {
|
||||
stock_str::audio(context).await
|
||||
@@ -200,6 +193,7 @@ impl Message {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::param::Param;
|
||||
use crate::test_utils as test;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
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(),
|
||||
|
||||
114
src/webxdc.rs
114
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;
|
||||
@@ -18,6 +32,7 @@ use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
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;
|
||||
@@ -535,13 +550,16 @@ impl Context {
|
||||
}
|
||||
|
||||
pub(crate) fn build_status_update_part(&self, json: &str) -> PartBuilder {
|
||||
let encoded_body = wrapped_base64_encode(json.as_bytes());
|
||||
|
||||
PartBuilder::new()
|
||||
.content_type(&"application/json".parse::<mime::Mime>().unwrap())
|
||||
.header((
|
||||
"Content-Disposition",
|
||||
"attachment; filename=\"status-update.json\"",
|
||||
))
|
||||
.body(json)
|
||||
.header(("Content-Transfer-Encoding", "base64"))
|
||||
.body(encoded_body)
|
||||
}
|
||||
|
||||
/// Receives status updates from receive_imf to the database
|
||||
@@ -561,7 +579,6 @@ impl Context {
|
||||
json: &str,
|
||||
) -> Result<()> {
|
||||
let msg = Message::load_from_db(self, msg_id).await?;
|
||||
let chat_id = msg.chat_id;
|
||||
let (timestamp, mut instance, can_info_msg) = if msg.viewtype == Viewtype::Webxdc {
|
||||
(msg.timestamp_sort, msg, false)
|
||||
} else if let Some(parent) = msg.parent(self).await? {
|
||||
@@ -575,17 +592,16 @@ impl Context {
|
||||
} else {
|
||||
bail!("receive_status_update: status message has no parent.")
|
||||
};
|
||||
let chat_id = instance.chat_id;
|
||||
|
||||
if from_id != ContactId::SELF
|
||||
&& !chat::is_contact_in_chat(self, instance.chat_id, from_id).await?
|
||||
{
|
||||
if from_id != ContactId::SELF && !chat::is_contact_in_chat(self, chat_id, from_id).await? {
|
||||
let chat_type: Chattype = self
|
||||
.sql
|
||||
.query_get_value("SELECT type FROM chats WHERE id=?", (chat_id,))
|
||||
.await?
|
||||
.with_context(|| format!("Chat type for chat {chat_id} not found"))?;
|
||||
if chat_type != Chattype::Mailinglist {
|
||||
bail!("receive_status_update: status sender not chat member.")
|
||||
bail!("receive_status_update: status sender {from_id} is not a member of chat {chat_id}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -663,6 +679,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,
|
||||
@@ -705,7 +725,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?;
|
||||
@@ -748,10 +768,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 {
|
||||
@@ -764,15 +784,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();
|
||||
@@ -893,10 +913,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)
|
||||
}
|
||||
|
||||
@@ -924,10 +942,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(())
|
||||
@@ -951,14 +969,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(())
|
||||
@@ -1086,7 +1105,7 @@ mod tests {
|
||||
.await?;
|
||||
let instance = t.get_last_msg().await;
|
||||
assert_eq!(instance.viewtype, Viewtype::Webxdc);
|
||||
assert_eq!(instance.get_filename(), Some("minimal.xdc".to_string()));
|
||||
assert_eq!(instance.get_filename().unwrap(), "minimal.xdc");
|
||||
|
||||
receive_imf(
|
||||
&t,
|
||||
@@ -1096,7 +1115,7 @@ mod tests {
|
||||
.await?;
|
||||
let instance = t.get_last_msg().await;
|
||||
assert_eq!(instance.viewtype, Viewtype::File); // we require the correct extension, only a mime type is not sufficient
|
||||
assert_eq!(instance.get_filename(), Some("index.html".to_string()));
|
||||
assert_eq!(instance.get_filename().unwrap(), "index.html");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1784,10 +1803,9 @@ mod tests {
|
||||
// bob receives the instance together with the initial updates in a single message
|
||||
let bob_instance = bob.recv_msg(&sent1).await;
|
||||
assert_eq!(bob_instance.viewtype, Viewtype::Webxdc);
|
||||
assert_eq!(bob_instance.get_filename(), Some("minimal.xdc".to_string()));
|
||||
assert_eq!(bob_instance.get_filename().unwrap(), "minimal.xdc");
|
||||
assert!(sent1.payload().contains("Content-Type: application/json"));
|
||||
assert!(sent1.payload().contains("status-update.json"));
|
||||
assert!(sent1.payload().contains(r#""payload":{"foo":"bar"}"#));
|
||||
assert_eq!(
|
||||
bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0))
|
||||
.await?,
|
||||
@@ -2567,4 +2585,42 @@ sth_for_the = "future""#
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests extensibility of WebXDC updates.
|
||||
///
|
||||
/// If an update sent by WebXDC contains unknown properties,
|
||||
/// such as `aNewUnknownProperty` or a reserved property
|
||||
/// like `serial` or `max_serial`,
|
||||
/// they are silently dropped and are not sent over the wire.
|
||||
///
|
||||
/// This ensures new WebXDC can try to send new properties
|
||||
/// added in later revisions of the WebXDC API
|
||||
/// and this will not result in a failure to send the whole update.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_webxdc_status_update_extensibility() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?;
|
||||
|
||||
let bob_instance = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
|
||||
alice
|
||||
.send_webxdc_status_update(
|
||||
alice_instance.id,
|
||||
r#"{"payload":"p","info":"i","aNewUnknownProperty":"x","max_serial":123}"#,
|
||||
"Some description",
|
||||
)
|
||||
.await?;
|
||||
alice.flush_status_updates().await?;
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
|
||||
assert_eq!(
|
||||
bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0))
|
||||
.await?,
|
||||
r#"[{"payload":"p","info":"i","serial":1,"max_serial":1}]"#
|
||||
);
|
||||
|
||||
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 |
76
test-data/message/jpeg-as-application-octet-stream.eml
Normal file
76
test-data/message/jpeg-as-application-octet-stream.eml
Normal file
@@ -0,0 +1,76 @@
|
||||
X-Mozilla-Status: 0801
|
||||
X-Mozilla-Status2: 10000000
|
||||
Content-Type: multipart/mixed; boundary="------------L1v4sF5IlAZ0HirXymXElgpK"
|
||||
Message-ID: <1e3b3bb0-f34f-71e2-6b86-bce80bef2c6f@example.org>
|
||||
Date: Thu, 3 Aug 2023 13:31:01 -0300
|
||||
MIME-Version: 1.0
|
||||
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101
|
||||
Thunderbird/102.13.0
|
||||
Content-Language: en-US
|
||||
To: bob@example.net
|
||||
From: Alice <alice@example.org>
|
||||
X-Identity-Key: id3
|
||||
Fcc: imap://alice%40example.org@in.example.org/Sent
|
||||
|
||||
This is a multi-part message in MIME format.
|
||||
--------------L1v4sF5IlAZ0HirXymXElgpK
|
||||
Content-Type: text/plain; charset=UTF-8; format=flowed
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
|
||||
--------------L1v4sF5IlAZ0HirXymXElgpK
|
||||
Content-Type: application/octet-stream; name="rusty_deltachat_logo.jpeg"
|
||||
Content-Disposition: attachment; filename="rusty_deltachat_logo.jpeg"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
/9j/4AAQSkZJRgABAQIAzADMAAD/2wBDAP//////////////////////////////////////
|
||||
////////////////////////////////////////////////2wBDAf//////////////////
|
||||
////////////////////////////////////////////////////////////////////wAAR
|
||||
CAEAAQADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAA
|
||||
AgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkK
|
||||
FhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWG
|
||||
h4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl
|
||||
5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREA
|
||||
AgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYk
|
||||
NOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOE
|
||||
hYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk
|
||||
5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwCSiiigAooooAKKKKACiiigAooprHAzQA6i
|
||||
mhgfY0McA0AAYGnVX6U/efagA3Hdnt6VL15qvS5OMZ4oAUt82R26f596lByM1BSgkcA0APZ+
|
||||
eO3X3qSq9PV8DBoAl6UVCzZ+lPQ8fSgB9FFICD0oAWiiigAooooAKKKKACiiigAooooAKKKK
|
||||
ACiiigApCcDNLTH6UAKGB9j6UjkYx3qKigAooooAKKKKACiiigAooooAKKKKAClBI6UlFACk
|
||||
k9akTofrUVKCR0oAnoqEuT7fSnp0P1oAfRRRQAUUUUAFFFFABRRRQAUUUUAFIwyDTd496QuM
|
||||
cUAMBI6GkJJ60UUAFFFFABRSqMmpgAOlAEYQnrxTtg96fSEgdTQAmxfT+dLtX0FJvX1o3r6/
|
||||
zoAXavoKTYvp/OlBB6GloAZsHvTSh7HNS0UAQEEdRSVYqNk7j8qAI6KKKACnq2ODTKKAJS47
|
||||
CnDkA1AOtWKACiiigAooooAKKKKACimscDOM03zPb9f/AK1ADXGD9eabSk5OTSUAFFFFABRR
|
||||
RQBMowB7806kXoPpQeh+hoAjZ+w/OmUUUAFFFFABTw/r+dMooAsdaKhDEfT0qUEHkUALRRRQ
|
||||
BG69x+NR1O33T9KgoAKKKKACnBiBim0UASKSTyeKkpiYwfWn0AFFFFABRRTWbb2zQA4jPFV6
|
||||
eX46Y/GmUAFFFFABRRRQAUUUUASIe35VJVepVbPB6/zoAay45HT+VMqxTCgPTg0ARUUpBHWk
|
||||
oAKKKKAClBI6UlFAEocd+KXevr+hqGigBzNn6U2iigAooooAKKKKAAEjpTgWJxk02pExk+tA
|
||||
ElFFFABSEZGKCQOtNLjtzQBF0ooooAKKKKACiiigAooooAKKKKAJFfsfz/xqSq9OViPcUATY
|
||||
z1qMp6flTwQelLQBX6UVOQD1qMoR05/nQAyiiigAooooAKKKKACiiigAooooAKAcdKKKAHBm
|
||||
+tTUxAMZ70+gBCARioSCDzU+cdahY5PHagBtFFFABRRRQAUUUUAFFFFABRRRQAUUdelPCHvx
|
||||
QA0EjpUqsD9fSkCD3NOCgdqAFooooAaVB+vrURUj/Gp6KAK9FSMncfl/hUdABRRRQAUUUUAF
|
||||
FFFABRRRQA4MR0p6tnjHNRgZOKmCgUARsp69R/KmVYqA9Tj1oASiiigAooooAKKKKACiilAJ
|
||||
4FACVIE9fypwUD6+tOoAQADpS0UhIHJoAWkLAdTUZcnpwKZQBIXHYUB+eelR0UAWKKiRux/C
|
||||
paACmMueR1/nT6KAK9FSsueR1/nUVABRRRQAUUUUAFFFFACg4OalDA+31qGpFTufyoAazE8d
|
||||
B6U2pyAetQsMEigBKKKKACiiigAooooABzxU4GBTEHf8qkoAKKKKADpUDHJ/lT3Pb8TUdABR
|
||||
RRQAUUUUAFTKcj3FQ05Tg/pQBNRRRQAVE4wc9j/OpaQjIIoAgooooAKKKKACiiigBRwQanqv
|
||||
T03Z9vegB7Nj61DUxUGmMuKAGUUUUAFFFFABRRSr1H1oAmAwAKWiigAooooAgY5JpKKKACii
|
||||
igAooooAKKKKAJwcgGlpifdp9ABRRRQBCwwx/Om09+o+lMoAKKKKACiiigCRB1qSoAcHNTA5
|
||||
GaAFqJ2zwPxqWo9h9aAI6KUjBwaSgAooooAKcv3hTaUdR9aAJ6KKKACkPQ/Q0tFAFeiiigAo
|
||||
oooAKKKKACiiigCVOh+v9BT6an3adQAUUUUARP1H0plOc5Y/lTaACiiigApVGSBSUdOaAJ8D
|
||||
0FLSA5GaWgAoooPTjrQBE5yfpTKfsb2ppBBwaAEooooAKKKKAJ1OQDS1Ehwcev8AOpaACiii
|
||||
gCAjBP1pKe45z60ygAooooAKKKKACiinIMn2FAEoGABS0UUAFITgZpaidsnHYUAMooooAKKK
|
||||
KACpti+n6moalVs8Hr/OgBwGOBS0UUAFFFFABUb9hUlRlCSTkUAR0U8oQM9aZQAUUUUAFSq2
|
||||
eD1/nUVFAFiimK2eD1/nT6AEIyMVCQQcGp6QgHrQBBRTyh7c03afQ0AJRS7T6GnBD34oAaAS
|
||||
cCpgMDFAAHSloAKKKjZ+w/P/AAoAGbsPxqOiigAooooAKKKKAFUZOKlCgf41EDg5FSBwevFA
|
||||
D6KKKACiiigAooooAQnAJqCpyMjFIEUds/WgCGinuoHI/KmUAFFFFABTw5HXn+dMooAnBB6G
|
||||
lqvTgxHegCaiot59BS+Z7frQBJRUfme1JvPsKAJaaXA96iJJ6mkoAcWJ+npTaKKACiiigAoo
|
||||
ooAKKKKACpVXHJ61FUocYGetAD6KQEHoaWgAooooAKKKKACiiigBj9B9aaEJ68VLRQBAQR1p
|
||||
KlfoPrTApNADaKUgjrSUAFFFFABRRRQAUUUUAFFFFABRRSkEdRQAlFKBkgVKUGOOKAIaKXBz
|
||||
ipto9BQBBRQRg4p+w4z39KAGUUUUASouOe5/lT6QHIzS0AFFFFABRRRQAUUUUAFFFFACEA9a
|
||||
WiigBr/d+lRqufpU1FAEbJgZFR1YqNU557dKAG7W9KbVioip3YHegBoBPQUlTgYGKay55HX+
|
||||
dADFXdSshHPWnqMD3p1AESdfwqWkAA6d6WgBgQA5H5U+iigBMDOe9LRRQAm0ZzS0UUARFTu9
|
||||
jzmpNoxjFLRQAgGOlLRRQB//2Q==
|
||||
|
||||
--------------L1v4sF5IlAZ0HirXymXElgpK--
|
||||
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