Compare commits

..

1 Commits

Author SHA1 Message Date
link2xt
48eb400a69 imap: skip sync flags update if highest modseq has not increased 2022-01-30 22:44:19 +00:00
90 changed files with 4202 additions and 5875 deletions

View File

@@ -3,7 +3,7 @@ updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "monthly"
interval: "daily"
commit-message:
prefix: "cargo"
open-pull-requests-limit: 50
open-pull-requests-limit: 10

View File

@@ -45,7 +45,7 @@ jobs:
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --workspace --tests --examples --benches
args: --workspace --tests --examples
docs:
name: Rust doc comments
@@ -77,19 +77,19 @@ jobs:
include:
# Currently used Rust version, same as in `rust-toolchain` file.
- os: ubuntu-latest
rust: 1.60.0
rust: 1.54.0
python: 3.9
- os: windows-latest
rust: 1.60.0
rust: 1.54.0
python: false # Python bindings compilation on Windows is not supported.
# Minimum Supported Rust Version = 1.56.0
# Minimum Supported Rust Version = 1.51.0
#
# Minimum Supported Python Version = 3.7
# This is the minimum version for which manylinux Python wheels are
# built.
- os: ubuntu-latest
rust: 1.56.0
rust: 1.51.0
python: 3.7
runs-on: ${{ matrix.os }}
steps:

View File

@@ -1,21 +0,0 @@
name: Dependabot auto-approve
on: pull_request
permissions:
pull-requests: write
jobs:
dependabot:
runs-on: ubuntu-latest
if: ${{ github.actor == 'dependabot[bot]' }}
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v1.1.1
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Approve a PR
run: gh pr review --approve "$PR_URL"
env:
PR_URL: ${{github.event.pull_request.html_url}}
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}

View File

@@ -1,28 +0,0 @@
name: Build & Deploy Documentation on rs.delta.chat
on:
push:
branches:
- master
- docs-gh-action
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
- name: Build the documentation with cargo
run: |
cargo doc --package deltachat --no-deps
- name: Upload to rs.delta.chat
uses: up9cloud/action-rsync@v1.3
env:
USER: ${{ secrets.USERNAME }}
KEY: ${{ secrets.KEY }}
HOST: "delta.chat"
SOURCE: "target/doc"
TARGET: "/var/www/html/rs/"

View File

@@ -1,28 +0,0 @@
name: Build & Deploy Documentation on cffi.delta.chat
on:
push:
branches:
- master
- docs-gh-action
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
- name: Build the documentation with cargo
run: |
cargo doc --package deltachat_ffi --no-deps
- name: Upload to cffi.delta.chat
uses: up9cloud/action-rsync@v1.3
env:
USER: ${{ secrets.USERNAME }}
KEY: ${{ secrets.KEY }}
HOST: "delta.chat"
SOURCE: "target/doc"
TARGET: "/var/www/html/cffi/"

4
.gitignore vendored
View File

@@ -29,7 +29,3 @@ deltachat-ffi/xml
coverage/
.DS_Store
.vscode/launch.json
python/accounts.txt
python/all-testaccounts.txt
tmp/

View File

@@ -2,124 +2,20 @@
## Unreleased
### API-Changes
- replaced stock string `DC_STR_ONE_MOMENT` by `DC_STR_NOT_CONNECTED` #3222
### Fixes
- Take `delete_device_after` into account when calculating ephemeral loop timeout #3211
- Fix a bug where a blocked contact could send a contact request #3218
- Make sure, videochat-room-names are always URL-safe #3231
- Try removing account folder multiple times in case of failure #3229
- Ignore messages from all spam folders if there are many #3246
### Changes
- Further improve finding the correct server after logging in #3208
- `get_connectivity_html()` returns HTML as non-scalable #3213
- add update-serial to `DC_EVENT_WEBXDC_STATUS_UPDATE` #3215
- Speed up message receiving via IMAP a bit #3225
- mark messages as seen on IMAP in batches #3223
- remove Received: based draft detection heuristic #3230
## 1.77.0
### API changes
- change semantics of `dc_get_webxdc_status_updates()` second parameter
and remove update-id from `DC_EVENT_WEBXDC_STATUS_UPDATE` #3081
### Changes
- add more SMTP logging #3093
- place common headers like `From:` before the large `Autocrypt:` header #3079
- keep track of securejoin joiner status in database to survive restarts #2920
- remove never used `SentboxMove` option #3111
- improve speed by caching config values #3131 #3145
- optimize `markseen_msgs` #3141
- automatically accept chats with outgoing messages #3143
- `dc_receive_imf` refactorings #3154 #3156 #3159
- add index to speedup deletion of expired ephemeral messages #3155
- muted chats stay archived on new messages #3184
- support `min_api` from Webxdc manifests #3206
- do not read whole webxdc file into memory #3109
- improve tests, refactorings #3073 #3096 #3102 #3108 #3139 #3128 #3133 #3142 #3153 #3151 #3174 #3170 #3148 #3179 #3185
- improve documentation #2983 #3112 #3103 #3118 #3120
### Fixes
- speed up loading of chat messages by a factor of 20 #3171 #3194 #3173
- fix an issue where the app crashes when trying to export a backup #3195
- hopefully fix a bug where outgoing messages appear twice with Amazon SES #3077
- do not delete messages without Message-IDs as duplicates #3095
- assign replies from a different email address to the correct chat #3119
- assing outgoing private replies to the correct chat #3177
- start ephemeral timer when seen status is synchronized via IMAP #3122
- do not create empty contact requests with "setup changed" messages;
instead, send a "setup changed" message into all chats we share with the peer #3187
- do not delete duplicate messages on IMAP immediately to accidentally deleting
the last copy #3138
- clear more columns when message expires due to `delete_device_after` setting #3181
- do not try to use stale SMTP connections #3180
- slightly improve finding the correct server after logging in #3207
- retry message sending automatically if loop is not interrupted #3183
- fix a bug where sometimes the file extension of a long filename containing a dot was cropped #3098
## 1.76.0
### Changes
- move messages in batches #3058
- delete messages in batches #3060
- python: remove arbitrary timeouts from tests #3059
- refactorings #3026
### Fixes
- avoid archived, fresh chats #3053
- Also resync UIDs in folders that are not configured #2289
- treat "NO" IMAP response to MOVE and COPY commands as an error #3058
- Fix a bug where messages in the Spam folder created contact requests #3015
- Fix a bug where drafts disappeared after some days #3067
- Parse MS Exchange read receipts and mark the original message as read #3075
- do not retry message sending infinitely in case of permanent SMTP failure #3070
- set message state to failed when retry limit is exceeded #3072
## 1.75.0
### Changes
- optimize `delete_expired_imap_messages()` #3047
## 1.74.0
### Fixes
- avoid reconnection loop when message without Message-ID is marked as seen #3044
## 1.73.0
### API changes
- added `only_fetch_mvbox` config #3028
### Changes
- don't watch Sent folder by default #3025
- use webxdc app name in chatlist/quotes/replies etc. #3027
- refactorings #3023
- remove direct dependency on `byteorder` crate #3031
- make it possible to cancel message sending by removing the message #3034,
this was previosuly removed in 1.71.0 #2939
- synchronize Seen flags only on watched folders to speed up
folder scanning #3041
- remove direct dependency on `byteorder` crate #3031
- refactorings #3023 #3013
- update provider database #3043
- improve documentation #3017 #3018 #3021
- always skip Seen flag synchronization when there are no updates #3039
### Fixes
- fix splitting off text from webxdc messages #3032
- call slow `delete_expired_imap_messages()` less often #3037
- make synchronization of Seen status more robust in case unsolicited FETCH
result without UID is returned #3022
- fetch Inbox before scanning folders to ensure iOS does
not kill the app before it gets to fetch the Inbox in background #3040
## 1.72.0

View File

@@ -4,18 +4,10 @@ include(GNUInstallDirs)
find_program(CARGO cargo)
if(APPLE)
set(DYNAMIC_EXT "dylib")
elseif(UNIX)
set(DYNAMIC_EXT "so")
else()
set(DYNAMIC_EXT "dll")
endif()
add_custom_command(
OUTPUT
"target/release/libdeltachat.a"
"target/release/libdeltachat.${DYNAMIC_EXT}"
"target/release/libdeltachat.so"
"target/release/pkgconfig/deltachat.pc"
COMMAND
PREFIX=${CMAKE_INSTALL_PREFIX}
@@ -40,11 +32,11 @@ add_custom_target(
ALL
DEPENDS
"target/release/libdeltachat.a"
"target/release/libdeltachat.${DYNAMIC_EXT}"
"target/release/libdeltachat.so"
"target/release/pkgconfig/deltachat.pc"
)
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(FILES "target/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "target/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "target/release/libdeltachat.so" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "target/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)

628
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
[package]
name = "deltachat"
version = "1.77.0"
version = "1.72.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021"
edition = "2018"
license = "MPL-2.0"
rust-version = "1.56"
resolver = "2"
[profile.dev]
debug = 0
@@ -12,6 +12,9 @@ debug = 0
[profile.release]
lto = true
[patch.crates-io]
rusqlite = { git = "https://github.com/rusqlite/rusqlite", branch="master" }
[dependencies]
deltachat_derive = { path = "./deltachat_derive" }
@@ -19,9 +22,9 @@ ansi_term = { version = "0.12.1", optional = true }
anyhow = "1"
async-imap = { git = "https://github.com/async-email/async-imap" }
async-native-tls = { version = "0.3" }
async-smtp = { git = "https://github.com/async-email/async-smtp", branch="master", default-features=false, features = ["smtp-transport", "socks5"] }
async-std-resolver = "0.21"
async-std = { version = "1" }
async-smtp = { git = "https://github.com/async-email/async-smtp", branch="master", features = ["socks5"] }
async-std-resolver = "0.20"
async-std = { version = "1", features = ["unstable"] }
async-tar = { version = "0.4", default-features=false }
async-trait = "0.1"
backtrace = "0.3"
@@ -34,26 +37,27 @@ encoded-words = { git = "https://github.com/async-email/encoded-words", branch="
escaper = "0.1"
futures = "0.3"
hex = "0.4.0"
image = { version = "0.24.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
imap-proto = "0.14.3"
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = "0.2"
log = {version = "0.4.16", optional = true }
log = {version = "0.4.8", optional = true }
mailparse = "0.13"
native-tls = "0.2"
num_cpus = "1.13"
num-derive = "0.3"
num-traits = "0.2"
once_cell = "1.10.0"
once_cell = "1.9.0"
percent-encoding = "2.0"
pgp = { version = "0.7", default-features = false }
pretty_env_logger = { version = "0.4", optional = true }
quick-xml = "0.22"
r2d2 = "0.8"
r2d2_sqlite = "0.20"
r2d2_sqlite = "0.19"
rand = "0.7"
regex = "1.5"
rusqlite = { version = "0.27", features = ["sqlcipher"] }
rusqlite = { version = "0.26", features = ["sqlcipher"] }
rust-hsluv = "0.1"
rustyline = { version = "9", optional = true }
sanitize-filename = "0.3"
@@ -62,8 +66,9 @@ serde = { version = "1.0", features = ["derive"] }
sha-1 = "0.10"
sha2 = "0.10"
smallvec = "1"
strum = "0.24"
strum_macros = "0.24"
stop-token = "0.7"
strum = "0.23"
strum_macros = "0.23"
surf = { version = "2.3", default-features = false, features = ["h1-client"] }
thiserror = "1"
toml = "0.5"
@@ -72,14 +77,15 @@ uuid = { version = "0.8", features = ["serde", "v4"] }
fast-socks5 = "0.4"
humansize = "1"
qrcodegen = "1.7.0"
tagger = "4.3.3"
textwrap = "0.15.0"
zip = { version = "0.6.2", default-features = false, features = ["deflate"] }
tagger = "4.2.1"
textwrap = "0.14.2"
zip = { version = "0.5.13", default-features = false, features = ["deflate"] }
[dev-dependencies]
ansi_term = "0.12.0"
async-std = { version = "1", features = ["unstable", "attributes"] }
criterion = { version = "0.3.4", features = ["async_std"] }
criterion = "0.3"
futures-lite = "1.12"
log = "0.4"
pretty_env_logger = "0.4"
proptest = { version = "1", default-features = false, features = ["std"] }
@@ -114,18 +120,6 @@ harness = false
name = "search_msgs"
harness = false
[[bench]]
name = "receive_emails"
harness = false
[[bench]]
name = "get_chat_msgs"
harness = false
[[bench]]
name = "get_chatlist"
harness = false
[features]
default = ["vendored"]
internals = []

View File

@@ -12,8 +12,6 @@ To download and install the official compiler for the Rust programming language,
$ curl https://sh.rustup.rs -sSf | sh
```
> On Windows, you may need to also install **Perl** to be able to compile deltachat-core.
## Using the CLI client
Compile and run Delta Chat Core command line utility, using `cargo`:

View File

@@ -1,40 +0,0 @@
use async_std::path::Path;
use criterion::async_executor::AsyncStdExecutor;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::chat::{self, ChatId};
use deltachat::chatlist::Chatlist;
use deltachat::context::Context;
async fn get_chat_msgs_benchmark(dbfile: &Path, chats: &[ChatId]) {
let id = 100;
let context = Context::new(dbfile.into(), id).await.unwrap();
for c in chats.iter().take(10) {
black_box(chat::get_chat_msgs(&context, *c, 0, None).await.ok());
}
}
fn criterion_benchmark(c: &mut Criterion) {
// To enable this benchmark, set `DELTACHAT_BENCHMARK_DATABASE` to some large database with many
// messages, such as your primary account.
if let Ok(path) = std::env::var("DELTACHAT_BENCHMARK_DATABASE") {
let chats: Vec<_> = async_std::task::block_on(async {
let context = Context::new((&path).into(), 100).await.unwrap();
let chatlist = Chatlist::try_load(&context, 0, None, None).await.unwrap();
let len = chatlist.len();
(0..len).map(|i| chatlist.get_chat_id(i).unwrap()).collect()
});
c.bench_function("chat::get_chat_msgs (load messages from 10 chats)", |b| {
b.to_async(AsyncStdExecutor)
.iter(|| get_chat_msgs_benchmark(black_box(path.as_ref()), black_box(&chats)))
});
} else {
println!("env var not set: DELTACHAT_BENCHMARK_DATABASE");
}
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

View File

@@ -1,27 +0,0 @@
use criterion::async_executor::AsyncStdExecutor;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::chatlist::Chatlist;
use deltachat::context::Context;
async fn get_chat_list_benchmark(context: &Context) {
Chatlist::try_load(context, 0, None, None).await.unwrap();
}
fn criterion_benchmark(c: &mut Criterion) {
// To enable this benchmark, set `DELTACHAT_BENCHMARK_DATABASE` to some large database with many
// messages, such as your primary account.
if let Ok(path) = std::env::var("DELTACHAT_BENCHMARK_DATABASE") {
let context =
async_std::task::block_on(async { Context::new(path.into(), 100).await.unwrap() });
c.bench_function("chatlist:try_load (Get Chatlist)", |b| {
b.to_async(AsyncStdExecutor)
.iter(|| get_chat_list_benchmark(black_box(&context)))
});
} else {
println!("env var not set: DELTACHAT_BENCHMARK_DATABASE");
}
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

View File

@@ -1,84 +0,0 @@
use async_std::{path::PathBuf, task::block_on};
use criterion::{
async_executor::AsyncStdExecutor, black_box, criterion_group, criterion_main, BatchSize,
Criterion,
};
use deltachat::{
config::Config,
context::Context,
dc_receive_imf::dc_receive_imf,
imex::{imex, ImexMode},
};
use tempfile::tempdir;
async fn recv_all_emails(context: Context) -> Context {
for i in 0..100 {
let imf_raw = format!(
"Subject: Benchmark
Message-ID: Mr.OssSYnOFkhR.{i}@testrun.org
Date: Sat, 07 Dec 2019 19:00:27 +0000
To: alice@example.com
From: sender@testrun.org
Chat-Version: 1.0
Chat-Disposition-Notification-To: sender@testrun.org
Chat-User-Avatar: 0
In-Reply-To: Mr.OssSYnOFkhR.{i_dec}@testrun.org
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Hello {i}",
i = i,
i_dec = i - 1,
);
dc_receive_imf(&context, black_box(imf_raw.as_bytes()), false)
.await
.unwrap();
}
context
}
async fn create_context() -> Context {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let id = 100;
let context = Context::new(dbfile.into(), id).await.unwrap();
let backup: PathBuf = std::env::current_dir()
.unwrap()
.join("delta-chat-backup.tar")
.into();
if backup.exists().await {
println!("Importing backup");
imex(&context, ImexMode::ImportBackup, &backup, None)
.await
.unwrap();
}
let addr = "alice@example.com";
context.set_config(Config::Addr, Some(addr)).await.unwrap();
context
.set_config(Config::ConfiguredAddr, Some(addr))
.await
.unwrap();
context
.set_config(Config::Configured, Some("1"))
.await
.unwrap();
context
}
fn criterion_benchmark(c: &mut Criterion) {
let mut group = c.benchmark_group("Receive messages");
group.bench_function("Receive 100 simple text msgs", |b| {
b.to_async(AsyncStdExecutor).iter_batched(
|| block_on(create_context()),
|context| recv_all_emails(black_box(context)),
BatchSize::LargeInput,
);
});
group.finish();
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

View File

@@ -20,8 +20,6 @@ fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("search hello", |b| {
b.iter(|| block_on(async { search_benchmark(black_box(&path)).await }))
});
} else {
println!("env var not set: DELTACHAT_BENCHMARK_DATABASE");
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.77.0"
version = "1.72.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"

File diff suppressed because it is too large Load Diff

View File

@@ -31,13 +31,13 @@ use rand::Rng;
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration, ProtectionStatus};
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
use deltachat::contact::{Contact, ContactId, Origin};
use deltachat::contact::{Contact, Origin};
use deltachat::context::Context;
use deltachat::ephemeral::Timer as EphemeralTimer;
use deltachat::key::DcKey;
use deltachat::message::MsgId;
use deltachat::stock_str::StockMessage;
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::webxdc::StatusUpdateId;
use deltachat::*;
use deltachat::{accounts::Accounts, log::LogExt};
@@ -493,16 +493,14 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::ChatEphemeralTimerModified { chat_id, .. } => chat_id.to_u32() as libc::c_int,
EventType::ContactsChanged(id) | EventType::LocationChanged(id) => {
let id = id.unwrap_or_default();
id.to_u32() as libc::c_int
id as libc::c_int
}
EventType::ConfigureProgress { progress, .. } | EventType::ImexProgress(progress) => {
*progress as libc::c_int
}
EventType::ImexFileWritten(_) => 0,
EventType::SecurejoinInviterProgress { contact_id, .. }
| EventType::SecurejoinJoinerProgress { contact_id, .. } => {
contact_id.to_u32() as libc::c_int
}
| EventType::SecurejoinJoinerProgress { contact_id, .. } => *contact_id as libc::c_int,
EventType::WebxdcStatusUpdate { msg_id, .. } => msg_id.to_u32() as libc::c_int,
}
}
@@ -535,8 +533,8 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::ImexFileWritten(_)
| EventType::MsgsNoticed(_)
| EventType::ConnectivityChanged
| EventType::SelfavatarChanged => 0,
EventType::ChatModified(_) => 0,
| EventType::SelfavatarChanged
| EventType::ChatModified(_) => 0,
EventType::MsgsChanged { msg_id, .. }
| EventType::IncomingMsg { msg_id, .. }
| EventType::MsgDelivered { msg_id, .. }
@@ -546,9 +544,8 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::SecurejoinJoinerProgress { progress, .. } => *progress as libc::c_int,
EventType::ChatEphemeralTimerModified { timer, .. } => timer.to_u32() as libc::c_int,
EventType::WebxdcStatusUpdate {
status_update_serial,
..
} => status_update_serial.to_u32() as libc::c_int,
status_update_id, ..
} => status_update_id.to_u32() as libc::c_int,
}
}
@@ -720,11 +717,7 @@ pub unsafe extern "C" fn dc_get_chatlist(
let ctx = &*context;
let qs = to_opt_string_lossy(query_str);
let qi = if query_id == 0 {
None
} else {
Some(ContactId::new(query_id))
};
let qi = if query_id == 0 { None } else { Some(query_id) };
block_on(async move {
match chatlist::Chatlist::try_load(ctx, flags as usize, qs.as_deref(), qi)
@@ -752,7 +745,7 @@ pub unsafe extern "C" fn dc_create_chat_by_contact_id(
let ctx = &*context;
block_on(async move {
ChatId::create_for_contact(ctx, ContactId::new(contact_id))
ChatId::create_for_contact(ctx, contact_id)
.await
.log_err(ctx, "Failed to create chat from contact_id")
.map(|id| id.to_u32())
@@ -772,7 +765,7 @@ pub unsafe extern "C" fn dc_get_chat_id_by_contact_id(
let ctx = &*context;
block_on(async move {
ChatId::lookup_by_contact(ctx, ContactId::new(contact_id))
ChatId::lookup_by_contact(ctx, contact_id)
.await
.log_err(ctx, "Failed to get chat for contact_id")
.unwrap_or_default() // unwraps the Result
@@ -910,7 +903,7 @@ pub unsafe extern "C" fn dc_send_webxdc_status_update(
pub unsafe extern "C" fn dc_get_webxdc_status_updates(
context: *mut dc_context_t,
msg_id: u32,
last_known_serial: u32,
status_update_id: u32,
) -> *mut libc::c_char {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_webxdc_status_updates()");
@@ -920,7 +913,11 @@ pub unsafe extern "C" fn dc_get_webxdc_status_updates(
block_on(ctx.get_webxdc_status_updates(
MsgId::new(msg_id),
StatusUpdateSerial::new(last_known_serial),
if status_update_id == 0 {
None
} else {
Some(StatusUpdateId::new(status_update_id))
},
))
.unwrap_or_else(|_| "".to_string())
.strdup()
@@ -1349,10 +1346,7 @@ pub unsafe extern "C" fn dc_get_chat_contacts(
let arr = dc_array_t::from(
chat::get_chat_contacts(ctx, ChatId::new(chat_id))
.await
.unwrap_or_log_default(ctx, "Failed get_chat_contacts")
.iter()
.map(|id| id.to_u32())
.collect::<Vec<u32>>(),
.unwrap_or_log_default(ctx, "Failed get_chat_contacts"),
);
Box::into_raw(Box::new(arr))
})
@@ -1462,7 +1456,7 @@ pub unsafe extern "C" fn dc_is_contact_in_chat(
block_on(chat::is_contact_in_chat(
ctx,
ChatId::new(chat_id),
ContactId::new(contact_id),
contact_id,
))
.log_err(ctx, "is_contact_in_chat failed")
.unwrap_or_default() as libc::c_int
@@ -1483,7 +1477,7 @@ pub unsafe extern "C" fn dc_add_contact_to_chat(
block_on(chat::add_contact_to_chat(
ctx,
ChatId::new(chat_id),
ContactId::new(contact_id),
contact_id,
))
.log_err(ctx, "Failed to add contact")
.is_ok() as libc::c_int
@@ -1504,7 +1498,7 @@ pub unsafe extern "C" fn dc_remove_contact_from_chat(
block_on(chat::remove_contact_from_chat(
ctx,
ChatId::new(chat_id),
ContactId::new(contact_id),
contact_id,
))
.log_err(ctx, "Failed to remove contact")
.is_ok() as libc::c_int
@@ -1839,8 +1833,7 @@ pub unsafe extern "C" fn dc_lookup_contact_id_by_addr(
Contact::lookup_id_by_addr(ctx, &to_string_lossy(addr), Origin::IncomingReplyTo)
.await
.unwrap_or_log_default(ctx, "failed to lookup id")
.map(|id| id.to_u32())
.unwrap_or_default()
.unwrap_or(0)
})
}
@@ -1860,7 +1853,6 @@ pub unsafe extern "C" fn dc_create_contact(
block_on(async move {
Contact::create(ctx, &name, &to_string_lossy(addr))
.await
.map(|id| id.to_u32())
.unwrap_or(0)
})
}
@@ -1899,9 +1891,7 @@ pub unsafe extern "C" fn dc_get_contacts(
block_on(async move {
match Contact::get_all(ctx, flags, query).await {
Ok(contacts) => Box::into_raw(Box::new(dc_array_t::from(
contacts.iter().map(|id| id.to_u32()).collect::<Vec<u32>>(),
))),
Ok(contacts) => Box::into_raw(Box::new(dc_array_t::from(contacts))),
Err(_) => ptr::null_mut(),
}
})
@@ -1938,10 +1928,7 @@ pub unsafe extern "C" fn dc_get_blocked_contacts(
Contact::get_all_blocked(ctx)
.await
.log_err(ctx, "Can't get blocked contacts")
.unwrap_or_default()
.iter()
.map(|id| id.to_u32())
.collect::<Vec<u32>>(),
.unwrap_or_default(),
)))
})
}
@@ -1952,8 +1939,7 @@ pub unsafe extern "C" fn dc_block_contact(
contact_id: u32,
block: libc::c_int,
) {
let contact_id = ContactId::new(contact_id);
if context.is_null() || contact_id.is_special() {
if context.is_null() || contact_id <= constants::DC_CONTACT_ID_LAST_SPECIAL as u32 {
eprintln!("ignoring careless call to dc_block_contact()");
return;
}
@@ -1983,7 +1969,7 @@ pub unsafe extern "C" fn dc_get_contact_encrinfo(
let ctx = &*context;
block_on(async move {
Contact::get_encrinfo(ctx, ContactId::new(contact_id))
Contact::get_encrinfo(ctx, contact_id)
.await
.map(|s| s.strdup())
.unwrap_or_else(|e| {
@@ -1998,8 +1984,7 @@ pub unsafe extern "C" fn dc_delete_contact(
context: *mut dc_context_t,
contact_id: u32,
) -> libc::c_int {
let contact_id = ContactId::new(contact_id);
if context.is_null() || contact_id.is_special() {
if context.is_null() || contact_id <= constants::DC_CONTACT_ID_LAST_SPECIAL as u32 {
eprintln!("ignoring careless call to dc_delete_contact()");
return 0;
}
@@ -2025,7 +2010,7 @@ pub unsafe extern "C" fn dc_get_contact(
let ctx = &*context;
block_on(async move {
Contact::get_by_id(ctx, ContactId::new(contact_id))
Contact::get_by_id(ctx, contact_id)
.await
.map(|contact| Box::into_raw(Box::new(ContactWrapper { context, contact })))
.unwrap_or_else(|_| ptr::null_mut())
@@ -2448,7 +2433,7 @@ pub unsafe extern "C" fn dc_array_get_contact_id(
return 0;
}
(*array).get_location(index).contact_id.to_u32()
(*array).get_location(index).contact_id
}
#[no_mangle]
pub unsafe extern "C" fn dc_array_get_msg_id(
@@ -2964,7 +2949,7 @@ pub unsafe extern "C" fn dc_msg_get_from_id(msg: *mut dc_msg_t) -> u32 {
return 0;
}
let ffi_msg = &*msg;
ffi_msg.message.get_from_id().to_u32()
ffi_msg.message.get_from_id()
}
#[no_mangle]
@@ -3675,7 +3660,7 @@ pub unsafe extern "C" fn dc_contact_get_id(contact: *mut dc_contact_t) -> u32 {
return 0;
}
let ffi_contact = &*contact;
ffi_contact.contact.get_id().to_u32()
ffi_contact.contact.get_id()
}
#[no_mangle]

View File

@@ -111,19 +111,19 @@ impl Lot {
match self {
Self::Summary(_) => Default::default(),
Self::Qr(qr) => match qr {
Qr::AskVerifyContact { contact_id, .. } => contact_id.to_u32(),
Qr::AskVerifyContact { contact_id, .. } => *contact_id,
Qr::AskVerifyGroup { .. } => Default::default(),
Qr::FprOk { contact_id } => contact_id.to_u32(),
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(),
Qr::FprOk { contact_id } => *contact_id,
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default(),
Qr::FprWithoutAddr { .. } => Default::default(),
Qr::Account { .. } => Default::default(),
Qr::WebrtcInstance { .. } => Default::default(),
Qr::Addr { contact_id } => contact_id.to_u32(),
Qr::Addr { contact_id } => *contact_id,
Qr::Url { .. } => Default::default(),
Qr::Text { .. } => Default::default(),
Qr::WithdrawVerifyContact { contact_id, .. } => contact_id.to_u32(),
Qr::WithdrawVerifyContact { contact_id, .. } => *contact_id,
Qr::WithdrawVerifyGroup { .. } => Default::default(),
Qr::ReviveVerifyContact { contact_id, .. } => contact_id.to_u32(),
Qr::ReviveVerifyContact { contact_id, .. } => *contact_id,
Qr::ReviveVerifyGroup { .. } => Default::default(),
},
Self::Error(_) => Default::default(),

View File

@@ -29,7 +29,7 @@ window.webxdc.sendUpdate(update, descr);
Webxdc apps are usually shared in a chat and run independently on each peer.
To get a shared state, the peers use `sendUpdate()` to send updates to each other.
- `update`: an object with the following properties:
- `update`: an object with the following fields:
- `update.payload`: any javascript primitive, array or object.
- `update.info`: optional, short, informational message that will be added to the chat,
eg. "Alice voted" or "Bob scored 123 in MyGame";
@@ -45,38 +45,48 @@ All peers, including the sending one,
will receive the update by the callback given to `setUpdateListener()`.
There are situations where the user cannot send messages to a chat,
eg. if the webxdc instance comes as a contact request or if the user has left a group.
eg. contact requests or if the user has left a group.
In these cases, you can still call `sendUpdate()`,
however, the update won't be sent to other peers
and you won't get the update by `setUpdateListener()`.
and you won't get the update by `setUpdateListener()` nor by `getAllUpdates()`.
### setUpdateListener()
```js
let promise = window.webxdc.setUpdateListener((update) => {}, serial);
window.webxdc.setUpdateListener((update) => {});
```
With `setUpdateListener()` you define a callback that receives the updates
sent by `sendUpdate()`. The callback is called for updates sent by you or other peers.
The `serial` specifies the last serial that you know about (defaults to 0).
The returned promise resolves when the listener has processed all the update messages known at the time when `setUpdateListener` was called.
sent by `sendUpdate()`.
Each `update` which is passed to the callback comes with the following properties:
- `update`: passed to the callback on updates with the following fields:
`update.payload`: equals the payload given to `sendUpdate()`
- `update.payload`: equals the payload given to `sendUpdate()`
The callback is called for updates sent by you or other peers.
- `update.serial`: the serial number of this update.
Serials are larger `0` and newer serials have higher numbers.
There may be gaps in the serials
and it is not guaranteed that the next serial is exactly incremented by one.
- `update.max_serial`: the maximum serial currently known.
If `max_serial` equals `serial` this update is the last update (until new network messages arrive).
### getAllUpdates()
- `update.info`: optional, short, informational message (see `send_update`)
```js
updates = await window.webxdc.getAllUpdates();
```
- `update.summary`: optional, short text, shown beside app icon (see `send_update`)
In case your Webxdc was just started,
you may want to reconstruct the state from the last run -
and also incorporate updates that may have arrived while the app was not running.
- `updates`: All previous updates in an array,
eg. `[{payload: "foo"},{payload: "bar"}]`
if `webxdc.sendUpdate({payload: "foo"}); webxdc.sendUpdate({payload: "bar"};` was called on the last run.
The updates are wrapped into a Promise that you can `await` for.
If you are not in an async function and cannot use `await` therefore,
you can get the updates with `then()`:
```js
window.webxdc.getAllUpdates().then(updates => {});
```
### selfAddr
@@ -152,7 +162,9 @@ The following example shows an input field and every input is show on all peers
document.getElementById('output').innerHTML += update.payload + "<br>";
}
window.webxdc.setUpdateListener(receiveUpdate, 0);
window.webxdc.setUpdateListener(receiveUpdate);
window.webxdc.getAllUpdates().then(updates => updates.forEach(receiveUpdate));
</script>
</body>
</html>
@@ -177,9 +189,6 @@ just clone and start adapting things to your need.
- older devices might not have the newest js features in their webview,
you may want to transpile your code down to an older js version eg. with https://babeljs.io
- viewport and scaling features are implementation specific,
if you want to have an explicit behavior, you can add eg.
`<meta name="viewport" content="initial-scale=1; user-scalable=no">` to your Webxdc
- there are tons of ideas for enhancements of the API and the file format,
eg. in the future, we will may define icon- and manifest-files,
allow to aggregate the state or add metadata.
allow to aggregate the state or add metadata.

View File

@@ -17,10 +17,11 @@ use deltachat::download::DownloadState;
use deltachat::imex::*;
use deltachat::location;
use deltachat::log::LogExt;
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
use deltachat::message::{self, Message, MessageState, MsgId};
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::sql;
use deltachat::EventType;
use deltachat::{config, provider};
use std::fs;
use std::time::{Duration, SystemTime};
@@ -83,7 +84,6 @@ async fn reset_tables(context: &Context, bits: i32) {
)
.await
.unwrap();
context.sql().config_cache().write().await.clear();
context
.sql()
.execute("DELETE FROM leftgrps;", paramsv![])
@@ -92,13 +92,16 @@ async fn reset_tables(context: &Context, bits: i32) {
println!("(8) Rest but server config reset.");
}
context.emit_msgs_changed_without_ids();
context.emit_event(EventType::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
}
async fn poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<()> {
let data = dc_read_file(context, filename).await?;
if let Err(err) = dc_receive_imf(context, &data, false).await {
if let Err(err) = dc_receive_imf(context, &data, "import", false).await {
println!("dc_receive_imf errored: {:?}", err);
}
Ok(())
@@ -160,7 +163,10 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
}
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
if read_cnt > 0 {
context.emit_msgs_changed_without_ids();
context.emit_event(EventType::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
}
true
}
@@ -203,7 +209,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
contact_id,
msgtext.unwrap_or_default(),
if msg.has_html() { "[HAS-HTML]" } else { "" },
if msg.get_from_id() == ContactId::SELF {
if msg.get_from_id() == 1 {
""
} else if msg.get_state() == MessageState::InSeen {
"[SEEN]"
@@ -261,8 +267,9 @@ async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<()> {
Ok(())
}
async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()> {
async fn log_contactlist(context: &Context, contacts: &[u32]) -> Result<()> {
for contact_id in contacts {
let line;
let mut line2 = "".to_string();
let contact = Contact::get_by_id(context, *contact_id).await?;
let name = contact.get_display_name();
@@ -277,20 +284,24 @@ async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()
} else {
""
};
let line = format!(
line = format!(
"{}{} <{}>",
if !name.is_empty() {
name
&name
} else {
"<name unset>"
},
verified_str,
if !addr.is_empty() { addr } else { "addr unset" }
if !addr.is_empty() {
&addr
} else {
"addr unset"
}
);
let peerstate = Peerstate::from_addr(context, addr)
let peerstate = Peerstate::from_addr(context, &addr)
.await
.expect("peerstate error");
if peerstate.is_some() && *contact_id != ContactId::SELF {
if peerstate.is_some() && *contact_id != 1 {
line2 = format!(
", prefer-encrypt={}",
peerstate.as_ref().unwrap().prefer_encrypt
@@ -419,6 +430,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
joinqr <qr-content>\n\
setqr <qr-content>\n\
providerinfo <addr>\n\
event <event-id to test>\n\
fileinfo <file>\n\
estimatedeletion <seconds>\n\
clear -- clear screen\n\
@@ -707,7 +719,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"createchat" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id = ContactId::new(arg1.parse()?);
let contact_id: u32 = arg1.parse()?;
let chat_id = ChatId::create_for_contact(&context, contact_id).await?;
println!("Single#{} created successfully.", chat_id,);
@@ -735,7 +747,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(sel_chat.is_some(), "No chat selected");
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id_0 = ContactId::new(arg1.parse()?);
let contact_id_0: u32 = arg1.parse()?;
chat::add_contact_to_chat(&context, sel_chat.as_ref().unwrap().get_id(), contact_id_0)
.await?;
println!("Contact added to chat.");
@@ -743,7 +755,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"removemember" => {
ensure!(sel_chat.is_some(), "No chat selected.");
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id_1 = ContactId::new(arg1.parse()?);
let contact_id_1: u32 = arg1.parse()?;
chat::remove_contact_from_chat(
&context,
sel_chat.as_ref().unwrap().get_id(),
@@ -759,7 +771,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
chat::set_chat_name(
&context,
sel_chat.as_ref().unwrap().get_id(),
format!("{} {}", arg1, arg2).trim(),
&format!("{} {}", arg1, arg2).trim(),
)
.await?;
@@ -925,24 +937,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"listmsgs" => {
ensure!(!arg1.is_empty(), "Argument <query> missing.");
let query = format!("{} {}", arg1, arg2).trim().to_string();
let chat = sel_chat.as_ref().map(|sel_chat| sel_chat.get_id());
let time_start = std::time::SystemTime::now();
let msglist = context.search_msgs(chat, &query).await?;
let msglist = context.search_msgs(chat, arg1).await?;
let time_needed = time_start.elapsed().unwrap_or_default();
log_msglist(&context, &msglist).await?;
println!(
"{}{} messages for {}search of \"{}\"",
msglist.len(),
if msglist.len() == 1000 { "+" } else { "" },
if chat.is_none() {
"global "
} else {
"in-chat-"
},
query,
);
println!("{} messages.", msglist.len());
println!("{:?} to create this list", time_needed);
}
"draft" => {
@@ -1138,7 +1139,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"contactinfo" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id = ContactId::new(arg1.parse()?);
let contact_id: u32 = arg1.parse()?;
let contact = Contact::get_by_id(&context, contact_id).await?;
let name_n_addr = contact.get_name_n_addr();
@@ -1173,16 +1174,16 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"delcontact" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
Contact::delete(&context, ContactId::new(arg1.parse()?)).await?;
Contact::delete(&context, arg1.parse()?).await?;
}
"block" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id = ContactId::new(arg1.parse()?);
let contact_id = arg1.parse()?;
Contact::block(&context, contact_id).await?;
}
"unblock" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id = ContactId::new(arg1.parse()?);
let contact_id = arg1.parse()?;
Contact::unblock(&context, contact_id).await?;
}
"listblocked" => {
@@ -1223,6 +1224,17 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
}
}
// TODO: implement this again, unclear how to match this through though, without writing a parser.
// "event" => {
// ensure!(!arg1.is_empty(), "Argument <id> missing.");
// let event = arg1.parse()?;
// let event = EventType::from_u32(event).ok_or(format_err!("EventType::from_u32({})", event))?;
// let r = context.emit_event(event, 0 as libc::uintptr_t, 0 as libc::uintptr_t);
// println!(
// "Sending event {:?}({}), received value {}.",
// event, event as usize, r,
// );
// }
"fileinfo" => {
ensure!(!arg1.is_empty(), "Argument <file> missing.");

View File

@@ -227,12 +227,13 @@ const CONTACT_COMMANDS: [&str; 9] = [
"unblock",
"listblocked",
];
const MISC_COMMANDS: [&str; 11] = [
const MISC_COMMANDS: [&str; 12] = [
"getqr",
"getqrsvg",
"getbadqr",
"checkqr",
"joinqr",
"event",
"fileinfo",
"clear",
"exit",
@@ -415,7 +416,7 @@ async fn handle_cmd(
}
"getqr" | "getbadqr" => {
ctx.start_io().await;
let group = arg1.parse::<u32>().ok().map(ChatId::new);
let group = arg1.parse::<u32>().ok().map(|id| ChatId::new(id));
let mut qr = dc_get_securejoin_qr(&ctx, group).await?;
if !qr.is_empty() {
if arg0 == "getbadqr" && qr.len() > 40 {
@@ -432,7 +433,7 @@ async fn handle_cmd(
}
"getqrsvg" => {
ctx.start_io().await;
let group = arg1.parse::<u32>().ok().map(ChatId::new);
let group = arg1.parse::<u32>().ok().map(|id| ChatId::new(id));
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
match get_securejoin_qr_svg(&ctx, group).await {
Ok(svg) => {

View File

@@ -24,7 +24,7 @@ if __name__ == "__main__":
print("running:", " ".join(cmd))
subprocess.check_call(cmd)
subprocess.check_call("rm -rf build/ src/deltachat/*.so src/deltachat/*.dylib src/deltachat/*.dll" , shell=True)
subprocess.check_call("rm -rf build/ src/deltachat/*.so" , shell=True)
if len(sys.argv) <= 1 or sys.argv[1] != "onlybuild":
subprocess.check_call([

View File

@@ -5,7 +5,7 @@ import calendar
import json
from datetime import datetime, timezone
import os
from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer, iter_array
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array
from .capi import lib, ffi
from . import const
from .message import Message
@@ -517,7 +517,7 @@ class Chat(object):
lib.dc_array_get_timestamp(dc_array, i),
timezone.utc
),
marker=from_optional_dc_charpointer(lib.dc_array_get_marker(dc_array, i)),
marker=from_dc_charpointer(lib.dc_array_get_marker(dc_array, i)),
)
for i in range(lib.dc_array_get_cnt(dc_array))
]

View File

@@ -40,6 +40,8 @@ def dc_account_extra_configure(account):
# We just deleted the folder, so we have to make DC forget about it, too
if account.get_config("configured_sentbox_folder") == folder:
account.set_config("configured_sentbox_folder", None)
if account.get_config("configured_spam_folder") == folder:
account.set_config("configured_spam_folder", None)
setattr(account, "direct_imap", imap)
@@ -222,21 +224,15 @@ class DirectImap:
""" (blocking) wait for next idle message from server. """
assert self._idling
self.account.log("imap-direct: calling idle_check")
res = self.conn.idle_check()
res = self.conn.idle_check(timeout=30)
if len(res) == 0:
raise TimeoutError
if terminate:
self.idle_done()
self.account.log("imap-direct: idle_check returned {!r}".format(res))
return res
def idle_wait_for_new_message(self, terminate=False):
while 1:
for item in self.idle_check():
if item[1] in (b'EXISTS', b'RECENT'):
if terminate:
self.idle_done()
return item
def idle_wait_for_seen(self, terminate=False):
def idle_wait_for_seen(self):
""" Return first message with SEEN flag
from a running idle-stream REtiurn.
"""
@@ -245,8 +241,6 @@ class DirectImap:
if item[1] == FETCH:
if item[2][0] == FLAGS:
if SEEN in item[2][1]:
if terminate:
self.idle_done()
return item[0]
def idle_done(self):

View File

@@ -241,6 +241,7 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
def make_account(self, path, logid, quiet=False):
ac = Account(path, logging=self._logging)
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
ac._evtracker.set_timeout(30)
ac.addr = ac.get_self_contact().addr
ac.set_config("displayname", logid)
if not quiet:
@@ -482,7 +483,7 @@ class BotProcess:
def kill(self) -> None:
self.popen.kill()
def wait(self, timeout=None) -> None:
def wait(self, timeout=30) -> None:
self.popen.wait(timeout=timeout)
def fnmatch_lines(self, pattern_lines):
@@ -491,7 +492,7 @@ class BotProcess:
print("+++FNMATCH:", next_pattern)
ignored = []
while 1:
line = self.stdout_queue.get()
line = self.stdout_queue.get(timeout=15)
if line is None:
if ignored:
print("BOT stdout terminated after these lines")

View File

@@ -652,6 +652,8 @@ class TestOnlineAccount:
pre_generated_key=False,
config={"key_gen_type": str(const.DC_KEY_GEN_ED25519)}
)
# rsa key gen can be slow especially on CI, adjust timeout
ac1._evtracker.set_timeout(240)
acfactory.wait_configure_and_start_io()
chat = acfactory.get_accepted_chat(ac1, ac2)
@@ -890,11 +892,11 @@ class TestOnlineAccount:
chat = acfactory.get_accepted_chat(ac1, ac2)
chat.send_text("message1")
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
chat.send_text("message2")
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
chat.send_text("message3")
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
def test_forward_messages(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
@@ -1013,7 +1015,7 @@ class TestOnlineAccount:
assert ev.data1 == msg2.chat.id
assert ev.data2 == 0
ac2.direct_imap.idle_wait_for_new_message(terminate=True)
ac2.direct_imap.idle_check(terminate=True)
lp.step("1")
for i in range(2):
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_READ")
@@ -1044,7 +1046,8 @@ class TestOnlineAccount:
ac1.create_chat(ac2).send_text("Hello!")
ac2.direct_imap.idle_wait_for_new_message(terminate=True)
# Wait for the message to arrive.
ac2.direct_imap.idle_check(terminate=True)
# Emulate moving of the message to DeltaChat folder by Sieve rule.
# mailcow server contains this rule by default.
@@ -1058,7 +1061,8 @@ class TestOnlineAccount:
# Accept the contact request.
msg.chat.accept()
ac2.mark_seen_messages([msg])
ac2.direct_imap.idle_wait_for_seen(terminate=True)
ac2.direct_imap.idle_wait_for_seen()
ac2.direct_imap.idle_done()
fetch = list(ac2.direct_imap.conn.fetch("*", b'FLAGS').values())
flags = fetch[-1][b'FLAGS']
@@ -1097,30 +1101,6 @@ class TestOnlineAccount:
assert ev.data1 == ac1_clone_chat.id
assert ac1_clone_message.is_in_seen
lp.sec("Send an ephemeral message from ac2 to ac1")
ac2_chat.set_ephemeral_timer(60)
ac1._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
ac1._evtracker.wait_next_incoming_message()
ac1_clone._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
ac1_clone._evtracker.wait_next_incoming_message()
ac2_chat.send_text("Foobar")
ac1_message = ac1._evtracker.wait_next_incoming_message()
ac1_clone_message = ac1_clone._evtracker.wait_next_incoming_message()
assert "Ephemeral timer: 60\n" in ac1_message.get_message_info()
assert "Expires: " not in ac1_clone_message.get_message_info()
assert "Ephemeral timer: 60\n" in ac1_message.get_message_info()
assert "Expires: " not in ac1_clone_message.get_message_info()
ac1.mark_seen_messages([ac1_message])
assert ac1_message.is_in_seen
assert "Expires: " in ac1_message.get_message_info()
ev = ac1_clone._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
assert ev.data1 == ac1_clone_chat.id
assert ac1_clone_message.is_in_seen
# Test that the timer is started on the second device after synchronizing the seen status.
assert "Expires: " in ac1_clone_message.get_message_info()
def test_message_override_sender_name(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
chat = acfactory.get_accepted_chat(ac1, ac2)
@@ -1420,13 +1400,11 @@ class TestOnlineAccount:
assert not device_chat.can_send()
assert device_chat.get_draft() is None
def test_dont_show_emails(self, acfactory, lp):
def test_dont_show_emails_in_draft_folder(self, acfactory, lp):
"""Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them.
So: If it's outgoing AND there is no Received header AND it's not in the sentbox, then ignore the email.
If the draft email is sent out later (i.e. moved to "Sent"), it must be shown.
Also, test that unknown emails in the Spam folder are not shown."""
If the draft email is sent out later (i.e. moved to "Sent"), it must be shown."""
ac1 = acfactory.get_online_configuring_account()
ac1.set_config("show_emails", "2")
ac1.create_contact("alice@example.org").create_chat()
@@ -1434,8 +1412,6 @@ class TestOnlineAccount:
acfactory.wait_configure(ac1)
ac1.direct_imap.create_folder("Drafts")
ac1.direct_imap.create_folder("Sent")
ac1.direct_imap.create_folder("Spam")
ac1.direct_imap.create_folder("Junk")
acfactory.wait_configure_and_start_io()
# Wait until each folder was selected once and we are IDLEing again:
@@ -1460,24 +1436,6 @@ class TestOnlineAccount:
message in Sent
""".format(ac1.get_config("configured_addr")))
ac1.direct_imap.append("Spam", """
From: unknown.address@junk.org
Subject: subj
To: {}
Message-ID: <spam.message@junk.org>
Content-Type: text/plain; charset=utf-8
Unknown message in Spam
""".format(ac1.get_config("configured_addr")))
ac1.direct_imap.append("Junk", """
From: unknown.address@junk.org
Subject: subj
To: {}
Message-ID: <spam.message@junk.org>
Content-Type: text/plain; charset=utf-8
Unknown message in Junk
""".format(ac1.get_config("configured_addr")))
ac1.set_config("scan_all_folders_debounce_secs", "0")
lp.sec("All prepared, now let DC find the message")
@@ -1491,10 +1449,6 @@ class TestOnlineAccount:
assert msg.text == "subj message in Sent"
assert len(msg.chat.get_messages()) == 1
assert not any("unknown.address" in c.get_name() for c in ac1.get_chats())
ac1.direct_imap.select_folder("Spam")
assert ac1.direct_imap.get_uid_by_message_id("spam.message@junk.org")
ac1.stop_io()
lp.sec("'Send out' the draft, i.e. move it to the Sent folder, and wait for DC to display it this time")
ac1.direct_imap.select_folder("Drafts")
@@ -1880,6 +1834,7 @@ class TestOnlineAccount:
lp.sec("trigger ac setup message and return setupcode")
assert ac1.get_info()["fingerprint"] != ac2.get_info()["fingerprint"]
setup_code = ac1.initiate_key_transfer()
ac2._evtracker.set_timeout(30)
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg = ac2.get_message_by_id(ev.data2)
assert msg.is_setup_message()
@@ -1896,6 +1851,7 @@ class TestOnlineAccount:
def test_ac_setup_message_twice(self, acfactory, lp):
ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.clone_online_account(ac1)
ac2._evtracker.set_timeout(30)
acfactory.wait_configure_and_start_io()
lp.sec("trigger ac setup message but ignore")
@@ -2069,7 +2025,7 @@ class TestOnlineAccount:
lp.sec("ac1: send a message to group chat to promote the group")
chat.send_text("afterwards promoted")
ev = in_list.get()
ev = in_list.get(timeout=10)
assert ev.action == "chat-modified"
assert chat.is_promoted()
assert sorted(x.addr for x in chat.get_contacts()) == \
@@ -2079,29 +2035,29 @@ class TestOnlineAccount:
# note that if the above create_chat() would not
# happen we would not receive a proper member_added event
contact2 = chat.add_contact("devnull@testrun.org")
ev = in_list.get()
ev = in_list.get(timeout=10)
assert ev.action == "chat-modified"
ev = in_list.get()
ev = in_list.get(timeout=10)
assert ev.action == "chat-modified"
ev = in_list.get()
ev = in_list.get(timeout=10)
assert ev.action == "added"
assert ev.message.get_sender_contact().addr == ac1_addr
assert ev.contact.addr == "devnull@testrun.org"
lp.sec("ac1: remove address2")
chat.remove_contact(contact2)
ev = in_list.get()
ev = in_list.get(timeout=10)
assert ev.action == "chat-modified"
ev = in_list.get()
ev = in_list.get(timeout=10)
assert ev.action == "removed"
assert ev.contact.addr == contact2.addr
assert ev.message.get_sender_contact().addr == ac1_addr
lp.sec("ac1: remove ac2 contact from chat")
chat.remove_contact(ac2)
ev = in_list.get()
ev = in_list.get(timeout=10)
assert ev.action == "chat-modified"
ev = in_list.get()
ev = in_list.get(timeout=10)
assert ev.action == "removed"
assert ev.message.get_sender_contact().addr == ac1_addr
@@ -2219,7 +2175,7 @@ class TestOnlineAccount:
ac1.direct_imap.idle_start()
ac2.create_chat(ac1).send_text("Hi")
ac1.direct_imap.idle_wait_for_new_message(terminate=False)
ac1.direct_imap.idle_check(terminate=False)
ac1.maybe_network()
ac1._evtracker.wait_for_all_work_done()
@@ -2231,7 +2187,7 @@ class TestOnlineAccount:
ac2.create_chat(ac1).send_text("Hi 2")
ac1.direct_imap.idle_wait_for_new_message(terminate=True)
ac1.direct_imap.idle_check(terminate=True)
ac1.maybe_network()
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTED, const.DC_CONNECTIVITY_WORKING)
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_WORKING, const.DC_CONNECTIVITY_CONNECTED)
@@ -2256,7 +2212,7 @@ class TestOnlineAccount:
ac1.direct_imap.idle_start()
ac2.create_chat(ac1).send_text("Hi")
ac1.direct_imap.idle_wait_for_new_message(terminate=True)
ac1.direct_imap.idle_check(terminate=True)
ac1.maybe_network()
while 1:
@@ -2446,23 +2402,25 @@ class TestOnlineAccount:
acfactory.wait_configure_and_start_io()
imap2 = ac2.direct_imap
imap2.idle_start()
lp.sec("ac1: create chat with ac2")
chat1 = ac1.create_chat(ac2)
ac2.create_chat(ac1)
lp.sec("ac1: send message to ac2")
sent_msg = chat1.send_text("hello")
imap2.idle_check(terminate=False)
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "hello"
lp.sec("ac2: wait for close/expunge on autodelete")
imap2.idle_check(terminate=True)
ac2._evtracker.get_info_contains("close/expunge succeeded")
lp.sec("ac2: check that message was autodeleted on server")
assert len(ac2.direct_imap.get_all_messages()) == 0
assert len(imap2.get_all_messages()) == 0
lp.sec("ac2: Mark deleted message as seen and check that read receipt arrives")
# Mark deleted message as seen and check that read receipt arrives
msg.mark_seen()
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_READ")
assert ev.data1 == chat1.id
@@ -2546,7 +2504,8 @@ class TestOnlineAccount:
lp.sec("ac2: deleting all messages except third")
assert len(to_delete) == len(texts) - 1
ac2.delete_messages(to_delete)
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
for msg in to_delete:
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
ac2._evtracker.get_info_contains("close/expunge succeeded")
@@ -2723,7 +2682,7 @@ class TestOnlineAccount:
ac1.direct_imap.select_config_folder("inbox")
ac1.direct_imap.idle_start()
acfactory.get_accepted_chat(ac2, ac1).send_text("hello")
ac1.direct_imap.idle_wait_for_new_message(terminate=True)
ac1.direct_imap.idle_check(terminate=True)
ac1.direct_imap.conn.move(["*"], folder) # "*" means "biggest UID in mailbox"
lp.sec("Everything prepared, now see if DeltaChat finds the message (" + variant + ")")
@@ -2756,6 +2715,7 @@ class TestOnlineAccount:
assert ac.get_config("configured_mvbox_folder")
ac1 = acfactory.get_online_configuring_account(move=mvbox_move)
ac1.set_config("sentbox_move", "1")
ac2 = acfactory.get_online_configuring_account()
acfactory.wait_configure(ac1)
@@ -2775,7 +2735,7 @@ class TestOnlineAccount:
if mvbox_move:
ac1.direct_imap.select_config_folder("mvbox")
else:
ac1.direct_imap.select_folder("INBOX")
ac1.direct_imap.select_config_folder("sentbox")
ac1.direct_imap.idle_start()
lp.sec("send out message with bcc to ourselves")

View File

@@ -8,7 +8,7 @@ envlist =
[testenv]
commands =
pytest -n1 --reruns 2 --reruns-delay 5 -v -rsXx --ignored --strict-tls {posargs: tests examples}
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored --strict-tls {posargs: tests examples}
python tests/package_wheels.py {toxworkdir}/wheelhouse
passenv =
TRAVIS
@@ -78,8 +78,8 @@ commands =
addopts = -v -ra --strict-markers
norecursedirs = .tox
xfail_strict=true
timeout = 150
timeout_func_only = True
timeout = 90
timeout_method = thread
markers =
ignored: ignore this test in default test runs, use --ignored to run.

View File

@@ -1 +1 @@
1.60.0
1.54.0

View File

@@ -8,7 +8,7 @@ set -e -x
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.60.0
RUST_VERSION=1.54.0
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu.tar.gz" | tar xz
cd "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"

View File

@@ -8,7 +8,7 @@ set -e -x
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.60.0
RUST_VERSION=1.54.0
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu.tar.gz" | tar xz
cd "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"

View File

@@ -54,11 +54,7 @@ impl Accounts {
ensure!(dir.exists().await, "directory does not exist");
let config_file = dir.join(CONFIG_NAME);
ensure!(
config_file.exists().await,
"{:?} does not exist",
config_file
);
ensure!(config_file.exists().await, "accounts.toml does not exist");
let config = Config::from_file(config_file)
.await
@@ -148,27 +144,9 @@ impl Accounts {
drop(ctx);
if let Some(cfg) = self.config.get_account(id).await {
// Spend up to 1 minute trying to remove the files.
// Files may remain locked up to 30 seconds due to r2d2 bug:
// https://github.com/sfackler/r2d2/issues/99
let mut counter = 0;
loop {
counter += 1;
if let Err(err) = fs::remove_dir_all(async_std::path::PathBuf::from(&cfg.dir))
.await
.context("failed to remove account data")
{
if counter > 60 {
return Err(err);
}
// Wait 1 second and try again.
async_std::task::sleep(std::time::Duration::from_millis(1000)).await;
} else {
break;
}
}
fs::remove_dir_all(async_std::path::PathBuf::from(&cfg.dir))
.await
.context("failed to remove account data")?;
}
self.config.remove_account(id).await?;

View File

@@ -3,26 +3,29 @@
use core::cmp::max;
use std::ffi::OsStr;
use std::fmt;
use std::io::Cursor;
use async_std::path::{Path, PathBuf};
use async_std::prelude::*;
use async_std::{fs, io};
use anyhow::{format_err, Context as _, Error};
use image::{DynamicImage, ImageFormat};
use anyhow::format_err;
use anyhow::Context as _;
use anyhow::Error;
use image::DynamicImage;
use image::GenericImageView;
use image::ImageFormat;
use num_traits::FromPrimitive;
use thiserror::Error;
use crate::config::Config;
use crate::constants::{
MediaQuality, BALANCED_AVATAR_SIZE, BALANCED_IMAGE_SIZE, WORSE_AVATAR_SIZE, WORSE_IMAGE_SIZE,
MediaQuality, Viewtype, BALANCED_AVATAR_SIZE, BALANCED_IMAGE_SIZE, WORSE_AVATAR_SIZE,
WORSE_IMAGE_SIZE,
};
use crate::context::Context;
use crate::events::EventType;
use crate::log::LogExt;
use crate::message;
use crate::message::Viewtype;
/// Represents a file in the blob directory.
///
@@ -289,7 +292,7 @@ impl<'a> BlobObject<'a> {
/// Returns the filename of the blob.
pub fn as_file_name(&self) -> &str {
self.name.rsplit('/').next().unwrap()
self.name.rsplitn(2, '/').next().unwrap()
}
/// The path relative in the blob directory.
@@ -302,7 +305,7 @@ impl<'a> BlobObject<'a> {
/// If a blob's filename has an extension, it is always guaranteed
/// to be lowercase.
pub fn suffix(&self) -> Option<&str> {
let ext = self.name.rsplit('.').next();
let ext = self.name.rsplitn(2, '.').next();
if ext == Some(&self.name) {
None
} else {
@@ -345,30 +348,13 @@ impl<'a> BlobObject<'a> {
};
let clean = sanitize_filename::sanitize_with_options(name, opts);
// Let's take the tricky filename
// "file.with_lots_of_characters_behind_point_and_double_ending.tar.gz" as an example.
// Split it into "file" and "with_lots_of_characters_behind_point_and_double_ending.tar.gz":
let mut iter = clean.splitn(2, '.');
let stem: String = iter.next().unwrap_or_default().chars().take(64).collect();
// stem == "file"
let ext_chars = iter.next().unwrap_or_default().chars();
let ext: String = ext_chars
.rev()
.take(32)
.collect::<Vec<_>>()
.iter()
.rev()
.collect();
// ext == "d_point_and_double_ending.tar.gz"
let ext: String = iter.next().unwrap_or_default().chars().take(32).collect();
if ext.is_empty() {
(stem, "".to_string())
} else {
(stem, format!(".{}", ext).to_lowercase())
// Return ("file", ".d_point_and_double_ending.tar.gz")
// which is not perfect but acceptable.
}
}
@@ -463,8 +449,7 @@ impl<'a> BlobObject<'a> {
fn encode_img(img: &DynamicImage, encoded: &mut Vec<u8>) -> anyhow::Result<()> {
encoded.clear();
let mut buf = Cursor::new(encoded);
img.write_to(&mut buf, image::ImageFormat::Jpeg)?;
img.write_to(encoded, image::ImageFormat::Jpeg)?;
Ok(())
}
fn encoded_img_exceeds_bytes(
@@ -634,15 +619,17 @@ pub enum BlobError {
mod tests {
use fs::File;
use anyhow::Result;
use image::{GenericImageView, Pixel};
use crate::chat::{self, create_group_chat, ProtectionStatus};
use crate::message::Message;
use crate::test_utils::{self, TestContext};
use super::*;
use crate::chat::{create_group_chat, ProtectionStatus};
use crate::{
chat,
message::Message,
test_utils::{self, TestContext},
};
use anyhow::Result;
use image::Pixel;
#[async_std::test]
async fn test_create() {
let t = TestContext::new().await;
@@ -940,7 +927,7 @@ mod tests {
}
#[async_std::test]
async fn test_recode_image_1() {
async fn test_recode_image() {
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
// BALANCED_IMAGE_SIZE > 1000, the original image size, so the image is not scaled down:
send_image_check_mediaquality(Some("0"), bytes, 1000, 1000, 0, 1000, 1000)
@@ -957,10 +944,7 @@ mod tests {
)
.await
.unwrap();
}
#[async_std::test]
async fn test_recode_image_2() {
// 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(
@@ -976,29 +960,22 @@ mod tests {
.unwrap();
assert_correct_rotation(&img_rotated);
let mut buf = Cursor::new(vec![]);
let mut bytes = vec![];
img_rotated
.write_to(&mut buf, image::ImageFormat::Jpeg)
.write_to(&mut bytes, image::ImageFormat::Jpeg)
.unwrap();
let bytes = buf.into_inner();
// Do this in parallel to speed up the test a bit
// (it still takes very long though)
let bytes2 = bytes.clone();
let join_handle = async_std::task::spawn(async move {
let img_rotated = send_image_check_mediaquality(
Some("0"),
&bytes2,
BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE,
0,
BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE,
)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
});
let img_rotated = send_image_check_mediaquality(
Some("0"),
&bytes,
BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE,
0,
BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE,
)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
let img_rotated = send_image_check_mediaquality(
Some("1"),
@@ -1013,11 +990,6 @@ mod tests {
.unwrap();
assert_correct_rotation(&img_rotated);
join_handle.await;
}
#[async_std::test]
async fn test_recode_image_3() {
let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg");
let img_rotated = send_image_check_mediaquality(Some("0"), bytes, 200, 180, 270, 180, 200)
.await

File diff suppressed because it is too large Load Diff

View File

@@ -4,11 +4,13 @@ use anyhow::{ensure, Context as _, Result};
use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
use crate::constants::{
Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_GCL_ADD_ALLDONE_HINT,
DC_GCL_ARCHIVED_ONLY, DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS,
Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CONTACT_ID_DEVICE,
DC_CONTACT_ID_SELF, DC_CONTACT_ID_UNDEFINED, DC_GCL_ADD_ALLDONE_HINT, DC_GCL_ARCHIVED_ONLY,
DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS,
};
use crate::contact::{Contact, ContactId};
use crate::contact::Contact;
use crate::context::Context;
use crate::ephemeral::delete_expired_messages;
use crate::message::{Message, MessageState, MsgId};
use crate::stock_str;
use crate::summary::Summary;
@@ -83,13 +85,19 @@ impl Chatlist {
context: &Context,
listflags: usize,
query: Option<&str>,
query_contact_id: Option<ContactId>,
query_contact_id: Option<u32>,
) -> Result<Self> {
let flag_archived_only = 0 != listflags & DC_GCL_ARCHIVED_ONLY;
let flag_for_forwarding = 0 != listflags & DC_GCL_FOR_FORWARDING;
let flag_no_specials = 0 != listflags & DC_GCL_NO_SPECIALS;
let flag_add_alldone_hint = 0 != listflags & DC_GCL_ADD_ALLDONE_HINT;
// Note that we do not emit DC_EVENT_MSGS_MODIFIED here even if some
// messages get deleted to avoid reloading the same chatlist.
if let Err(err) = delete_expired_messages(context).await {
warn!(context, "Failed to hide expired messages: {}", err);
}
let mut add_archived_link_item = false;
let process_row = |row: &rusqlite::Row| {
@@ -104,7 +112,7 @@ impl Chatlist {
};
let skip_id = if flag_for_forwarding {
ChatId::lookup_by_contact(context, ContactId::DEVICE)
ChatId::lookup_by_contact(context, DC_CONTACT_ID_DEVICE)
.await?
.unwrap_or_default()
} else {
@@ -139,7 +147,7 @@ impl Chatlist {
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
GROUP BY c.id
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
paramsv![MessageState::OutDraft, query_contact_id, ChatVisibility::Pinned],
paramsv![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned],
process_row,
process_rows,
).await?
@@ -208,7 +216,7 @@ impl Chatlist {
} else {
// show normal chatlist
let sort_id_up = if flag_for_forwarding {
ChatId::lookup_by_contact(context, ContactId::SELF)
ChatId::lookup_by_contact(context, DC_CONTACT_ID_SELF)
.await?
.unwrap_or_default()
} else {
@@ -318,7 +326,7 @@ impl Chatlist {
let (lastmsg, lastcontact) = if let Some(lastmsg_id) = lastmsg_id {
let lastmsg = Message::load_from_db(context, lastmsg_id).await?;
if lastmsg.from_id == ContactId::SELF {
if lastmsg.from_id == DC_CONTACT_ID_SELF {
(Some(lastmsg), None)
} else {
match chat.typ {
@@ -335,7 +343,7 @@ impl Chatlist {
if chat.id.is_archived_link() {
Ok(Default::default())
} else if let Some(lastmsg) = lastmsg.filter(|msg| msg.from_id != ContactId::UNDEFINED) {
} else if let Some(lastmsg) = lastmsg.filter(|msg| msg.from_id != DC_CONTACT_ID_UNDEFINED) {
Ok(Summary::new(context, &lastmsg, chat, lastcontact.as_ref()).await)
} else {
Ok(Summary {
@@ -348,10 +356,6 @@ impl Chatlist {
pub fn get_index_for_id(&self, id: ChatId) -> Option<usize> {
self.ids.iter().position(|(chat_id, _)| chat_id == &id)
}
pub fn iter(&self) -> impl Iterator<Item = &(ChatId, Option<MsgId>)> {
self.ids.iter()
}
}
/// Returns the number of archived chats
@@ -371,8 +375,8 @@ mod tests {
use super::*;
use crate::chat::{create_group_chat, get_chat_contacts, ProtectionStatus};
use crate::constants::Viewtype;
use crate::dc_receive_imf::dc_receive_imf;
use crate::message::Viewtype;
use crate::stock_str::StockMessage;
use crate::test_utils::TestContext;
@@ -503,6 +507,7 @@ mod tests {
Date: Sun, 22 Mar 2021 22:37:57 +0000\n\
\n\
hello foo\n",
"INBOX",
false,
)
.await?;
@@ -563,6 +568,7 @@ mod tests {
Date: Sun, 22 Mar 2021 22:38:57 +0000\n\
\n\
hello foo\n",
"INBOX",
false,
)
.await?;

View File

@@ -1,14 +1,16 @@
//! # Key-value configuration management.
use anyhow::{ensure, Context as _, Result};
use anyhow::{ensure, Result};
use strum::{EnumProperty, IntoEnumIterator};
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
use crate::blob::BlobObject;
use crate::chat::ChatId;
use crate::constants::DC_VERSION_STR;
use crate::context::Context;
use crate::dc_tools::{dc_get_abs_path, improve_single_line_input};
use crate::events::EventType;
use crate::message::MsgId;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::provider::{get_provider_by_id, Provider};
@@ -69,12 +71,8 @@ pub enum Config {
#[strum(props(default = "1"))]
MvboxMove,
/// Watch for new messages in the "Mvbox" (aka DeltaChat folder) only.
///
/// This will not entirely disable other folders, e.g. the spam folder will also still
/// be watched for new messages.
#[strum(props(default = "0"))]
OnlyFetchMvbox,
SentboxMove, // If `MvboxMove` is true, this config is ignored. Currently only used in tests.
#[strum(props(default = "0"))] // also change ShowEmails.default() on changes
ShowEmails,
@@ -129,6 +127,7 @@ pub enum Config {
ConfiguredInboxFolder,
ConfiguredMvboxFolder,
ConfiguredSentboxFolder,
ConfiguredSpamFolder,
ConfiguredTimestamp,
ConfiguredProvider,
Configured,
@@ -226,17 +225,6 @@ impl Context {
Ok(self.get_config_int(key).await? != 0)
}
pub(crate) async fn get_configured_addr(&self) -> Result<String> {
self.get_config(Config::ConfiguredAddr)
.await?
.context("no address configured")
}
pub(crate) async fn should_watch_mvbox(&self) -> Result<bool> {
Ok(self.get_config_bool(Config::MvboxMove).await?
|| self.get_config_bool(Config::OnlyFetchMvbox).await?)
}
/// Gets configured "delete_server_after" value.
///
/// `None` means never delete the message, `Some(0)` means delete
@@ -245,7 +233,7 @@ impl Context {
match self.get_config_int(Config::DeleteServerAfter).await? {
0 => Ok(None),
1 => Ok(Some(0)),
x => Ok(Some(i64::from(x))),
x => Ok(Some(x as i64)),
}
}
@@ -267,7 +255,7 @@ impl Context {
pub async fn get_config_delete_device_after(&self) -> Result<Option<i64>> {
match self.get_config_int(Config::DeleteDeviceAfter).await? {
0 => Ok(None),
x => Ok(Some(i64::from(x))),
x => Ok(Some(x as i64)),
}
}
@@ -293,22 +281,31 @@ impl Context {
}
}
self.emit_event(EventType::SelfavatarChanged);
Ok(())
}
Config::DeleteDeviceAfter => {
let ret = self.sql.set_raw_config(key, value).await;
// Interrupt ephemeral loop to delete old messages immediately.
self.interrupt_ephemeral_task().await;
ret?
let ret = self
.sql
.set_raw_config(key, value)
.await
.map_err(Into::into);
// Force chatlist reload to delete old messages immediately.
self.emit_event(EventType::MsgsChanged {
msg_id: MsgId::new(0),
chat_id: ChatId::new(0),
});
ret
}
Config::Displayname => {
let value = value.map(improve_single_line_input);
self.sql.set_raw_config(key, value.as_deref()).await?;
Ok(())
}
_ => {
self.sql.set_raw_config(key, value).await?;
Ok(())
}
}
Ok(())
}
pub async fn set_config_bool(&self, key: Config, value: bool) -> Result<()> {

View File

@@ -11,20 +11,21 @@ use async_std::task;
use job::Action;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use crate::config::Config;
use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2};
use crate::context::Context;
use crate::dc_tools::{time, EmailAddress};
use crate::dc_tools::EmailAddress;
use crate::imap::Imap;
use crate::job;
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam, Socks5Config};
use crate::message::{Message, Viewtype};
use crate::message::Message;
use crate::oauth2::dc_get_oauth2_addr;
use crate::param::Params;
use crate::provider::{Protocol, Socket, UsernamePattern};
use crate::smtp::Smtp;
use crate::stock_str;
use crate::{chat, e2ee, provider};
use crate::{config::Config, dc_tools::time};
use crate::{
constants::{Viewtype, DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2},
job,
};
use crate::{context::Context, param::Params};
use auto_mozilla::moz_autoconfigure;
use auto_outlook::outlk_autodiscover;
@@ -85,7 +86,7 @@ impl Context {
async fn inner_configure(&self) -> Result<()> {
info!(self, "Configure ...");
let mut param = LoginParam::load_candidate_params(self).await?;
let mut param = LoginParam::from_database(self, "").await?;
let success = configure(self, &mut param).await;
self.set_config(Config::NotifyAboutWrongPw, None).await?;
@@ -442,7 +443,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
progress!(ctx, 900);
let create_mvbox = ctx.should_watch_mvbox().await?;
let create_mvbox = ctx.get_config_bool(Config::MvboxMove).await?;
imap.configure_folders(ctx, create_mvbox).await?;
@@ -454,7 +455,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
progress!(ctx, 910);
// the trailing underscore is correct
param.save_as_configured_params(ctx).await?;
param.save_to_database(ctx, "configured_").await?;
ctx.set_config(Config::ConfiguredTimestamp, Some(&time().to_string()))
.await?;
@@ -699,11 +700,11 @@ pub enum Error {
error: quick_xml::Error,
},
#[error("Failed to get URL: {0}")]
ReadUrl(#[from] self::read_url::Error),
#[error("Number of redirection is exceeded")]
Redirection,
#[error("{0:#}")]
Other(#[from] anyhow::Error),
}
#[cfg(test)]

View File

@@ -1,40 +1,20 @@
use crate::context::Context;
use anyhow::format_err;
use anyhow::Context as _;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("URL request error")]
GetError(surf::Error),
}
pub async fn read_url(context: &Context, url: &str) -> anyhow::Result<String> {
match read_url_inner(context, url).await {
Ok(s) => {
info!(context, "Successfully read url {}", url);
Ok(s)
}
Err(e) => {
info!(context, "Can't read URL {}: {:#}", url, e);
Err(format_err!("Can't read URL {}: {:#}", url, e))
pub async fn read_url(context: &Context, url: &str) -> Result<String, Error> {
info!(context, "Requesting URL {}", url);
match surf::get(url).recv_string().await {
Ok(res) => Ok(res),
Err(err) => {
info!(context, "Can\'t read URL {}: {}", url, err);
Err(Error::GetError(err))
}
}
}
pub async fn read_url_inner(context: &Context, mut url: &str) -> anyhow::Result<String> {
let mut _temp; // For the borrow checker
// Follow up to 10 http-redirects
for _i in 0..10 {
let mut response = surf::get(url).send().await.map_err(|e| e.into_inner())?;
if response.status().is_redirection() {
_temp = response
.header("location")
.context("Redirection doesn't have a target location")?
.last()
.to_string();
info!(context, "Following redirect to {}", _temp);
url = &_temp;
continue;
}
return response.body_string().await.map_err(|e| e.into_inner());
}
Err(format_err!("Followed 10 redirections"))
}

View File

@@ -52,8 +52,10 @@ impl ServerParams {
fn expand_hostnames(self, param_domain: &str) -> Vec<ServerParams> {
if self.hostname.is_empty() {
vec![
// Try "imap.ex.org"/"smtp.ex.org" and "mail.ex.org" first because if a server exists
// under this address, it's likely the correct one.
Self {
hostname: param_domain.to_string(),
..self.clone()
},
Self {
hostname: match self.protocol {
Protocol::Imap => "imap.".to_string() + param_domain,
@@ -63,12 +65,6 @@ impl ServerParams {
},
Self {
hostname: "mail.".to_string() + param_domain,
..self.clone()
},
// Try "ex.org" last because if it's wrong and the server is configured to
// not answer at all, configuration may be stuck for several minutes.
Self {
hostname: param_domain.to_string(),
..self
},
]
@@ -300,48 +296,5 @@ mod tests {
strict_tls: Some(true)
}],
);
// Test that "example.net" is tried after "*.example.net".
let v = expand_param_vector(
vec![ServerParams {
protocol: Protocol::Imap,
hostname: "".to_string(),
port: 10480,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true),
}],
"foobar@example.net",
"example.net",
);
assert_eq!(
v,
vec![
ServerParams {
protocol: Protocol::Imap,
hostname: "imap.example.net".to_string(),
port: 10480,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true)
},
ServerParams {
protocol: Protocol::Imap,
hostname: "mail.example.net".to_string(),
port: 10480,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true)
},
ServerParams {
protocol: Protocol::Imap,
hostname: "example.net".to_string(),
port: 10480,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true)
}
],
);
}
}

View File

@@ -179,6 +179,16 @@ pub const DC_ELLIPSIS: &str = "[...]";
/// `char`s), not Unicode Grapheme Clusters.
pub const DC_DESIRED_TEXT_LEN: usize = 5000;
pub const DC_CONTACT_ID_UNDEFINED: u32 = 0;
pub const DC_CONTACT_ID_SELF: u32 = 1;
pub const DC_CONTACT_ID_INFO: u32 = 2;
pub const DC_CONTACT_ID_DEVICE: u32 = 5;
pub const DC_CONTACT_ID_LAST_SPECIAL: u32 = 9;
// decorative address that is used for DC_CONTACT_ID_DEVICE
// when an api that returns an email is called.
pub const DC_CONTACT_ID_DEVICE_ADDR: &str = "device@localhost";
// Flags for empty server job
pub const DC_EMPTY_MVBOX: u32 = 0x01;
@@ -220,6 +230,82 @@ pub const DC_FOLDERS_CONFIGURED_VERSION: i32 = 3;
// can be overwritten by the setting `max_smtp_rcpt_to` in provider-db.
pub const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;
#[derive(
Debug,
Display,
Clone,
Copy,
PartialEq,
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Serialize,
Deserialize,
)]
#[repr(u32)]
pub enum Viewtype {
Unknown = 0,
/// Text message.
/// The text of the message is set using dc_msg_set_text()
/// and retrieved with dc_msg_get_text().
Text = 10,
/// Image message.
/// If the image is an animated GIF, the type DC_MSG_GIF should be used.
/// File, width and height are set via dc_msg_set_file(), dc_msg_set_dimension
/// and retrieved via dc_msg_set_file(), dc_msg_set_dimension().
Image = 20,
/// Animated GIF message.
/// File, width and height are set via dc_msg_set_file(), dc_msg_set_dimension()
/// and retrieved via dc_msg_get_file(), dc_msg_get_width(), dc_msg_get_height().
Gif = 21,
/// Message containing a sticker, similar to image.
/// If possible, the ui should display the image without borders in a transparent way.
/// A click on a sticker will offer to install the sticker set in some future.
Sticker = 23,
/// Message containing an Audio file.
/// File and duration are set via dc_msg_set_file(), dc_msg_set_duration()
/// and retrieved via dc_msg_get_file(), dc_msg_get_duration().
Audio = 40,
/// A voice message that was directly recorded by the user.
/// For all other audio messages, the type #DC_MSG_AUDIO should be used.
/// File and duration are set via dc_msg_set_file(), dc_msg_set_duration()
/// and retrieved via dc_msg_get_file(), dc_msg_get_duration()
Voice = 41,
/// Video messages.
/// File, width, height and durarion
/// are set via dc_msg_set_file(), dc_msg_set_dimension(), dc_msg_set_duration()
/// and retrieved via
/// dc_msg_get_file(), dc_msg_get_width(),
/// dc_msg_get_height(), dc_msg_get_duration().
Video = 50,
/// Message containing any file, eg. a PDF.
/// The file is set via dc_msg_set_file()
/// and retrieved via dc_msg_get_file().
File = 60,
/// Message is an invitation to a videochat.
VideochatInvitation = 70,
/// Message is an webxdc instance.
Webxdc = 80,
}
impl Default for Viewtype {
fn default() -> Self {
Viewtype::Unknown
}
}
pub const DC_JOB_DELETE_MSG_ON_IMAP: i32 = 110;
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
@@ -231,9 +317,33 @@ pub enum KeyType {
#[cfg(test)]
mod tests {
use super::*;
use num_traits::FromPrimitive;
use super::*;
#[test]
fn test_derive_display_works_as_expected() {
assert_eq!(format!("{}", Viewtype::Audio), "Audio");
}
#[test]
fn test_viewtype_values() {
// values may be written to disk and must not change
assert_eq!(Viewtype::Unknown, Viewtype::default());
assert_eq!(Viewtype::Unknown, Viewtype::from_i32(0).unwrap());
assert_eq!(Viewtype::Text, Viewtype::from_i32(10).unwrap());
assert_eq!(Viewtype::Image, Viewtype::from_i32(20).unwrap());
assert_eq!(Viewtype::Gif, Viewtype::from_i32(21).unwrap());
assert_eq!(Viewtype::Sticker, Viewtype::from_i32(23).unwrap());
assert_eq!(Viewtype::Audio, Viewtype::from_i32(40).unwrap());
assert_eq!(Viewtype::Voice, Viewtype::from_i32(41).unwrap());
assert_eq!(Viewtype::Video, Viewtype::from_i32(50).unwrap());
assert_eq!(Viewtype::File, Viewtype::from_i32(60).unwrap());
assert_eq!(
Viewtype::VideochatInvitation,
Viewtype::from_i32(70).unwrap()
);
assert_eq!(Viewtype::Webxdc, Viewtype::from_i32(80).unwrap());
}
#[test]
fn test_chattype_values() {

View File

@@ -1,20 +1,21 @@
//! Contacts module
use std::convert::{TryFrom, TryInto};
use std::fmt;
use anyhow::{bail, ensure, Context as _, Result};
use async_std::path::PathBuf;
use deltachat_derive::{FromSql, ToSql};
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
use crate::aheader::EncryptPreference;
use crate::chat::ChatId;
use crate::color::str_to_color;
use crate::config::Config;
use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY};
use crate::constants::{
Blocked, Chattype, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_DEVICE_ADDR, DC_CONTACT_ID_LAST_SPECIAL,
DC_CONTACT_ID_SELF, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY,
};
use crate::context::Context;
use crate::dc_tools::{dc_get_abs_path, improve_single_line_input, EmailAddress};
use crate::events::EventType;
@@ -26,91 +27,6 @@ use crate::param::{Param, Params};
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::{chat, stock_str};
/// Contact ID, including reserved IDs.
///
/// Some contact IDs are reserved to identify special contacts. This
/// type can represent both the special as well as normal contacts.
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ContactId(u32);
impl ContactId {
pub const UNDEFINED: ContactId = ContactId::new(0);
/// The owner of the account.
///
/// The email-address is set by `dc_set_config` using "addr".
pub const SELF: ContactId = ContactId::new(1);
pub const INFO: ContactId = ContactId::new(2);
pub const DEVICE: ContactId = ContactId::new(5);
const LAST_SPECIAL: ContactId = ContactId::new(9);
/// Address to go with [`ContactId::DEVICE`].
///
/// This is used by APIs which need to return an email address for this contact.
pub const DEVICE_ADDR: &'static str = "device@localhost";
/// Creates a new [`ContactId`].
pub const fn new(id: u32) -> ContactId {
ContactId(id)
}
/// Whether this is a special [`ContactId`].
///
/// Some [`ContactId`]s are reserved for special contacts like [`ContactId::SELF`],
/// [`ContactId::INFO`] and [`ContactId::DEVICE`]. This function indicates whether this
/// [`ContactId`] is any of the reserved special [`ContactId`]s (`true`) or whether it
/// is the [`ContactId`] of a real contact (`false`).
pub fn is_special(&self) -> bool {
self.0 <= Self::LAST_SPECIAL.0
}
/// Numerical representation of the [`ContactId`].
///
/// Each contact ID has a unique numerical representation which is used in the database
/// (via [`rusqlite::ToSql`]) and also for FFI purposes. In Rust code you should never
/// need to use this directly.
pub const fn to_u32(&self) -> u32 {
self.0
}
}
impl fmt::Display for ContactId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if *self == ContactId::UNDEFINED {
write!(f, "Contact#Undefined")
} else if *self == ContactId::SELF {
write!(f, "Contact#Self")
} else if *self == ContactId::INFO {
write!(f, "Contact#Info")
} else if *self == ContactId::DEVICE {
write!(f, "Contact#Device")
} else if self.is_special() {
write!(f, "Contact#Special{}", self.0)
} else {
write!(f, "Contact#{}", self.0)
}
}
}
/// Allow converting [`ContactId`] to an SQLite type.
impl rusqlite::types::ToSql for ContactId {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let val = rusqlite::types::Value::Integer(i64::from(self.0));
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
}
/// Allow converting an SQLite integer directly into [`ContactId`].
impl rusqlite::types::FromSql for ContactId {
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
i64::column_result(value).and_then(|val| {
val.try_into()
.map(ContactId::new)
.map_err(|_| rusqlite::types::FromSqlError::OutOfRange(val))
})
}
}
/// An object representing a single contact in memory.
///
/// The contact object is not updated.
@@ -126,7 +42,13 @@ impl rusqlite::types::FromSql for ContactId {
#[derive(Debug)]
pub struct Contact {
/// The contact ID.
pub id: ContactId,
///
/// Special message IDs:
/// - DC_CONTACT_ID_SELF (1) - this is the owner of the account with the email-address set by
/// `dc_set_config` using "addr".
///
/// Normal contact IDs are larger than these special ones (larger than DC_CONTACT_ID_LAST_SPECIAL).
pub id: u32,
/// Contact name. It is recommended to use `Contact::get_name`,
/// `Contact::get_display_name` or `Contact::get_name_n_addr` to access this field.
@@ -261,7 +183,7 @@ impl Default for VerifiedStatus {
}
impl Contact {
pub async fn load_from_db(context: &Context, contact_id: ContactId) -> Result<Self> {
pub async fn load_from_db(context: &Context, contact_id: u32) -> Result<Self> {
let mut contact = context
.sql
.query_row(
@@ -269,7 +191,7 @@ impl Contact {
c.authname, c.param, c.status
FROM contacts c
WHERE c.id=?;",
paramsv![contact_id],
paramsv![contact_id as i32],
|row| {
let name: String = row.get(0)?;
let addr: String = row.get(1)?;
@@ -294,16 +216,19 @@ impl Contact {
},
)
.await?;
if contact_id == ContactId::SELF {
if contact_id == DC_CONTACT_ID_SELF {
contact.name = stock_str::self_msg(context).await;
contact.addr = context.get_configured_addr().await.unwrap_or_default();
contact.addr = context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
contact.status = context
.get_config(Config::Selfstatus)
.await?
.unwrap_or_default();
} else if contact_id == ContactId::DEVICE {
} else if contact_id == DC_CONTACT_ID_DEVICE {
contact.name = stock_str::device_messages(context).await;
contact.addr = ContactId::DEVICE_ADDR.to_string();
contact.addr = DC_CONTACT_ID_DEVICE_ADDR.to_string();
contact.status = stock_str::device_messages_hint(context).await;
}
Ok(contact)
@@ -320,18 +245,18 @@ impl Contact {
}
/// Check if a contact is blocked.
pub async fn is_blocked_load(context: &Context, id: ContactId) -> Result<bool> {
pub async fn is_blocked_load(context: &Context, id: u32) -> Result<bool> {
let blocked = Self::load_from_db(context, id).await?.blocked;
Ok(blocked)
}
/// Block the given contact.
pub async fn block(context: &Context, id: ContactId) -> Result<()> {
pub async fn block(context: &Context, id: u32) -> Result<()> {
set_block_contact(context, id, true).await
}
/// Unblock the given contact.
pub async fn unblock(context: &Context, id: ContactId) -> Result<()> {
pub async fn unblock(context: &Context, id: u32) -> Result<()> {
set_block_contact(context, id, false).await
}
@@ -344,7 +269,7 @@ impl Contact {
/// a bunch of addresses.
///
/// May result in a `#DC_EVENT_CONTACTS_CHANGED` event.
pub async fn create(context: &Context, name: &str, addr: &str) -> Result<ContactId> {
pub async fn create(context: &Context, name: &str, addr: &str) -> Result<u32> {
let name = improve_single_line_input(name);
ensure!(!addr.is_empty(), "Cannot create contact with empty address");
@@ -367,12 +292,12 @@ impl Contact {
}
/// Mark messages from a contact as noticed.
pub async fn mark_noticed(context: &Context, id: ContactId) -> Result<()> {
pub async fn mark_noticed(context: &Context, id: u32) -> Result<()> {
context
.sql
.execute(
"UPDATE msgs SET state=? WHERE from_id=? AND state=?;",
paramsv![MessageState::InNoticed, id, MessageState::InFresh],
paramsv![MessageState::InNoticed, id as i32, MessageState::InFresh],
)
.await?;
Ok(())
@@ -388,15 +313,17 @@ impl Contact {
context: &Context,
addr: &str,
min_origin: Origin,
) -> Result<Option<ContactId>> {
) -> Result<Option<u32>> {
if addr.is_empty() {
bail!("lookup_id_by_addr: empty address");
}
let addr_normalized = addr_normalize(addr);
if context.is_self_addr(addr_normalized).await? {
return Ok(Some(ContactId::SELF));
if let Some(addr_self) = context.get_config(Config::ConfiguredAddr).await? {
if addr_cmp(addr_normalized, &addr_self) {
return Ok(Some(DC_CONTACT_ID_SELF));
}
}
let id = context
.sql
@@ -404,7 +331,11 @@ impl Contact {
"SELECT id FROM contacts \
WHERE addr=?1 COLLATE NOCASE \
AND id>?2 AND origin>=?3 AND blocked=0;",
paramsv![addr_normalized, ContactId::LAST_SPECIAL, min_origin as u32,],
paramsv![
addr_normalized,
DC_CONTACT_ID_LAST_SPECIAL as i32,
min_origin as u32,
],
)
.await?;
Ok(id)
@@ -440,19 +371,20 @@ impl Contact {
name: &str,
addr: &str,
mut origin: Origin,
) -> Result<(ContactId, Modifier)> {
) -> Result<(u32, Modifier)> {
let mut sth_modified = Modifier::None;
ensure!(!addr.is_empty(), "Can not add_or_lookup empty address");
ensure!(origin != Origin::Unknown, "Missing valid origin");
let addr = addr_normalize(addr).to_string();
let addr_self = context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
// during configuration process some chats are added and addr
// might not be configured yet
let addr_self = context.get_configured_addr().await.unwrap_or_default();
if addr_cmp(&addr, &addr_self) {
return Ok((ContactId::SELF, sth_modified));
return Ok((DC_CONTACT_ID_SELF, sth_modified));
}
if !may_be_valid_addr(&addr) {
@@ -568,7 +500,7 @@ impl Contact {
paramsv![Chattype::Single, isize::try_from(row_id)?]
).await?;
if let Some(chat_id) = chat_id {
let contact = Contact::get_by_id(context, ContactId::new(row_id)).await?;
let contact = Contact::get_by_id(context, row_id as u32).await?;
let chat_name = contact.get_display_name();
match context
.sql
@@ -625,7 +557,7 @@ impl Contact {
}
}
Ok((ContactId::new(row_id), sth_modified))
Ok((row_id, sth_modified))
}
/// Add a number of contacts.
@@ -685,8 +617,12 @@ impl Contact {
context: &Context,
listflags: u32,
query: Option<impl AsRef<str>>,
) -> Result<Vec<ContactId>> {
let self_addr = context.get_configured_addr().await.unwrap_or_default();
) -> Result<Vec<u32>> {
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
let mut add_self = false;
let mut ret = Vec::new();
let flag_verified_only = (listflags & DC_GCL_VERIFIED_ONLY) != 0;
@@ -708,16 +644,16 @@ impl Contact {
ORDER BY LOWER(iif(c.name='',c.authname,c.name)||c.addr),c.id;",
paramsv![
self_addr,
ContactId::LAST_SPECIAL,
DC_CONTACT_ID_LAST_SPECIAL as i32,
Origin::IncomingReplyTo,
s3str_like_cmd,
s3str_like_cmd,
if flag_verified_only { 0i32 } else { 1i32 },
],
|row| row.get::<_, ContactId>(0),
|row| row.get::<_, i32>(0),
|ids| {
for id in ids {
ret.push(id?);
ret.push(id? as u32);
}
Ok(())
},
@@ -752,11 +688,15 @@ impl Contact {
AND origin>=?3
AND blocked=0
ORDER BY LOWER(iif(name='',authname,name)||addr),id;",
paramsv![self_addr, ContactId::LAST_SPECIAL, Origin::IncomingReplyTo],
|row| row.get::<_, ContactId>(0),
paramsv![
self_addr,
DC_CONTACT_ID_LAST_SPECIAL as i32,
Origin::IncomingReplyTo
],
|row| row.get::<_, i32>(0),
|ids| {
for id in ids {
ret.push(id?);
ret.push(id? as u32);
}
Ok(())
},
@@ -765,7 +705,7 @@ impl Contact {
}
if flag_add_self && add_self {
ret.push(ContactId::SELF);
ret.push(DC_CONTACT_ID_SELF);
}
Ok(ret)
@@ -820,14 +760,14 @@ impl Contact {
.sql
.count(
"SELECT COUNT(*) FROM contacts WHERE id>? AND blocked!=0",
paramsv![ContactId::LAST_SPECIAL],
paramsv![DC_CONTACT_ID_LAST_SPECIAL],
)
.await?;
Ok(count as usize)
}
/// Get blocked contacts.
pub async fn get_all_blocked(context: &Context) -> Result<Vec<ContactId>> {
pub async fn get_all_blocked(context: &Context) -> Result<Vec<u32>> {
Contact::update_blocked_mailinglist_contacts(context)
.await
.context("cannot update blocked mailinglist contacts")?;
@@ -836,8 +776,8 @@ impl Contact {
.sql
.query_map(
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY LOWER(iif(name='',authname,name)||addr),id;",
paramsv![ContactId::LAST_SPECIAL],
|row| row.get::<_, ContactId>(0),
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
|row| row.get::<_, u32>(0),
|ids| {
ids.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
@@ -852,15 +792,15 @@ impl Contact {
/// This function returns a string explaining the encryption state
/// of the contact and if the connection is encrypted the
/// fingerprints of the keys involved.
pub async fn get_encrinfo(context: &Context, contact_id: ContactId) -> Result<String> {
pub async fn get_encrinfo(context: &Context, contact_id: u32) -> Result<String> {
ensure!(
!contact_id.is_special(),
contact_id > DC_CONTACT_ID_LAST_SPECIAL,
"Can not provide encryption info for special contact"
);
let mut ret = String::new();
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
let loginparam = LoginParam::load_configured_params(context).await?;
let loginparam = LoginParam::from_database(context, "configured_").await?;
let peerstate = Peerstate::from_addr(context, &contact.addr).await?;
if let Some(peerstate) = peerstate.filter(|peerstate| {
@@ -921,21 +861,27 @@ impl Contact {
/// possible as the contact is in use. In this case, the contact can be blocked.
///
/// May result in a `#DC_EVENT_CONTACTS_CHANGED` event.
pub async fn delete(context: &Context, contact_id: ContactId) -> Result<()> {
ensure!(!contact_id.is_special(), "Can not delete special contact");
pub async fn delete(context: &Context, contact_id: u32) -> Result<()> {
ensure!(
contact_id > DC_CONTACT_ID_LAST_SPECIAL,
"Can not delete special contact"
);
let count_chats = context
.sql
.count(
"SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;",
paramsv![contact_id],
paramsv![contact_id as i32],
)
.await?;
if count_chats == 0 {
match context
.sql
.execute("DELETE FROM contacts WHERE id=?;", paramsv![contact_id])
.execute(
"DELETE FROM contacts WHERE id=?;",
paramsv![contact_id as i32],
)
.await
{
Ok(_) => {
@@ -958,10 +904,10 @@ impl Contact {
/// Get a single contact object. For a list, see eg. dc_get_contacts().
///
/// For contact ContactId::SELF (1), the function returns sth.
/// For contact DC_CONTACT_ID_SELF (1), the function returns sth.
/// like "Me" in the selected language and the email address
/// defined by dc_set_config().
pub async fn get_by_id(context: &Context, contact_id: ContactId) -> Result<Contact> {
pub async fn get_by_id(context: &Context, contact_id: u32) -> Result<Contact> {
let contact = Contact::load_from_db(context, contact_id).await?;
Ok(contact)
@@ -973,7 +919,7 @@ impl Contact {
.sql
.execute(
"UPDATE contacts SET param=? WHERE id=?",
paramsv![self.param.to_string(), self.id],
paramsv![self.param.to_string(), self.id as i32],
)
.await?;
Ok(())
@@ -985,14 +931,14 @@ impl Contact {
.sql
.execute(
"UPDATE contacts SET status=? WHERE id=?",
paramsv![self.status, self.id],
paramsv![self.status, self.id as i32],
)
.await?;
Ok(())
}
/// Get the ID of the contact.
pub fn get_id(&self) -> ContactId {
pub fn get_id(&self) -> u32 {
self.id
}
@@ -1051,7 +997,7 @@ impl Contact {
/// This is the image set by each remote user on their own
/// using dc_set_config(context, "selfavatar", image).
pub async fn get_profile_image(&self, context: &Context) -> Result<Option<PathBuf>> {
if self.id == ContactId::SELF {
if self.id == DC_CONTACT_ID_SELF {
if let Some(p) = context.get_config(Config::Selfavatar).await? {
return Ok(Some(PathBuf::from(p)));
}
@@ -1097,7 +1043,7 @@ impl Contact {
) -> Result<VerifiedStatus> {
// We're always sort of secured-verified as we could verify the key on this device any time with the key
// on this device
if self.id == ContactId::SELF {
if self.id == DC_CONTACT_ID_SELF {
return Ok(VerifiedStatus::BidirectVerified);
}
@@ -1119,7 +1065,7 @@ impl Contact {
pub async fn addr_equals_contact(
context: &Context,
addr: &str,
contact_id: ContactId,
contact_id: u32,
) -> Result<bool> {
if addr.is_empty() {
return Ok(false);
@@ -1145,14 +1091,14 @@ impl Contact {
.sql
.count(
"SELECT COUNT(*) FROM contacts WHERE id>?;",
paramsv![ContactId::LAST_SPECIAL],
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
)
.await?;
Ok(count)
}
pub async fn real_exists_by_id(context: &Context, contact_id: ContactId) -> Result<bool> {
if contact_id.is_special() {
pub async fn real_exists_by_id(context: &Context, contact_id: u32) -> Result<bool> {
if contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
return Ok(false);
}
@@ -1160,7 +1106,7 @@ impl Contact {
.sql
.exists(
"SELECT COUNT(*) FROM contacts WHERE id=?;",
paramsv![contact_id],
paramsv![contact_id as i32],
)
.await?;
Ok(exists)
@@ -1168,14 +1114,14 @@ impl Contact {
pub async fn scaleup_origin_by_id(
context: &Context,
contact_id: ContactId,
contact_id: u32,
origin: Origin,
) -> Result<()> {
context
.sql
.execute(
"UPDATE contacts SET origin=? WHERE id=? AND origin<?;",
paramsv![origin, contact_id, origin],
paramsv![origin, contact_id as i32, origin],
)
.await?;
Ok(())
@@ -1219,13 +1165,9 @@ fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
}
}
async fn set_block_contact(
context: &Context,
contact_id: ContactId,
new_blocking: bool,
) -> Result<()> {
async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: bool) -> Result<()> {
ensure!(
!contact_id.is_special(),
contact_id > DC_CONTACT_ID_LAST_SPECIAL,
"Can't block special contact {}",
contact_id
);
@@ -1237,7 +1179,7 @@ async fn set_block_contact(
.sql
.execute(
"UPDATE contacts SET blocked=? WHERE id=?;",
paramsv![i32::from(new_blocking), contact_id],
paramsv![new_blocking as i32, contact_id as i32],
)
.await?;
@@ -1288,14 +1230,14 @@ WHERE type=? AND id IN (
/// this typically happens if we see message with our own profile image, sent from another device.
pub(crate) async fn set_profile_image(
context: &Context,
contact_id: ContactId,
contact_id: u32,
profile_image: &AvatarAction,
was_encrypted: bool,
) -> Result<()> {
let mut contact = Contact::load_from_db(context, contact_id).await?;
let changed = match profile_image {
AvatarAction::Change(profile_image) => {
if contact_id == ContactId::SELF {
if contact_id == DC_CONTACT_ID_SELF {
if was_encrypted {
context
.set_config(Config::Selfavatar, Some(profile_image))
@@ -1309,7 +1251,7 @@ pub(crate) async fn set_profile_image(
true
}
AvatarAction::Delete => {
if contact_id == ContactId::SELF {
if contact_id == DC_CONTACT_ID_SELF {
if was_encrypted {
context.set_config(Config::Selfavatar, None).await?;
} else {
@@ -1335,12 +1277,12 @@ pub(crate) async fn set_profile_image(
/// between Delta Chat devices.
pub(crate) async fn set_status(
context: &Context,
contact_id: ContactId,
contact_id: u32,
status: String,
encrypted: bool,
has_chat_version: bool,
) -> Result<()> {
if contact_id == ContactId::SELF {
if contact_id == DC_CONTACT_ID_SELF {
if encrypted && has_chat_version {
context
.set_config(Config::Selfstatus, Some(&status))
@@ -1361,11 +1303,11 @@ pub(crate) async fn set_status(
/// Updates last seen timestamp of the contact if it is earlier than the given `timestamp`.
pub(crate) async fn update_last_seen(
context: &Context,
contact_id: ContactId,
contact_id: u32,
timestamp: i64,
) -> Result<()> {
ensure!(
!contact_id.is_special(),
contact_id > DC_CONTACT_ID_LAST_SPECIAL,
"Can not update special contact last seen timestamp"
);
@@ -1469,17 +1411,6 @@ mod tests {
use crate::message::Message;
use crate::test_utils::{self, TestContext};
#[test]
fn test_contact_id_values() {
// Some FFI users need to have the values of these fixed, how naughty. But let's
// make sure we don't modify them anyway.
assert_eq!(ContactId::UNDEFINED.to_u32(), 0);
assert_eq!(ContactId::SELF.to_u32(), 1);
assert_eq!(ContactId::INFO.to_u32(), 2);
assert_eq!(ContactId::DEVICE.to_u32(), 5);
assert_eq!(ContactId::LAST_SPECIAL.to_u32(), 9);
}
#[test]
fn test_may_be_valid_addr() {
assert_eq!(may_be_valid_addr(""), false);
@@ -1543,7 +1474,7 @@ mod tests {
Origin::IncomingReplyTo,
)
.await?;
assert_ne!(id, ContactId::UNDEFINED);
assert_ne!(id, 0);
let contact = Contact::load_from_db(&context.ctx, id).await.unwrap();
assert_eq!(contact.get_name(), "");
@@ -1621,7 +1552,7 @@ mod tests {
Contact::add_or_lookup(&t, "bla foo", "one@eins.org", Origin::IncomingUnknownTo)
.await
.unwrap();
assert!(!contact_id.is_special());
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
assert_eq!(contact.get_id(), contact_id);
@@ -1648,7 +1579,7 @@ mod tests {
Contact::add_or_lookup(&t, "", "three@drei.sam", Origin::IncomingUnknownTo)
.await
.unwrap();
assert!(!contact_id.is_special());
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
assert_eq!(sth_modified, Modifier::None);
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
assert_eq!(contact.get_name(), "");
@@ -1688,7 +1619,7 @@ mod tests {
Contact::add_or_lookup(&t, "", "alice@w.de", Origin::IncomingUnknownTo)
.await
.unwrap();
assert!(!contact_id.is_special());
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
assert_eq!(sth_modified, Modifier::None);
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
assert_eq!(contact.get_name(), "Wonderland, Alice");
@@ -1697,7 +1628,8 @@ mod tests {
assert_eq!(contact.get_name_n_addr(), "Wonderland, Alice (alice@w.de)");
// check SELF
let contact = Contact::load_from_db(&t, ContactId::SELF).await.unwrap();
let contact = Contact::load_from_db(&t, DC_CONTACT_ID_SELF).await.unwrap();
assert_eq!(DC_CONTACT_ID_SELF, 1);
assert_eq!(contact.get_name(), stock_str::self_msg(&t).await);
assert_eq!(contact.get_addr(), ""); // we're not configured
assert!(!contact.is_blocked());
@@ -1707,7 +1639,7 @@ mod tests {
async fn test_delete() -> Result<()> {
let alice = TestContext::new_alice().await;
assert!(Contact::delete(&alice, ContactId::SELF).await.is_err());
assert!(Contact::delete(&alice, DC_CONTACT_ID_SELF).await.is_err());
// Create Bob contact
let (contact_id, _) =
@@ -1740,7 +1672,7 @@ mod tests {
Contact::add_or_lookup(&t, "bob1", "bob@example.org", Origin::IncomingUnknownFrom)
.await
.unwrap();
assert!(!contact_id.is_special());
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
assert_eq!(sth_modified, Modifier::Created);
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "bob1");
@@ -1752,7 +1684,7 @@ mod tests {
Contact::add_or_lookup(&t, "bob2", "bob@example.org", Origin::IncomingUnknownFrom)
.await
.unwrap();
assert!(!contact_id.is_special());
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "bob2");
@@ -1763,7 +1695,7 @@ mod tests {
let contact_id = Contact::create(&t, "bob3", "bob@example.org")
.await
.unwrap();
assert!(!contact_id.is_special());
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "bob2");
assert_eq!(contact.get_name(), "bob3");
@@ -1774,7 +1706,7 @@ mod tests {
Contact::add_or_lookup(&t, "bob4", "bob@example.org", Origin::IncomingUnknownFrom)
.await
.unwrap();
assert!(!contact_id.is_special());
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "bob4");
@@ -1788,7 +1720,7 @@ mod tests {
// manually create "claire@example.org" without a given name
let contact_id = Contact::create(&t, "", "claire@example.org").await.unwrap();
assert!(!contact_id.is_special());
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "");
assert_eq!(contact.get_name(), "");
@@ -1962,7 +1894,7 @@ mod tests {
let id = Contact::lookup_id_by_addr(&alice.ctx, "alice@example.org", Origin::Unknown)
.await
.unwrap();
assert_eq!(id, Some(ContactId::SELF));
assert_eq!(id, Some(DC_CONTACT_ID_SELF));
}
#[async_std::test]
@@ -1970,9 +1902,9 @@ mod tests {
let alice = TestContext::new_alice().await;
// Return error for special IDs
let encrinfo = Contact::get_encrinfo(&alice, ContactId::SELF).await;
let encrinfo = Contact::get_encrinfo(&alice, DC_CONTACT_ID_SELF).await;
assert!(encrinfo.is_err());
let encrinfo = Contact::get_encrinfo(&alice, ContactId::DEVICE).await;
let encrinfo = Contact::get_encrinfo(&alice, DC_CONTACT_ID_DEVICE).await;
assert!(encrinfo.is_err());
let (contact_bob_id, _modified) =
@@ -2153,7 +2085,7 @@ Chat-Version: 1.0
Date: Sun, 22 Mar 2020 22:37:55 +0000
Hi."#;
dc_receive_imf(&alice, mime, false).await?;
dc_receive_imf(&alice, mime, "Inbox", false).await?;
let msg = alice.get_last_msg().await;
let timestamp = msg.get_timestamp();

View File

@@ -10,6 +10,7 @@ use async_std::{
channel::{self, Receiver, Sender},
path::{Path, PathBuf},
sync::{Arc, Mutex, RwLock},
task,
};
use crate::chat::{get_chat_cnt, ChatId};
@@ -23,6 +24,7 @@ use crate::login_param::LoginParam;
use crate::message::{self, MessageState, MsgId};
use crate::quota::QuotaInfo;
use crate::scheduler::Scheduler;
use crate::securejoin::Bob;
use crate::sql::Sql;
#[derive(Clone, Debug)]
@@ -43,6 +45,7 @@ pub struct InnerContext {
/// Blob directory path
pub(crate) blobdir: PathBuf,
pub(crate) sql: Sql,
pub(crate) bob: Bob,
pub(crate) last_smeared_timestamp: RwLock<i64>,
pub(crate) running_state: RwLock<RunningState>,
/// Mutex to avoid generating the key for the user more than once.
@@ -55,6 +58,7 @@ pub struct InnerContext {
pub(crate) events: Events,
pub(crate) scheduler: RwLock<Scheduler>,
pub(crate) ephemeral_task: RwLock<Option<task::JoinHandle<()>>>,
/// Recently loaded quota information, if any.
/// Set to `None` if quota was never tried to load.
@@ -167,6 +171,7 @@ impl Context {
blobdir,
running_state: RwLock::new(Default::default()),
sql: Sql::new(dbfile),
bob: Default::default(),
last_smeared_timestamp: RwLock::new(0),
generating_key_mutex: Mutex::new(()),
oauth2_mutex: Mutex::new(()),
@@ -174,6 +179,7 @@ impl Context {
translated_stockstrings: RwLock::new(HashMap::new()),
events: Events::default(),
scheduler: RwLock::new(Scheduler::Stopped),
ephemeral_task: RwLock::new(None),
quota: RwLock::new(None),
creation_time: std::time::SystemTime::now(),
last_full_folder_scan: Mutex::new(None),
@@ -195,11 +201,6 @@ impl Context {
return;
}
if let Ok(false) = self.is_configured().await {
warn!(self, "can not start io on a context that is not configured");
return;
}
{
let l = &mut *self.inner.scheduler.write().await;
if let Err(err) = l.start(self.clone()).await {
@@ -241,24 +242,6 @@ impl Context {
});
}
/// Emits a generic MsgsChanged event (without chat or message id)
pub fn emit_msgs_changed_without_ids(&self) {
self.emit_event(EventType::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
}
/// Emits a MsgsChanged event with specified chat and message ids
pub fn emit_msgs_changed(&self, chat_id: ChatId, msg_id: MsgId) {
self.emit_event(EventType::MsgsChanged { chat_id, msg_id });
}
/// Emits an IncomingMsg event with specified chat and message ids
pub fn emit_incoming_msg(&self, chat_id: ChatId, msg_id: MsgId) {
self.emit_event(EventType::IncomingMsg { chat_id, msg_id });
}
/// Returns a receiver for emitted events.
///
/// Multiple emitters can be created, but note that in this case each emitted event will
@@ -334,8 +317,8 @@ impl Context {
pub async fn get_info(&self) -> Result<BTreeMap<&'static str, String>> {
let unset = "0";
let l = LoginParam::load_candidate_params(self).await?;
let l2 = LoginParam::load_configured_params(self).await?;
let l = LoginParam::from_database(self, "").await?;
let l2 = LoginParam::from_database(self, "configured_").await?;
let displayname = self.get_config(Config::Displayname).await?;
let chats = get_chat_cnt(self).await? as usize;
let unblocked_msgs = message::get_unblocked_msg_cnt(self).await as usize;
@@ -374,7 +357,7 @@ impl Context {
let sentbox_watch = self.get_config_int(Config::SentboxWatch).await?;
let mvbox_move = self.get_config_int(Config::MvboxMove).await?;
let only_fetch_mvbox = self.get_config_int(Config::OnlyFetchMvbox).await?;
let sentbox_move = self.get_config_int(Config::SentboxMove).await?;
let folders_configured = self
.sql
.get_raw_config_int("folders_configured")
@@ -438,7 +421,7 @@ impl Context {
);
res.insert("sentbox_watch", sentbox_watch.to_string());
res.insert("mvbox_move", mvbox_move.to_string());
res.insert("only_fetch_mvbox", only_fetch_mvbox.to_string());
res.insert("sentbox_move", sentbox_move.to_string());
res.insert("folders_configured", folders_configured.to_string());
res.insert("configured_sentbox_folder", configured_sentbox_folder);
res.insert("configured_mvbox_folder", configured_mvbox_folder);
@@ -626,6 +609,11 @@ impl Context {
Ok(mvbox.as_deref() == Some(folder_name))
}
pub async fn is_spam_folder(&self, folder_name: &str) -> Result<bool> {
let spam = self.get_config(Config::ConfiguredSpamFolder).await?;
Ok(spam.as_deref() == Some(folder_name))
}
pub(crate) fn derive_blobdir(dbfile: &PathBuf) -> PathBuf {
let mut blob_fname = OsString::new();
blob_fname.push(dbfile.file_name().unwrap_or_default());
@@ -657,6 +645,10 @@ impl InnerContext {
lock.stop(token).await;
}
}
if let Some(ephemeral_task) = self.ephemeral_task.write().await.take() {
ephemeral_task.cancel().await;
}
}
}
@@ -681,10 +673,10 @@ mod tests {
use crate::chat::{
get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat, ChatId, MuteDuration,
};
use crate::contact::ContactId;
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
use crate::dc_receive_imf::dc_receive_imf;
use crate::dc_tools::dc_create_outgoing_rfc724_mid;
use crate::message::{Message, Viewtype};
use crate::message::Message;
use crate::test_utils::TestContext;
use anyhow::Context as _;
use std::time::Duration;
@@ -727,7 +719,9 @@ mod tests {
dc_create_outgoing_rfc724_mid(None, contact.get_addr())
);
println!("{}", msg);
dc_receive_imf(t, msg.as_bytes(), false).await.unwrap();
dc_receive_imf(t, msg.as_bytes(), "INBOX", false)
.await
.unwrap();
}
#[async_std::test]
@@ -965,7 +959,7 @@ mod tests {
#[async_std::test]
async fn test_search_msgs() -> Result<()> {
let alice = TestContext::new_alice().await;
let self_talk = ChatId::create_for_contact(&alice, ContactId::SELF).await?;
let self_talk = ChatId::create_for_contact(&alice, DC_CONTACT_ID_SELF).await?;
let chat = alice
.create_chat_with_contact("Bob", "bob@example.org")
.await;

File diff suppressed because it is too large Load Diff

View File

@@ -21,10 +21,10 @@ use mailparse::MailHeaderMap;
use rand::{thread_rng, Rng};
use crate::chat::{add_device_msg, add_device_msg_with_importance};
use crate::constants::{DC_ELLIPSIS, DC_OUTDATED_WARNING_DAYS};
use crate::constants::{Viewtype, DC_ELLIPSIS, DC_OUTDATED_WARNING_DAYS};
use crate::context::Context;
use crate::events::EventType;
use crate::message::{Message, Viewtype};
use crate::message::Message;
use crate::provider::get_provider_update_timestamp;
use crate::stock_str;
@@ -71,7 +71,7 @@ pub(crate) fn dc_gm2local_offset() -> i64 {
/* returns the offset that must be _added_ to an UTC/GMT-time to create the localtime.
the function may return negative values. */
let lt = Local::now();
i64::from(lt.offset().local_minus_utc())
lt.offset().local_minus_utc() as i64
}
// timesmearing
@@ -86,7 +86,7 @@ pub(crate) fn dc_gm2local_offset() -> i64 {
// `last_smeared_timestamp` is again in sync with the normal time.
// - however, we do not do all this for the far future,
// but at max `MAX_SECONDS_TO_LEND_FROM_FUTURE`
pub(crate) const MAX_SECONDS_TO_LEND_FROM_FUTURE: i64 = 5;
const MAX_SECONDS_TO_LEND_FROM_FUTURE: i64 = 5;
/// Returns the current smeared timestamp,
///
@@ -214,10 +214,7 @@ pub(crate) fn dc_create_id() -> String {
rng.fill(&mut arr[..]);
// Take 11 base64 characters containing 66 random bits.
base64::encode_config(&arr, base64::URL_SAFE)
.chars()
.take(11)
.collect()
base64::encode(&arr).chars().take(11).collect()
}
/// Function generates a Message-ID that can be used for a new outgoing message.
@@ -537,8 +534,8 @@ impl rusqlite::types::ToSql for EmailAddress {
/// Makes sure that a user input that is not supposed to contain newlines does not contain newlines.
pub(crate) fn improve_single_line_input(input: &str) -> String {
input
.replace('\n', " ")
.replace('\r', " ")
.replace("\n", " ")
.replace("\r", " ")
.trim()
.to_string()
}
@@ -703,7 +700,7 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
async fn check_parse_receive_headers_integration(raw: &[u8], expected: &str) {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
dc_receive_imf(&t, raw, false).await.unwrap();
dc_receive_imf(&t, raw, "INBOX", false).await.unwrap();
let msg = t.get_last_msg().await;
let msg_info = get_msg_info(&t, msg.id).await.unwrap();
@@ -765,15 +762,6 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
assert_eq!(buf.len(), 11);
}
#[test]
fn test_dc_create_id_invalid_chars() {
for _ in 1..1000 {
let buf = dc_create_id();
assert!(!buf.contains('/')); // `/` must not be used to be URL-safe
assert!(!buf.contains('.')); // `.` is used as a delimiter when extracting grpid from Message-ID
}
}
#[test]
fn test_dc_extract_grpid_from_rfc724_mid() {
// Should return None if we pass invalid mid

View File

@@ -3,14 +3,14 @@
use anyhow::{anyhow, Result};
use deltachat_derive::{FromSql, ToSql};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use crate::config::Config;
use crate::constants::Viewtype;
use crate::context::Context;
use crate::dc_tools::time;
use crate::imap::{Imap, ImapActionResult};
use crate::job::{self, Action, Job, Status};
use crate::message::{Message, MsgId, Viewtype};
use crate::message::{Message, MsgId};
use crate::mimeparser::{MimeMessage, Part};
use crate::param::Params;
use crate::{job_try, stock_str, EventType};
@@ -146,7 +146,7 @@ impl Job {
if let Some((server_uid, server_folder)) = row {
match imap
.fetch_single_msg(context, &server_folder, server_uid, msg.rfc724_mid.clone())
.fetch_single_msg(context, &server_folder, server_uid)
.await
{
ImapActionResult::RetryLater | ImapActionResult::Failed => {
@@ -185,7 +185,6 @@ impl Imap {
context: &Context,
folder: &str,
uid: u32,
rfc724_mid: String,
) -> ImapActionResult {
if let Some(imapresult) = self
.prepare_imap_operation_on_msg(context, folder, uid)
@@ -197,10 +196,8 @@ impl Imap {
// we are connected, and the folder is selected
info!(context, "Downloading message {}/{} fully...", folder, uid);
let mut uid_message_ids: BTreeMap<u32, String> = BTreeMap::new();
uid_message_ids.insert(uid, rfc724_mid);
let (last_uid, _received) = match self
.fetch_many_msgs(context, folder, vec![uid], &uid_message_ids, false, false)
.fetch_many_msgs(context, folder, vec![uid], false, false)
.await
{
Ok(res) => res,
@@ -254,15 +251,13 @@ impl MimeMessage {
#[cfg(test)]
mod tests {
use num_traits::FromPrimitive;
use super::*;
use crate::chat::send_msg;
use crate::constants::Viewtype;
use crate::dc_receive_imf::dc_receive_imf_inner;
use crate::ephemeral::Timer;
use crate::message::Viewtype;
use crate::test_utils::TestContext;
use super::*;
use num_traits::FromPrimitive;
#[test]
fn test_downloadstate_values() {
@@ -342,15 +337,7 @@ mod tests {
Date: Sun, 22 Mar 2020 22:37:57 +0000\
Content-Type: text/plain";
dc_receive_imf_inner(
&t,
"Mr.12345678901@example.com",
header.as_bytes(),
false,
Some(100000),
false,
)
.await?;
dc_receive_imf_inner(&t, header.as_bytes(), "INBOX", false, Some(100000), false).await?;
let msg = t.get_last_msg().await;
assert_eq!(msg.download_state(), DownloadState::Available);
assert_eq!(msg.get_subject(), "foo");
@@ -361,8 +348,8 @@ mod tests {
dc_receive_imf_inner(
&t,
"Mr.12345678901@example.com",
format!("{}\n\n100k text...", header).as_bytes(),
"INBOX",
false,
None,
false,
@@ -390,7 +377,6 @@ mod tests {
// download message from bob partially, this must not change the ephemeral timer
dc_receive_imf_inner(
&t,
"first@example.org",
b"From: Bob <bob@example.org>\n\
To: Alice <alice@example.org>\n\
Chat-Version: 1.0\n\
@@ -398,6 +384,7 @@ mod tests {
Message-ID: <first@example.org>\n\
Date: Sun, 14 Nov 2021 00:10:00 +0000\
Content-Type: text/plain",
"INBOX",
false,
Some(100000),
false,

View File

@@ -2,7 +2,7 @@
use std::collections::HashSet;
use anyhow::{format_err, Context as _, Result};
use anyhow::{bail, format_err, Context as _, Result};
use mailparse::ParsedMail;
use num_traits::FromPrimitive;
@@ -28,7 +28,13 @@ impl EncryptHelper {
let prefer_encrypt =
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await?)
.unwrap_or_default();
let addr = context.get_configured_addr().await?;
let addr = match context.get_config(Config::ConfiguredAddr).await? {
None => {
bail!("addr not configured!");
}
Some(addr) => addr,
};
let public_key = SignedPublicKey::load_self(context).await?;
Ok(EncryptHelper {
@@ -381,21 +387,28 @@ fn contains_report(mail: &ParsedMail<'_>) -> bool {
/// [Config::ConfiguredAddr] is configured, this address is returned.
// TODO, remove this once deltachat::key::Key no longer exists.
pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
let self_addr = context.get_configured_addr().await?;
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await?
.context(concat!(
"Failed to get self address, ",
"cannot ensure secret key if not configured."
))?;
SignedPublicKey::load_self(context).await?;
Ok(self_addr)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chat;
use crate::message::{Message, Viewtype};
use crate::constants::Viewtype;
use crate::message::Message;
use crate::param::Param;
use crate::peerstate::ToSave;
use crate::test_utils::{bob_keypair, TestContext};
use super::*;
mod ensure_secret_key_exists {
use super::*;

View File

@@ -48,9 +48,9 @@
//!
//! ## When messages are deleted
//!
//! The `ephemeral_loop` task schedules the next due running of
//! `delete_expired_messages` which in turn emits `MsgsChanged` events
//! when deleting local messages to make UIs reload displayed messages.
//! Local deletion happens when the chatlist or chat is loaded. A
//! `MsgsChanged` event is emitted when a message deletion is due, to
//! make UI reload displayed messages and cause actual deletion.
//!
//! Server deletion happens by updating the `imap` table based on
//! the database entries which are expired either according to their
@@ -62,21 +62,19 @@ use std::str::FromStr;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use anyhow::{ensure, Context as _, Result};
use async_std::channel::Receiver;
use async_std::future::timeout;
use async_std::task;
use serde::{Deserialize, Serialize};
use crate::chat::{send_msg, ChatId};
use crate::constants::{DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH};
use crate::contact::ContactId;
use crate::constants::{
Viewtype, DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_SELF,
};
use crate::context::Context;
use crate::dc_tools::{duration_to_str, time};
use crate::dc_tools::time;
use crate::download::MIN_DELETE_SERVER_AFTER;
use crate::events::EventType;
use crate::log::LogExt;
use crate::message::{Message, MessageState, MsgId, Viewtype};
use crate::message::{Message, MessageState, MsgId};
use crate::mimeparser::SystemMessage;
use crate::sql;
use crate::stock_str;
use std::cmp::max;
@@ -198,7 +196,7 @@ impl ChatId {
}
self.inner_set_ephemeral_timer(context, timer).await?;
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(stock_ephemeral_timer_changed(context, timer, ContactId::SELF).await);
msg.text = Some(stock_ephemeral_timer_changed(context, timer, DC_CONTACT_ID_SELF).await);
msg.param.set_cmd(SystemMessage::EphemeralTimerChanged);
if let Err(err) = send_msg(context, self, &mut msg).await {
error!(
@@ -214,7 +212,7 @@ impl ChatId {
pub(crate) async fn stock_ephemeral_timer_changed(
context: &Context,
timer: Timer,
from_id: ContactId,
from_id: u32,
) -> String {
match timer {
Timer::Disabled => stock_str::msg_ephemeral_timer_disabled(context, from_id).await,
@@ -293,43 +291,12 @@ impl MsgId {
paramsv![ephemeral_timestamp, ephemeral_timestamp, self],
)
.await?;
context.interrupt_ephemeral_task().await;
schedule_ephemeral_task(context).await;
}
Ok(())
}
}
pub(crate) async fn start_ephemeral_timers_msgids(
context: &Context,
msg_ids: &[MsgId],
) -> Result<()> {
let msg_ids: Vec<&dyn crate::ToSql> = msg_ids
.iter()
.map(|msg_id| msg_id as &dyn crate::ToSql)
.collect();
let now = time();
let count = context
.sql
.execute(
format!(
"UPDATE msgs SET ephemeral_timestamp = ? + ephemeral_timer
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ? + ephemeral_timer) AND ephemeral_timer > 0
AND id IN ({})",
sql::repeat_vars(msg_ids.len())?
),
rusqlite::params_from_iter(
std::iter::once(&now as &dyn crate::ToSql)
.chain(std::iter::once(&now as &dyn crate::ToSql))
.chain(msg_ids),
),
)
.await?;
if count > 0 {
context.interrupt_ephemeral_task().await;
}
Ok(())
}
/// Deletes messages which are expired according to
/// `delete_device_after` setting or `ephemeral_timestamp` column.
///
@@ -338,7 +305,7 @@ pub(crate) async fn start_ephemeral_timers_msgids(
/// false. This function does not emit the MsgsChanged event itself,
/// because it is also called when chatlist is reloaded, and emitting
/// MsgsChanged there will cause infinite reload loop.
pub(crate) async fn delete_expired_messages(context: &Context, now: i64) -> Result<()> {
pub(crate) async fn delete_expired_messages(context: &Context) -> Result<bool> {
let mut updated = context
.sql
.execute(
@@ -354,21 +321,21 @@ WHERE
AND ephemeral_timestamp <= ?
AND chat_id != ?
"#,
paramsv![DC_CHAT_ID_TRASH, now, DC_CHAT_ID_TRASH],
paramsv![DC_CHAT_ID_TRASH, time(), DC_CHAT_ID_TRASH],
)
.await
.context("update failed")?
> 0;
if let Some(delete_device_after) = context.get_config_delete_device_after().await? {
let self_chat_id = ChatId::lookup_by_contact(context, ContactId::SELF)
let self_chat_id = ChatId::lookup_by_contact(context, DC_CONTACT_ID_SELF)
.await?
.unwrap_or_default();
let device_chat_id = ChatId::lookup_by_contact(context, ContactId::DEVICE)
let device_chat_id = ChatId::lookup_by_contact(context, DC_CONTACT_ID_DEVICE)
.await?
.unwrap_or_default();
let threshold_timestamp = now.saturating_sub(delete_device_after);
let threshold_timestamp = time() - delete_device_after;
// Delete expired messages
//
@@ -378,8 +345,7 @@ WHERE
.sql
.execute(
"UPDATE msgs \
SET chat_id = ?, txt = '', subject='', txt_raw='', \
mime_headers='', from_id=0, to_id=0, param='' \
SET txt = 'DELETED', chat_id = ? \
WHERE timestamp < ? \
AND chat_id > ? \
AND chat_id != ? \
@@ -398,116 +364,72 @@ WHERE
updated |= rows_modified > 0;
}
if updated {
context.emit_msgs_changed_without_ids();
}
Ok(())
schedule_ephemeral_task(context).await;
Ok(updated)
}
/// Calculates the next timestamp when a message will be deleted due to
/// `delete_device_after` setting being set.
async fn next_delete_device_after_timestamp(context: &Context) -> Result<Option<i64>> {
if let Some(delete_device_after) = context.get_config_delete_device_after().await? {
let self_chat_id = ChatId::lookup_by_contact(context, ContactId::SELF)
.await?
.unwrap_or_default();
let device_chat_id = ChatId::lookup_by_contact(context, ContactId::DEVICE)
.await?
.unwrap_or_default();
let oldest_message_timestamp: Option<i64> = context
.sql
.query_get_value(
r#"
SELECT min(timestamp)
FROM msgs
WHERE chat_id > ?
AND chat_id != ?
AND chat_id != ?;
"#,
paramsv![DC_CHAT_ID_TRASH, self_chat_id, device_chat_id],
)
.await?;
Ok(oldest_message_timestamp.map(|x| x.saturating_add(delete_device_after)))
} else {
Ok(None)
}
}
/// Calculates next timestamp when expiration of some message will happen.
/// Schedule a task to emit MsgsChanged event when the next local
/// deletion happens. Existing task is cancelled to make sure at most
/// one such task is scheduled at a time.
///
/// Expiration can happen either because user has set `delete_device_after` setting or because the
/// message itself has an ephemeral timer.
async fn next_expiration_timestamp(context: &Context) -> Option<i64> {
/// UI is expected to reload the chatlist or the chat in response to
/// MsgsChanged event, this will trigger actual deletion.
///
/// This takes into account only per-chat timeouts, because global device
/// timeouts are at least one hour long and deletion is triggered often enough
/// by user actions.
pub async fn schedule_ephemeral_task(context: &Context) {
let ephemeral_timestamp: Option<i64> = match context
.sql
.query_get_value(
r#"
SELECT min(ephemeral_timestamp)
FROM msgs
WHERE ephemeral_timestamp != 0
AND chat_id != ?;
"#,
SELECT ephemeral_timestamp
FROM msgs
WHERE ephemeral_timestamp != 0
AND chat_id != ?
ORDER BY ephemeral_timestamp ASC
LIMIT 1;
"#,
paramsv![DC_CHAT_ID_TRASH], // Trash contains already deleted messages, skip them
)
.await
{
Err(err) => {
warn!(context, "Can't calculate next ephemeral timeout: {}", err);
None
return;
}
Ok(ephemeral_timestamp) => ephemeral_timestamp,
};
let delete_device_after_timestamp: Option<i64> =
match next_delete_device_after_timestamp(context).await {
Err(err) => {
warn!(
context,
"Can't calculate timestamp of the next message expiration: {}", err
);
None
}
Ok(timestamp) => timestamp,
};
ephemeral_timestamp
.into_iter()
.chain(delete_device_after_timestamp.into_iter())
.min()
}
pub(crate) async fn ephemeral_loop(context: &Context, interrupt_receiver: Receiver<()>) {
loop {
let ephemeral_timestamp = next_expiration_timestamp(context).await;
// Cancel existing task, if any
if let Some(ephemeral_task) = context.ephemeral_task.write().await.take() {
ephemeral_task.cancel().await;
}
if let Some(ephemeral_timestamp) = ephemeral_timestamp {
let now = SystemTime::now();
let until = if let Some(ephemeral_timestamp) = ephemeral_timestamp {
UNIX_EPOCH
+ Duration::from_secs(ephemeral_timestamp.try_into().unwrap_or(u64::MAX))
+ Duration::from_secs(1)
} else {
// no messages to be deleted for now, wait long for one to occur
now + Duration::from_secs(86400)
};
let until = UNIX_EPOCH
+ Duration::from_secs(ephemeral_timestamp.try_into().unwrap_or(u64::MAX))
+ Duration::from_secs(1);
if let Ok(duration) = until.duration_since(now) {
info!(
context,
"Ephemeral loop waiting for deletion in {} or interrupt",
duration_to_str(duration)
);
if timeout(duration, interrupt_receiver.recv()).await.is_ok() {
// received an interruption signal, recompute waiting time (if any)
continue;
}
// Schedule a task, ephemeral_timestamp is in the future
let context1 = context.clone();
let ephemeral_task = task::spawn(async move {
async_std::task::sleep(duration).await;
context1.emit_event(EventType::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
});
*context.ephemeral_task.write().await = Some(ephemeral_task);
} else {
// Emit event immediately
context.emit_event(EventType::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
}
delete_expired_messages(context, time())
.await
.ok_or_log(context);
}
}
@@ -529,11 +451,12 @@ pub(crate) async fn delete_expired_imap_messages(context: &Context) -> Result<()
.execute(
"UPDATE imap
SET target=''
WHERE rfc724_mid IN (
SELECT rfc724_mid FROM msgs
WHERE ((download_state = 0 AND timestamp < ?) OR
(download_state != 0 AND timestamp < ?) OR
(ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?))
WHERE EXISTS (
SELECT * FROM msgs
WHERE rfc724_mid=imap.rfc724_mid
AND ((download_state = 0 AND timestamp < ?) OR
(download_state != 0 AND timestamp < ?) OR
(ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?))
)",
paramsv![threshold_timestamp, threshold_timestamp_extended, now],
)
@@ -577,7 +500,6 @@ mod tests {
use super::*;
use crate::config::Config;
use crate::dc_receive_imf::dc_receive_imf;
use crate::dc_tools::MAX_SECONDS_TO_LEND_FROM_FUTURE;
use crate::download::DownloadState;
use crate::test_utils::TestContext;
use crate::{
@@ -590,7 +512,7 @@ mod tests {
let context = TestContext::new().await;
assert_eq!(
stock_ephemeral_timer_changed(&context, Timer::Disabled, ContactId::SELF).await,
stock_ephemeral_timer_changed(&context, Timer::Disabled, DC_CONTACT_ID_SELF).await,
"Message deletion timer is disabled by me."
);
@@ -598,7 +520,7 @@ mod tests {
stock_ephemeral_timer_changed(
&context,
Timer::Enabled { duration: 1 },
ContactId::SELF
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 1 s by me."
@@ -607,7 +529,7 @@ mod tests {
stock_ephemeral_timer_changed(
&context,
Timer::Enabled { duration: 30 },
ContactId::SELF
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 30 s by me."
@@ -616,7 +538,7 @@ mod tests {
stock_ephemeral_timer_changed(
&context,
Timer::Enabled { duration: 60 },
ContactId::SELF
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 1 minute by me."
@@ -625,7 +547,7 @@ mod tests {
stock_ephemeral_timer_changed(
&context,
Timer::Enabled { duration: 90 },
ContactId::SELF
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 1.5 minutes by me."
@@ -634,7 +556,7 @@ mod tests {
stock_ephemeral_timer_changed(
&context,
Timer::Enabled { duration: 30 * 60 },
ContactId::SELF
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 30 minutes by me."
@@ -643,7 +565,7 @@ mod tests {
stock_ephemeral_timer_changed(
&context,
Timer::Enabled { duration: 60 * 60 },
ContactId::SELF
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 1 hour by me."
@@ -652,7 +574,7 @@ mod tests {
stock_ephemeral_timer_changed(
&context,
Timer::Enabled { duration: 5400 },
ContactId::SELF
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 1.5 hours by me."
@@ -663,7 +585,7 @@ mod tests {
Timer::Enabled {
duration: 2 * 60 * 60
},
ContactId::SELF
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 2 hours by me."
@@ -674,7 +596,7 @@ mod tests {
Timer::Enabled {
duration: 24 * 60 * 60
},
ContactId::SELF
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 1 day by me."
@@ -685,7 +607,7 @@ mod tests {
Timer::Enabled {
duration: 2 * 24 * 60 * 60
},
ContactId::SELF
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 2 days by me."
@@ -696,7 +618,7 @@ mod tests {
Timer::Enabled {
duration: 7 * 24 * 60 * 60
},
ContactId::SELF
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 1 week by me."
@@ -707,7 +629,7 @@ mod tests {
Timer::Enabled {
duration: 4 * 7 * 24 * 60 * 60
},
ContactId::SELF
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 4 weeks by me."
@@ -866,106 +788,31 @@ mod tests {
#[async_std::test]
async fn test_ephemeral_delete_msgs() -> Result<()> {
let t = TestContext::new_alice().await;
let self_chat = t.get_self_chat().await;
let chat = t.get_self_chat().await;
assert_eq!(next_expiration_timestamp(&t).await, None);
t.send_text(self_chat.id, "Saved message, which we delete manually")
t.send_text(chat.id, "Saved message, which we delete manually")
.await;
let msg = t.get_last_msg_in(self_chat.id).await;
let msg = t.get_last_msg_in(chat.id).await;
msg.id.delete_from_db(&t).await?;
check_msg_is_deleted(&t, &self_chat, msg.id).await;
check_msg_was_deleted(&t, &chat, msg.id).await;
self_chat
.id
.set_ephemeral_timer(&t, Timer::Enabled { duration: 3600 })
chat.id
.set_ephemeral_timer(&t, Timer::Enabled { duration: 1 })
.await
.unwrap();
let msg = t
.send_text(chat.id, "Saved message, disappearing after 1s")
.await;
// Send a saved message which will be deleted after 3600s
let now = time();
let msg = t.send_text(self_chat.id, "Message text").await;
check_msg_will_be_deleted(&t, msg.sender_msg_id, &self_chat, now + 3599, time() + 3601)
.await
.unwrap();
// Set DeleteDeviceAfter to 1800s. Thend send a saved message which will
// still be deleted after 3600s because DeleteDeviceAfter doesn't apply to saved messages.
t.set_config(Config::DeleteDeviceAfter, Some("1800"))
.await?;
let now = time();
let msg = t.send_text(self_chat.id, "Message text").await;
check_msg_will_be_deleted(&t, msg.sender_msg_id, &self_chat, now + 3559, time() + 3601)
.await
.unwrap();
// Send a message to Bob which will be deleted after 1800s because of DeleteDeviceAfter.
let bob_chat = t.create_chat_with_contact("", "bob@example.net").await;
let now = time();
let msg = t.send_text(bob_chat.id, "Message text").await;
check_msg_will_be_deleted(
&t,
msg.sender_msg_id,
&bob_chat,
now + 1799,
// The message may appear to be sent MAX_SECONDS_TO_LEND_FROM_FUTURE later and
// therefore be deleted MAX_SECONDS_TO_LEND_FROM_FUTURE later.
time() + 1801 + MAX_SECONDS_TO_LEND_FROM_FUTURE,
)
.await
.unwrap();
// Enable ephemeral messages with Bob -> message will be deleted after 60s.
// This tests that the message is deleted at min(ephemeral deletion time, DeleteDeviceAfter deletion time).
bob_chat
.id
.set_ephemeral_timer(&t, Timer::Enabled { duration: 60 })
.await?;
let now = time();
let msg = t.send_text(bob_chat.id, "Message text").await;
check_msg_will_be_deleted(&t, msg.sender_msg_id, &bob_chat, now + 59, time() + 61)
.await
.unwrap();
Ok(())
}
async fn check_msg_will_be_deleted(
t: &TestContext,
msg_id: MsgId,
chat: &Chat,
not_deleted_at: i64,
deleted_at: i64,
) -> Result<()> {
let next_expiration = next_expiration_timestamp(t).await.unwrap();
assert!(next_expiration > not_deleted_at);
delete_expired_messages(t, not_deleted_at).await?;
let loaded = Message::load_from_db(t, msg_id).await?;
assert_eq!(loaded.text.unwrap(), "Message text");
assert_eq!(loaded.chat_id, chat.id);
assert!(next_expiration < deleted_at);
delete_expired_messages(t, deleted_at).await?;
let loaded = Message::load_from_db(t, msg_id).await?;
assert_eq!(loaded.text.unwrap(), "");
assert_eq!(loaded.chat_id, DC_CHAT_ID_TRASH);
async_std::task::sleep(Duration::from_millis(1100)).await;
// Check that the msg was deleted locally.
check_msg_is_deleted(t, chat, msg_id).await;
check_msg_was_deleted(&t, &chat, msg.sender_msg_id).await;
Ok(())
}
async fn check_msg_is_deleted(t: &TestContext, chat: &Chat, msg_id: MsgId) {
async fn check_msg_was_deleted(t: &TestContext, chat: &Chat, msg_id: MsgId) {
let chat_items = chat::get_chat_msgs(t, chat.id, 0, None).await.unwrap();
// Check that the chat is empty except for possibly info messages:
for item in &chat_items {
@@ -977,8 +824,8 @@ mod tests {
// Check that if there is a message left, the text and metadata are gone
if let Ok(msg) = Message::load_from_db(t, msg_id).await {
assert_eq!(msg.from_id, ContactId::UNDEFINED);
assert_eq!(msg.to_id, ContactId::UNDEFINED);
assert_eq!(msg.from_id, 0);
assert_eq!(msg.to_id, 0);
assert!(msg.text.is_none_or_empty(), "{:?}", msg.text);
let rawtxt: Option<String> = t
.sql
@@ -1115,6 +962,7 @@ mod tests {
Date: Sun, 22 Mar 2020 00:10:00 +0000\n\
\n\
hello\n",
"INBOX",
false,
)
.await?;
@@ -1135,6 +983,7 @@ mod tests {
Ephemeral-Timer: 60\n\
\n\
second message\n",
"INBOX",
false,
)
.await?;
@@ -1171,6 +1020,7 @@ mod tests {
In-Reply-To: <first@example.com>\n\
\n\
> hello\n",
"INBOX",
false,
)
.await?;

View File

@@ -7,10 +7,9 @@ use async_std::path::PathBuf;
use strum::EnumProperty;
use crate::chat::ChatId;
use crate::contact::ContactId;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::message::MsgId;
use crate::webxdc::StatusUpdateSerial;
use crate::webxdc::StatusUpdateId;
#[derive(Debug)]
pub struct Events {
@@ -254,7 +253,7 @@ pub enum EventType {
///
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
#[strum(props(id = "2030"))]
ContactsChanged(Option<ContactId>),
ContactsChanged(Option<u32>),
/// Location of one or more contact has changed.
///
@@ -262,7 +261,7 @@ pub enum EventType {
/// If the locations of several contacts have been changed,
/// eg. after calling dc_delete_all_locations(), this parameter is set to `None`.
#[strum(props(id = "2035"))]
LocationChanged(Option<ContactId>),
LocationChanged(Option<u32>),
/// Inform about the configuration progress started by configure().
#[strum(props(id = "2041"))]
@@ -306,10 +305,7 @@ pub enum EventType {
/// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
/// 1000=Protocol finished for this contact.
#[strum(props(id = "2060"))]
SecurejoinInviterProgress {
contact_id: ContactId,
progress: usize,
},
SecurejoinInviterProgress { contact_id: u32, progress: usize },
/// Progress information of a secure-join handshake from the view of the joiner
/// (Bob, the person who scans the QR code).
@@ -320,10 +316,7 @@ pub enum EventType {
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
/// (Bob has verified alice and waits until Alice does the same for him)
#[strum(props(id = "2061"))]
SecurejoinJoinerProgress {
contact_id: ContactId,
progress: usize,
},
SecurejoinJoinerProgress { contact_id: u32, progress: usize },
/// The connectivity to the server changed.
/// This means that you should refresh the connectivity view
@@ -338,6 +331,6 @@ pub enum EventType {
#[strum(props(id = "2120"))]
WebxdcStatusUpdate {
msg_id: MsgId,
status_update_serial: StatusUpdateSerial,
status_update_id: StatusUpdateId,
},
}

View File

@@ -279,9 +279,9 @@ mod tests {
use crate::chat;
use crate::chat::forward_msgs;
use crate::config::Config;
use crate::contact::ContactId;
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
use crate::dc_receive_imf::dc_receive_imf;
use crate::message::{MessengerMessage, Viewtype};
use crate::message::MessengerMessage;
use crate::test_utils::TestContext;
#[async_std::test]
@@ -365,7 +365,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
// however, rust multiline-strings use just `\n`;
// therefore, we just remove `\r` before comparison.
assert_eq!(
parser.html.replace('\r', ""),
parser.html.replace("\r", ""),
r##"
<html>
<p>mime-modified <b>set</b>; simplify is always regarded as lossy.</p>
@@ -379,7 +379,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
let raw = include_bytes!("../test-data/message/text_alt_html.eml");
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html.replace('\r', ""), // see comment in test_htmlparse_html()
parser.html.replace("\r", ""), // see comment in test_htmlparse_html()
r##"<html>
<p>mime-modified <b>set</b>; simplify is always regarded as lossy.</p>
</html>
@@ -394,7 +394,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html.replace('\r', ""), // see comment in test_htmlparse_html()
parser.html.replace("\r", ""), // see comment in test_htmlparse_html()
r##"<html>
<p>
this is <b>html</b>
@@ -440,9 +440,9 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
.create_chat_with_contact("", "sender@testrun.org")
.await;
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
dc_receive_imf(&alice, raw, false).await.unwrap();
dc_receive_imf(&alice, raw, "INBOX", false).await.unwrap();
let msg = alice.get_last_msg_in(chat.get_id()).await;
assert_ne!(msg.get_from_id(), ContactId::SELF);
assert_ne!(msg.get_from_id(), DC_CONTACT_ID_SELF);
assert_eq!(msg.is_dc_message, MessengerMessage::No);
assert!(!msg.is_forwarded());
assert!(msg.get_text().unwrap().contains("this is plain"));
@@ -456,7 +456,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
.await
.unwrap();
let msg = alice.get_last_msg_in(chat.get_id()).await;
assert_eq!(msg.get_from_id(), ContactId::SELF);
assert_eq!(msg.get_from_id(), DC_CONTACT_ID_SELF);
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
assert!(msg.is_forwarded());
assert!(msg.get_text().unwrap().contains("this is plain"));
@@ -469,7 +469,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
let chat = bob.create_chat_with_contact("", "alice@example.org").await;
bob.recv_msg(&alice.pop_sent_msg().await).await;
let msg = bob.get_last_msg_in(chat.get_id()).await;
assert_ne!(msg.get_from_id(), ContactId::SELF);
assert_ne!(msg.get_from_id(), DC_CONTACT_ID_SELF);
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
assert!(msg.is_forwarded());
assert!(msg.get_text().unwrap().contains("this is plain"));
@@ -489,7 +489,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
.create_chat_with_contact("", "sender@testrun.org")
.await;
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
dc_receive_imf(&alice, raw, false).await.unwrap();
dc_receive_imf(&alice, raw, "INBOX", false).await.unwrap();
let msg = alice.get_last_msg_in(chat.get_id()).await;
// forward the message to saved-messages,
@@ -506,7 +506,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
alice.recv_msg(&msg).await;
let chat = alice.get_self_chat().await;
let msg = alice.get_last_msg_in(chat.get_id()).await;
assert_eq!(msg.get_from_id(), ContactId::SELF);
assert_eq!(msg.get_from_id(), DC_CONTACT_ID_SELF);
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
assert!(msg.get_showpadlock());
assert!(msg.is_forwarded());
@@ -555,6 +555,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
dc_receive_imf(
&t,
include_bytes!("../test-data/message/cp1252-html.eml"),
"INBOX",
false,
)
.await?;

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@ use super::Imap;
use anyhow::{bail, Context as _, Result};
use async_imap::extensions::idle::IdleResponse;
use async_std::prelude::*;
use imap_proto::types::{AttributeValue, Response};
use std::time::{Duration, SystemTime};
use crate::{context::Context, scheduler::InterruptInfo};
@@ -71,6 +72,13 @@ impl Imap {
match fut.await {
Ok(Event::IdleResponse(IdleResponse::NewData(x))) => {
info!(context, "Idle has NewData {:?}", x);
if let Response::Fetch(_message, attrs) = x.parsed() {
for attr in attrs {
if let AttributeValue::ModSeq(modseq) = attr {
self.update_modseq(*modseq);
}
}
}
}
Ok(Event::IdleResponse(IdleResponse::Timeout)) => {
info!(context, "Idle-wait timeout or interruption");
@@ -157,10 +165,8 @@ impl Imap {
// in anything. If so, we behave as if IDLE had data but
// will have already fetched the messages so perform_*_fetch
// will not find any new.
match self
.fetch_new_messages(context, &watch_folder, false, false)
.await
{
match self.fetch_new_messages(context, &watch_folder, false).await {
Ok(res) => {
info!(context, "fetch_new_messages returned {:?}", res);
if res {

View File

@@ -2,18 +2,15 @@ use std::{collections::BTreeMap, time::Instant};
use anyhow::{Context as _, Result};
use crate::config::Config;
use crate::imap::Imap;
use crate::log::LogExt;
use crate::{config::Config, log::LogExt};
use crate::{context::Context, imap::FolderMeaning};
use async_std::stream::StreamExt;
use async_std::prelude::*;
use super::{get_folder_meaning, get_folder_meaning_by_name};
impl Imap {
/// Returns true if folders were scanned, false if scanning was postponed.
pub(crate) async fn scan_folders(&mut self, context: &Context) -> Result<bool> {
pub(crate) async fn scan_folders(&mut self, context: &Context) -> Result<()> {
// First of all, debounce to once per minute:
let mut last_scan = context.last_full_folder_scan.lock().await;
if let Some(last_scan) = *last_scan {
@@ -23,18 +20,28 @@ impl Imap {
.await?;
if elapsed_secs < debounce_secs {
return Ok(false);
return Ok(());
}
}
info!(context, "Starting full folder scan");
self.prepare(context).await?;
let folders = self.list_folders(context).await?;
let session = self.session.as_mut();
let session = session.context("scan_folders(): IMAP No Connection established")?;
let folders: Vec<_> = session.list(Some(""), Some("*")).await?.collect().await;
let watched_folders = get_watched_folders(context).await?;
let mut folder_configs = BTreeMap::new();
for folder in folders {
let folder = match folder {
Ok(f) => f,
Err(e) => {
warn!(context, "Can't get folder: {}", e);
continue;
}
};
// Gmail labels are not folders and should be skipped. For example,
// emails appear in the inbox and under "All Mail" as soon as it is
// received. The code used to wrongly conclude that the email had
@@ -60,9 +67,6 @@ impl Imap {
let is_drafts = folder_meaning == FolderMeaning::Drafts
|| (folder_meaning == FolderMeaning::Unknown
&& folder_name_meaning == FolderMeaning::Drafts);
let is_spam_folder = folder_meaning == FolderMeaning::Spam
|| (folder_meaning == FolderMeaning::Unknown
&& folder_name_meaning == FolderMeaning::Spam);
// Don't scan folders that are watched anyway
if !watched_folders.contains(&folder.name().to_string()) && !is_drafts {
@@ -70,7 +74,7 @@ impl Imap {
self.server_sent_unsolicited_exists(context)?;
loop {
self.fetch_move_delete(context, folder.name(), is_spam_folder)
self.fetch_move_delete(context, folder.name())
.await
.ok_or_log_msg(context, "Can't fetch new msgs in scanned folder");
@@ -82,51 +86,36 @@ impl Imap {
}
}
// Set the `ConfiguredSentboxFolder` or set it to `None` if the folder was deleted.
context
.set_config(
Config::ConfiguredSentboxFolder,
folder_configs
.get(&Config::ConfiguredSentboxFolder)
.map(|s| s.as_str()),
)
.await?;
// We iterate over both folder meanings to make sure that if e.g. the "Sent" folder was deleted,
// `ConfiguredSentboxFolder` is set to `None`:
for config in &[
Config::ConfiguredSentboxFolder,
Config::ConfiguredSpamFolder,
] {
context
.set_config(*config, folder_configs.get(config).map(|s| s.as_str()))
.await?;
}
last_scan.replace(Instant::now());
Ok(true)
Ok(())
}
/// Returns the names of all folders on the IMAP server.
pub async fn list_folders(
self: &mut Imap,
context: &Context,
) -> Result<Vec<async_imap::types::Name>> {
let session = self.session.as_mut();
let session = session.context("No IMAP connection")?;
let list = session
.list(Some(""), Some("*"))
.await?
.filter_map(|f| f.ok_or_log_msg(context, "list_folders() can't get folder"));
Ok(list.collect().await)
}
}
pub(crate) async fn get_watched_folder_configs(context: &Context) -> Result<Vec<Config>> {
let mut res = vec![Config::ConfiguredInboxFolder];
if context.get_config_bool(Config::SentboxWatch).await? {
res.push(Config::ConfiguredSentboxFolder);
}
if context.should_watch_mvbox().await? {
res.push(Config::ConfiguredMvboxFolder);
}
Ok(res)
}
pub(crate) async fn get_watched_folders(context: &Context) -> Result<Vec<String>> {
let mut res = Vec::new();
for folder_config in get_watched_folder_configs(context).await? {
if let Some(folder) = context.get_config(folder_config).await? {
res.push(folder);
if let Some(inbox_folder) = context.get_config(Config::ConfiguredInboxFolder).await? {
res.push(inbox_folder);
}
let folder_watched_configured = &[
(Config::SentboxWatch, Config::ConfiguredSentboxFolder),
(Config::MvboxMove, Config::ConfiguredMvboxFolder),
];
for (watched, configured) in folder_watched_configured {
if context.get_config_bool(*watched).await? {
if let Some(folder) = context.get_config(*configured).await? {
res.push(folder);
}
}
}
Ok(res)

View File

@@ -16,7 +16,7 @@ use rand::{thread_rng, Rng};
use crate::blob::BlobObject;
use crate::chat::{self, delete_and_reset_all_device_msgs, ChatId};
use crate::config::Config;
use crate::contact::ContactId;
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
use crate::context::Context;
use crate::dc_tools::{
dc_create_folder, dc_delete_file, dc_delete_files_in_dir, dc_get_filesuffix_lc,
@@ -26,7 +26,7 @@ use crate::e2ee;
use crate::events::EventType;
use crate::key::{self, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey};
use crate::log::LogExt;
use crate::message::{Message, MsgId, Viewtype};
use crate::message::{Message, MsgId};
use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::pgp;
@@ -165,6 +165,7 @@ pub async fn initiate_key_transfer(context: &Context) -> Result<String> {
}
async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
let mut msg: Message;
let setup_code = create_setup_code(context);
/* this may require a keypair to be created. this may take a second ... */
let setup_file_content = render_setup_file(context, &setup_code).await?;
@@ -176,11 +177,9 @@ async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
)
.await?;
let chat_id = ChatId::create_for_contact(context, ContactId::SELF).await?;
let mut msg = Message {
viewtype: Viewtype::File,
..Default::default()
};
let chat_id = ChatId::create_for_contact(context, DC_CONTACT_ID_SELF).await?;
msg = Message::default();
msg.viewtype = Viewtype::File;
msg.param.set(Param::File, setup_file_blob.as_name());
msg.subject = stock_str::ac_setup_msg_subject(context).await;
msg.param
@@ -237,7 +236,7 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
let msg_subj = stock_str::ac_setup_msg_subject(context).await;
let msg_body = stock_str::ac_setup_msg_body(context).await;
let msg_body_html = msg_body.replace('\r', "").replace('\n', "<br>");
let msg_body_html = msg_body.replace("\r", "").replace("\n", "<br>");
Ok(format!(
concat!(
"<!DOCTYPE html>\r\n",
@@ -351,8 +350,9 @@ async fn set_self_key(
}
};
let self_addr = context.get_configured_addr().await?;
let addr = EmailAddress::new(&self_addr)?;
let self_addr = context.get_config(Config::ConfiguredAddr).await?;
ensure!(self_addr.is_some(), "Missing self addr");
let addr = EmailAddress::new(&self_addr.unwrap_or_default())?;
let keypair = pgp::KeyPair {
addr,
public: public_key,
@@ -440,6 +440,13 @@ async fn import_backup(
backup_to_import: &Path,
passphrase: String,
) -> Result<()> {
info!(
context,
"Import \"{}\" to \"{}\".",
backup_to_import.display(),
context.get_dbfile().display()
);
ensure!(
!context.is_configured().await?,
"Cannot import backups to accounts in use."
@@ -451,16 +458,6 @@ async fn import_backup(
let backup_file = File::open(backup_to_import).await?;
let file_size = backup_file.metadata().await?.len();
info!(
context,
"Import \"{}\" ({} bytes) to \"{}\".",
backup_to_import.display(),
file_size,
context.get_dbfile().display()
);
context.sql.config_cache.write().await.clear();
let archive = Archive::new(backup_file);
let mut entries = archive.entries()?;
@@ -902,54 +899,6 @@ mod tests {
}
}
#[async_std::test]
async fn test_export_and_import_backup() -> Result<()> {
let backup_dir = tempfile::tempdir().unwrap();
let context1 = TestContext::new_alice().await;
assert!(context1.is_configured().await?);
let context2 = TestContext::new().await;
assert!(!context2.is_configured().await?);
assert!(has_backup(&context2, backup_dir.path().as_ref())
.await
.is_err());
// export from context1
assert!(imex(
&context1,
ImexMode::ExportBackup,
backup_dir.path().as_ref(),
None,
)
.await
.is_ok());
let _event = context1
.evtracker
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
.await;
// import to context2
let backup = has_backup(&context2, backup_dir.path().as_ref()).await?;
assert!(
imex(&context2, ImexMode::ImportBackup, backup.as_ref(), None)
.await
.is_ok()
);
let _event = context2
.evtracker
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
.await;
assert!(context2.is_configured().await?);
assert_eq!(
context2.get_config(Config::Addr).await?,
Some("alice@example.org".to_string())
);
Ok(())
}
#[test]
fn test_normalize_setup_code() {
let norm = normalize_setup_code("123422343234423452346234723482349234");

View File

@@ -4,22 +4,23 @@
//! and job types.
use std::fmt;
use anyhow::{format_err, Context as _, Result};
use anyhow::{bail, format_err, Context as _, Error, Result};
use deltachat_derive::{FromSql, ToSql};
use rand::{thread_rng, Rng};
use crate::config::Config;
use crate::contact::{normalize_name, Contact, ContactId, Modifier, Origin};
use crate::contact::{normalize_name, Contact, Modifier, Origin};
use crate::context::Context;
use crate::dc_tools::time;
use crate::events::EventType;
use crate::imap::Imap;
use crate::imap::{Imap, ImapActionResult};
use crate::location;
use crate::log::LogExt;
use crate::message::{Message, MsgId};
use crate::mimefactory::MimeFactory;
use crate::param::{Param, Params};
use crate::scheduler::InterruptInfo;
use crate::smtp::{smtp_send, SendResult, Smtp};
use crate::smtp::{smtp_send, Smtp};
use crate::sql;
// results in ~3 weeks for the last backoff timespan
@@ -31,6 +32,7 @@ const JOB_RETRIES: u32 = 17;
)]
#[repr(u32)]
pub(crate) enum Thread {
Unknown = 0,
Imap = 100,
Smtp = 5000,
}
@@ -38,7 +40,7 @@ pub(crate) enum Thread {
/// Job try result.
#[derive(Debug, Display)]
pub enum Status {
Finished(Result<()>),
Finished(std::result::Result<(), Error>),
RetryNow,
RetryLater,
}
@@ -58,6 +60,12 @@ macro_rules! job_try {
};
}
impl Default for Thread {
fn default() -> Self {
Thread::Unknown
}
}
#[derive(
Debug,
Display,
@@ -73,8 +81,12 @@ macro_rules! job_try {
)]
#[repr(u32)]
pub enum Action {
Unknown = 0,
// Jobs in the INBOX-thread, range from DC_IMAP_THREAD..DC_IMAP_THREAD+999
Housekeeping = 105, // low priority ...
FetchExistingMsgs = 110,
MarkseenMsgOnImap = 130,
// this is user initiated so it should have a fairly high priority
UpdateRecentQuota = 140,
@@ -95,13 +107,23 @@ pub enum Action {
SendMdn = 5010,
}
impl Default for Action {
fn default() -> Self {
Action::Unknown
}
}
impl From<Action> for Thread {
fn from(action: Action) -> Thread {
use Action::*;
match action {
Unknown => Thread::Unknown,
Housekeeping => Thread::Imap,
FetchExistingMsgs => Thread::Imap,
ResyncFolders => Thread::Imap,
MarkseenMsgOnImap => Thread::Imap,
UpdateRecentQuota => Thread::Imap,
DownloadMsg => Thread::Imap,
@@ -175,7 +197,7 @@ impl Job {
"UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;",
paramsv![
self.desired_timestamp,
i64::from(self.tries),
self.tries as i64,
self.param.to_string(),
self.job_id as i32,
],
@@ -202,7 +224,7 @@ impl Job {
async fn get_additional_mdn_jobs(
&self,
context: &Context,
contact_id: ContactId,
contact_id: u32,
) -> Result<(Vec<u32>, Vec<String>)> {
// Extract message IDs from job parameters
let res: Vec<(u32, MsgId)> = context
@@ -249,7 +271,7 @@ impl Job {
return Status::Finished(Err(format_err!("MDNs are disabled")));
}
let contact_id = ContactId::new(self.foreign_id);
let contact_id = self.foreign_id;
let contact = job_try!(Contact::load_from_db(context, contact_id).await);
if contact.is_blocked() {
return Status::Finished(Err(format_err!("Contact is blocked")));
@@ -296,18 +318,13 @@ impl Job {
return Status::RetryLater;
}
match smtp_send(context, &recipients, &body, smtp, msg_id, 0).await {
SendResult::Success => {
// Remove additional SendMdn jobs we have aggregated into this one.
job_try!(kill_ids(context, &additional_job_ids).await);
Status::Finished(Ok(()))
}
SendResult::Retry => {
info!(context, "Temporary SMTP failure while sending an MDN");
Status::RetryLater
}
SendResult::Failure(err) => Status::Finished(Err(err)),
let status = smtp_send(context, &recipients, &body, smtp, msg_id, 0).await;
if matches!(status, Status::Finished(Ok(_))) {
// Remove additional SendMdn jobs we have aggregated into this one.
job_try!(kill_ids(context, &additional_job_ids).await);
}
status
}
/// Read the recipients from old emails sent by the user and add them as contacts.
@@ -335,7 +352,7 @@ impl Job {
Config::ConfiguredSentboxFolder,
] {
if let Some(folder) = job_try!(context.get_config(*config).await) {
if let Err(e) = imap.fetch_new_messages(context, &folder, false, true).await {
if let Err(e) = imap.fetch_new_messages(context, &folder, true).await {
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
warn!(context, "Could not fetch messages, retrying: {:#}", e);
return Status::RetryLater;
@@ -348,38 +365,90 @@ impl Job {
Status::Finished(Ok(()))
}
/// Synchronizes UIDs for all folders.
/// Synchronizes UIDs for sentbox, inbox and mvbox.
async fn resync_folders(&mut self, context: &Context, imap: &mut Imap) -> Status {
if let Err(err) = imap.prepare(context).await {
warn!(context, "could not connect: {:?}", err);
return Status::RetryLater;
}
let all_folders = match imap.list_folders(context).await {
Ok(v) => v,
Err(e) => {
warn!(context, "Listing folders for resync failed: {:#}", e);
return Status::RetryLater;
}
};
let sentbox_folder = job_try!(context.get_config(Config::ConfiguredSentboxFolder).await);
if let Some(sentbox_folder) = sentbox_folder {
job_try!(imap.resync_folder_uids(context, sentbox_folder).await);
}
let mut any_failed = false;
let inbox_folder = job_try!(context.get_config(Config::ConfiguredInboxFolder).await);
if let Some(inbox_folder) = inbox_folder {
job_try!(imap.resync_folder_uids(context, inbox_folder).await);
}
for folder in all_folders {
if let Err(e) = imap
.resync_folder_uids(context, folder.name().to_string())
let mvbox_folder = job_try!(context.get_config(Config::ConfiguredMvboxFolder).await);
if let Some(mvbox_folder) = mvbox_folder {
job_try!(imap.resync_folder_uids(context, mvbox_folder).await);
}
Status::Finished(Ok(()))
}
async fn markseen_msg_on_imap(&mut self, context: &Context, imap: &mut Imap) -> Status {
if let Err(err) = imap.prepare(context).await {
warn!(context, "could not connect: {:?}", err);
return Status::RetryLater;
}
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await);
let row = job_try!(
context
.sql
.query_row_optional(
"SELECT uid, folder FROM imap
WHERE rfc724_mid=? AND folder=target
ORDER BY uid ASC
LIMIT 1",
paramsv![msg.rfc724_mid],
|row| {
let uid: u32 = row.get(0)?;
let folder: String = row.get(1)?;
Ok((uid, folder))
}
)
.await
{
warn!(context, "{:#}", e);
any_failed = true;
);
if let Some((server_uid, server_folder)) = row {
let result = imap.set_seen(context, &server_folder, server_uid).await;
match result {
ImapActionResult::RetryLater => return Status::RetryLater,
ImapActionResult::Success | ImapActionResult::Failed => {}
}
} else {
info!(
context,
"Can't mark the message {} as seen on IMAP because there is no known UID",
msg.rfc724_mid
);
}
// XXX we send MDN even in case of failure to mark the messages as seen, e.g. if it was
// already deleted on the server by another device. The job will not be retried so locally
// there is no risk of double-sending MDNs.
//
// Read receipts for system messages are never sent. These messages have no place to
// display received read receipt anyway. And since their text is locally generated,
// quoting them is dangerous as it may contain contact names. E.g., for original message
// "Group left by me", a read receipt will quote "Group left by <name>", and the name can
// be a display name stored in address book rather than the name sent in the From field by
// the user.
if msg.param.get_bool(Param::WantsMdn).unwrap_or_default() && !msg.is_system_message() {
let mdns_enabled = job_try!(context.get_config_bool(Config::MdnsEnabled).await);
if mdns_enabled {
if let Err(err) = send_mdn(context, &msg).await {
warn!(context, "could not send out mdn for {}: {}", msg.id, err);
return Status::Finished(Err(err));
}
}
}
if any_failed {
Status::RetryLater
} else {
Status::Finished(Ok(()))
}
Status::Finished(Ok(()))
}
}
@@ -394,12 +463,9 @@ pub async fn kill_action(context: &Context, action: Action) -> Result<()> {
/// Remove jobs with specified IDs.
async fn kill_ids(context: &Context, job_ids: &[u32]) -> Result<()> {
if job_ids.is_empty() {
return Ok(());
}
let q = format!(
"DELETE FROM jobs WHERE id IN({})",
sql::repeat_vars(job_ids.len())?
job_ids.iter().map(|_| "?").collect::<Vec<&str>>().join(",")
);
context
.sql
@@ -570,13 +636,19 @@ async fn perform_job_action(
);
let try_res = match job.action {
Action::Unknown => Status::Finished(Err(format_err!("Unknown job id found"))),
Action::SendMdn => job.send_mdn(context, connection.smtp()).await,
Action::MaybeSendLocations => location::job_maybe_send_locations(context, job).await,
Action::MaybeSendLocationsEnded => {
location::job_maybe_send_locations_ended(context, job).await
}
Action::ResyncFolders => job.resync_folders(context, connection.inbox()).await,
Action::MarkseenMsgOnImap => job.markseen_msg_on_imap(context, connection.inbox()).await,
Action::FetchExistingMsgs => job.fetch_existing_msgs(context, connection.inbox()).await,
Action::Housekeeping => {
sql::housekeeping(context).await.ok_or_log(context);
Status::Finished(Ok(()))
}
Action::UpdateRecentQuota => match context.update_recent_quota(connection.inbox()).await {
Ok(status) => status,
Err(err) => Status::Finished(Err(err)),
@@ -604,20 +676,16 @@ fn get_backoff_time_offset(tries: u32, action: Action) -> i64 {
if seconds < 1 {
seconds = 1;
}
i64::from(seconds)
seconds as i64
}
}
}
pub(crate) async fn send_mdn(context: &Context, msg_id: MsgId, from_id: ContactId) -> Result<()> {
async fn send_mdn(context: &Context, msg: &Message) -> Result<()> {
let mut param = Params::new();
param.set(Param::MsgId, msg_id.to_u32().to_string());
param.set(Param::MsgId, msg.id.to_u32().to_string());
add(
context,
Job::new(Action::SendMdn, from_id.to_u32(), param, 0),
)
.await?;
add(context, Job::new(Action::SendMdn, msg.from_id, param, 0)).await?;
Ok(())
}
@@ -640,7 +708,10 @@ pub async fn add(context: &Context, job: Job) -> Result<()> {
if delay_seconds == 0 {
match action {
Action::ResyncFolders
Action::Unknown => unreachable!(),
Action::Housekeeping
| Action::ResyncFolders
| Action::MarkseenMsgOnImap
| Action::FetchExistingMsgs
| Action::UpdateRecentQuota
| Action::DownloadMsg => {
@@ -656,6 +727,18 @@ pub async fn add(context: &Context, job: Job) -> Result<()> {
Ok(())
}
async fn load_housekeeping_job(context: &Context) -> Result<Option<Job>> {
let last_time = context.get_config_i64(Config::LastHousekeeping).await?;
let next_time = last_time + (60 * 60 * 24);
if next_time <= time() {
kill_action(context, Action::Housekeeping).await?;
Ok(Some(Job::new(Action::Housekeeping, 0, Params::new(), 0)))
} else {
Ok(None)
}
}
/// Load jobs from the database.
///
/// Load jobs for this "[Thread]", i.e. either load SMTP jobs or load
@@ -699,7 +782,7 @@ LIMIT 1;
params = paramsv![thread_i];
};
loop {
let job = loop {
let job_res = context
.sql
.query_row_optional(query, params.clone(), |row| {
@@ -718,7 +801,7 @@ LIMIT 1;
.await;
match job_res {
Ok(job) => return Ok(job),
Ok(job) => break job,
Err(err) => {
// Remove invalid job from the DB
info!(context, "cleaning up job, because of {}", err);
@@ -736,6 +819,20 @@ LIMIT 1;
.with_context(|| format!("Failed to delete invalid job {}", id))?;
}
}
};
match thread {
Thread::Unknown => {
bail!("unknown thread for job")
}
Thread::Imap => {
if let Some(job) = job {
Ok(Some(job))
} else {
Ok(load_housekeeping_job(context).await?)
}
}
Thread::Smtp => Ok(job),
}
}
@@ -783,7 +880,8 @@ mod tests {
&InterruptInfo::new(false),
)
.await?;
assert!(jobs.is_none());
// The housekeeping job should be loaded as we didn't run housekeeping in the last day:
assert_eq!(jobs.unwrap().action, Action::Housekeeping);
insert_job(&t, 1, true).await;
let jobs = load_next(

View File

@@ -198,7 +198,10 @@ impl DcSecretKey for SignedSecretKey {
}
async fn generate_keypair(context: &Context) -> Result<KeyPair> {
let addr = context.get_configured_addr().await?;
let addr = context
.get_config(Config::ConfiguredAddr)
.await?
.context("no address configured")?;
let addr = EmailAddress::new(&addr)?;
let _guard = context.generating_key_mutex.lock().await;
@@ -294,8 +297,8 @@ pub async fn store_self_keypair(
.context("failed to clear default")?;
}
let is_default = match default {
KeyPairUse::Default => i32::from(true),
KeyPairUse::ReadOnly => i32::from(false),
KeyPairUse::Default => true as i32,
KeyPairUse::ReadOnly => false as i32,
};
let addr = keypair.addr.to_string();
@@ -315,7 +318,7 @@ pub async fn store_self_keypair(
}
/// A key fingerprint
#[derive(Clone, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Eq, PartialEq, Hash)]
pub struct Fingerprint(Vec<u8>);
impl Fingerprint {

View File

@@ -7,8 +7,7 @@
clippy::all,
clippy::indexing_slicing,
clippy::wildcard_imports,
clippy::needless_borrow,
clippy::cast_lossless
clippy::needless_borrow
)]
#![allow(
clippy::match_bool,

View File

@@ -6,12 +6,13 @@ use bitflags::bitflags;
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
use crate::chat::{self, ChatId};
use crate::contact::ContactId;
use crate::config::Config;
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
use crate::context::Context;
use crate::dc_tools::time;
use crate::events::EventType;
use crate::job::{self, Job};
use crate::message::{Message, MsgId, Viewtype};
use crate::message::{Message, MsgId};
use crate::mimeparser::SystemMessage;
use crate::param::Params;
use crate::stock_str;
@@ -24,7 +25,7 @@ pub struct Location {
pub longitude: f64,
pub accuracy: f64,
pub timestamp: i64,
pub contact_id: ContactId,
pub contact_id: u32,
pub msg_id: u32,
pub chat_id: ChatId,
pub marker: Option<String>,
@@ -100,10 +101,10 @@ impl Kml {
let val = event.unescape_and_decode(reader).unwrap_or_default();
let val = val
.replace('\n', "")
.replace('\r', "")
.replace('\t', "")
.replace(' ', "");
.replace("\n", "")
.replace("\r", "")
.replace("\t", "")
.replace(" ", "");
if self.tag.contains(KmlTag::WHEN) && val.len() >= 19 {
// YYYY-MM-DDTHH:MM:SSZ
@@ -313,7 +314,7 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64
accuracy,
time(),
chat_id,
ContactId::SELF,
DC_CONTACT_ID_SELF,
]
).await {
warn!(context, "failed to store location {:?}", err);
@@ -322,7 +323,7 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64
}
}
if continue_streaming {
context.emit_event(EventType::LocationChanged(Some(ContactId::SELF)));
context.emit_event(EventType::LocationChanged(Some(DC_CONTACT_ID_SELF)));
};
schedule_maybe_send_locations(context, false).await.ok();
}
@@ -423,7 +424,10 @@ pub async fn delete_all(context: &Context) -> Result<()> {
pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)> {
let mut last_added_location_id = 0;
let self_addr = context.get_configured_addr().await?;
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
let (locations_send_begin, locations_send_until, locations_last_sent) = context.sql.query_row(
"SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;",
@@ -458,10 +462,10 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)
GROUP BY timestamp \
ORDER BY timestamp;",
paramsv![
ContactId::SELF,
DC_CONTACT_ID_SELF,
locations_send_begin,
locations_last_sent,
ContactId::SELF
DC_CONTACT_ID_SELF
],
|row| {
let location_id: i32 = row.get(0)?;
@@ -554,7 +558,7 @@ pub async fn set_msg_location_id(context: &Context, msg_id: MsgId, location_id:
pub(crate) async fn save(
context: &Context,
chat_id: ChatId,
contact_id: ContactId,
contact_id: u32,
locations: &[Location],
independent: bool,
) -> Result<Option<u32>> {
@@ -581,12 +585,12 @@ pub(crate) async fn save(
conn.prepare_cached("SELECT id FROM locations WHERE timestamp=? AND from_id=?")?;
let mut stmt_insert = conn.prepare_cached(stmt_insert)?;
let exists = stmt_test.exists(paramsv![timestamp, contact_id])?;
let exists = stmt_test.exists(paramsv![timestamp, contact_id as i32])?;
if independent || !exists {
stmt_insert.execute(paramsv![
timestamp,
contact_id,
contact_id as i32,
chat_id,
latitude,
longitude,
@@ -662,7 +666,7 @@ pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> j
for (chat_id, locations_send_begin, locations_last_sent) in &rows {
if !stmt_locations
.exists(paramsv![
ContactId::SELF,
DC_CONTACT_ID_SELF,
*locations_send_begin,
*locations_last_sent,
])

View File

@@ -139,18 +139,8 @@ pub struct LoginParam {
}
impl LoginParam {
/// Load entered (candidate) account settings
pub async fn load_candidate_params(context: &Context) -> Result<Self> {
LoginParam::from_database(context, "").await
}
/// Load configured (working) account settings
pub async fn load_configured_params(context: &Context) -> Result<Self> {
LoginParam::from_database(context, "configured_").await
}
/// Read the login parameters from the database.
async fn from_database(context: &Context, prefix: impl AsRef<str>) -> Result<Self> {
pub async fn from_database(context: &Context, prefix: impl AsRef<str>) -> Result<Self> {
let prefix = prefix.as_ref();
let sql = &context.sql;
@@ -252,8 +242,8 @@ impl LoginParam {
}
/// Save this loginparam to the database.
pub async fn save_as_configured_params(&self, context: &Context) -> Result<()> {
let prefix = "configured_";
pub async fn save_to_database(&self, context: &Context, prefix: impl AsRef<str>) -> Result<()> {
let prefix = prefix.as_ref();
let sql = &context.sql;
let key = format!("{}addr", prefix);
@@ -263,8 +253,7 @@ impl LoginParam {
sql.set_raw_config(key, Some(&self.imap.server)).await?;
let key = format!("{}mail_port", prefix);
sql.set_raw_config_int(key, i32::from(self.imap.port))
.await?;
sql.set_raw_config_int(key, self.imap.port as i32).await?;
let key = format!("{}mail_user", prefix);
sql.set_raw_config(key, Some(&self.imap.user)).await?;
@@ -284,8 +273,7 @@ impl LoginParam {
sql.set_raw_config(key, Some(&self.smtp.server)).await?;
let key = format!("{}send_port", prefix);
sql.set_raw_config_int(key, i32::from(self.smtp.port))
.await?;
sql.set_raw_config_int(key, self.smtp.port as i32).await?;
let key = format!("{}send_user", prefix);
sql.set_raw_config(key, Some(&self.smtp.user)).await?;
@@ -448,8 +436,8 @@ mod tests {
socks5_config: None,
};
param.save_as_configured_params(&t).await?;
let loaded = LoginParam::load_configured_params(&t).await?;
param.save_to_database(&t, "foobar_").await?;
let loaded = LoginParam::from_database(&t, "foobar_").await?;
assert_eq!(param, loaded);
Ok(())

View File

@@ -1,6 +1,7 @@
//! # Messages and their identifiers.
use std::collections::BTreeSet;
use std::convert::TryInto;
use anyhow::{ensure, format_err, Context as _, Result};
use async_std::path::{Path, PathBuf};
@@ -9,27 +10,25 @@ use rusqlite::types::ValueRef;
use serde::{Deserialize, Serialize};
use crate::chat::{self, Chat, ChatId};
use crate::config::Config;
use crate::constants::{
Blocked, Chattype, VideochatType, DC_CHAT_ID_TRASH, DC_DESIRED_TEXT_LEN, DC_MSG_ID_LAST_SPECIAL,
Blocked, Chattype, VideochatType, Viewtype, DC_CHAT_ID_TRASH, DC_CONTACT_ID_INFO,
DC_CONTACT_ID_SELF, DC_DESIRED_TEXT_LEN, DC_MSG_ID_LAST_SPECIAL,
};
use crate::contact::{Contact, ContactId, Origin};
use crate::contact::{Contact, Origin};
use crate::context::Context;
use crate::dc_tools::{
dc_create_smeared_timestamp, dc_get_filebytes, dc_get_filemeta, dc_gm2local_offset,
dc_read_file, dc_timestamp_to_str, dc_truncate, time,
};
use crate::download::DownloadState;
use crate::ephemeral::{start_ephemeral_timers_msgids, Timer as EphemeralTimer};
use crate::ephemeral::Timer as EphemeralTimer;
use crate::events::EventType;
use crate::imap::markseen_on_imap_table;
use crate::job;
use crate::job::{self, Action};
use crate::log::LogExt;
use crate::mimeparser::{parse_message_id, FailureReport, SystemMessage};
use crate::param::{Param, Params};
use crate::pgp::split_armored_data;
use crate::scheduler::InterruptInfo;
use crate::sql;
use crate::stock_str;
use crate::summary::Summary;
@@ -184,7 +183,7 @@ impl rusqlite::types::ToSql for MsgId {
format_err!("Invalid MsgId {}", self.0).into(),
));
}
let val = rusqlite::types::Value::Integer(i64::from(self.0));
let val = rusqlite::types::Value::Integer(self.0 as i64);
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
@@ -195,7 +194,7 @@ impl rusqlite::types::FromSql for MsgId {
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
// Would be nice if we could use match here, but alas.
i64::column_result(value).and_then(|val| {
if 0 <= val && val <= i64::from(std::u32::MAX) {
if 0 <= val && val <= std::u32::MAX as i64 {
Ok(MsgId::new(val as u32))
} else {
Err(rusqlite::types::FromSqlError::OutOfRange(val))
@@ -241,8 +240,8 @@ impl Default for MessengerMessage {
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Message {
pub(crate) id: MsgId,
pub(crate) from_id: ContactId,
pub(crate) to_id: ContactId,
pub(crate) from_id: u32,
pub(crate) to_id: u32,
pub(crate) chat_id: ChatId,
pub(crate) viewtype: Viewtype,
pub(crate) state: MessageState,
@@ -388,7 +387,7 @@ impl Message {
}
pub async fn try_calc_and_set_dimensions(&mut self, context: &Context) -> Result<()> {
if self.viewtype.has_file() {
if chat::msgtype_has_file(self.viewtype) {
let file_param = self.param.get_path(Param::File, context)?;
if let Some(path_and_filename) = file_param {
if (self.viewtype == Viewtype::Image || self.viewtype == Viewtype::Gif)
@@ -432,7 +431,7 @@ impl Message {
/// this is done by dc_set_location() and dc_send_locations_to_chat().
///
/// Typically results in the event #DC_EVENT_LOCATION_CHANGED with
/// contact_id set to ContactId::SELF.
/// contact_id set to DC_CONTACT_ID_SELF.
///
/// @param latitude North-south position of the location.
/// @param longitude East-west position of the location.
@@ -457,7 +456,7 @@ impl Message {
self.id
}
pub fn get_from_id(&self) -> ContactId {
pub fn get_from_id(&self) -> u32 {
self.from_id
}
@@ -545,7 +544,7 @@ impl Message {
&chat_loaded
};
let contact = if self.from_id != ContactId::SELF {
let contact = if self.from_id != DC_CONTACT_ID_SELF {
match chat.typ {
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
Some(Contact::get_by_id(context, self.from_id).await?)
@@ -598,8 +597,8 @@ impl Message {
pub fn is_info(&self) -> bool {
let cmd = self.param.get_cmd();
self.from_id == ContactId::INFO
|| self.to_id == ContactId::INFO
self.from_id == DC_CONTACT_ID_INFO
|| self.to_id == DC_CONTACT_ID_INFO
|| cmd != SystemMessage::Unknown && cmd != SystemMessage::AutocryptSetupMessage
}
@@ -621,7 +620,7 @@ impl Message {
/// copied to the blobdir. Thus those attachments need to be
/// created immediately in the blobdir with a valid filename.
pub fn is_increation(&self) -> bool {
self.viewtype.has_file() && self.state == MessageState::OutPreparing
chat::msgtype_has_file(self.viewtype) && self.state == MessageState::OutPreparing
}
pub fn is_setupmessage(&self) -> bool {
@@ -1018,7 +1017,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
ret += &format!(" by {}", name);
ret += "\n";
if msg.from_id != ContactId::SELF {
if msg.from_id != DC_CONTACT_ID_SELF {
let s = dc_timestamp_to_str(if 0 != msg.timestamp_rcvd {
msg.timestamp_rcvd
} else {
@@ -1039,7 +1038,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
);
}
if msg.from_id == ContactId::INFO || msg.to_id == ContactId::INFO {
if msg.from_id == DC_CONTACT_ID_INFO || msg.to_id == DC_CONTACT_ID_INFO {
// device-internal message, no further details needed
return Ok(ret);
}
@@ -1050,7 +1049,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
"SELECT contact_id, timestamp_sent FROM msgs_mdns WHERE msg_id=?;",
paramsv![msg_id],
|row| {
let contact_id: ContactId = row.get(0)?;
let contact_id: i32 = row.get(0)?;
let ts: i64 = row.get(1)?;
Ok((contact_id, ts))
},
@@ -1062,7 +1061,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
let fts = dc_timestamp_to_str(ts);
ret += &format!("Read: {}", fts);
let name = Contact::load_from_db(context, contact_id)
let name = Contact::load_from_db(context, contact_id.try_into()?)
.await
.map(|contact| contact.get_name_n_addr())
.unwrap_or_default();
@@ -1238,7 +1237,7 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
for msg_id in msg_ids.iter() {
let msg = Message::load_from_db(context, *msg_id).await?;
if msg.location_id > 0 {
delete_poi_location(context, msg.location_id).await?;
delete_poi_location(context, msg.location_id).await;
}
msg_id
.trash(context)
@@ -1254,26 +1253,32 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
}
if !msg_ids.is_empty() {
context.emit_msgs_changed_without_ids();
// Run housekeeping to delete unused blobs.
context.set_config(Config::LastHousekeeping, None).await?;
context.emit_event(EventType::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
job::kill_action(context, Action::Housekeeping).await?;
job::add(
context,
job::Job::new(Action::Housekeeping, 0, Params::new(), 10),
)
.await?;
}
// Interrupt Inbox loop to start message deletion and run housekeeping.
// Interrupt Inbox loop to start message deletion.
context.interrupt_inbox(InterruptInfo::new(false)).await;
Ok(())
}
async fn delete_poi_location(context: &Context, location_id: u32) -> Result<()> {
async fn delete_poi_location(context: &Context, location_id: u32) -> bool {
context
.sql
.execute(
"DELETE FROM locations WHERE independent = 1 AND id=?;",
paramsv![location_id as i32],
)
.await?;
Ok(())
.await
.is_ok()
}
pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()> {
@@ -1281,94 +1286,61 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
return Ok(());
}
let msgs = context
.sql
.query_map(
format!(
"SELECT
m.id AS id,
m.chat_id AS chat_id,
m.state AS state,
m.ephemeral_timer AS ephemeral_timer,
m.param AS param,
m.from_id AS from_id,
m.rfc724_mid AS rfc724_mid,
c.blocked AS blocked
FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id
WHERE m.id IN ({}) AND m.chat_id>9",
sql::repeat_vars(msg_ids.len())?
),
rusqlite::params_from_iter(&msg_ids),
|row| {
let id: MsgId = row.get("id")?;
let chat_id: ChatId = row.get("chat_id")?;
let state: MessageState = row.get("state")?;
let param: Params = row.get::<_, String>("param")?.parse().unwrap_or_default();
let from_id: ContactId = row.get("from_id")?;
let rfc724_mid: String = row.get("rfc724_mid")?;
let blocked: Option<Blocked> = row.get("blocked")?;
let ephemeral_timer: EphemeralTimer = row.get("ephemeral_timer")?;
Ok((
id,
chat_id,
state,
param,
from_id,
rfc724_mid,
blocked.unwrap_or_default(),
ephemeral_timer,
))
},
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await?;
let conn = context.sql.get_conn().await?;
let msgs = async_std::task::spawn_blocking(move || -> Result<_> {
let mut stmt = conn.prepare_cached(concat!(
"SELECT",
" m.chat_id AS chat_id,",
" m.state AS state,",
" c.blocked AS blocked",
" FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id",
" WHERE m.id=? AND m.chat_id>9"
))?;
if msgs.iter().any(
|(_id, _chat_id, _state, _param, _from_id, _rfc724_mid, _blocked, ephemeral_timer)| {
*ephemeral_timer != EphemeralTimer::Disabled
},
) {
start_ephemeral_timers_msgids(context, &msg_ids)
.await
.context("failed to start ephemeral timers")?;
}
let mut msgs = Vec::with_capacity(msg_ids.len());
for id in msg_ids.into_iter() {
let query_res = stmt.query_row(paramsv![id], |row| {
Ok((
row.get::<_, ChatId>("chat_id")?,
row.get::<_, MessageState>("state")?,
row.get::<_, Option<Blocked>>("blocked")?
.unwrap_or_default(),
))
});
if let Err(rusqlite::Error::QueryReturnedNoRows) = query_res {
continue;
}
let (chat_id, state, blocked) = query_res.map_err(Into::<anyhow::Error>::into)?;
msgs.push((id, chat_id, state, blocked));
}
drop(stmt);
drop(conn);
Ok(msgs)
})
.await?;
let mut updated_chat_ids = BTreeSet::new();
for (
id,
curr_chat_id,
curr_state,
curr_param,
curr_from_id,
curr_rfc724_mid,
curr_blocked,
_curr_ephemeral_timer,
) in msgs.into_iter()
{
for (id, curr_chat_id, curr_state, curr_blocked) in msgs.into_iter() {
if let Err(err) = id.start_ephemeral_timer(context).await {
error!(
context,
"Failed to start ephemeral timer for message {}: {}", id, err
);
continue;
}
if curr_blocked == Blocked::Not
&& (curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed)
{
update_msg_state(context, id, MessageState::InSeen).await?;
info!(context, "Seen message {}.", id);
markseen_on_imap_table(context, &curr_rfc724_mid).await?;
// Read receipts for system messages are never sent. These messages have no place to
// display received read receipt anyway. And since their text is locally generated,
// quoting them is dangerous as it may contain contact names. E.g., for original message
// "Group left by me", a read receipt will quote "Group left by <name>", and the name can
// be a display name stored in address book rather than the name sent in the From field by
// the user.
if curr_param.get_bool(Param::WantsMdn).unwrap_or_default()
&& curr_param.get_cmd() == SystemMessage::Unknown
{
let mdns_enabled = context.get_config_bool(Config::MdnsEnabled).await?;
if mdns_enabled {
if let Err(err) = job::send_mdn(context, id, curr_from_id).await {
warn!(context, "could not send out mdn for {}: {}", id, err);
}
}
}
job::add(
context,
job::Job::new(Action::MarkseenMsgOnImap, id.to_u32(), Params::new(), 0),
)
.await?;
updated_chat_ids.insert(curr_chat_id);
}
}
@@ -1454,11 +1426,11 @@ pub async fn set_msg_failed(context: &Context, msg_id: MsgId, error: Option<impl
/// returns Some if an event should be send
pub async fn handle_mdn(
context: &Context,
from_id: ContactId,
from_id: u32,
rfc724_mid: &str,
timestamp_sent: i64,
) -> Result<Option<(ChatId, MsgId)>> {
if from_id == ContactId::SELF {
if from_id == DC_CONTACT_ID_SELF {
warn!(
context,
"ignoring MDN sent to self, this is a bug on the sender device"
@@ -1507,7 +1479,7 @@ pub async fn handle_mdn(
.sql
.exists(
"SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=? AND contact_id=?;",
paramsv![msg_id, from_id],
paramsv![msg_id, from_id as i32,],
)
.await?
{
@@ -1515,7 +1487,7 @@ pub async fn handle_mdn(
.sql
.execute(
"INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?);",
paramsv![msg_id, from_id, timestamp_sent],
paramsv![msg_id, from_id as i32, timestamp_sent],
)
.await?;
}
@@ -1663,7 +1635,7 @@ pub async fn estimate_deletion_cnt(
from_server: bool,
seconds: i64,
) -> Result<usize> {
let self_chat_id = ChatId::lookup_by_contact(context, ContactId::SELF)
let self_chat_id = ChatId::lookup_by_contact(context, DC_CONTACT_ID_SELF)
.await?
.unwrap_or_default();
let threshold_timestamp = time() - seconds;
@@ -1729,114 +1701,16 @@ pub(crate) async fn rfc724_mid_exists(
Ok(res)
}
/// How a message is primarily displayed.
#[derive(
Debug,
Display,
Clone,
Copy,
PartialEq,
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Serialize,
Deserialize,
)]
#[repr(u32)]
pub enum Viewtype {
Unknown = 0,
/// Text message.
/// The text of the message is set using dc_msg_set_text()
/// and retrieved with dc_msg_get_text().
Text = 10,
/// Image message.
/// If the image is an animated GIF, the type DC_MSG_GIF should be used.
/// File, width and height are set via dc_msg_set_file(), dc_msg_set_dimension
/// and retrieved via dc_msg_set_file(), dc_msg_set_dimension().
Image = 20,
/// Animated GIF message.
/// File, width and height are set via dc_msg_set_file(), dc_msg_set_dimension()
/// and retrieved via dc_msg_get_file(), dc_msg_get_width(), dc_msg_get_height().
Gif = 21,
/// Message containing a sticker, similar to image.
/// If possible, the ui should display the image without borders in a transparent way.
/// A click on a sticker will offer to install the sticker set in some future.
Sticker = 23,
/// Message containing an Audio file.
/// File and duration are set via dc_msg_set_file(), dc_msg_set_duration()
/// and retrieved via dc_msg_get_file(), dc_msg_get_duration().
Audio = 40,
/// A voice message that was directly recorded by the user.
/// For all other audio messages, the type #DC_MSG_AUDIO should be used.
/// File and duration are set via dc_msg_set_file(), dc_msg_set_duration()
/// and retrieved via dc_msg_get_file(), dc_msg_get_duration()
Voice = 41,
/// Video messages.
/// File, width, height and durarion
/// are set via dc_msg_set_file(), dc_msg_set_dimension(), dc_msg_set_duration()
/// and retrieved via
/// dc_msg_get_file(), dc_msg_get_width(),
/// dc_msg_get_height(), dc_msg_get_duration().
Video = 50,
/// Message containing any file, eg. a PDF.
/// The file is set via dc_msg_set_file()
/// and retrieved via dc_msg_get_file().
File = 60,
/// Message is an invitation to a videochat.
VideochatInvitation = 70,
/// Message is an webxdc instance.
Webxdc = 80,
}
impl Default for Viewtype {
fn default() -> Self {
Viewtype::Unknown
}
}
impl Viewtype {
/// Whether a message with this [`Viewtype`] should have a file attachment.
pub fn has_file(&self) -> bool {
match self {
Viewtype::Unknown => false,
Viewtype::Text => false,
Viewtype::Image => true,
Viewtype::Gif => true,
Viewtype::Sticker => true,
Viewtype::Audio => true,
Viewtype::Voice => true,
Viewtype::Video => true,
Viewtype::File => true,
Viewtype::VideochatInvitation => false,
Viewtype::Webxdc => true,
}
}
}
#[cfg(test)]
mod tests {
use num_traits::FromPrimitive;
use super::*;
use crate::chat::{marknoticed_chat, ChatItem};
use crate::chatlist::Chatlist;
use crate::constants::DC_CONTACT_ID_DEVICE;
use crate::dc_receive_imf::dc_receive_imf;
use crate::test_utils as test;
use crate::test_utils::TestContext;
use super::*;
#[test]
fn test_guess_msgtype_from_suffix() {
assert_eq!(
@@ -1970,7 +1844,7 @@ mod tests {
// test that get_width() and get_height() are returning some dimensions for images;
// (as the device-chat contains a welcome-images, we check that)
t.update_device_chats().await.ok();
let device_chat_id = ChatId::get_for_contact(&t, ContactId::DEVICE)
let device_chat_id = ChatId::get_for_contact(&t, DC_CONTACT_ID_DEVICE)
.await
.unwrap();
@@ -2044,6 +1918,7 @@ mod tests {
Date: Fri, 29 Jan 2021 21:37:55 +0000\n\
\n\
hello\n",
"INBOX",
false,
)
.await
@@ -2252,6 +2127,7 @@ mod tests {
Date: Fri, 29 Jan 2021 21:37:55 +0000\n\
\n\
hello\n",
"INBOX",
false,
)
.await?;
@@ -2269,6 +2145,7 @@ mod tests {
Date: Fri, 29 Jan 2021 21:37:55 +0000\n\
\n\
hello again\n",
"INBOX",
false,
)
.await?;
@@ -2278,29 +2155,4 @@ mod tests {
Ok(())
}
#[test]
fn test_viewtype_derive_display_works_as_expected() {
assert_eq!(format!("{}", Viewtype::Audio), "Audio");
}
#[test]
fn test_viewtype_values() {
// values may be written to disk and must not change
assert_eq!(Viewtype::Unknown, Viewtype::default());
assert_eq!(Viewtype::Unknown, Viewtype::from_i32(0).unwrap());
assert_eq!(Viewtype::Text, Viewtype::from_i32(10).unwrap());
assert_eq!(Viewtype::Image, Viewtype::from_i32(20).unwrap());
assert_eq!(Viewtype::Gif, Viewtype::from_i32(21).unwrap());
assert_eq!(Viewtype::Sticker, Viewtype::from_i32(23).unwrap());
assert_eq!(Viewtype::Audio, Viewtype::from_i32(40).unwrap());
assert_eq!(Viewtype::Voice, Viewtype::from_i32(41).unwrap());
assert_eq!(Viewtype::Video, Viewtype::from_i32(50).unwrap());
assert_eq!(Viewtype::File, Viewtype::from_i32(60).unwrap());
assert_eq!(
Viewtype::VideochatInvitation,
Viewtype::from_i32(70).unwrap()
);
assert_eq!(Viewtype::Webxdc, Viewtype::from_i32(80).unwrap());
}
}

View File

@@ -7,9 +7,9 @@ use chrono::TimeZone;
use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder};
use crate::blob::BlobObject;
use crate::chat::Chat;
use crate::chat::{self, Chat};
use crate::config::Config;
use crate::constants::{Chattype, DC_FROM_HANDSHAKE};
use crate::constants::{Chattype, Viewtype, DC_FROM_HANDSHAKE};
use crate::contact::Contact;
use crate::context::{get_version_str, Context};
use crate::dc_tools::IsNoneOrEmpty;
@@ -22,7 +22,7 @@ use crate::ephemeral::Timer as EphemeralTimer;
use crate::format_flowed::{format_flowed, format_flowed_quote};
use crate::html::new_html_mimepart;
use crate::location;
use crate::message::{self, Message, MsgId, Viewtype};
use crate::message::{self, Message, MsgId};
use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
@@ -133,7 +133,11 @@ impl<'a> MimeFactory<'a> {
) -> Result<MimeFactory<'a>> {
let chat = Chat::load_from_db(context, msg.chat_id).await?;
let from_addr = context.get_configured_addr().await?;
let from_addr = context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
let config_displayname = context
.get_config(Config::Displayname)
.await?
@@ -233,7 +237,10 @@ impl<'a> MimeFactory<'a> {
ensure!(!msg.chat_id.is_special(), "Invalid chat id");
let contact = Contact::load_from_db(context, msg.from_id).await?;
let from_addr = context.get_configured_addr().await?;
let from_addr = context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
let from_displayname = context
.get_config(Config::Displayname)
.await?
@@ -271,7 +278,11 @@ impl<'a> MimeFactory<'a> {
&self,
context: &Context,
) -> Result<Vec<(Option<Peerstate>, &str)>> {
let self_addr = context.get_configured_addr().await?;
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await?
.context("not configured")?;
let mut res = Vec::new();
for (_, addr) in self
.recipients
@@ -492,80 +503,33 @@ impl<'a> MimeFactory<'a> {
}
}
// Start with Internet Message Format headers in the order of the standard example
// <https://datatracker.ietf.org/doc/html/rfc5322#appendix-A.1.1>.
headers
.unprotected
.push(Header::new_with_value("From".into(), vec![from]).unwrap());
if let Some(sender_displayname) = &self.sender_displayname {
let sender =
Address::new_mailbox_with_name(sender_displayname.clone(), self.from_addr.clone());
headers
.unprotected
.push(Header::new_with_value("Sender".into(), vec![sender]).unwrap());
}
headers
.unprotected
.push(Header::new_with_value("To".into(), to).unwrap());
.push(Header::new("MIME-Version".into(), "1.0".into()));
let subject_str = self.subject_str(context).await?;
let encoded_subject = if subject_str
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == ' ')
// We do not use needs_encoding() here because needs_encoding() returns true if the string contains a space
// but we do not want to encode all subjects just because they contain a space.
{
subject_str.clone()
} else {
encode_words(&subject_str)
};
headers
.protected
.push(Header::new("Subject".into(), encoded_subject));
let date = chrono::Utc
.from_local_datetime(&chrono::NaiveDateTime::from_timestamp(self.timestamp, 0))
.unwrap()
.to_rfc2822();
headers.unprotected.push(Header::new("Date".into(), date));
let rfc724_mid = match self.loaded {
Loaded::Message { .. } => self.msg.rfc724_mid.clone(),
Loaded::Mdn { .. } => dc_create_outgoing_rfc724_mid(None, &self.from_addr),
};
let rfc724_mid_headervalue = render_rfc724_mid(&rfc724_mid);
// Amazon's SMTP servers change the `Message-ID`, just as Outlook's SMTP servers do.
// Outlook's servers add an `X-Microsoft-Original-Message-ID` header with the original `Message-ID`,
// and when downloading messages we look for this header in order to correctly identify
// messages.
// Amazon's servers do not add such a header, so we just add it ourselves.
if let Some(server) = context.get_config(Config::ConfiguredSendServer).await? {
if server.ends_with(".amazonaws.com") {
headers.unprotected.push(Header::new(
"X-Microsoft-Original-Message-ID".into(),
rfc724_mid_headervalue.clone(),
))
}
}
headers
.unprotected
.push(Header::new("Message-ID".into(), rfc724_mid_headervalue));
// Reply headers as in <https://datatracker.ietf.org/doc/html/rfc5322#appendix-A.2>.
if !self.in_reply_to.is_empty() {
headers
.unprotected
.push(Header::new("In-Reply-To".into(), self.in_reply_to.clone()));
}
if !self.references.is_empty() {
headers
.unprotected
.push(Header::new("References".into(), self.references.clone()));
}
// Automatic Response headers <https://www.rfc-editor.org/rfc/rfc3834>
if !self.in_reply_to.is_empty() {
headers
.unprotected
.push(Header::new("In-Reply-To".into(), self.in_reply_to.clone()));
}
let date = chrono::Utc
.from_local_datetime(&chrono::NaiveDateTime::from_timestamp(self.timestamp, 0))
.unwrap()
.to_rfc2822();
headers.unprotected.push(Header::new("Date".into(), date));
headers
.unprotected
.push(Header::new("Chat-Version".to_string(), "1.0".to_string()));
if let Loaded::Mdn { .. } = self.loaded {
headers.unprotected.push(Header::new(
"Auto-Submitted".to_string(),
@@ -578,11 +542,6 @@ impl<'a> MimeFactory<'a> {
));
}
// Non-standard headers.
headers
.unprotected
.push(Header::new("Chat-Version".to_string(), "1.0".to_string()));
if self.req_mdn {
// we use "Chat-Disposition-Notification-To"
// because replies to "Disposition-Notification-To" are weird in many cases
@@ -597,9 +556,21 @@ impl<'a> MimeFactory<'a> {
let grpimage = self.grpimage();
let force_plaintext = self.should_force_plaintext();
let skip_autocrypt = self.should_skip_autocrypt();
let subject_str = self.subject_str(context).await?;
let e2ee_guaranteed = self.is_e2ee_guaranteed();
let encrypt_helper = EncryptHelper::new(context).await?;
let encoded_subject = if subject_str
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == ' ')
// We do not use needs_encoding() here because needs_encoding() returns true if the string contains a space
// but we do not want to encode all subjects just because they contain a space.
{
subject_str.clone()
} else {
encode_words(&subject_str)
};
if !skip_autocrypt {
// unless determined otherwise we add the Autocrypt header
let aheader = encrypt_helper.get_aheader().to_string();
@@ -608,6 +579,15 @@ impl<'a> MimeFactory<'a> {
.push(Header::new("Autocrypt".into(), aheader));
}
headers
.protected
.push(Header::new("Subject".into(), encoded_subject));
let rfc724_mid = match self.loaded {
Loaded::Message { .. } => self.msg.rfc724_mid.clone(),
Loaded::Mdn { .. } => dc_create_outgoing_rfc724_mid(None, &self.from_addr),
};
let ephemeral_timer = self.msg.chat_id.get_ephemeral_timer(context).await?;
if let EphemeralTimer::Enabled { duration } = ephemeral_timer {
headers.protected.push(Header::new(
@@ -616,11 +596,25 @@ impl<'a> MimeFactory<'a> {
));
}
// MIME header <https://datatracker.ietf.org/doc/html/rfc2045>.
// Content-Type
headers.unprotected.push(Header::new(
"Message-ID".into(),
render_rfc724_mid(&rfc724_mid),
));
headers
.unprotected
.push(Header::new("MIME-Version".into(), "1.0".into()));
.push(Header::new_with_value("To".into(), to).unwrap());
headers
.unprotected
.push(Header::new_with_value("From".into(), vec![from]).unwrap());
if let Some(sender_displayname) = &self.sender_displayname {
let sender =
Address::new_mailbox_with_name(sender_displayname.clone(), self.from_addr.clone());
headers
.unprotected
.push(Header::new_with_value("Sender".into(), vec![sender]).unwrap());
}
let mut is_gossiped = false;
@@ -1129,7 +1123,7 @@ impl<'a> MimeFactory<'a> {
}
// add attachment part
if self.msg.viewtype.has_file() {
if chat::msgtype_has_file(self.msg.viewtype) {
if !is_file_size_okay(context, self.msg).await? {
bail!(
"Message exceeds the recommended {} MB.",
@@ -1441,13 +1435,12 @@ fn maybe_encode_words(words: &str) -> String {
#[cfg(test)]
mod tests {
use async_std::fs::File;
use super::*;
use async_std::prelude::*;
use mailparse::{addrparse_header, MailHeaderMap};
use crate::chat::ChatId;
use crate::chat::{
self, add_contact_to_chat, create_group_chat, remove_contact_from_chat, send_text_msg,
add_contact_to_chat, create_group_chat, remove_contact_from_chat, send_text_msg,
ProtectionStatus,
};
use crate::chatlist::Chatlist;
@@ -1456,7 +1449,9 @@ mod tests {
use crate::mimeparser::MimeMessage;
use crate::test_utils::{get_chat_msg, TestContext};
use super::*;
use async_std::fs::File;
use mailparse::{addrparse_header, MailHeaderMap};
#[test]
fn test_render_email_address() {
let display_name = "ä space";
@@ -1661,6 +1656,7 @@ mod tests {
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
\n\
hello\n",
"INBOX",
false,
)
.await
@@ -1753,6 +1749,7 @@ mod tests {
t.get_last_msg().await.rfc724_mid
)
.as_bytes(),
"INBOX",
false,
)
.await?;
@@ -1863,6 +1860,7 @@ mod tests {
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
\n\
Some other, completely unrelated content\n",
"INBOX",
false,
)
.await
@@ -1887,7 +1885,9 @@ mod tests {
.await
.unwrap();
dc_receive_imf(context, imf_raw, false).await.unwrap();
dc_receive_imf(context, imf_raw, "INBOX", false)
.await
.unwrap();
let chats = Chatlist::try_load(context, 0, None, None).await.unwrap();
@@ -2084,27 +2084,4 @@ mod tests {
Ok(())
}
/// Tests that standard IMF header "From:" comes before non-standard "Autocrypt:" header.
#[async_std::test]
async fn test_from_before_autocrypt() -> Result<()> {
// create chat with bob
let t = TestContext::new_alice().await;
let chat = t.create_chat_with_contact("bob", "bob@example.org").await;
// send message to bob: that should get multipart/mixed because of the avatar moved to inner header;
// make sure, `Subject:` stays in the outer header (imf header)
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("this is the text!".to_string()));
let sent_msg = t.send_msg(chat.id, &mut msg).await;
let payload = sent_msg.payload();
assert_eq!(payload.match_indices("Autocrypt:").count(), 1);
assert_eq!(payload.match_indices("From:").count(), 1);
assert!(payload.match_indices("From:").next() < payload.match_indices("Autocrypt:").next());
Ok(())
}
}

View File

@@ -2,7 +2,6 @@
use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::io::Cursor;
use std::pin::Pin;
use anyhow::{bail, Result};
@@ -13,8 +12,8 @@ use once_cell::sync::Lazy;
use crate::aheader::Aheader;
use crate::blob::BlobObject;
use crate::constants::{DC_DESIRED_TEXT_LEN, DC_ELLIPSIS};
use crate::contact::{addr_normalize, ContactId};
use crate::constants::{Viewtype, DC_DESIRED_TEXT_LEN, DC_ELLIPSIS};
use crate::contact::addr_normalize;
use crate::context::Context;
use crate::dc_tools::{dc_get_filemeta, dc_truncate, parse_receive_headers};
use crate::dehtml::dehtml;
@@ -24,7 +23,7 @@ use crate::format_flowed::unformat_flowed;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::Fingerprint;
use crate::location;
use crate::message::{self, Viewtype};
use crate::message;
use crate::param::{Param, Params};
use crate::peerstate::Peerstate;
use crate::simplify::simplify;
@@ -369,16 +368,6 @@ impl MimeMessage {
} else if value == "protection-disabled" {
self.is_system_message = SystemMessage::ChatProtectionDisabled;
}
} else if self.get_header(HeaderDef::ChatGroupMemberRemoved).is_some() {
self.is_system_message = SystemMessage::MemberRemovedFromGroup;
} else if self.get_header(HeaderDef::ChatGroupMemberAdded).is_some() {
self.is_system_message = SystemMessage::MemberAddedToGroup;
} else if self.get_header(HeaderDef::ChatGroupNameChanged).is_some() {
self.is_system_message = SystemMessage::GroupNameChanged;
} else if let Some(value) = self.get_header(HeaderDef::ChatContent) {
if value == "group-avatar-changed" {
self.is_system_message = SystemMessage::GroupImageChanged;
}
}
}
@@ -1031,9 +1020,8 @@ impl MimeMessage {
if decoded_data.is_empty() {
return;
}
let reader = Cursor::new(decoded_data);
let msg_type = if context
.is_webxdc_file(filename, reader)
.is_webxdc_file(filename, decoded_data)
.await
.unwrap_or(false)
{
@@ -1150,7 +1138,7 @@ impl MimeMessage {
}
}
pub(crate) fn get_rfc724_mid(&self) -> Option<String> {
pub fn get_rfc724_mid(&self) -> Option<String> {
self.get_header(HeaderDef::XMicrosoftOriginalMessageId)
.or_else(|| self.get_header(HeaderDef::MessageId))
.and_then(|msgid| parse_message_id(msgid).ok())
@@ -1215,9 +1203,6 @@ impl MimeMessage {
if let Some(_disposition) = report_fields.get_header_value(HeaderDef::Disposition) {
let original_message_id = report_fields
.get_header_value(HeaderDef::OriginalMessageId)
// MS Exchange doesn't add an Original-Message-Id header. Instead, they put
// the original message id into the In-Reply-To header:
.or_else(|| report.headers.get_header_value(HeaderDef::InReplyTo))
.and_then(|v| parse_message_id(&v).ok());
let additional_message_ids = report_fields
.get_header_value(HeaderDef::AdditionalMessageIds)
@@ -1381,7 +1366,7 @@ impl MimeMessage {
pub async fn handle_reports(
&self,
context: &Context,
from_id: ContactId,
from_id: u32,
sent_timestamp: i64,
parts: &[Part],
) {
@@ -1495,8 +1480,8 @@ async fn update_gossip_peerstates(
pub(crate) struct Report {
/// Original-Message-ID header
///
/// It MUST be present if the original message has a Message-ID according to RFC 8098.
/// In case we can't find it (shouldn't happen), this is None.
/// It MUST be present if the original message has a Message-ID according to RFC 8098, but MS
/// Exchange does not add it nevertheless, in which case it is `None`.
original_message_id: Option<String>,
/// Additional-Message-IDs
additional_message_ids: Vec<String>,
@@ -2915,6 +2900,7 @@ On 2020-10-25, Bob wrote:
dc_receive_imf(
&t.ctx,
include_bytes!("../test-data/message/subj_with_multimedia_msg.eml"),
"INBOX",
false,
)
.await
@@ -3063,7 +3049,7 @@ Subject: ...
Some quote.
"###;
dc_receive_imf(&t, raw, false).await?;
dc_receive_imf(&t, raw, "INBOX", false).await?;
// Delta Chat generates In-Reply-To with a starting tab when Message-ID is too long.
let raw = br###"In-Reply-To:
@@ -3080,7 +3066,7 @@ Subject: ...
Some reply
"###;
dc_receive_imf(&t, raw, false).await?;
dc_receive_imf(&t, raw, "INBOX", false).await?;
let msg = t.get_last_msg().await;
assert_eq!(msg.get_text().unwrap(), "Some reply");
@@ -3108,13 +3094,13 @@ Message.
"###;
// Bob receives message.
dc_receive_imf(&bob, raw, false).await?;
dc_receive_imf(&bob, raw, "INBOX", false).await?;
let msg = bob.get_last_msg().await;
// Message is incoming.
assert!(msg.param.get_bool(Param::WantsMdn).unwrap());
// Alice receives copy-to-self.
dc_receive_imf(&alice, raw, false).await?;
dc_receive_imf(&alice, raw, "INBOX", false).await?;
let msg = alice.get_last_msg().await;
// Message is outgoing, don't send read receipt to self.
assert!(msg.param.get_bool(Param::WantsMdn).is_none());
@@ -3140,6 +3126,7 @@ Message.
\n\
hello\n"
.as_bytes(),
"INBOX",
false,
)
.await?;
@@ -3177,6 +3164,7 @@ Message.
\n\
--SNIPP--"
.as_bytes(),
"INBOX",
false,
)
.await?;
@@ -3195,32 +3183,10 @@ Message.
#[async_std::test]
async fn test_ms_exchange_mdn() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await?;
let original =
include_bytes!("../test-data/message/ms_exchange_report_original_message.eml");
dc_receive_imf(&t, original, false).await?;
let original_msg_id = t.get_last_msg().await.id;
// 1. Test mimeparser directly
let mdn =
let raw =
include_bytes!("../test-data/message/ms_exchange_report_disposition_notification.eml");
let mimeparser = MimeMessage::from_bytes(&t.ctx, mdn).await?;
assert_eq!(mimeparser.mdn_reports.len(), 1);
assert_eq!(
mimeparser.mdn_reports[0].original_message_id.as_deref(),
Some("d5904dc344eeb5deaf9bb44603f0c716@posteo.de")
);
assert!(mimeparser.mdn_reports[0].additional_message_ids.is_empty());
// 2. Test that marking the original msg as read works
dc_receive_imf(&t, mdn, false).await?;
assert_eq!(
original_msg_id.get_state(&t).await?,
MessageState::OutMdnRcvd
);
let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await?;
assert!(!mimeparser.mdn_reports.is_empty());
Ok(())
}
}

View File

@@ -4,13 +4,11 @@ use std::collections::HashSet;
use std::fmt;
use crate::aheader::{Aheader, EncryptPreference};
use crate::chat::{self};
use crate::chatlist::Chatlist;
use crate::chat::{self, ChatIdBlocked};
use crate::constants::Blocked;
use crate::context::Context;
use crate::events::EventType;
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
use crate::message::Message;
use crate::mimeparser::SystemMessage;
use crate::sql::Sql;
use crate::stock_str;
use anyhow::{bail, Result};
@@ -273,34 +271,14 @@ impl Peerstate {
.query_get_value("SELECT id FROM contacts WHERE addr=?;", paramsv![self.addr])
.await?
{
let chats = Chatlist::try_load(context, 0, None, contact_id).await?;
let chat_id = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Request)
.await?
.id;
let msg = stock_str::contact_setup_changed(context, self.addr.clone()).await;
for (chat_id, msg_id) in chats.iter() {
let timestamp_sort = if let Some(msg_id) = msg_id {
let lastmsg = Message::load_from_db(context, *msg_id).await?;
lastmsg.timestamp_sort
} else {
context
.sql
.query_get_value(
"SELECT created_timestamp FROM chats WHERE id=?;",
paramsv![chat_id],
)
.await?
.unwrap_or(0)
};
chat::add_info_msg_with_cmd(
context,
*chat_id,
&msg,
SystemMessage::Unknown,
timestamp_sort,
Some(timestamp),
None,
)
.await?;
context.emit_event(EventType::ChatModified(*chat_id));
}
chat::add_info_msg(context, chat_id, &msg, timestamp).await?;
context.emit_event(EventType::ChatModified(chat_id));
} else {
bail!("contact with peerstate.addr {:?} not found", &self.addr);
}
@@ -401,9 +379,10 @@ impl Peerstate {
pub fn peek_key(&self, min_verified: PeerstateVerifiedStatus) -> Option<&SignedPublicKey> {
match min_verified {
PeerstateVerifiedStatus::BidirectVerified => self.verified_key.as_ref(),
PeerstateVerifiedStatus::Unverified => {
self.public_key.as_ref().or(self.gossip_key.as_ref())
}
PeerstateVerifiedStatus::Unverified => self
.public_key
.as_ref()
.or_else(|| self.gossip_key.as_ref()),
}
}

View File

@@ -479,7 +479,7 @@ mod tests {
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_public.clone());
keyring.add(KEYS.bob_public.clone());
async_std::task::block_on(pk_encrypt(
futures_lite::future::block_on(pk_encrypt(
CLEARTEXT,
keyring,
Some(KEYS.alice_secret.clone()),
@@ -492,7 +492,7 @@ mod tests {
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_public.clone());
keyring.add(KEYS.bob_public.clone());
async_std::task::block_on(pk_encrypt(CLEARTEXT, keyring, None)).unwrap()
futures_lite::future::block_on(pk_encrypt(CLEARTEXT, keyring, None)).unwrap()
});
#[test]

View File

@@ -41,7 +41,7 @@ impl PlainText {
// as <http://example.org> cannot be handled correctly
// (they would become &lt;http://example.org&gt; where the trailing &gt; would become a valid url part).
// to avoid double encoding, we escape our html-entities by \r that must not be used in the string elsewhere.
let line = line.to_string().replace('\r', "");
let line = line.to_string().replace("\r", "");
let mut line = LINKIFY_MAIL_RE
.replace_all(&*line, "\rLTa href=\rQUOTmailto:$1\rQUOT\rGT$1\rLT/a\rGT")

View File

@@ -113,7 +113,7 @@ pub async fn get_provider_info(
domain: &str,
skip_mx: bool,
) -> Option<&'static Provider> {
let domain = domain.rsplit('@').next()?;
let domain = domain.rsplitn(2, '@').next()?;
if let Some(provider) = get_provider_by_domain(domain) {
return Some(provider);

View File

@@ -366,7 +366,7 @@ static P_EXAMPLE_COM: Lazy<Provider> = Lazy::new(|| {
}
});
// fastmail.md: 123mail.org, 150mail.com, 150ml.com, 16mail.com, 2-mail.com, 4email.net, 50mail.com, airpost.net, allmail.net, bestmail.us, cluemail.com, elitemail.org, emailcorner.net, emailengine.net, emailengine.org, emailgroups.net, emailplus.org, emailuser.net, eml.cc, f-m.fm, fast-email.com, fast-mail.org, fastem.com, fastemail.us, fastemailer.com, fastest.cc, fastimap.com, fastmail.cn, fastmail.co.uk, fastmail.com, fastmail.com.au, fastmail.de, fastmail.es, fastmail.fm, fastmail.fr, fastmail.im, fastmail.in, fastmail.jp, fastmail.mx, fastmail.net, fastmail.nl, fastmail.org, fastmail.se, fastmail.to, fastmail.tw, fastmail.uk, fastmail.us, fastmailbox.net, fastmessaging.com, fea.st, fmail.co.uk, fmailbox.com, fmgirl.com, fmguy.com, ftml.net, h-mail.us, hailmail.net, imap-mail.com, imap.cc, imapmail.org, inoutbox.com, internet-e-mail.com, internet-mail.org, internetemails.net, internetmailing.net, jetemail.net, justemail.net, letterboxes.org, mail-central.com, mail-page.com, mailandftp.com, mailas.com, mailbolt.com, mailc.net, mailcan.com, mailforce.net, mailftp.com, mailhaven.com, mailingaddress.org, mailite.com, mailmight.com, mailnew.com, mailsent.net, mailservice.ms, mailup.net, mailworks.org, ml1.net, mm.st, myfastmail.com, mymacmail.com, nospammail.net, ownmail.net, petml.com, postinbox.com, postpro.net, proinbox.com, promessage.com, realemail.net, reallyfast.biz, reallyfast.info, rushpost.com, sent.as, sent.at, sent.com, speedpost.net, speedymail.org, ssl-mail.com, swift-mail.com, the-fastest.net, the-quickest.com, theinternetemail.com, veryfast.biz, veryspeedy.net, warpmail.net, xsmail.com, yepmail.net, your-mail.com
// fastmail.md: fastmail.com
static P_FASTMAIL: Lazy<Provider> = Lazy::new(|| Provider {
id: "fastmail",
status: Status::Preparation,
@@ -389,6 +389,13 @@ static P_FASTMAIL: Lazy<Provider> = Lazy::new(|| Provider {
port: 465,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Starttls,
hostname: "smtp.fastmail.com",
port: 587,
username_pattern: Email,
},
],
config_defaults: None,
strict_tls: true,
@@ -709,8 +716,8 @@ static P_MAIL_DE: Lazy<Provider> = Lazy::new(|| Provider {
static P_MAIL_RU: Lazy<Provider> = Lazy::new(|| {
Provider {
id: "mail.ru",
status: Status::Preparation,
before_login_hint: "Вам необходимо сгенерировать \"пароль для внешнего приложения\" в веб-интерфейсе mail.ru, чтобы mail.ru работал с Delta Chat.",
status: Status::Ok,
before_login_hint: "Не рекомендуется использовать mail.ru, потому что он разряжает вашу батарею быстрее, чем другие провайдеры.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/mail-ru",
server: vec![
@@ -898,7 +905,7 @@ static P_NAVER: Lazy<Provider> = Lazy::new(|| Provider {
oauth2_authorizer: None,
});
// outlook.com.md: hotmail.com, outlook.com, office365.com, outlook.com.tr, live.com, outlook.de
// outlook.com.md: hotmail.com, outlook.com, office365.com, outlook.com.tr, live.com
static P_OUTLOOK_COM: Lazy<Provider> = Lazy::new(|| Provider {
id: "outlook.com",
status: Status::Ok,
@@ -1488,123 +1495,7 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
("example.com", &*P_EXAMPLE_COM),
("example.org", &*P_EXAMPLE_COM),
("example.net", &*P_EXAMPLE_COM),
("123mail.org", &*P_FASTMAIL),
("150mail.com", &*P_FASTMAIL),
("150ml.com", &*P_FASTMAIL),
("16mail.com", &*P_FASTMAIL),
("2-mail.com", &*P_FASTMAIL),
("4email.net", &*P_FASTMAIL),
("50mail.com", &*P_FASTMAIL),
("airpost.net", &*P_FASTMAIL),
("allmail.net", &*P_FASTMAIL),
("bestmail.us", &*P_FASTMAIL),
("cluemail.com", &*P_FASTMAIL),
("elitemail.org", &*P_FASTMAIL),
("emailcorner.net", &*P_FASTMAIL),
("emailengine.net", &*P_FASTMAIL),
("emailengine.org", &*P_FASTMAIL),
("emailgroups.net", &*P_FASTMAIL),
("emailplus.org", &*P_FASTMAIL),
("emailuser.net", &*P_FASTMAIL),
("eml.cc", &*P_FASTMAIL),
("f-m.fm", &*P_FASTMAIL),
("fast-email.com", &*P_FASTMAIL),
("fast-mail.org", &*P_FASTMAIL),
("fastem.com", &*P_FASTMAIL),
("fastemail.us", &*P_FASTMAIL),
("fastemailer.com", &*P_FASTMAIL),
("fastest.cc", &*P_FASTMAIL),
("fastimap.com", &*P_FASTMAIL),
("fastmail.cn", &*P_FASTMAIL),
("fastmail.co.uk", &*P_FASTMAIL),
("fastmail.com", &*P_FASTMAIL),
("fastmail.com.au", &*P_FASTMAIL),
("fastmail.de", &*P_FASTMAIL),
("fastmail.es", &*P_FASTMAIL),
("fastmail.fm", &*P_FASTMAIL),
("fastmail.fr", &*P_FASTMAIL),
("fastmail.im", &*P_FASTMAIL),
("fastmail.in", &*P_FASTMAIL),
("fastmail.jp", &*P_FASTMAIL),
("fastmail.mx", &*P_FASTMAIL),
("fastmail.net", &*P_FASTMAIL),
("fastmail.nl", &*P_FASTMAIL),
("fastmail.org", &*P_FASTMAIL),
("fastmail.se", &*P_FASTMAIL),
("fastmail.to", &*P_FASTMAIL),
("fastmail.tw", &*P_FASTMAIL),
("fastmail.uk", &*P_FASTMAIL),
("fastmail.us", &*P_FASTMAIL),
("fastmailbox.net", &*P_FASTMAIL),
("fastmessaging.com", &*P_FASTMAIL),
("fea.st", &*P_FASTMAIL),
("fmail.co.uk", &*P_FASTMAIL),
("fmailbox.com", &*P_FASTMAIL),
("fmgirl.com", &*P_FASTMAIL),
("fmguy.com", &*P_FASTMAIL),
("ftml.net", &*P_FASTMAIL),
("h-mail.us", &*P_FASTMAIL),
("hailmail.net", &*P_FASTMAIL),
("imap-mail.com", &*P_FASTMAIL),
("imap.cc", &*P_FASTMAIL),
("imapmail.org", &*P_FASTMAIL),
("inoutbox.com", &*P_FASTMAIL),
("internet-e-mail.com", &*P_FASTMAIL),
("internet-mail.org", &*P_FASTMAIL),
("internetemails.net", &*P_FASTMAIL),
("internetmailing.net", &*P_FASTMAIL),
("jetemail.net", &*P_FASTMAIL),
("justemail.net", &*P_FASTMAIL),
("letterboxes.org", &*P_FASTMAIL),
("mail-central.com", &*P_FASTMAIL),
("mail-page.com", &*P_FASTMAIL),
("mailandftp.com", &*P_FASTMAIL),
("mailas.com", &*P_FASTMAIL),
("mailbolt.com", &*P_FASTMAIL),
("mailc.net", &*P_FASTMAIL),
("mailcan.com", &*P_FASTMAIL),
("mailforce.net", &*P_FASTMAIL),
("mailftp.com", &*P_FASTMAIL),
("mailhaven.com", &*P_FASTMAIL),
("mailingaddress.org", &*P_FASTMAIL),
("mailite.com", &*P_FASTMAIL),
("mailmight.com", &*P_FASTMAIL),
("mailnew.com", &*P_FASTMAIL),
("mailsent.net", &*P_FASTMAIL),
("mailservice.ms", &*P_FASTMAIL),
("mailup.net", &*P_FASTMAIL),
("mailworks.org", &*P_FASTMAIL),
("ml1.net", &*P_FASTMAIL),
("mm.st", &*P_FASTMAIL),
("myfastmail.com", &*P_FASTMAIL),
("mymacmail.com", &*P_FASTMAIL),
("nospammail.net", &*P_FASTMAIL),
("ownmail.net", &*P_FASTMAIL),
("petml.com", &*P_FASTMAIL),
("postinbox.com", &*P_FASTMAIL),
("postpro.net", &*P_FASTMAIL),
("proinbox.com", &*P_FASTMAIL),
("promessage.com", &*P_FASTMAIL),
("realemail.net", &*P_FASTMAIL),
("reallyfast.biz", &*P_FASTMAIL),
("reallyfast.info", &*P_FASTMAIL),
("rushpost.com", &*P_FASTMAIL),
("sent.as", &*P_FASTMAIL),
("sent.at", &*P_FASTMAIL),
("sent.com", &*P_FASTMAIL),
("speedpost.net", &*P_FASTMAIL),
("speedymail.org", &*P_FASTMAIL),
("ssl-mail.com", &*P_FASTMAIL),
("swift-mail.com", &*P_FASTMAIL),
("the-fastest.net", &*P_FASTMAIL),
("the-quickest.com", &*P_FASTMAIL),
("theinternetemail.com", &*P_FASTMAIL),
("veryfast.biz", &*P_FASTMAIL),
("veryspeedy.net", &*P_FASTMAIL),
("warpmail.net", &*P_FASTMAIL),
("xsmail.com", &*P_FASTMAIL),
("yepmail.net", &*P_FASTMAIL),
("your-mail.com", &*P_FASTMAIL),
("firemail.at", &*P_FIREMAIL_DE),
("firemail.de", &*P_FIREMAIL_DE),
("five.chat", &*P_FIVE_CHAT),
@@ -1648,7 +1539,6 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
("office365.com", &*P_OUTLOOK_COM),
("outlook.com.tr", &*P_OUTLOOK_COM),
("live.com", &*P_OUTLOOK_COM),
("outlook.de", &*P_OUTLOOK_COM),
("posteo.de", &*P_POSTEO),
("posteo.af", &*P_POSTEO),
("posteo.at", &*P_POSTEO),
@@ -1853,4 +1743,4 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
});
pub static PROVIDER_UPDATED: Lazy<chrono::NaiveDate> =
Lazy::new(|| chrono::NaiveDate::from_ymd(2022, 1, 31));
Lazy::new(|| chrono::NaiveDate::from_ymd(2022, 1, 11));

View File

@@ -9,7 +9,7 @@ use std::collections::BTreeMap;
use crate::chat::{self, get_chat_id_by_grpid, ChatIdBlocked};
use crate::config::Config;
use crate::constants::Blocked;
use crate::contact::{addr_normalize, may_be_valid_addr, Contact, ContactId, Origin};
use crate::contact::{addr_normalize, may_be_valid_addr, Contact, Origin};
use crate::context::Context;
use crate::dc_tools::time;
use crate::key::Fingerprint;
@@ -30,7 +30,7 @@ const HTTPS_SCHEME: &str = "https://";
#[derive(Debug, Clone, PartialEq)]
pub enum Qr {
AskVerifyContact {
contact_id: ContactId,
contact_id: u32,
fingerprint: Fingerprint,
invitenumber: String,
authcode: String,
@@ -38,16 +38,16 @@ pub enum Qr {
AskVerifyGroup {
grpname: String,
grpid: String,
contact_id: ContactId,
contact_id: u32,
fingerprint: Fingerprint,
invitenumber: String,
authcode: String,
},
FprOk {
contact_id: ContactId,
contact_id: u32,
},
FprMismatch {
contact_id: Option<ContactId>,
contact_id: Option<u32>,
},
FprWithoutAddr {
fingerprint: String,
@@ -60,7 +60,7 @@ pub enum Qr {
instance_pattern: String,
},
Addr {
contact_id: ContactId,
contact_id: u32,
},
Url {
url: String,
@@ -69,7 +69,7 @@ pub enum Qr {
text: String,
},
WithdrawVerifyContact {
contact_id: ContactId,
contact_id: u32,
fingerprint: Fingerprint,
invitenumber: String,
authcode: String,
@@ -77,13 +77,13 @@ pub enum Qr {
WithdrawVerifyGroup {
grpname: String,
grpid: String,
contact_id: ContactId,
contact_id: u32,
fingerprint: Fingerprint,
invitenumber: String,
authcode: String,
},
ReviveVerifyContact {
contact_id: ContactId,
contact_id: u32,
fingerprint: Fingerprint,
invitenumber: String,
authcode: String,
@@ -91,7 +91,7 @@ pub enum Qr {
ReviveVerifyGroup {
grpname: String,
grpid: String,
contact_id: ContactId,
contact_id: u32,
fingerprint: Fingerprint,
invitenumber: String,
authcode: String,
@@ -173,7 +173,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
};
let name = if let Some(encoded_name) = param.get("n") {
let encoded_name = encoded_name.replace('+', "%20"); // sometimes spaces are encoded as `+`
let encoded_name = encoded_name.replace("+", "%20"); // sometimes spaces are encoded as `+`
match percent_decode_str(&encoded_name).decode_utf8() {
Ok(name) => name.to_string(),
Err(err) => bail!("Invalid name: {}", err),
@@ -188,7 +188,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
let grpname = if grpid.is_some() {
if let Some(encoded_name) = param.get("g") {
let encoded_name = encoded_name.replace('+', "%20"); // sometimes spaces are encoded as `+`
let encoded_name = encoded_name.replace("+", "%20"); // sometimes spaces are encoded as `+`
match percent_decode_str(&encoded_name).decode_utf8() {
Ok(name) => Some(name.to_string()),
Err(err) => bail!("Invalid group name: {}", err),
@@ -711,7 +711,7 @@ mod tests {
..
} = qr
{
assert_ne!(contact_id, ContactId::UNDEFINED);
assert_ne!(contact_id, 0);
assert_eq!(grpname, "test ? test !");
} else {
bail!("Wrong QR code type");
@@ -729,7 +729,7 @@ mod tests {
..
} = qr
{
assert_ne!(contact_id, ContactId::UNDEFINED);
assert_ne!(contact_id, 0);
assert_eq!(grpname, "test ? test !");
let contact = Contact::get_by_id(&ctx.ctx, contact_id).await?;
@@ -751,7 +751,7 @@ mod tests {
).await?;
if let Qr::AskVerifyContact { contact_id, .. } = qr {
assert_ne!(contact_id, ContactId::UNDEFINED);
assert_ne!(contact_id, 0);
} else {
bail!("Wrong QR code type");
}

View File

@@ -6,7 +6,8 @@ use crate::{
chat::{Chat, ChatId},
color::color_int_to_hex_string,
config::Config,
contact::{Contact, ContactId},
constants::DC_CONTACT_ID_SELF,
contact::Contact,
context::Context,
securejoin, stock_str,
};
@@ -40,7 +41,7 @@ async fn generate_join_group_qr_code(context: &Context, chat_id: ChatId) -> Resu
}
async fn generate_verification_qr(context: &Context) -> Result<String> {
let contact = Contact::get_by_id(context, ContactId::SELF).await?;
let contact = Contact::get_by_id(context, DC_CONTACT_ID_SELF).await?;
let avatar = match contact.get_profile_image(context).await? {
Some(path) => {

View File

@@ -6,12 +6,13 @@ use std::collections::BTreeMap;
use crate::chat::add_device_msg_with_importance;
use crate::config::Config;
use crate::constants::Viewtype;
use crate::context::Context;
use crate::dc_tools::time;
use crate::imap::scan_folders::get_watched_folders;
use crate::imap::Imap;
use crate::job::{Action, Status};
use crate::message::{Message, Viewtype};
use crate::message::Message;
use crate::param::Params;
use crate::{job, stock_str, EventType};

View File

@@ -8,13 +8,10 @@ use async_std::{
use crate::config::Config;
use crate::context::Context;
use crate::dc_tools::maybe_add_time_based_warnings;
use crate::dc_tools::time;
use crate::ephemeral::{self, delete_expired_imap_messages};
use crate::ephemeral::delete_expired_imap_messages;
use crate::imap::Imap;
use crate::job::{self, Thread};
use crate::log::LogExt;
use crate::smtp::{send_smtp_messages, Smtp};
use crate::sql;
use self::connectivity::ConnectivityStore;
@@ -36,8 +33,6 @@ pub(crate) enum Scheduler {
sentbox_handle: Option<task::JoinHandle<()>>,
smtp: SmtpConnectionState,
smtp_handle: Option<task::JoinHandle<()>>,
ephemeral_handle: Option<task::JoinHandle<()>>,
ephemeral_interrupt_send: Sender<()>,
},
}
@@ -63,10 +58,6 @@ impl Context {
pub(crate) async fn interrupt_smtp(&self, info: InterruptInfo) {
self.scheduler.read().await.interrupt_smtp(info).await;
}
pub(crate) async fn interrupt_ephemeral_task(&self) {
self.scheduler.read().await.interrupt_ephemeral_task().await;
}
}
async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConnectionHandlers) {
@@ -87,6 +78,8 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
.expect("inbox loop, missing started receiver");
let ctx = ctx1;
// track number of continously executed jobs
let mut jobs_loaded = 0;
let mut info = InterruptInfo::default();
loop {
let job = match job::load_next(&ctx, Thread::Imap, &info).await {
@@ -98,25 +91,21 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
};
match job {
Some(job) => {
Some(job) if jobs_loaded <= 20 => {
jobs_loaded += 1;
job::perform_job(&ctx, job::Connection::Inbox(&mut connection), job).await;
info = Default::default();
}
Some(job) => {
// Let the fetch run, but return back to the job afterwards.
jobs_loaded = 0;
info!(ctx, "postponing imap-job {} to run fetch...", job);
fetch(&ctx, &mut connection).await;
}
None => {
maybe_add_time_based_warnings(&ctx).await;
jobs_loaded = 0;
match ctx.get_config_i64(Config::LastHousekeeping).await {
Ok(last_housekeeping_time) => {
let next_housekeeping_time =
last_housekeeping_time.saturating_add(60 * 60 * 24);
if next_housekeeping_time <= time() {
sql::housekeeping(&ctx).await.ok_or_log(&ctx);
}
}
Err(err) => {
warn!(ctx, "Failed to get last housekeeping time: {}", err);
}
};
maybe_add_time_based_warnings(&ctx).await;
info = fetch_idle(&ctx, &mut connection, Config::ConfiguredInboxFolder).await;
}
@@ -137,6 +126,32 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
.expect("inbox loop, missing shutdown receiver");
}
async fn fetch(ctx: &Context, connection: &mut Imap) {
match ctx.get_config(Config::ConfiguredInboxFolder).await {
Ok(Some(watch_folder)) => {
if let Err(err) = connection.prepare(ctx).await {
warn!(ctx, "Could not connect: {}", err);
return;
}
// fetch
if let Err(err) = connection.fetch_move_delete(ctx, &watch_folder).await {
connection.trigger_reconnect(ctx).await;
warn!(ctx, "{:#}", err);
}
}
Ok(None) => {
info!(ctx, "Can not fetch inbox folder, not set");
}
Err(err) => {
warn!(
ctx,
"Can not fetch inbox folder, failed to get config: {:?}", err
);
}
}
}
async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> InterruptInfo {
match ctx.get_config(folder).await {
Ok(Some(watch_folder)) => {
@@ -146,30 +161,7 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
return connection.fake_idle(ctx, Some(watch_folder)).await;
}
if folder == Config::ConfiguredInboxFolder {
if let Err(err) = connection
.store_seen_flags_on_imap(ctx)
.await
.context("store_seen_flags_on_imap failed")
{
warn!(ctx, "{:#}", err);
}
}
// Fetch the watched folder.
if let Err(err) = connection
.fetch_move_delete(ctx, &watch_folder, false)
.await
{
connection.trigger_reconnect(ctx).await;
warn!(ctx, "{:#}", err);
return InterruptInfo::new(false);
}
// Mark expired messages for deletion. Marked messages will be deleted from the server
// on the next iteration of `fetch_move_delete`. `delete_expired_imap_messages` is not
// called right before `fetch_move_delete` because it is not well optimized and would
// otherwise slow down message fetching.
// Mark expired messages for deletion.
if let Err(err) = delete_expired_imap_messages(ctx)
.await
.context("delete_expired_imap_messages failed")
@@ -177,44 +169,24 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
warn!(ctx, "{:#}", err);
}
// Scan additional folders only after finishing fetching the watched folder.
//
// On iOS the application has strictly limited time to work in background, so we may not
// be able to scan all folders before time is up if there are many of them.
// Scan other folders before fetching from watched folder. This may result in the
// messages being moved into the watched folder, for example from the Spam folder to
// the Inbox folder.
if folder == Config::ConfiguredInboxFolder {
// Only scan on the Inbox thread in order to prevent parallel scans, which might lead to duplicate messages
match connection.scan_folders(ctx).await {
Err(err) => {
// Don't reconnect, if there is a problem with the connection we will realize this when IDLEing
// but maybe just one folder can't be selected or something
warn!(ctx, "{}", err);
}
Ok(true) => {
// Fetch the watched folder again in case scanning other folder moved messages
// there.
//
// In most cases this will select the watched folder and return because there are
// no new messages. We want to select the watched folder anyway before going IDLE
// there, so this does not take additional protocol round-trip.
if let Err(err) = connection
.fetch_move_delete(ctx, &watch_folder, false)
.await
{
connection.trigger_reconnect(ctx).await;
warn!(ctx, "{:#}", err);
return InterruptInfo::new(false);
}
}
Ok(false) => {}
if let Err(err) = connection.scan_folders(ctx).await {
// Don't reconnect, if there is a problem with the connection we will realize this when IDLEing
// but maybe just one folder can't be selected or something
warn!(ctx, "{}", err);
}
}
// Synchronize Seen flags.
connection
.sync_seen_flags(ctx, &watch_folder)
.await
.context("sync_seen_flags")
.ok_or_log(ctx);
// fetch
if let Err(err) = connection.fetch_move_delete(ctx, &watch_folder).await {
connection.trigger_reconnect(ctx).await;
warn!(ctx, "{:#}", err);
return InterruptInfo::new(false);
}
connection.connectivity.set_connected(ctx).await;
@@ -308,7 +280,6 @@ async fn smtp_loop(ctx: Context, started: Sender<()>, smtp_handlers: SmtpConnect
.expect("smtp loop, missing started receiver");
let ctx = ctx1;
let mut timeout = None;
let mut interrupt_info = Default::default();
loop {
let job = match job::load_next(&ctx, Thread::Smtp, &interrupt_info).await {
@@ -326,16 +297,9 @@ async fn smtp_loop(ctx: Context, started: Sender<()>, smtp_handlers: SmtpConnect
interrupt_info = Default::default();
}
None => {
let res = send_smtp_messages(&ctx, &mut connection).await;
if let Err(err) = &res {
if let Err(err) = send_smtp_messages(&ctx, &mut connection).await {
warn!(ctx, "send_smtp_messages failed: {:#}", err);
}
let success = res.unwrap_or(false);
timeout = if success {
None
} else {
Some(timeout.map_or(30, |timeout: u64| timeout.saturating_mul(3)))
};
// Fake Idle
info!(ctx, "smtp fake idle - started");
@@ -344,27 +308,7 @@ async fn smtp_loop(ctx: Context, started: Sender<()>, smtp_handlers: SmtpConnect
Some(err) => connection.connectivity.set_err(&ctx, err).await,
}
// If send_smtp_messages() failed, we set a timeout for the fake-idle so that
// sending is retried (at the latest) after the timeout. If sending fails
// again, we increase the timeout exponentially, in order not to do lots of
// unnecessary retries.
if let Some(timeout) = timeout {
info!(
ctx,
"smtp has messages to retry, planning to retry {} seconds later",
timeout
);
let duration = std::time::Duration::from_secs(timeout);
interrupt_info = async_std::future::timeout(duration, async {
idle_interrupt_receiver.recv().await.unwrap_or_default()
})
.await
.unwrap_or_default();
} else {
info!(ctx, "smtp has no messages to retry, waiting for interrupt");
interrupt_info = idle_interrupt_receiver.recv().await.unwrap_or_default();
};
interrupt_info = idle_interrupt_receiver.recv().await.unwrap_or_default();
info!(ctx, "smtp fake idle - interrupted")
}
}
@@ -398,7 +342,6 @@ impl Scheduler {
let (sentbox_start_send, sentbox_start_recv) = channel::bounded(1);
let mut sentbox_handle = None;
let (smtp_start_send, smtp_start_recv) = channel::bounded(1);
let (ephemeral_interrupt_send, ephemeral_interrupt_recv) = channel::bounded(1);
let inbox_handle = {
let ctx = ctx.clone();
@@ -407,7 +350,7 @@ impl Scheduler {
}))
};
if ctx.should_watch_mvbox().await? {
if ctx.get_config_bool(Config::MvboxMove).await? {
let ctx = ctx.clone();
mvbox_handle = Some(task::spawn(async move {
simple_imap_loop(
@@ -460,13 +403,6 @@ impl Scheduler {
}))
};
let ephemeral_handle = {
let ctx = ctx.clone();
Some(task::spawn(async move {
ephemeral::ephemeral_loop(&ctx, ephemeral_interrupt_recv).await;
}))
};
*self = Scheduler::Running {
inbox,
mvbox,
@@ -476,8 +412,6 @@ impl Scheduler {
mvbox_handle,
sentbox_handle,
smtp_handle,
ephemeral_handle,
ephemeral_interrupt_send,
};
// wait for all loops to be started
@@ -543,16 +477,6 @@ impl Scheduler {
}
}
async fn interrupt_ephemeral_task(&self) {
if let Scheduler::Running {
ref ephemeral_interrupt_send,
..
} = self
{
ephemeral_interrupt_send.try_send(()).ok();
}
}
/// Halts the scheduler, must be called first, and then `stop`.
pub(crate) async fn pre_stop(&self) -> StopToken {
match self {
@@ -599,7 +523,6 @@ impl Scheduler {
mvbox_handle,
sentbox_handle,
smtp_handle,
ephemeral_handle,
..
} => {
if let Some(handle) = inbox_handle.take() {
@@ -614,9 +537,6 @@ impl Scheduler {
if let Some(handle) = smtp_handle.take() {
handle.await;
}
if let Some(handle) = ephemeral_handle.take() {
handle.cancel().await;
}
*self = Scheduler::Stopped;
}

View File

@@ -5,7 +5,6 @@ use async_std::sync::{Mutex, RwLockReadGuard};
use crate::dc_tools::time;
use crate::events::EventType;
use crate::imap::scan_folders::get_watched_folder_configs;
use crate::quota::{
QUOTA_ERROR_THRESHOLD_PERCENTAGE, QUOTA_MAX_AGE_SECONDS, QUOTA_WARN_THRESHOLD_PERCENTAGE,
};
@@ -304,7 +303,7 @@ impl Context {
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="initial-scale=1.0; user-scalable=no" />
<meta name="viewport" content="initial-scale=1.0" />
<style>
ul {
list-style-type: none;
@@ -363,14 +362,17 @@ impl Context {
[
(
Config::ConfiguredInboxFolder,
None,
inbox.state.connectivity.clone(),
),
(
Config::ConfiguredMvboxFolder,
Some(Config::MvboxMove),
mvbox.state.connectivity.clone(),
),
(
Config::ConfiguredSentboxFolder,
Some(Config::SentboxWatch),
sentbox.state.connectivity.clone(),
),
],
@@ -389,12 +391,20 @@ impl Context {
// - "Sent": Connected
// =============================================================================================
let watched_folders = get_watched_folder_configs(self).await?;
ret += &format!("<h3>{}</h3><ul>", stock_str::incoming_messages(self).await);
for (folder, state) in &folders_states {
let mut folder_added = false;
for (folder, watch, state) in &folders_states {
let w = if let Some(watch_config) = *watch {
self.get_config(watch_config)
.await
.ok_or_log(self)
.flatten()
== Some("1".to_string())
} else {
true
};
if watched_folders.contains(folder) {
let mut folder_added = false;
if w {
let f = self.get_config(*folder).await.ok_or_log(self).flatten();
if let Some(foldername) = f {
@@ -449,7 +459,13 @@ impl Context {
// [======67%===== ]
// =============================================================================================
let domain = dc_tools::EmailAddress::new(&self.get_configured_addr().await?)?.domain;
let domain = dc_tools::EmailAddress::new(
&self
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default(),
)?
.domain;
ret += &format!(
"<h3>{}</h3><ul>",
stock_str::storage_on_domain(self, domain).await
@@ -538,7 +554,7 @@ impl Context {
self.schedule_quota_update().await?;
}
} else {
ret += &format!("<li>{}</li>", stock_str::not_connected(self).await);
ret += &format!("<li>{}</li>", stock_str::one_moment(self).await);
self.schedule_quota_update().await?;
}
ret += "</ul>";

View File

@@ -3,20 +3,21 @@
use std::convert::TryFrom;
use anyhow::{bail, Context as _, Error, Result};
use async_std::sync::Mutex;
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
use crate::aheader::EncryptPreference;
use crate::chat::{self, Chat, ChatId, ChatIdBlocked};
use crate::chat::{self, is_contact_in_chat, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
use crate::config::Config;
use crate::constants::Blocked;
use crate::contact::{Contact, ContactId, Origin, VerifiedStatus};
use crate::constants::{Blocked, Chattype, Viewtype, DC_CONTACT_ID_LAST_SPECIAL};
use crate::contact::{Contact, Origin, VerifiedStatus};
use crate::context::Context;
use crate::dc_tools::time;
use crate::e2ee::ensure_secret_key_exists;
use crate::events::EventType;
use crate::headerdef::HeaderDef;
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
use crate::message::{Message, Viewtype};
use crate::message::Message;
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::param::Param;
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus, ToSave};
@@ -24,16 +25,28 @@ use crate::qr::check_qr;
use crate::stock_str;
use crate::token;
mod bob;
mod bobstate;
mod qrinvite;
use crate::token::Namespace;
use bobstate::BobState;
use bobstate::{BobHandshakeStage, BobState, BobStateHandle};
use qrinvite::QrInvite;
pub const NON_ALPHANUMERIC_WITHOUT_DOT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'.');
macro_rules! joiner_progress {
($context:tt, $contact_id:expr, $progress:expr) => {
assert!(
$progress >= 0 && $progress <= 1000,
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
);
$context.emit_event($crate::events::EventType::SecurejoinJoinerProgress {
contact_id: $contact_id,
progress: $progress,
});
};
}
macro_rules! inviter_progress {
($context:tt, $contact_id:expr, $progress:expr) => {
assert!(
@@ -47,6 +60,100 @@ macro_rules! inviter_progress {
};
}
/// State for setup-contact/secure-join protocol joiner's side, aka Bob's side.
///
/// The setup-contact protocol needs to carry state for both the inviter (Alice) and the
/// joiner/invitee (Bob). For Alice this state is minimal and in the `tokens` table in the
/// database. For Bob this state is only carried live on the [`Context`] in this struct.
#[derive(Debug, Default)]
pub(crate) struct Bob {
inner: Mutex<Option<BobState>>,
}
/// Return value for [`Bob::start_protocol`].
///
/// This indicates which protocol variant was started and provides the required information
/// about it.
enum StartedProtocolVariant {
/// The setup-contact protocol, to verify a contact.
SetupContact,
/// The secure-join protocol, to join a group.
SecureJoin {
group_id: String,
group_name: String,
},
}
impl Bob {
/// Starts the securejoin protocol with the QR `invite`.
///
/// This will try to start the securejoin protocol for the given QR `invite`. If it
/// succeeded the protocol state will be tracked in `self`.
///
/// This function takes care of starting the "ongoing" mechanism if required and
/// handling errors while starting the protocol.
///
/// # Returns
///
/// If the started protocol is joining a group the returned struct contains information
/// about the group and ongoing process.
async fn start_protocol(
&self,
context: &Context,
invite: QrInvite,
) -> Result<StartedProtocolVariant, JoinError> {
let mut guard = self.inner.lock().await;
if guard.is_some() {
warn!(context, "The new securejoin will replace the ongoing one.");
*guard = None;
}
let variant = match invite {
QrInvite::Group {
ref grpid,
ref name,
..
} => StartedProtocolVariant::SecureJoin {
group_id: grpid.clone(),
group_name: name.clone(),
},
_ => StartedProtocolVariant::SetupContact,
};
match BobState::start_protocol(context, invite).await {
Ok((state, stage)) => {
if matches!(stage, BobHandshakeStage::RequestWithAuthSent) {
joiner_progress!(context, state.invite().contact_id(), 400);
}
*guard = Some(state);
Ok(variant)
}
Err(err) => {
if let StartedProtocolVariant::SecureJoin { .. } = variant {
context.free_ongoing().await;
}
Err(err)
}
}
}
/// Returns a handle to the [`BobState`] of the handshake.
///
/// If there currently isn't a handshake running this will return `None`. Otherwise
/// this will return a handle to the current [`BobState`]. This handle allows
/// processing an incoming message and allows terminating the handshake.
///
/// The handle contains an exclusive lock, which is held until the handle is dropped.
/// This guarantees all state and state changes are correct and allows safely
/// terminating the handshake without worrying about concurrency.
async fn state(&self, context: &Context) -> Option<BobStateHandle<'_>> {
let guard = self.inner.lock().await;
let ret = BobStateHandle::from_guard(guard);
if ret.is_none() {
info!(context, "No active BobState found for securejoin handshake");
}
ret
}
}
/// Generates a Secure Join QR code.
///
/// With `group` set to `None` this generates a setup-contact QR code, with `group` set to a
@@ -66,7 +173,19 @@ pub async fn dc_get_securejoin_qr(context: &Context, group: Option<ChatId>) -> R
.is_none();
let invitenumber = token::lookup_or_new(context, Namespace::InviteNumber, group).await;
let auth = token::lookup_or_new(context, Namespace::Auth, group).await;
let self_addr = context.get_configured_addr().await?;
let self_addr = match context.get_config(Config::ConfiguredAddr).await {
Ok(Some(addr)) => addr,
Ok(None) => {
bail!("Not configured, cannot generate QR code.");
}
Err(err) => {
bail!(
"Unable to retrieve configuration, cannot generate QR code: {:?}",
err
);
}
};
let self_name = context
.get_config(Config::Displayname)
.await?
@@ -161,7 +280,7 @@ pub enum JoinError {
pub async fn dc_join_securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
securejoin(context, qr).await.map_err(|err| {
warn!(context, "Fatal joiner error: {:#}", err);
// The user just scanned this QR code so has context on what failed.
// This is a modal operation, the user has context on what failed.
error!(context, "QR process failed");
err
})
@@ -178,7 +297,47 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
let invite = QrInvite::try_from(qr_scan)?;
bob::start_protocol(context, invite).await
match context.bob.start_protocol(context, invite.clone()).await? {
StartedProtocolVariant::SetupContact => {
// for a one-to-one-chat, the chat is already known, return the chat-id,
// the verification runs in background
let chat_id = ChatId::create_for_contact(context, invite.contact_id())
.await
.map_err(JoinError::UnknownContact)?;
Ok(chat_id)
}
StartedProtocolVariant::SecureJoin {
group_id,
group_name,
} => {
// for a group-join, also create the chat soon and let the verification run in background.
// however, the group will become usable only when the protocol has finished.
let contact_id = invite.contact_id();
let chat_id = if let Some((chat_id, _protected, _blocked)) =
chat::get_chat_id_by_grpid(context, &group_id).await?
{
chat_id.unblock(context).await?;
chat_id
} else {
ChatId::create_multiuser_record(
context,
Chattype::Group,
&group_id,
&group_name,
Blocked::Not,
ProtectionStatus::Unprotected, // protection is added later as needed
None,
)
.await?
};
if !is_contact_in_chat(context, chat_id, contact_id).await? {
chat::add_to_chat_contacts_table(context, chat_id, contact_id).await?;
}
let msg = stock_str::secure_join_started(context, contact_id).await;
chat::add_info_msg(context, chat_id, &msg, time()).await?;
Ok(chat_id)
}
}
}
/// Error when failing to send a protocol handshake message.
@@ -193,7 +352,7 @@ pub struct SendMsgError(#[from] anyhow::Error);
/// Bob's handshake messages are sent in `BobState::send_handshake_message()`.
async fn send_alice_handshake_msg(
context: &Context,
contact_id: ContactId,
contact_id: u32,
step: &str,
fingerprint: Option<Fingerprint>,
) -> Result<(), SendMsgError> {
@@ -221,7 +380,7 @@ async fn send_alice_handshake_msg(
}
/// Get an unblocked chat that can be used for info messages.
async fn info_chat_id(context: &Context, contact_id: ContactId) -> Result<ChatId> {
async fn info_chat_id(context: &Context, contact_id: u32) -> Result<ChatId> {
let chat_id_blocked = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Not).await?;
Ok(chat_id_blocked.id)
}
@@ -229,7 +388,7 @@ async fn info_chat_id(context: &Context, contact_id: ContactId) -> Result<ChatId
async fn fingerprint_equals_sender(
context: &Context,
fingerprint: &Fingerprint,
contact_id: ContactId,
contact_id: u32,
) -> Result<bool, Error> {
let contact = Contact::load_from_db(context, contact_id).await?;
let peerstate = match Peerstate::from_addr(context, contact.get_addr()).await {
@@ -283,8 +442,9 @@ pub(crate) enum HandshakeMessage {
/// Handle incoming secure-join handshake.
///
/// This function will update the securejoin state in the database as the protocol
/// progresses.
/// This function will update the securejoin state in [`InnerContext::bob`] and also
/// terminate the ongoing process using [`Context::stop_ongoing`] as required by the
/// protocol.
///
/// A message which results in [`Err`] will be hidden from the user but not deleted, it may
/// be a valid message for something else we are not aware off. E.g. it could be part of a
@@ -292,13 +452,15 @@ pub(crate) enum HandshakeMessage {
///
/// When `handle_securejoin_handshake()` is called, the message is not yet filed in the
/// database; this is done by `receive_imf()` later on as needed.
///
/// [`InnerContext::bob`]: crate::context::InnerContext::bob
#[allow(clippy::indexing_slicing)]
pub(crate) async fn handle_securejoin_handshake(
context: &Context,
mime_message: &MimeMessage,
contact_id: ContactId,
contact_id: u32,
) -> Result<HandshakeMessage> {
if contact_id.is_special() {
if contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
return Err(Error::msg("Can not be called with special contact ID"));
}
let step = mime_message
@@ -359,7 +521,38 @@ pub(crate) async fn handle_securejoin_handshake(
==== Bob - the joiner's side =====
==== Step 4 in "Setup verified contact" protocol =====
========================================================*/
bob::handle_auth_required(context, mime_message).await
match context.bob.state(context).await {
Some(mut bobstate) => match bobstate.handle_message(context, mime_message).await {
Some(BobHandshakeStage::Terminated(why)) => {
could_not_establish_secure_connection(
context,
contact_id,
bobstate.chat_id(context).await?,
why,
)
.await?;
Ok(HandshakeMessage::Done)
}
Some(_stage) => {
if join_vg {
// the message reads "Alice replied, waiting for being added to the group…";
// show it only on secure-join and not on setup-contact therefore.
let msg = stock_str::secure_join_replies(context, contact_id).await;
chat::add_info_msg(
context,
bobstate.chat_id(context).await?,
&msg,
time(),
)
.await?;
}
joiner_progress!(context, bobstate.invite().contact_id(), 400);
Ok(HandshakeMessage::Done)
}
None => Ok(HandshakeMessage::Ignore),
},
None => Ok(HandshakeMessage::Ignore),
}
}
"vg-request-with-auth" | "vc-request-with-auth" => {
/*==========================================================
@@ -490,14 +683,44 @@ pub(crate) async fn handle_securejoin_handshake(
==== Bob - the joiner's side ====
==== Step 7 in "Setup verified contact" protocol ====
=======================================================*/
match BobState::from_db(&context.sql).await? {
Some(bobstate) => {
bob::handle_contact_confirm(context, bobstate, mime_message).await
}
None => match join_vg {
true => Ok(HandshakeMessage::Propagate),
false => Ok(HandshakeMessage::Ignore),
info!(context, "matched vc-contact-confirm step");
let retval = if join_vg {
HandshakeMessage::Propagate
} else {
HandshakeMessage::Ignore
};
match context.bob.state(context).await {
Some(mut bobstate) => match bobstate.handle_message(context, mime_message).await {
Some(BobHandshakeStage::Terminated(why)) => {
could_not_establish_secure_connection(
context,
contact_id,
bobstate.chat_id(context).await?,
why,
)
.await?;
Ok(HandshakeMessage::Done)
}
Some(BobHandshakeStage::Completed) => {
// Can only be BobHandshakeStage::Completed
secure_connection_established(
context,
contact_id,
bobstate.chat_id(context).await?,
)
.await?;
Ok(retval)
}
Some(_) => {
warn!(
context,
"Impossible state returned from handling handshake message"
);
Ok(retval)
}
None => Ok(retval),
},
None => Ok(retval),
}
}
"vg-member-added-received" | "vc-contact-confirm-received" => {
@@ -559,9 +782,9 @@ pub(crate) async fn handle_securejoin_handshake(
pub(crate) async fn observe_securejoin_on_other_device(
context: &Context,
mime_message: &MimeMessage,
contact_id: ContactId,
contact_id: u32,
) -> Result<HandshakeMessage> {
if contact_id.is_special() {
if contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
return Err(Error::msg("Can not be called with special contact ID"));
}
let step = mime_message
@@ -624,11 +847,11 @@ pub(crate) async fn observe_securejoin_on_other_device(
async fn secure_connection_established(
context: &Context,
contact_id: ContactId,
contact_id: u32,
chat_id: ChatId,
) -> Result<(), Error> {
let contact = Contact::get_by_id(context, contact_id).await?;
let msg = stock_str::contact_verified(context, &contact).await;
let msg = stock_str::contact_verified(context, contact.get_name_n_addr()).await;
chat::add_info_msg(context, chat_id, &msg, time()).await?;
context.emit_event(EventType::ChatModified(chat_id));
Ok(())
@@ -636,12 +859,20 @@ async fn secure_connection_established(
async fn could_not_establish_secure_connection(
context: &Context,
contact_id: ContactId,
contact_id: u32,
chat_id: ChatId,
details: &str,
) -> Result<(), Error> {
let contact = Contact::get_by_id(context, contact_id).await?;
let msg = stock_str::contact_not_verified(context, &contact).await;
let contact = Contact::get_by_id(context, contact_id).await;
let msg = stock_str::contact_not_verified(
context,
if let Ok(ref contact) = contact {
contact.get_addr()
} else {
"?"
},
)
.await;
chat::add_info_msg(context, chat_id, &msg, time()).await?;
warn!(
context,
@@ -711,34 +942,30 @@ mod tests {
use crate::chatlist::Chatlist;
use crate::constants::Chattype;
use crate::peerstate::Peerstate;
use crate::test_utils::{TestContext, TestContextManager};
use crate::test_utils::{LogSink, TestContext};
#[async_std::test]
async fn test_setup_contact() {
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;
let bob = tcm.bob().await;
assert_eq!(
Chatlist::try_load(&alice, 0, None, None)
.await
.unwrap()
.len(),
0
);
assert_eq!(
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
0
);
async fn test_setup_contact() -> Result<()> {
let (log_tx, _log_sink) = LogSink::create();
let alice = TestContext::builder()
.configure_alice()
.with_log_sink(log_tx.clone())
.build()
.await;
let bob = TestContext::builder()
.configure_bob()
.with_log_sink(log_tx)
.build()
.await;
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
// Step 1: Generate QR-code, ChatId(0) indicates setup-contact
let qr = dc_get_securejoin_qr(&alice.ctx, None).await.unwrap();
let qr = dc_get_securejoin_qr(&alice.ctx, None).await?;
// Step 2: Bob scans QR-code, sends vc-request
dc_join_securejoin(&bob.ctx, &qr).await.unwrap();
assert_eq!(
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
1
);
dc_join_securejoin(&bob.ctx, &qr).await?;
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
let sent = bob.pop_sent_msg().await;
assert!(!bob.ctx.has_ongoing().await);
@@ -750,13 +977,7 @@ mod tests {
// Step 3: Alice receives vc-request, sends vc-auth-required
alice.recv_msg(&sent).await;
assert_eq!(
Chatlist::try_load(&alice, 0, None, None)
.await
.unwrap()
.len(),
1
);
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
@@ -818,30 +1039,21 @@ mod tests {
.await
.unwrap();
assert_eq!(
contact_bob.is_verified(&alice.ctx).await.unwrap(),
contact_bob.is_verified(&alice.ctx).await?,
VerifiedStatus::Unverified
);
// Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm
alice.recv_msg(&sent).await;
assert_eq!(
contact_bob.is_verified(&alice.ctx).await.unwrap(),
contact_bob.is_verified(&alice.ctx).await?,
VerifiedStatus::BidirectVerified
);
// exactly one one-to-one chat should be visible for both now
// (check this before calling alice.create_chat() explicitly below)
assert_eq!(
Chatlist::try_load(&alice, 0, None, None)
.await
.unwrap()
.len(),
1
);
assert_eq!(
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
1
);
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
// Check Alice got the verified message in her 1:1 chat.
{
@@ -881,14 +1093,14 @@ mod tests {
.await
.unwrap();
assert_eq!(
contact_bob.is_verified(&bob.ctx).await.unwrap(),
contact_bob.is_verified(&bob.ctx).await?,
VerifiedStatus::Unverified
);
// Step 7: Bob receives vc-contact-confirm, sends vc-contact-confirm-received
bob.recv_msg(&sent).await;
assert_eq!(
contact_alice.is_verified(&bob.ctx).await.unwrap(),
contact_alice.is_verified(&bob.ctx).await?,
VerifiedStatus::BidirectVerified
);
@@ -919,6 +1131,7 @@ mod tests {
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vc-contact-confirm-received"
);
Ok(())
}
#[async_std::test]
@@ -930,9 +1143,17 @@ mod tests {
#[async_std::test]
async fn test_setup_contact_bob_knows_alice() -> Result<()> {
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let (log_tx, _log_sink) = LogSink::create();
let alice = TestContext::builder()
.configure_alice()
.with_log_sink(log_tx.clone())
.build()
.await;
let bob = TestContext::builder()
.configure_bob()
.with_log_sink(log_tx)
.build()
.await;
// Ensure Bob knows Alice_FP
let alice_pubkey = SignedPublicKey::load_self(&alice.ctx).await?;
@@ -1055,9 +1276,17 @@ mod tests {
#[async_std::test]
async fn test_setup_contact_concurrent_calls() -> Result<()> {
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let (log_tx, _log_sink) = LogSink::create();
let alice = TestContext::builder()
.configure_alice()
.with_log_sink(log_tx.clone())
.build()
.await;
let bob = TestContext::builder()
.configure_bob()
.with_log_sink(log_tx)
.build()
.await;
// do a scan that is not working as claire is never responding
let qr_stale = "OPENPGP4FPR:1234567890123456789012345678901234567890#a=claire%40foo.de&n=&i=12345678901&s=23456789012";
@@ -1086,19 +1315,25 @@ mod tests {
#[async_std::test]
async fn test_secure_join() -> Result<()> {
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;
let bob = tcm.bob().await;
// We start with empty chatlists.
let (log_tx, _log_sink) = LogSink::create();
let alice = TestContext::builder()
.configure_alice()
.with_log_sink(log_tx.clone())
.build()
.await;
let bob = TestContext::builder()
.configure_bob()
.with_log_sink(log_tx)
.build()
.await;
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
let alice_chatid =
let chatid =
chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat").await?;
// Step 1: Generate QR-code, secure-join implied by chatid
let qr = dc_get_securejoin_qr(&alice.ctx, Some(alice_chatid))
let qr = dc_get_securejoin_qr(&alice.ctx, Some(chatid))
.await
.unwrap();
@@ -1189,35 +1424,6 @@ mod tests {
"vg-member-added"
);
{
// Now Alice's chat with Bob should still be hidden, the verified message should
// appear in the group chat.
let chat = alice
.get_chat(&bob)
.await
.expect("Alice has no 1:1 chat with bob");
assert_eq!(
chat.blocked,
Blocked::Yes,
"Alice's 1:1 chat with Bob is not hidden"
);
let msg_id = chat::get_chat_msgs(&alice.ctx, alice_chatid, 0x1, None)
.await
.unwrap()
.into_iter()
.filter_map(|item| match item {
chat::ChatItem::Message { msg_id } => Some(msg_id),
_ => None,
})
.min()
.expect("No messages in Alice's group chat");
let msg = Message::load_from_db(&alice.ctx, msg_id).await.unwrap();
assert!(msg.is_info());
let text = msg.get_text().unwrap();
assert!(text.contains("bob@example.net verified"));
}
// Bob should not yet have Alice verified
let contact_alice_id =
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
@@ -1232,53 +1438,10 @@ mod tests {
// Step 7: Bob receives vg-member-added, sends vg-member-added-received
bob.recv_msg(&sent).await;
{
// Bob has Alice verified, message shows up in the group chat.
assert_eq!(
contact_alice.is_verified(&bob.ctx).await?,
VerifiedStatus::BidirectVerified
);
let chat = bob
.get_chat(&alice)
.await
.expect("Bob has no 1:1 chat with Alice");
assert_eq!(
chat.blocked,
Blocked::Yes,
"Bob's 1:1 chat with Alice is not hidden"
);
for item in chat::get_chat_msgs(&bob.ctx, bob_chatid, 0x1, None)
.await
.unwrap()
{
if let chat::ChatItem::Message { msg_id } = item {
let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap();
let text = msg.get_text().unwrap();
println!("msg {} text: {}", msg_id, text);
}
}
let mut msg_iter = chat::get_chat_msgs(&bob.ctx, bob_chatid, 0x1, None)
.await
.unwrap()
.into_iter();
loop {
match msg_iter.next() {
Some(chat::ChatItem::Message { msg_id }) => {
let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap();
let text = msg.get_text().unwrap();
match text.contains("alice@example.org verified") {
true => {
assert!(msg.is_info());
break;
}
false => continue,
}
}
Some(_) => continue,
None => panic!("Verified message not found in Bob's group chat"),
}
}
}
assert_eq!(
contact_alice.is_verified(&bob.ctx).await?,
VerifiedStatus::BidirectVerified
);
let sent = bob.pop_sent_msg().await;
let msg = alice.parse_msg(&sent).await;

View File

@@ -1,257 +0,0 @@
//! Bob's side of SecureJoin handling.
//!
//! This are some helper functions around [`BobState`] which augment the state changes with
//! the required user interactions.
use anyhow::Result;
use crate::chat::{is_contact_in_chat, ChatId, ProtectionStatus};
use crate::constants::{Blocked, Chattype};
use crate::contact::Contact;
use crate::context::Context;
use crate::dc_tools::time;
use crate::events::EventType;
use crate::mimeparser::MimeMessage;
use crate::{chat, stock_str};
use super::bobstate::{BobHandshakeStage, BobState};
use super::qrinvite::QrInvite;
use super::{HandshakeMessage, JoinError};
/// Starts the securejoin protocol with the QR `invite`.
///
/// This will try to start the securejoin protocol for the given QR `invite`. If it
/// succeeded the protocol state will be tracked in `self`.
///
/// This function takes care of handling multiple concurrent joins and handling errors while
/// starting the protocol.
///
/// # Returns
///
/// The [`ChatId`] of the created chat is returned, for a SetupContact QR this is the 1:1
/// chat with Alice, for a SecureJoin QR this is the group chat.
pub(super) async fn start_protocol(
context: &Context,
invite: QrInvite,
) -> Result<ChatId, JoinError> {
// A 1:1 chat is needed to send messages to Alice. When joining a group this chat is
// hidden, if a user starts sending messages in it it will be unhidden in
// dc_receive_imf.
let hidden = match invite {
QrInvite::Contact { .. } => Blocked::Not,
QrInvite::Group { .. } => Blocked::Yes,
};
let chat_id = ChatId::create_for_contact_with_blocked(context, invite.contact_id(), hidden)
.await
.map_err(JoinError::UnknownContact)?;
// Now start the protocol and initialise the state
let (state, stage, aborted_states) =
BobState::start_protocol(context, invite.clone(), chat_id).await?;
for state in aborted_states {
error!(context, "Aborting previously unfinished QR Join process.");
state.notify_aborted(context, "new QR scanned").await?;
state.emit_progress(context, JoinerProgress::Error);
}
if matches!(stage, BobHandshakeStage::RequestWithAuthSent) {
state.emit_progress(context, JoinerProgress::RequestWithAuthSent);
}
match invite {
QrInvite::Group { .. } => {
// For a secure-join we need to create the group and add the contact. The group will
// only become usable once the protocol is finished.
// TODO: how does this group become usable?
let group_chat_id = state.joining_chat_id(context).await?;
if !is_contact_in_chat(context, group_chat_id, invite.contact_id()).await? {
chat::add_to_chat_contacts_table(context, group_chat_id, invite.contact_id())
.await?;
}
let msg = stock_str::secure_join_started(context, invite.contact_id()).await;
chat::add_info_msg(context, group_chat_id, &msg, time()).await?;
Ok(group_chat_id)
}
QrInvite::Contact { .. } => {
// For setup-contact the BobState already ensured the 1:1 chat exists because it
// uses it to send the handshake messages.
Ok(state.alice_chat())
}
}
}
/// Handles `vc-auth-required` and `vg-auth-required` handshake messages.
///
/// # Bob - the joiner's side
/// ## Step 4 in the "Setup Contact protocol"
pub(super) async fn handle_auth_required(
context: &Context,
message: &MimeMessage,
) -> Result<HandshakeMessage> {
match BobState::from_db(&context.sql).await? {
Some(mut bobstate) => match bobstate.handle_message(context, message).await? {
Some(BobHandshakeStage::Terminated(why)) => {
bobstate.notify_aborted(context, why).await?;
Ok(HandshakeMessage::Done)
}
Some(_stage) => {
if bobstate.is_join_group() {
// The message reads "Alice replied, waiting to be added to the group…",
// so only show it on secure-join and not on setup-contact.
let contact_id = bobstate.invite().contact_id();
let msg = stock_str::secure_join_replies(context, contact_id).await;
let chat_id = bobstate.joining_chat_id(context).await?;
chat::add_info_msg(context, chat_id, &msg, time()).await?;
}
bobstate.emit_progress(context, JoinerProgress::RequestWithAuthSent);
Ok(HandshakeMessage::Done)
}
None => Ok(HandshakeMessage::Ignore),
},
None => Ok(HandshakeMessage::Ignore),
}
}
/// Handles `vc-contact-confirm` and `vg-member-added` handshake messages.
///
/// # Bob - the joiner's side
/// ## Step 4 in the "Setup Contact protocol"
pub(super) async fn handle_contact_confirm(
context: &Context,
mut bobstate: BobState,
message: &MimeMessage,
) -> Result<HandshakeMessage> {
let retval = if bobstate.is_join_group() {
HandshakeMessage::Propagate
} else {
HandshakeMessage::Ignore
};
match bobstate.handle_message(context, message).await? {
Some(BobHandshakeStage::Terminated(why)) => {
bobstate.notify_aborted(context, why).await?;
Ok(HandshakeMessage::Done)
}
Some(BobHandshakeStage::Completed) => {
// Note this goes to the 1:1 chat, as when joining a group we implicitly also
// verify both contacts (this could be a bug/security issue, see
// e.g. https://github.com/deltachat/deltachat-core-rust/issues/1177).
bobstate.notify_peer_verified(context).await?;
Ok(retval)
}
Some(_) => {
warn!(
context,
"Impossible state returned from handling handshake message"
);
Ok(retval)
}
None => Ok(retval),
}
}
/// Private implementations for user interactions about this [`BobState`].
impl BobState {
fn is_join_group(&self) -> bool {
match self.invite() {
QrInvite::Contact { .. } => false,
QrInvite::Group { .. } => true,
}
}
fn emit_progress(&self, context: &Context, progress: JoinerProgress) {
let contact_id = self.invite().contact_id();
context.emit_event(EventType::SecurejoinJoinerProgress {
contact_id,
progress: progress.into(),
});
}
/// Returns the [`ChatId`] of the chat being joined.
///
/// This is the chat in which you want to notify the user as well.
///
/// When joining a group this is the [`ChatId`] of the group chat, when verifying a
/// contact this is the [`ChatId`] of the 1:1 chat. The 1:1 chat is assumed to exist
/// because a [`BobState`] can not exist without, the group chat will be created if it
/// does not yet exist.
async fn joining_chat_id(&self, context: &Context) -> Result<ChatId> {
match self.invite() {
QrInvite::Contact { .. } => Ok(self.alice_chat()),
QrInvite::Group {
ref grpid,
ref name,
..
} => {
let group_chat_id = match chat::get_chat_id_by_grpid(context, grpid).await? {
Some((chat_id, _protected, _blocked)) => {
chat_id.unblock(context).await?;
chat_id
}
None => {
ChatId::create_multiuser_record(
context,
Chattype::Group,
grpid,
name,
Blocked::Not,
ProtectionStatus::Unprotected, // protection is added later as needed
None,
)
.await?
}
};
Ok(group_chat_id)
}
}
}
/// Notifies the user that the SecureJoin was aborted.
///
/// This creates an info message in the chat being joined.
async fn notify_aborted(&self, context: &Context, why: &str) -> Result<()> {
let contact = Contact::get_by_id(context, self.invite().contact_id()).await?;
let msg = stock_str::contact_not_verified(context, &contact).await;
let chat_id = self.joining_chat_id(context).await?;
chat::add_info_msg(context, chat_id, &msg, time()).await?;
warn!(
context,
"StockMessage::ContactNotVerified posted to joining chat ({})", why
);
Ok(())
}
/// Notifies the user that the SecureJoin peer is verified.
///
/// This creates an info message in the chat being joined.
async fn notify_peer_verified(&self, context: &Context) -> Result<()> {
let contact = Contact::get_by_id(context, self.invite().contact_id()).await?;
let msg = stock_str::contact_verified(context, &contact).await;
let chat_id = self.joining_chat_id(context).await?;
chat::add_info_msg(context, chat_id, &msg, time()).await?;
context.emit_event(EventType::ChatModified(chat_id));
Ok(())
}
}
/// Progress updates for [`EventType::SecurejoinJoinerProgress`].
///
/// This has an `From<JoinerProgress> for usize` impl yielding numbers between 0 and a 1000
/// which can be shown as a progress bar.
enum JoinerProgress {
/// An error occurred.
Error,
/// vg-vc-request-with-auth sent.
///
/// Typically shows as "alice@addr verified, introducing myself."
RequestWithAuthSent,
// /// Completed securejoin.
// Succeeded,
}
impl From<JoinerProgress> for usize {
fn from(progress: JoinerProgress) -> Self {
match progress {
JoinerProgress::Error => 0,
JoinerProgress::RequestWithAuthSent => 400,
// JoinerProgress::Succeeded => 1000,
}
}
}

View File

@@ -5,21 +5,22 @@
//! provides all the information to its driver so it can perform the correct interactions.
//!
//! The [`BobState`] is only directly used to initially create it when starting the
//! protocol.
//! protocol. Afterwards it must be stored in a mutex and the [`BobStateHandle`] should be
//! used to work with the state.
use anyhow::{Error, Result};
use rusqlite::Connection;
use anyhow::{bail, Error, Result};
use async_std::sync::MutexGuard;
use crate::chat::{self, ChatId};
use crate::constants::{Blocked, Viewtype};
use crate::contact::{Contact, Origin};
use crate::context::Context;
use crate::events::EventType;
use crate::headerdef::HeaderDef;
use crate::key::{DcKey, SignedPublicKey};
use crate::message::{Message, Viewtype};
use crate::message::Message;
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::param::Param;
use crate::sql::Sql;
use super::qrinvite::QrInvite;
use super::{
@@ -45,14 +46,123 @@ pub enum BobHandshakeStage {
Terminated(&'static str),
}
/// The securejoin state kept while Bob is joining.
/// A handle to work with the [`BobState`] of Bob's securejoin protocol.
///
/// This is stored in the database and loaded from there using [`BobState::from_db`]. To
/// create a new one use [`BobState::start_protocol`].
/// This handle can only be created for when an underlying [`BobState`] exists. It keeps
/// open a lock which guarantees unique access to the state and this struct must be dropped
/// to return the lock.
pub struct BobStateHandle<'a> {
guard: MutexGuard<'a, Option<BobState>>,
bobstate: BobState,
clear_state_on_drop: bool,
}
impl<'a> BobStateHandle<'a> {
/// Creates a new instance, upholding the guarantee that [`BobState`] must exist.
pub fn from_guard(mut guard: MutexGuard<'a, Option<BobState>>) -> Option<Self> {
guard.take().map(|bobstate| Self {
guard,
bobstate,
clear_state_on_drop: false,
})
}
/// Returns the [`ChatId`] of the group chat to join or the 1:1 chat with Alice.
pub async fn chat_id(&self, context: &Context) -> Result<ChatId> {
match self.bobstate.invite {
QrInvite::Group { ref grpid, .. } => {
if let Some((chat_id, _, _)) = chat::get_chat_id_by_grpid(context, grpid).await? {
Ok(chat_id)
} else {
bail!("chat not found")
}
}
QrInvite::Contact { .. } => Ok(self.bobstate.chat_id),
}
}
/// Returns a reference to the [`QrInvite`] of the joiner process.
pub fn invite(&self) -> &QrInvite {
&self.bobstate.invite
}
/// Handles the given message for the securejoin handshake for Bob.
///
/// This proxies to [`BobState::handle_message`] and makes sure to clear the state when
/// the protocol state is terminal. It returns `Some` if the message successfully
/// advanced the state of the protocol state machine, `None` otherwise.
pub async fn handle_message(
&mut self,
context: &Context,
mime_message: &MimeMessage,
) -> Option<BobHandshakeStage> {
info!(context, "Handling securejoin message for BobStateHandle");
match self.bobstate.handle_message(context, mime_message).await {
Ok(Some(stage)) => {
if matches!(
stage,
BobHandshakeStage::Completed | BobHandshakeStage::Terminated(_)
) {
self.finish_protocol(context).await;
}
Some(stage)
}
Ok(None) => None,
Err(err) => {
warn!(
context,
"Error handling handshake message, aborting handshake: {}", err
);
self.finish_protocol(context).await;
None
}
}
}
/// Marks the bob handshake as finished.
///
/// This will clear the state on [`InnerContext::bob`] once this handle is dropped,
/// allowing a new handshake to be started from [`Bob`].
///
/// Note that the state is only cleared on Drop since otherwise the invariant that the
/// state is always consistent is violated. However the "ongoing" process is released
/// here a little bit earlier as this requires access to the Context, which we do not
/// have on Drop (Drop can not run asynchronous code). Stopping the "ongoing" process
/// will release [`securejoin`](super::securejoin) which in turn will finally free the
/// ongoing process using [`Context::free_ongoing`].
///
/// [`InnerContext::bob`]: crate::context::InnerContext::bob
/// [`Bob`]: super::Bob
async fn finish_protocol(&mut self, context: &Context) {
info!(context, "Finishing securejoin handshake protocol for Bob");
self.clear_state_on_drop = true;
if let QrInvite::Group { .. } = self.bobstate.invite {
context.stop_ongoing().await;
}
}
}
impl<'a> Drop for BobStateHandle<'a> {
fn drop(&mut self) {
if self.clear_state_on_drop {
// The Option should already be empty because we take it out in the ctor,
// however the typesystem doesn't guarantee this so do it again anyway.
self.guard.take();
} else {
// Make sure to put back the BobState into the Option of the Mutex, it was taken
// out by the constructor.
self.guard.replace(self.bobstate.clone());
}
}
}
/// The securejoin state kept in-memory while Bob is joining.
///
/// This purposefully has nothing optional, the state is always fully valid. However once a
/// terminal state is reached in [`BobState::next`] the entry in the database will already
/// have been deleted.
/// This is currently stored in [`Bob`] which is stored on the [`Context`], thus Bob can
/// only run one securejoin joiner protocol at a time.
///
/// This purposefully has nothing optional, the state is always fully valid. See
/// [`Bob::state`] to get access to this state.
///
/// # Conducting the securejoin handshake
///
@@ -67,8 +177,6 @@ pub enum BobHandshakeStage {
/// [`Bob::state`]: super::Bob::state
#[derive(Debug, Clone)]
pub struct BobState {
/// Database primary key.
id: i64,
/// The QR Invite code.
invite: QrInvite,
/// The next expected message from Alice.
@@ -80,120 +188,39 @@ pub struct BobState {
impl BobState {
/// Starts the securejoin protocol and creates a new [`BobState`].
///
/// The `chat_id` needs to be the ID of the 1:1 chat with Alice, this chat will be used
/// to exchange the SecureJoin handshake messages as well as for showing error messages.
///
/// # Bob - the joiner's side
/// ## Step 2 in the "Setup Contact protocol", section 2.1 of countermitm 0.10.0
///
/// This currently aborts any other securejoin process if any did not yet complete. The
/// ChatIds of the relevant 1:1 chat of any aborted handshakes are returned so that you
/// can report the aboreted handshake in the chat. (Yes, there can only ever be one
/// ChatId in that Vec, the database doesn't care though.)
pub async fn start_protocol(
context: &Context,
invite: QrInvite,
chat_id: ChatId,
) -> Result<(Self, BobHandshakeStage, Vec<Self>), JoinError> {
let (stage, next) =
if fingerprint_equals_sender(context, invite.fingerprint(), invite.contact_id()).await?
{
// The scanned fingerprint matches Alice's key, we can proceed to step 4b.
info!(context, "Taking securejoin protocol shortcut");
send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth)
.await?;
(
BobHandshakeStage::RequestWithAuthSent,
SecureJoinStep::ContactConfirm,
)
} else {
send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::Request).await?;
(BobHandshakeStage::RequestSent, SecureJoinStep::AuthRequired)
) -> Result<(Self, BobHandshakeStage), JoinError> {
let chat_id =
ChatId::create_for_contact_with_blocked(context, invite.contact_id(), Blocked::Yes)
.await
.map_err(JoinError::UnknownContact)?;
if fingerprint_equals_sender(context, invite.fingerprint(), invite.contact_id()).await? {
// The scanned fingerprint matches Alice's key, we can proceed to step 4b.
info!(context, "Taking securejoin protocol shortcut");
let state = Self {
invite,
next: SecureJoinStep::ContactConfirm,
chat_id,
};
let (id, aborted_states) =
Self::insert_new_db_entry(context, next, invite.clone(), chat_id).await?;
let state = Self {
id,
invite,
next,
chat_id,
};
Ok((state, stage, aborted_states))
}
/// Inserts a new entry in the bobstate table, deleting all previous entries.
///
/// Returns the ID of the newly inserted entry and all the aborted states.
async fn insert_new_db_entry(
context: &Context,
next: SecureJoinStep,
invite: QrInvite,
chat_id: ChatId,
) -> Result<(i64, Vec<Self>)> {
context
.sql
.transaction(move |transaction| {
// We need to start a write transaction right away, so that we have the
// database locked and no one else can write to this table while we read the
// rows that we will delete. So start with a dummy UPDATE.
transaction.execute(
r#"UPDATE bobstate SET next_step=?;"#,
params![SecureJoinStep::Terminated],
)?;
let mut stmt = transaction.prepare("SELECT id FROM bobstate;")?;
let mut aborted = Vec::new();
for id in stmt.query_map(params![], |row| row.get::<_, i64>(0))? {
let id = id?;
let state = BobState::from_db_id(transaction, id)?;
aborted.push(state);
}
// Finally delete everything and insert new row.
transaction.execute("DELETE FROM bobstate;", params![])?;
transaction.execute(
"INSERT INTO bobstate (invite, next_step, chat_id) VALUES (?, ?, ?);",
params![invite, next, chat_id],
)?;
let id = transaction.last_insert_rowid();
Ok((id, aborted))
})
.await
}
/// Load [`BobState`] from the database.
pub async fn from_db(sql: &Sql) -> Result<Option<Self>> {
// Because of how Self::start_protocol() updates the database we are currently
// guaranteed to only have one row.
sql.query_row_optional(
"SELECT id, invite, next_step, chat_id FROM bobstate;",
paramsv![],
|row| {
let s = BobState {
id: row.get(0)?,
invite: row.get(1)?,
next: row.get(2)?,
chat_id: row.get(3)?,
};
Ok(s)
},
)
.await
}
fn from_db_id(connection: &Connection, id: i64) -> rusqlite::Result<Self> {
connection.query_row(
"SELECT invite, next_step, chat_id FROM bobstate WHERE id=?;",
params![id],
|row| {
let s = BobState {
id,
invite: row.get(0)?,
next: row.get(1)?,
chat_id: row.get(2)?,
};
Ok(s)
},
)
state
.send_handshake_message(context, BobHandshakeMsg::RequestWithAuth)
.await?;
Ok((state, BobHandshakeStage::RequestWithAuthSent))
} else {
let state = Self {
invite,
next: SecureJoinStep::AuthRequired,
chat_id,
};
state
.send_handshake_message(context, BobHandshakeMsg::Request)
.await?;
Ok((state, BobHandshakeStage::RequestSent))
}
}
/// Returns the [`QrInvite`] used to create this [`BobState`].
@@ -201,45 +228,20 @@ impl BobState {
&self.invite
}
/// Returns the [`ChatId`] of the 1:1 chat with the inviter (Alice).
pub fn alice_chat(&self) -> ChatId {
self.chat_id
}
/// Updates the [`BobState::next`] field in memory and the database.
///
/// If the next state is a terminal state it will remove this [`BobState`] from the
/// database.
///
/// If a user scanned a new QR code after this [`BobState`] was loaded this update will
/// fail currently because starting a new joiner process currently kills any previously
/// running processes. This is a limitation which will go away in the future.
async fn update_next(&mut self, sql: &Sql, next: SecureJoinStep) -> Result<()> {
// TODO: write test verifying how this would fail.
match next {
SecureJoinStep::AuthRequired | SecureJoinStep::ContactConfirm => {
sql.execute(
"UPDATE bobstate SET next_step=? WHERE id=?;",
paramsv![next, self.id],
)
.await?;
}
SecureJoinStep::Terminated | SecureJoinStep::Completed => {
sql.execute("DELETE FROM bobstate WHERE id=?;", paramsv!(self.id))
.await?;
}
}
self.next = next;
Ok(())
}
/// Handles the given message for the securejoin handshake for Bob.
///
/// If the message was not used for this handshake `None` is returned, otherwise the new
/// stage is returned. Once [`BobHandshakeStage::Completed`] or
/// [`BobHandshakeStage::Terminated`] are reached this [`BobState`] should be destroyed,
/// further calling it will just result in the messages being unused by this handshake.
pub async fn handle_message(
///
/// # Errors
///
/// Under normal operation this should never return an error, regardless of what kind of
/// message it is called with. Any errors therefore should be treated as fatal internal
/// errors and this entire [`BobState`] should be thrown away as the state machine can
/// no longer be considered consistent.
async fn handle_message(
&mut self,
context: &Context,
mime_message: &MimeMessage,
@@ -302,20 +304,17 @@ impl BobState {
} else {
"Required encryption missing"
};
self.update_next(&context.sql, SecureJoinStep::Terminated)
.await?;
self.next = SecureJoinStep::Terminated;
return Ok(Some(BobHandshakeStage::Terminated(reason)));
}
if !fingerprint_equals_sender(context, self.invite.fingerprint(), self.invite.contact_id())
.await?
{
self.update_next(&context.sql, SecureJoinStep::Terminated)
.await?;
self.next = SecureJoinStep::Terminated;
return Ok(Some(BobHandshakeStage::Terminated("Fingerprint mismatch")));
}
info!(context, "Fingerprint verified.",);
self.update_next(&context.sql, SecureJoinStep::ContactConfirm)
.await?;
self.next = SecureJoinStep::ContactConfirm;
self.send_handshake_message(context, BobHandshakeMsg::RequestWithAuth)
.await?;
Ok(Some(BobHandshakeStage::RequestWithAuthSent))
@@ -363,8 +362,7 @@ impl BobState {
if vg_expect_encrypted
&& !encrypted_and_signed(context, mime_message, Some(self.invite.fingerprint()))
{
self.update_next(&context.sql, SecureJoinStep::Terminated)
.await?;
self.next = SecureJoinStep::Terminated;
return Ok(Some(BobHandshakeStage::Terminated(
"Contact confirm message not encrypted",
)));
@@ -396,8 +394,7 @@ impl BobState {
// This is not an error affecting the protocol outcome.
.ok();
self.update_next(&context.sql, SecureJoinStep::Completed)
.await?;
self.next = SecureJoinStep::Completed;
Ok(Some(BobHandshakeStage::Completed))
}
@@ -409,60 +406,48 @@ impl BobState {
context: &Context,
step: BobHandshakeMsg,
) -> Result<(), SendMsgError> {
send_handshake_message(context, &self.invite, self.chat_id, step).await
let mut msg = Message {
viewtype: Viewtype::Text,
text: Some(step.body_text(&self.invite)),
hidden: true,
..Default::default()
};
msg.param.set_cmd(SystemMessage::SecurejoinMessage);
// Sends the step in Secure-Join header.
msg.param
.set(Param::Arg, step.securejoin_header(&self.invite));
match step {
BobHandshakeMsg::Request => {
// Sends the Secure-Join-Invitenumber header in mimefactory.rs.
msg.param.set(Param::Arg2, self.invite.invitenumber());
msg.force_plaintext();
}
BobHandshakeMsg::RequestWithAuth => {
// Sends the Secure-Join-Auth header in mimefactory.rs.
msg.param.set(Param::Arg2, self.invite.authcode());
msg.param.set_int(Param::GuaranteeE2ee, 1);
}
BobHandshakeMsg::ContactConfirmReceived => {
msg.param.set_int(Param::GuaranteeE2ee, 1);
}
};
// Sends our own fingerprint in the Secure-Join-Fingerprint header.
let bob_fp = SignedPublicKey::load_self(context).await?.fingerprint();
msg.param.set(Param::Arg3, bob_fp.hex());
// Sends the grpid in the Secure-Join-Group header.
if let QrInvite::Group { ref grpid, .. } = self.invite {
msg.param.set(Param::Arg4, grpid);
}
chat::send_msg(context, self.chat_id, &mut msg).await?;
Ok(())
}
}
/// Sends the requested handshake message to Alice.
///
/// Same as [`BobState::send_handshake_message`] but this variation allows us to send this
/// message before we create the state in [`BobState::start_protocol`].
async fn send_handshake_message(
context: &Context,
invite: &QrInvite,
chat_id: ChatId,
step: BobHandshakeMsg,
) -> Result<(), SendMsgError> {
let mut msg = Message {
viewtype: Viewtype::Text,
text: Some(step.body_text(invite)),
hidden: true,
..Default::default()
};
msg.param.set_cmd(SystemMessage::SecurejoinMessage);
// Sends the step in Secure-Join header.
msg.param.set(Param::Arg, step.securejoin_header(invite));
match step {
BobHandshakeMsg::Request => {
// Sends the Secure-Join-Invitenumber header in mimefactory.rs.
msg.param.set(Param::Arg2, invite.invitenumber());
msg.force_plaintext();
}
BobHandshakeMsg::RequestWithAuth => {
// Sends the Secure-Join-Auth header in mimefactory.rs.
msg.param.set(Param::Arg2, invite.authcode());
msg.param.set_int(Param::GuaranteeE2ee, 1);
}
BobHandshakeMsg::ContactConfirmReceived => {
msg.param.set_int(Param::GuaranteeE2ee, 1);
}
};
// Sends our own fingerprint in the Secure-Join-Fingerprint header.
let bob_fp = SignedPublicKey::load_self(context).await?.fingerprint();
msg.param.set(Param::Arg3, bob_fp.hex());
// Sends the grpid in the Secure-Join-Group header.
if let QrInvite::Group { ref grpid, .. } = invite {
msg.param.set(Param::Arg4, grpid);
}
chat::send_msg(context, chat_id, &mut msg).await?;
Ok(())
}
/// Identifies the SecureJoin handshake messages Bob can send.
enum BobHandshakeMsg {
/// vc-request or vg-request
@@ -507,8 +492,8 @@ impl BobHandshakeMsg {
}
/// The next message expected by [`BobState`] in the setup-contact/secure-join protocol.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SecureJoinStep {
#[derive(Debug, Clone, PartialEq)]
enum SecureJoinStep {
/// Expecting the auth-required message.
///
/// This corresponds to the `vc-auth-required` or `vg-auth-required` message of step 3d.
@@ -548,29 +533,3 @@ impl SecureJoinStep {
}
}
}
impl rusqlite::types::ToSql for SecureJoinStep {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
let num = match &self {
SecureJoinStep::AuthRequired => 0,
SecureJoinStep::ContactConfirm => 1,
SecureJoinStep::Terminated => 2,
SecureJoinStep::Completed => 3,
};
let val = rusqlite::types::Value::Integer(num);
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
}
impl rusqlite::types::FromSql for SecureJoinStep {
fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
i64::column_result(value).and_then(|val| match val {
0 => Ok(SecureJoinStep::AuthRequired),
1 => Ok(SecureJoinStep::ContactConfirm),
2 => Ok(SecureJoinStep::Terminated),
3 => Ok(SecureJoinStep::Completed),
_ => Err(rusqlite::types::FromSqlError::OutOfRange(val)),
})
}
}

View File

@@ -8,23 +8,22 @@ use std::convert::TryFrom;
use anyhow::{bail, Error, Result};
use crate::contact::ContactId;
use crate::key::Fingerprint;
use crate::qr::Qr;
/// Represents the data from a QR-code scan.
///
/// There are methods to conveniently access fields present in both variants.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone)]
pub enum QrInvite {
Contact {
contact_id: ContactId,
contact_id: u32,
fingerprint: Fingerprint,
invitenumber: String,
authcode: String,
},
Group {
contact_id: ContactId,
contact_id: u32,
fingerprint: Fingerprint,
name: String,
grpid: String,
@@ -38,7 +37,7 @@ impl QrInvite {
///
/// The actual QR-code contains a URL-encoded email address, but upon scanning this is
/// translated to a contact ID.
pub fn contact_id(&self) -> ContactId {
pub fn contact_id(&self) -> u32 {
match self {
Self::Contact { contact_id, .. } | Self::Group { contact_id, .. } => *contact_id,
}
@@ -101,22 +100,3 @@ impl TryFrom<Qr> for QrInvite {
}
}
}
impl rusqlite::types::ToSql for QrInvite {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
let json = serde_json::to_string(self)
.map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
let val = rusqlite::types::Value::Text(json);
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
}
impl rusqlite::types::FromSql for QrInvite {
fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
String::column_result(value).and_then(|val| {
serde_json::from_str(&val)
.map_err(|err| rusqlite::types::FromSqlError::Other(Box::new(err)))
})
}
}

View File

@@ -240,7 +240,7 @@ fn render_message(lines: &[&str], is_cut_at_end: bool) -> String {
ret += " [...]";
}
// redo escaping done by escape_message_footer_marks()
ret.replace('\u{200B}', "")
ret.replace("\u{200B}", "")
}
/// Returns true if the line contains only whitespace.

View File

@@ -4,14 +4,14 @@ pub mod send;
use std::time::{Duration, SystemTime};
use anyhow::{bail, format_err, Context as _, Error, Result};
use anyhow::{bail, format_err, Context as _, Result};
use async_smtp::smtp::client::net::ClientTlsParameters;
use async_smtp::smtp::response::{Category, Code, Detail};
use async_smtp::{smtp, EmailAddress, ServerAddress};
use async_std::task;
use crate::constants::DC_LP_AUTH_OAUTH2;
use crate::events::EventType;
use crate::job::Status;
use crate::login_param::{
dc_build_tls, CertificateChecks, LoginParam, ServerLoginParam, Socks5Config,
};
@@ -50,10 +50,7 @@ impl Smtp {
/// Disconnect the SMTP transport and drop it entirely.
pub async 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
// separate task to avoid waiting for reply or timeout.
task::spawn(async move { transport.close().await });
transport.close().await.ok();
}
self.last_success = None;
}
@@ -87,7 +84,7 @@ impl Smtp {
}
self.connectivity.set_connecting(context).await;
let lp = LoginParam::load_configured_params(context).await?;
let lp = LoginParam::from_database(context, "configured_").await?;
self.connect(
context,
&lp.smtp,
@@ -199,18 +196,12 @@ impl Smtp {
}
}
pub(crate) enum SendResult {
/// Message was sent successfully.
Success,
/// Permanent error, message sending has failed.
Failure(Error),
/// Temporary error, the message should be retried later.
Retry,
}
/// Tries to send a message.
///
/// Returns Status::Finished if sending the message should not be retried anymore,
/// Status::RetryLater if sending should be postponed and Status::RetryNow if it is suspected that
/// temporary failure is caused by stale connection, in which case a second attempt to send the
/// same message may be done immediately.
pub(crate) async fn smtp_send(
context: &Context,
recipients: &[async_smtp::EmailAddress],
@@ -218,7 +209,7 @@ pub(crate) async fn smtp_send(
smtp: &mut Smtp,
msg_id: MsgId,
rowid: i64,
) -> SendResult {
) -> Status {
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(context, "smtp-sending out mime message:");
println!("{}", message);
@@ -226,20 +217,6 @@ pub(crate) async fn smtp_send(
smtp.connectivity.set_working(context).await;
if smtp.has_maybe_stale_connection().await {
info!(context, "Closing stale connection");
smtp.disconnect().await;
if let Err(err) = smtp
.connect_configured(context)
.await
.context("failed to reopen stale SMTP connection")
{
smtp.last_send_error = Some(format!("{:#}", err));
return SendResult::Retry;
}
}
let send_result = smtp
.send(context, recipients, message.as_bytes(), rowid)
.await;
@@ -248,7 +225,7 @@ pub(crate) async fn smtp_send(
let status = match send_result {
Err(crate::smtp::send::Error::SmtpSend(err)) => {
// Remote error, retry later.
info!(context, "SMTP failed to send: {:?}", &err);
warn!(context, "SMTP failed to send: {:?}", &err);
let res = match err {
async_smtp::smtp::error::Error::Permanent(ref response) => {
@@ -269,21 +246,19 @@ pub(crate) async fn smtp_send(
// Other enhanced status codes, such as Postfix
// "550 5.1.1 <foobar@example.org>: Recipient address rejected: User unknown in local recipient table"
// are not ignored.
response.first_word() == Some("5.5.0")
response.first_word() == Some(&"5.5.0".to_string())
}
_ => false,
};
if maybe_transient {
info!(context, "Permanent error that is likely to actually be transient, postponing retry for later");
SendResult::Retry
Status::RetryLater
} else {
info!(context, "Permanent error, message sending failed");
// If we do not retry, add an info message to the chat.
// Yandex error "554 5.7.1 [2] Message rejected under suspicion of SPAM; https://ya.cc/..."
// should definitely go here, because user has to open the link to
// resume message sending.
SendResult::Failure(format_err!("Permanent SMTP error: {}", err))
Status::Finished(Err(format_err!("Permanent SMTP error: {}", err)))
}
}
async_smtp::smtp::error::Error::Transient(ref response) => {
@@ -300,34 +275,25 @@ pub(crate) async fn smtp_send(
// receive as a transient error are misconfigurations of the smtp server.
// See <https://tools.ietf.org/html/rfc3463#section-3.2>
info!(context, "Received extended status code {} for a transient error. This looks like a misconfigured smtp server, let's fail immediatly", first_word);
SendResult::Failure(format_err!("Permanent SMTP error: {}", err))
Status::Finished(Err(format_err!("Permanent SMTP error: {}", err)))
} else {
info!(
context,
"Transient error with status code {}, postponing retry for later",
first_word
);
SendResult::Retry
Status::RetryLater
}
} else {
info!(
context,
"Transient error without status code, postponing retry for later"
);
SendResult::Retry
Status::RetryLater
}
}
_ => {
info!(
context,
"Message sending failed without error returned by the server, retry later"
);
SendResult::Retry
if smtp.has_maybe_stale_connection().await {
info!(context, "stale connection? immediately reconnecting");
Status::RetryNow
} else {
Status::RetryLater
}
}
};
// this clears last_success info
info!(context, "Failed to send message over SMTP, disconnecting");
smtp.disconnect().await;
res
@@ -336,24 +302,24 @@ pub(crate) async fn smtp_send(
// Local error, job is invalid, do not retry.
smtp.disconnect().await;
warn!(context, "SMTP job is invalid: {}", err);
SendResult::Failure(err.into())
Status::Finished(Err(err.into()))
}
Err(crate::smtp::send::Error::NoTransport) => {
// Should never happen.
// It does not even make sense to disconnect here.
error!(context, "SMTP job failed because SMTP has no transport");
SendResult::Failure(format_err!("SMTP has not transport"))
Status::Finished(Err(format_err!("SMTP has not transport")))
}
Err(crate::smtp::send::Error::Other(err)) => {
// Local error, job is invalid, do not retry.
smtp.disconnect().await;
warn!(context, "unable to load job: {}", err);
SendResult::Failure(err)
Status::Finished(Err(err))
}
Ok(()) => SendResult::Success,
Ok(()) => Status::Finished(Ok(())),
};
if let SendResult::Failure(err) = &status {
if let Status::Finished(Err(err)) = &status {
// We couldn't send the message, so mark it as failed
message::set_msg_failed(context, msg_id, Some(err.to_string())).await;
}
@@ -377,52 +343,19 @@ pub(crate) async fn send_msg_to_smtp(
return Err(err);
}
// Increase retry count as soon as we have an SMTP connection. This ensures that the message is
// eventually removed from the queue by exceeding retry limit even in case of an error that
// keeps happening early in the message sending code, e.g. failure to read the message from the
// database.
context
.sql
.execute(
"UPDATE smtp SET retries=retries+1 WHERE id=?",
paramsv![rowid],
)
.await
.context("failed to update retries count")?;
let (body, recipients, msg_id, retries) = context
let (body, recipients, msg_id) = context
.sql
.query_row(
"SELECT mime, recipients, msg_id, retries FROM smtp WHERE id=?",
"SELECT mime, recipients, msg_id FROM smtp WHERE id=?",
paramsv![rowid],
|row| {
let mime: String = row.get(0)?;
let recipients: String = row.get(1)?;
let msg_id: MsgId = row.get(2)?;
let retries: i64 = row.get(3)?;
Ok((mime, recipients, msg_id, retries))
Ok((mime, recipients, msg_id))
},
)
.await?;
if retries > 6 {
message::set_msg_failed(
context,
msg_id,
Some("Number of retries exceeded the limit."),
)
.await;
context
.sql
.execute("DELETE FROM smtp WHERE id=?", paramsv![rowid])
.await
.context("failed to remove message with exceeded retry limit from smtp table")?;
bail!("Number of retries exceeded the limit");
}
info!(
context,
"Retry number {} to send message {} over SMTP", retries, msg_id
);
let recipients_list = recipients
.split(' ')
.filter_map(
@@ -449,7 +382,7 @@ pub(crate) async fn send_msg_to_smtp(
return Ok(());
}
let status = smtp_send(
let status = match smtp_send(
context,
&recipients_list,
body.as_str(),
@@ -457,25 +390,58 @@ pub(crate) async fn send_msg_to_smtp(
msg_id,
rowid,
)
.await;
.await
{
Status::RetryNow => {
// Do a single retry immediately without increasing retry counter in case of stale
// connection.
info!(context, "Doing immediate retry to send message.");
// smtp_send just closed stale SMTP connection, reconnect and try again.
if let Err(err) = smtp
.connect_configured(context)
.await
.context("failed to reopen stale SMTP connection")
{
smtp.last_send_error = Some(format!("{:#}", err));
return Err(err);
}
smtp_send(
context,
&recipients_list,
body.as_str(),
smtp,
msg_id,
rowid,
)
.await
}
status => status,
};
match status {
SendResult::Retry => {}
SendResult::Success | SendResult::Failure(_) => {
Status::Finished(res) => {
if res.is_ok() {
msg_id.set_delivered(context).await?;
context
.sql
.execute("DELETE FROM smtp WHERE id=?", paramsv![rowid])
.await?;
}
res
}
Status::RetryNow | Status::RetryLater => {
context
.sql
.execute("DELETE FROM smtp WHERE id=?", paramsv![rowid])
.await?;
.execute(
"UPDATE smtp SET retries=retries+1 WHERE id=?",
paramsv![rowid],
)
.await
.context("failed to update retries count")?;
Err(format_err!("Retry"))
}
};
match status {
SendResult::Retry => Err(format_err!("Retry")),
SendResult::Success => {
msg_id.set_delivered(context).await?;
Ok(())
}
SendResult::Failure(err) => Err(format_err!("{}", err)),
}
}
@@ -483,10 +449,15 @@ pub(crate) async fn send_msg_to_smtp(
///
/// Logs and ignores SMTP errors to ensure that a single SMTP message constantly failing to be sent
/// does not block other messages in the queue from being sent.
///
/// Returns true if all messages were sent successfully, false otherwise.
pub(crate) async fn send_smtp_messages(context: &Context, connection: &mut Smtp) -> Result<bool> {
pub(crate) async fn send_smtp_messages(
context: &Context,
connection: &mut Smtp,
) -> anyhow::Result<()> {
context.send_sync_msg().await?; // Add sync message to the end of the queue if needed.
context
.sql
.execute("DELETE FROM smtp WHERE retries > 5", paramsv![])
.await?;
let rowids = context
.sql
.query_map(
@@ -503,12 +474,10 @@ pub(crate) async fn send_smtp_messages(context: &Context, connection: &mut Smtp)
},
)
.await?;
let mut success = true;
for rowid in rowids {
if let Err(err) = send_msg_to_smtp(context, connection, rowid).await {
info!(context, "Failed to send message over SMTP: {:#}.", err);
success = false;
}
}
Ok(success)
Ok(())
}

View File

@@ -3,7 +3,7 @@
use async_std::path::Path;
use async_std::sync::RwLock;
use std::collections::{HashMap, HashSet};
use std::collections::HashSet;
use std::convert::TryFrom;
use std::time::Duration;
@@ -15,11 +15,11 @@ use rusqlite::{config::DbConfig, Connection, OpenFlags};
use crate::blob::BlobObject;
use crate::chat::{add_device_msg, update_device_icon, update_saved_messages_icon};
use crate::config::Config;
use crate::constants::DC_CHAT_ID_TRASH;
use crate::constants::{Viewtype, DC_CHAT_ID_TRASH};
use crate::context::Context;
use crate::dc_tools::{dc_delete_file, time};
use crate::ephemeral::start_ephemeral_timers;
use crate::message::{Message, Viewtype};
use crate::message::Message;
use crate::param::{Param, Params};
use crate::peerstate::{deduplicate_peerstates, Peerstate};
use crate::stock_str;
@@ -47,8 +47,6 @@ pub struct Sql {
/// None if the database is not open, true if it is open with passphrase and false if it is
/// open without a passphrase.
is_encrypted: RwLock<Option<bool>>,
pub(crate) config_cache: RwLock<HashMap<String, Option<String>>>,
}
impl Sql {
@@ -57,7 +55,6 @@ impl Sql {
dbfile,
pool: Default::default(),
is_encrypted: Default::default(),
config_cache: Default::default(),
}
}
@@ -181,7 +178,6 @@ impl Sql {
PRAGMA secure_delete=on;
PRAGMA busy_timeout = {};
PRAGMA temp_store=memory; -- Avoid SQLITE_IOERR_GETTEMPPATH errors on Android
PRAGMA foreign_keys=on;
",
Duration::from_secs(10).as_millis()
))?;
@@ -501,8 +497,6 @@ impl Sql {
/// will already have been logged.
pub async fn set_raw_config(&self, key: impl AsRef<str>, value: Option<&str>) -> Result<()> {
let key = key.as_ref();
let mut lock = self.config_cache.write().await;
if let Some(value) = value {
let exists = self
.exists(
@@ -528,23 +522,12 @@ impl Sql {
self.execute("DELETE FROM config WHERE keyname=?;", paramsv![key])
.await?;
}
lock.insert(key.to_string(), value.map(|s| s.to_string()));
drop(lock);
Ok(())
}
/// Get configuration options from the database.
pub async fn get_raw_config(&self, key: impl AsRef<str>) -> Result<Option<String>> {
let lock = self.config_cache.read().await;
let cached = lock.get(key.as_ref()).cloned();
drop(lock);
if let Some(c) = cached {
return Ok(c);
}
let mut lock = self.config_cache.write().await;
let value = self
.query_get_value(
"SELECT value FROM config WHERE keyname=?;",
@@ -552,8 +535,6 @@ impl Sql {
)
.await
.context(format!("failed to fetch raw config: {}", key.as_ref()))?;
lock.insert(key.as_ref().to_string(), value.clone());
drop(lock);
Ok(value)
}
@@ -592,63 +573,13 @@ impl Sql {
.await
.map(|s| s.and_then(|r| r.parse().ok()))
}
#[cfg(feature = "internals")]
pub fn config_cache(&self) -> &RwLock<HashMap<String, Option<String>>> {
&self.config_cache
}
}
pub async fn housekeeping(context: &Context) -> Result<()> {
if let Err(err) = remove_unused_files(context).await {
warn!(
context,
"Housekeeping: cannot remove unusued files: {}", err
);
if let Err(err) = crate::ephemeral::delete_expired_messages(context).await {
warn!(context, "Failed to delete expired messages: {}", err);
}
if let Err(err) = start_ephemeral_timers(context).await {
warn!(
context,
"Housekeeping: cannot start ephemeral timers: {}", err
);
}
if let Err(err) = prune_tombstones(&context.sql).await {
warn!(
context,
"Housekeeping: Cannot prune message tombstones: {}", err
);
}
if let Err(err) = deduplicate_peerstates(&context.sql).await {
warn!(context, "Failed to deduplicate peerstates: {}", err)
}
context.schedule_quota_update().await?;
// Try to clear the freelist to free some space on the disk. This
// only works if auto_vacuum is enabled.
if let Err(err) = context
.sql
.execute("PRAGMA incremental_vacuum", paramsv![])
.await
{
warn!(context, "Failed to run incremental vacuum: {}", err);
}
if let Err(e) = context
.set_config(Config::LastHousekeeping, Some(&time().to_string()))
.await
{
warn!(context, "Can't set config: {}", e);
}
info!(context, "Housekeeping done.");
Ok(())
}
pub async fn remove_unused_files(context: &Context) -> Result<()> {
let mut files_in_use = HashSet::new();
let mut unreferenced_count = 0;
@@ -763,6 +694,44 @@ pub async fn remove_unused_files(context: &Context) -> Result<()> {
}
}
if let Err(err) = start_ephemeral_timers(context).await {
warn!(
context,
"Housekeeping: cannot start ephemeral timers: {}", err
);
}
if let Err(err) = prune_tombstones(&context.sql).await {
warn!(
context,
"Housekeeping: Cannot prune message tombstones: {}", err
);
}
if let Err(err) = deduplicate_peerstates(&context.sql).await {
warn!(context, "Failed to deduplicate peerstates: {}", err)
}
context.schedule_quota_update().await?;
// Try to clear the freelist to free some space on the disk. This
// only works if auto_vacuum is enabled.
if let Err(err) = context
.sql
.execute("PRAGMA incremental_vacuum", paramsv![])
.await
{
warn!(context, "Failed to run incremental vacuum: {}", err);
}
if let Err(e) = context
.set_config(Config::LastHousekeeping, Some(&time().to_string()))
.await
{
warn!(context, "Can't set config: {}", e);
}
info!(context, "Housekeeping done.");
Ok(())
}
@@ -818,7 +787,7 @@ async fn maybe_add_from_param(
async fn prune_tombstones(sql: &Sql) -> Result<()> {
sql.execute(
"DELETE FROM msgs
WHERE chat_id=?
WHERE (chat_id=? OR hidden)
AND NOT EXISTS (
SELECT * FROM imap WHERE msgs.rfc724_mid=rfc724_mid AND target!=''
)",
@@ -828,19 +797,6 @@ async fn prune_tombstones(sql: &Sql) -> Result<()> {
Ok(())
}
/// Helper function to return comma-separated sequence of `?` chars.
///
/// Use this together with [`rusqlite::ParamsFromIter`] to use dynamically generated
/// parameter lists.
pub fn repeat_vars(count: usize) -> Result<String> {
if count == 0 {
bail!("Must have at least one repeat variable");
}
let mut s = "?,".repeat(count);
s.pop(); // Remove trailing comma
Ok(s)
}
#[cfg(test)]
mod tests {
use async_std::channel;
@@ -950,23 +906,6 @@ mod tests {
}
}
/// Regression test for a bug where housekeeping deleted drafts since their
/// `hidden` flag is set.
#[async_std::test]
async fn test_housekeeping_dont_delete_drafts() {
let t = TestContext::new_alice().await;
let chat = t.create_chat_with_contact("bob", "bob@example.com").await;
let mut new_draft = Message::new(Viewtype::Text);
new_draft.set_text(Some("This is my draft".to_string()));
chat.id.set_draft(&t, Some(&mut new_draft)).await.unwrap();
housekeeping(&t).await.unwrap();
let loaded_draft = chat.id.get_draft(&t).await.unwrap();
assert_eq!(loaded_draft.unwrap().text.unwrap(), "This is my draft");
}
/// Regression test.
///
/// Previously the code checking for existence of `config` table

View File

@@ -36,13 +36,6 @@ pub async fn run(context: &Context, sql: &Sql) -> Result<(bool, bool, bool, bool
Ok(())
})
.await?;
let mut lock = context.sql.config_cache.write().await;
lock.insert(
VERSION_CFG.to_string(),
Some(format!("{}", dbversion_before_update)),
);
drop(lock);
} else {
exists_before_update = true;
dbversion_before_update = sql
@@ -395,7 +388,7 @@ UPDATE chats SET protected=1, type=120 WHERE type=130;"#,
if dbversion < 71 {
info!(context, "[migration] v71");
if let Ok(addr) = context.get_configured_addr().await {
if let Some(addr) = context.get_config(Config::ConfiguredAddr).await? {
if let Ok(domain) = addr.parse::<EmailAddress>().map(|email| email.domain) {
context
.set_config(
@@ -586,44 +579,6 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
)
.await?;
}
if dbversion < 86 {
info!(context, "[migration] v86");
sql.execute_migration(
r#"CREATE TABLE bobstate (
id INTEGER PRIMARY KEY AUTOINCREMENT,
invite TEXT NOT NULL,
next_step INTEGER NOT NULL,
chat_id INTEGER NOT NULL
);"#,
86,
)
.await?;
}
if dbversion < 87 {
info!(context, "[migration] v87");
// the index is used to speed up delete_expired_messages()
sql.execute_migration(
"CREATE INDEX IF NOT EXISTS msgs_index8 ON msgs (ephemeral_timestamp);",
87,
)
.await?;
}
if dbversion < 88 {
info!(context, "[migration] v88");
sql.execute_migration("DROP TABLE IF EXISTS backup_blobs;", 88)
.await?;
}
if dbversion < 89 {
info!(context, "[migration] v89");
sql.execute_migration(
r#"CREATE TABLE imap_markseen (
id INTEGER,
FOREIGN KEY(id) REFERENCES imap(id) ON DELETE CASCADE
);"#,
89,
)
.await?;
}
Ok((
recalc_fingerprints,
@@ -654,10 +609,6 @@ impl Sql {
.await
.with_context(|| format!("execute_migration failed for version {}", version))?;
let mut lock = self.config_cache.write().await;
lock.insert(VERSION_CFG.to_string(), Some(format!("{}", version)));
drop(lock);
Ok(())
}
}

View File

@@ -168,14 +168,6 @@ CREATE TABLE tokens (
timestamp INTEGER DEFAULT 0
);
-- The currently running securejoin protocols, joiner-side.
-- CREATE TABLE bobstate (
-- id INTEGER PRIMARY KEY AUTOINCREMENT,
-- invite TEXT NOT NULL,
-- next_step INTEGER NOT NULL,
-- chat_id INTEGER NOT NULL
-- );
CREATE TABLE locations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
latitude REAL DEFAULT 0.0,

View File

@@ -10,10 +10,11 @@ use strum_macros::EnumProperty;
use crate::blob::BlobObject;
use crate::chat::{self, Chat, ChatId, ProtectionStatus};
use crate::config::Config;
use crate::contact::{Contact, ContactId, Origin};
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
use crate::contact::{Contact, Origin};
use crate::context::Context;
use crate::dc_tools::dc_timestamp_to_str;
use crate::message::{Message, Viewtype};
use crate::message::Message;
use crate::param::Param;
use humansize::{file_size_opts, FileSize};
@@ -287,6 +288,9 @@ pub enum StockMessage {
#[strum(props(fallback = "Storage on %1$s"))]
StorageOnDomain = 105,
#[strum(props(fallback = "One moment…"))]
OneMoment = 106,
#[strum(props(fallback = "Connected"))]
Connected = 107,
@@ -329,9 +333,6 @@ pub enum StockMessage {
#[strum(props(fallback = "Scan to join group %1$s"))]
SecureJoinGroupQRDescription = 120,
#[strum(props(fallback = "Not connected"))]
NotConnected = 121,
}
impl StockMessage {
@@ -388,7 +389,7 @@ trait StockStringMods: AsRef<str> + Sized {
fn action_by_contact<'a>(
self,
context: &'a Context,
contact_id: ContactId,
contact_id: u32,
) -> Pin<Box<dyn Future<Output = String> + Send + 'a>>
where
Self: Send + 'a,
@@ -396,7 +397,7 @@ trait StockStringMods: AsRef<str> + Sized {
Box::pin(async move {
let message = self.as_ref().trim_end_matches('.');
match contact_id {
ContactId::SELF => msg_action_by_me(context, message).await,
DC_CONTACT_ID_SELF => msg_action_by_me(context, message).await,
_ => {
let displayname = Contact::get_by_id(context, contact_id)
.await
@@ -456,7 +457,7 @@ pub(crate) async fn msg_grp_name(
context: &Context,
from_group: impl AsRef<str>,
to_group: impl AsRef<str>,
by_contact: ContactId,
by_contact: u32,
) -> String {
translated(context, StockMessage::MsgGrpName)
.await
@@ -467,7 +468,7 @@ pub(crate) async fn msg_grp_name(
}
/// Stock string: `Group image changed.`.
pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId) -> String {
pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: u32) -> String {
translated(context, StockMessage::MsgGrpImgChanged)
.await
.action_by_contact(context, by_contact)
@@ -481,7 +482,7 @@ pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId
pub(crate) async fn msg_add_member(
context: &Context,
added_member_addr: impl AsRef<str>,
by_contact: ContactId,
by_contact: u32,
) -> String {
let addr = added_member_addr.as_ref();
let who = match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
@@ -505,7 +506,7 @@ pub(crate) async fn msg_add_member(
pub(crate) async fn msg_del_member(
context: &Context,
removed_member_addr: impl AsRef<str>,
by_contact: ContactId,
by_contact: u32,
) -> String {
let addr = removed_member_addr.as_ref();
let who = match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
@@ -523,7 +524,7 @@ pub(crate) async fn msg_del_member(
}
/// Stock string: `Group left.`.
pub(crate) async fn msg_group_left(context: &Context, by_contact: ContactId) -> String {
pub(crate) async fn msg_group_left(context: &Context, by_contact: u32) -> String {
translated(context, StockMessage::MsgGroupLeft)
.await
.action_by_contact(context, by_contact)
@@ -573,7 +574,7 @@ pub(crate) async fn read_rcpt_mail_body(context: &Context, message: impl AsRef<s
}
/// Stock string: `Group image deleted.`.
pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId) -> String {
pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: u32) -> String {
translated(context, StockMessage::MsgGrpImgDeleted)
.await
.action_by_contact(context, by_contact)
@@ -586,10 +587,7 @@ pub(crate) async fn e2e_preferred(context: &Context) -> String {
}
/// Stock string: `%1$s invited you to join this group. Waiting for the device of %2$s to reply…`.
pub(crate) async fn secure_join_started(
context: &Context,
inviter_contact_id: ContactId,
) -> String {
pub(crate) async fn secure_join_started(context: &Context, inviter_contact_id: u32) -> String {
if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
translated(context, StockMessage::SecureJoinStarted)
.await
@@ -604,7 +602,7 @@ pub(crate) async fn secure_join_started(
}
/// Stock string: `%1$s replied, waiting for being added to the group…`.
pub(crate) async fn secure_join_replies(context: &Context, contact_id: ContactId) -> String {
pub(crate) async fn secure_join_replies(context: &Context, contact_id: u32) -> String {
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
translated(context, StockMessage::SecureJoinReplies)
.await
@@ -638,19 +636,20 @@ pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &C
}
/// Stock string: `%1$s verified.`.
pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String {
let addr = contact.get_name_n_addr();
pub(crate) async fn contact_verified(context: &Context, contact_addr: impl AsRef<str>) -> String {
translated(context, StockMessage::ContactVerified)
.await
.replace1(addr)
.replace1(contact_addr)
}
/// Stock string: `Cannot verify %1$s`.
pub(crate) async fn contact_not_verified(context: &Context, contact: &Contact) -> String {
let addr = contact.get_name_n_addr();
pub(crate) async fn contact_not_verified(
context: &Context,
contact_addr: impl AsRef<str>,
) -> String {
translated(context, StockMessage::ContactNotVerified)
.await
.replace1(addr)
.replace1(contact_addr)
}
/// Stock string: `Changed setup for %1$s`.
@@ -720,7 +719,7 @@ pub(crate) async fn msg_location_enabled(context: &Context) -> String {
}
/// Stock string: `Location streaming enabled by ...`.
pub(crate) async fn msg_location_enabled_by(context: &Context, contact: ContactId) -> String {
pub(crate) async fn msg_location_enabled_by(context: &Context, contact: u32) -> String {
translated(context, StockMessage::MsgLocationEnabled)
.await
.action_by_contact(context, contact)
@@ -786,10 +785,7 @@ pub(crate) async fn failed_sending_to(context: &Context, name: impl AsRef<str>)
}
/// Stock string: `Message deletion timer is disabled.`.
pub(crate) async fn msg_ephemeral_timer_disabled(
context: &Context,
by_contact: ContactId,
) -> String {
pub(crate) async fn msg_ephemeral_timer_disabled(context: &Context, by_contact: u32) -> String {
translated(context, StockMessage::MsgEphemeralTimerDisabled)
.await
.action_by_contact(context, by_contact)
@@ -800,7 +796,7 @@ pub(crate) async fn msg_ephemeral_timer_disabled(
pub(crate) async fn msg_ephemeral_timer_enabled(
context: &Context,
timer: impl AsRef<str>,
by_contact: ContactId,
by_contact: u32,
) -> String {
translated(context, StockMessage::MsgEphemeralTimerEnabled)
.await
@@ -810,7 +806,7 @@ pub(crate) async fn msg_ephemeral_timer_enabled(
}
/// Stock string: `Message deletion timer is set to 1 minute.`.
pub(crate) async fn msg_ephemeral_timer_minute(context: &Context, by_contact: ContactId) -> String {
pub(crate) async fn msg_ephemeral_timer_minute(context: &Context, by_contact: u32) -> String {
translated(context, StockMessage::MsgEphemeralTimerMinute)
.await
.action_by_contact(context, by_contact)
@@ -818,7 +814,7 @@ pub(crate) async fn msg_ephemeral_timer_minute(context: &Context, by_contact: Co
}
/// Stock string: `Message deletion timer is set to 1 hour.`.
pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: ContactId) -> String {
pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: u32) -> String {
translated(context, StockMessage::MsgEphemeralTimerHour)
.await
.action_by_contact(context, by_contact)
@@ -826,7 +822,7 @@ pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: Cont
}
/// Stock string: `Message deletion timer is set to 1 day.`.
pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: ContactId) -> String {
pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: u32) -> String {
translated(context, StockMessage::MsgEphemeralTimerDay)
.await
.action_by_contact(context, by_contact)
@@ -834,7 +830,7 @@ pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: Conta
}
/// Stock string: `Message deletion timer is set to 1 week.`.
pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: ContactId) -> String {
pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: u32) -> String {
translated(context, StockMessage::MsgEphemeralTimerWeek)
.await
.action_by_contact(context, by_contact)
@@ -879,7 +875,7 @@ pub(crate) async fn error_no_network(context: &Context) -> String {
}
/// Stock string: `Chat protection enabled.`.
pub(crate) async fn protection_enabled(context: &Context, by_contact: ContactId) -> String {
pub(crate) async fn protection_enabled(context: &Context, by_contact: u32) -> String {
translated(context, StockMessage::ProtectionEnabled)
.await
.action_by_contact(context, by_contact)
@@ -887,7 +883,7 @@ pub(crate) async fn protection_enabled(context: &Context, by_contact: ContactId)
}
/// Stock string: `Chat protection disabled.`.
pub(crate) async fn protection_disabled(context: &Context, by_contact: ContactId) -> String {
pub(crate) async fn protection_disabled(context: &Context, by_contact: u32) -> String {
translated(context, StockMessage::ProtectionDisabled)
.await
.action_by_contact(context, by_contact)
@@ -913,7 +909,7 @@ pub(crate) async fn delete_server_turned_off(context: &Context) -> String {
pub(crate) async fn msg_ephemeral_timer_minutes(
context: &Context,
minutes: impl AsRef<str>,
by_contact: ContactId,
by_contact: u32,
) -> String {
translated(context, StockMessage::MsgEphemeralTimerMinutes)
.await
@@ -926,7 +922,7 @@ pub(crate) async fn msg_ephemeral_timer_minutes(
pub(crate) async fn msg_ephemeral_timer_hours(
context: &Context,
hours: impl AsRef<str>,
by_contact: ContactId,
by_contact: u32,
) -> String {
translated(context, StockMessage::MsgEphemeralTimerHours)
.await
@@ -939,7 +935,7 @@ pub(crate) async fn msg_ephemeral_timer_hours(
pub(crate) async fn msg_ephemeral_timer_days(
context: &Context,
days: impl AsRef<str>,
by_contact: ContactId,
by_contact: u32,
) -> String {
translated(context, StockMessage::MsgEphemeralTimerDays)
.await
@@ -952,7 +948,7 @@ pub(crate) async fn msg_ephemeral_timer_days(
pub(crate) async fn msg_ephemeral_timer_weeks(
context: &Context,
weeks: impl AsRef<str>,
by_contact: ContactId,
by_contact: u32,
) -> String {
translated(context, StockMessage::MsgEphemeralTimerWeeks)
.await
@@ -1009,9 +1005,9 @@ pub(crate) async fn storage_on_domain(context: &Context, domain: impl AsRef<str>
.replace1(domain)
}
/// Stock string: `Not connected`.
pub(crate) async fn not_connected(context: &Context) -> String {
translated(context, StockMessage::NotConnected).await
/// Stock string: `One moment…`.
pub(crate) async fn one_moment(context: &Context) -> String {
translated(context, StockMessage::OneMoment).await
}
/// Stock string: `Connected`.
@@ -1109,7 +1105,7 @@ impl Context {
pub(crate) async fn stock_protection_msg(
&self,
protect: ProtectionStatus,
from_id: ContactId,
from_id: u32,
) -> String {
match protect {
ProtectionStatus::Unprotected => protection_enabled(self, from_id).await,
@@ -1128,7 +1124,7 @@ impl Context {
self.sql
.set_raw_config_bool("self-chat-added", true)
.await?;
ChatId::create_for_contact(self, ContactId::SELF).await?;
ChatId::create_for_contact(self, DC_CONTACT_ID_SELF).await?;
}
// add welcome-messages. by the label, this is done only once,
@@ -1148,13 +1144,14 @@ impl Context {
#[cfg(test)]
mod tests {
use num_traits::ToPrimitive;
use super::*;
use crate::test_utils::TestContext;
use crate::constants::DC_CONTACT_ID_SELF;
use crate::chat::Chat;
use crate::chatlist::Chatlist;
use crate::test_utils::TestContext;
use super::*;
use num_traits::ToPrimitive;
#[test]
fn test_enum_mapping() {
@@ -1200,15 +1197,8 @@ mod tests {
#[async_std::test]
async fn test_stock_string_repl_str() {
let t = TestContext::new().await;
let contact_id = Contact::create(&t.ctx, "Someone", "someone@example.org")
.await
.unwrap();
let contact = Contact::load_from_db(&t.ctx, contact_id).await.unwrap();
// uses %1$s substitution
assert_eq!(
contact_verified(&t, &contact).await,
"Someone (someone@example.org) verified."
);
assert_eq!(contact_verified(&t, "Foo").await, "Foo verified.");
// We have no string using %1$d to test...
}
@@ -1231,7 +1221,7 @@ mod tests {
async fn test_stock_system_msg_add_member_by_me() {
let t = TestContext::new().await;
assert_eq!(
msg_add_member(&t, "alice@example.org", ContactId::SELF).await,
msg_add_member(&t, "alice@example.org", DC_CONTACT_ID_SELF).await,
"Member alice@example.org added by me."
)
}
@@ -1243,7 +1233,7 @@ mod tests {
.await
.expect("failed to create contact");
assert_eq!(
msg_add_member(&t, "alice@example.org", ContactId::SELF).await,
msg_add_member(&t, "alice@example.org", DC_CONTACT_ID_SELF).await,
"Member Alice (alice@example.org) added by me."
);
}

View File

@@ -1,11 +1,11 @@
//! # Message summary for chatlist.
use crate::chat::Chat;
use crate::constants::Chattype;
use crate::contact::{Contact, ContactId};
use crate::constants::{Chattype, Viewtype, DC_CONTACT_ID_SELF};
use crate::contact::Contact;
use crate::context::Context;
use crate::dc_tools::dc_truncate;
use crate::message::{Message, MessageState, Viewtype};
use crate::message::{Message, MessageState};
use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::stock_str;
@@ -60,7 +60,7 @@ impl Summary {
) -> Self {
let prefix = if msg.state == MessageState::OutDraft {
Some(SummaryPrefix::Draft(stock_str::draft(context).await))
} else if msg.from_id == ContactId::SELF {
} else if msg.from_id == DC_CONTACT_ID_SELF {
if msg.is_info() || chat.is_self_talk() {
None
} else {

View File

@@ -2,11 +2,10 @@
use crate::chat::{Chat, ChatId};
use crate::config::Config;
use crate::constants::Blocked;
use crate::contact::ContactId;
use crate::constants::{Blocked, Viewtype, DC_CONTACT_ID_SELF};
use crate::context::Context;
use crate::dc_tools::time;
use crate::message::{Message, MsgId, Viewtype};
use crate::message::{Message, MsgId};
use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::sync::SyncData::{AddQrToken, DeleteQrToken};
@@ -127,7 +126,7 @@ impl Context {
pub async fn send_sync_msg(&self) -> Result<Option<MsgId>> {
if let Some((json, ids)) = self.build_sync_json().await? {
let chat_id =
ChatId::create_for_contact_with_blocked(self, ContactId::SELF, Blocked::Yes)
ChatId::create_for_contact_with_blocked(self, DC_CONTACT_ID_SELF, Blocked::Yes)
.await?;
let mut msg = Message {
chat_id,
@@ -484,7 +483,7 @@ mod tests {
// check that the used self-talk is not visible to the user
// but that creation will still work (in this case, the chat is empty)
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
let chat_id = ChatId::create_for_contact(&alice, ContactId::SELF).await?;
let chat_id = ChatId::create_for_contact(&alice, DC_CONTACT_ID_SELF).await?;
let chat = Chat::load_from_db(&alice, chat_id).await?;
assert!(chat.is_self_talk());
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);

View File

@@ -22,14 +22,14 @@ use crate::chat::{self, Chat, ChatId};
use crate::chatlist::Chatlist;
use crate::config::Config;
use crate::constants::Chattype;
use crate::constants::{DC_MSG_ID_DAYMARKER, DC_MSG_ID_MARKER1};
use crate::contact::{Contact, ContactId, Modifier, Origin};
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF, DC_MSG_ID_DAYMARKER, DC_MSG_ID_MARKER1};
use crate::contact::{Contact, Origin};
use crate::context::Context;
use crate::dc_receive_imf::dc_receive_imf;
use crate::dc_tools::EmailAddress;
use crate::events::{Event, EventType};
use crate::key::{self, DcKey, KeyPair, KeyPairUse};
use crate::message::{update_msg_state, Message, MessageState, MsgId, Viewtype};
use crate::message::{update_msg_state, Message, MessageState, MsgId};
use crate::mimeparser::MimeMessage;
#[allow(non_upper_case_globals)]
@@ -39,34 +39,6 @@ pub const AVATAR_900x900_BYTES: &[u8] = include_bytes!("../test-data/image/avata
static CONTEXT_NAMES: Lazy<std::sync::RwLock<BTreeMap<u32, String>>> =
Lazy::new(|| std::sync::RwLock::new(BTreeMap::new()));
pub struct TestContextManager {
log_tx: Sender<Event>,
_log_sink: LogSink,
}
impl TestContextManager {
pub async fn new() -> Self {
let (log_tx, _log_sink) = LogSink::create();
Self { log_tx, _log_sink }
}
pub async fn alice(&mut self) -> TestContext {
TestContext::builder()
.configure_alice()
.with_log_sink(self.log_tx.clone())
.build()
.await
}
pub async fn bob(&mut self) -> TestContext {
TestContext::builder()
.configure_bob()
.with_log_sink(self.log_tx.clone())
.build()
.await
}
}
#[derive(Debug, Clone, Default)]
pub struct TestContextBuilder {
key_pair: Option<KeyPair>,
@@ -364,7 +336,7 @@ impl TestContext {
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n"
.to_owned()
+ msg.payload();
dc_receive_imf(&self.ctx, received_msg.as_bytes(), false)
dc_receive_imf(&self.ctx, received_msg.as_bytes(), "INBOX", false)
.await
.unwrap();
}
@@ -399,51 +371,31 @@ impl TestContext {
.expect("failed to load msg")
}
/// Returns the [`Contact`] for the other [`TestContext`], creating it if necessary.
pub async fn add_or_lookup_contact(&self, other: &TestContext) -> Contact {
let name = other
.ctx
.get_config(Config::Displayname)
.await
.unwrap_or_default()
.unwrap_or_default();
let addr = other.ctx.get_configured_addr().await.unwrap();
// MailinglistAddress is the lowest allowed origin, we'd prefer to not modify the
// origin when creating this contact.
let (contact_id, modified) =
Contact::add_or_lookup(self, &name, &addr, Origin::MailinglistAddress)
.await
.unwrap();
match modified {
Modifier::None => (),
Modifier::Modified => warn!(&self.ctx, "Contact {} modified by TestContext", &addr),
Modifier::Created => warn!(&self.ctx, "Contact {} created by TestContext", &addr),
}
Contact::load_from_db(&self.ctx, contact_id).await.unwrap()
}
/// Returns 1:1 [`Chat`] with another account, if it exists.
///
/// This first creates a contact using the configured details on the other account, then
/// creates a 1:1 chat with this contact.
pub async fn get_chat(&self, other: &TestContext) -> Option<Chat> {
let contact = self.add_or_lookup_contact(other).await;
match ChatId::lookup_by_contact(&self.ctx, contact.id)
.await
.unwrap()
{
Some(id) => Some(Chat::load_from_db(&self.ctx, id).await.unwrap()),
None => None,
}
}
/// Creates or returns an existing 1:1 [`Chat`] with another account.
///
/// This first creates a contact using the configured details on the other account, then
/// creates a 1:1 chat with this contact.
pub async fn create_chat(&self, other: &TestContext) -> Chat {
let contact = self.add_or_lookup_contact(other).await;
let chat_id = ChatId::create_for_contact(self, contact.id).await.unwrap();
let (contact_id, _modified) = Contact::add_or_lookup(
self,
&other
.ctx
.get_config(Config::Displayname)
.await
.unwrap_or_default()
.unwrap_or_default(),
&other
.ctx
.get_config(Config::ConfiguredAddr)
.await
.unwrap()
.unwrap(),
Origin::ManuallyCreated,
)
.await
.unwrap();
let chat_id = ChatId::create_for_contact(self, contact_id).await.unwrap();
Chat::load_from_db(self, chat_id).await.unwrap()
}
@@ -461,7 +413,7 @@ impl TestContext {
/// Retrieves the "self" chat.
pub async fn get_self_chat(&self) -> Chat {
let chat_id = ChatId::create_for_contact(self, ContactId::SELF)
let chat_id = ChatId::create_for_contact(self, DC_CONTACT_ID_SELF)
.await
.unwrap();
Chat::load_from_db(self, chat_id).await.unwrap()
@@ -846,7 +798,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
&contact_name,
contact_id,
msgtext.unwrap_or_default(),
if msg.get_from_id() == ContactId::SELF {
if msg.get_from_id() == 1u32 {
""
} else if msg.get_state() == MessageState::InSeen {
"[SEEN]"
@@ -897,10 +849,17 @@ mod tests {
#[async_std::test]
async fn test_with_both() {
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let (log_sender, _log_sink) = LogSink::create();
let alice = TestContext::builder()
.configure_alice()
.with_log_sink(log_sender.clone())
.build()
.await;
let bob = TestContext::builder()
.configure_bob()
.with_log_sink(log_sender)
.build()
.await;
alice.ctx.emit_event(EventType::Info("hello".into()));
bob.ctx.emit_event(EventType::Info("there".into()));
// panic!("Both fail");

View File

@@ -1,7 +1,7 @@
//! # Functions to update timestamps.
use crate::chat::{Chat, ChatId};
use crate::contact::{Contact, ContactId};
use crate::contact::Contact;
use crate::context::Context;
use crate::param::{Param, Params};
use anyhow::Result;
@@ -12,7 +12,7 @@ impl Context {
/// (if we have a ContactId type at some point, the function should go there)
pub(crate) async fn update_contacts_timestamp(
&self,
contact_id: ContactId,
contact_id: u32,
scope: Param,
new_timestamp: i64,
) -> Result<bool> {
@@ -99,6 +99,7 @@ mod tests {
Date: Sun, 22 Mar 2021 23:37:57 +0000\n\
\n\
second message\n",
"INBOX",
false,
)
.await?;
@@ -112,6 +113,7 @@ mod tests {
Date: Sun, 22 Mar 2021 22:37:57 +0000\n\
\n\
first message\n",
"INBOX",
false,
)
.await?;
@@ -141,6 +143,7 @@ mod tests {
Date: Sun, 22 Mar 2021 01:00:00 +0000\n\
\n\
first message\n",
"INBOX",
false,
)
.await?;
@@ -160,6 +163,7 @@ mod tests {
Date: Sun, 22 Mar 2021 03:00:00 +0000\n\
\n\
third message\n",
"INBOX",
false,
)
.await?;
@@ -175,6 +179,7 @@ mod tests {
Date: Sun, 22 Mar 2021 02:00:00 +0000\n\
\n\
second message\n",
"INBOX",
false,
)
.await?;

View File

@@ -1,30 +1,24 @@
//! # Handle webxdc messages.
use crate::chat::Chat;
use crate::constants::Viewtype;
use crate::context::Context;
use crate::dc_tools::{dc_create_smeared_timestamp, dc_open_file_std};
use crate::message::{Message, MessageState, MsgId, Viewtype};
use crate::message::{Message, MessageState, MsgId};
use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::{chat, EventType};
use anyhow::{bail, ensure, format_err, Result};
use async_std::path::PathBuf;
use deltachat_derive::FromSql;
use lettre_email::mime;
use lettre_email::mime::{self};
use lettre_email::PartBuilder;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::convert::TryFrom;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::io::Read;
use zip::ZipArchive;
/// The current API version.
/// If `min_api` in manifest.toml is set to a larger value,
/// the Webxdc's index.html is replaced by an error message.
/// In the future, that may be useful to avoid new Webxdc being loaded on old Delta Chats.
const WEBXDC_API_VERSION: u32 = 1;
pub const WEBXDC_SUFFIX: &str = "xdc";
const WEBXDC_DEFAULT_ICON: &str = "__webxdc__/default-icon.png";
@@ -38,19 +32,18 @@ const WEBXDC_DEFAULT_ICON: &str = "__webxdc__/default-icon.png";
///
/// The limit is also an experiment to see how small we can go;
/// it is planned to raise that limit as needed in subsequent versions.
const WEBXDC_SENDING_LIMIT: u64 = 655360;
const WEBXDC_SENDING_LIMIT: usize = 655360;
/// Be more tolerant for .xdc sizes on receiving -
/// might be, the senders version uses already a larger limit
/// and not showing the .xdc on some devices would be even worse ux.
const WEBXDC_RECEIVING_LIMIT: u64 = 4194304;
const WEBXDC_RECEIVING_LIMIT: usize = 4194304;
/// Raw information read from manifest.toml
#[derive(Debug, Deserialize)]
#[non_exhaustive]
struct WebxdcManifest {
name: Option<String>,
min_api: Option<u32>,
}
/// Parsed information from WebxdcManifest and fallbacks.
@@ -63,26 +56,14 @@ pub struct WebxdcInfo {
/// Status Update ID.
#[derive(
Debug,
Copy,
Clone,
Default,
PartialEq,
Eq,
Hash,
PartialOrd,
Ord,
Serialize,
Deserialize,
FromSql,
FromPrimitive,
Debug, Copy, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
)]
pub struct StatusUpdateSerial(u32);
pub struct StatusUpdateId(u32);
impl StatusUpdateSerial {
impl StatusUpdateId {
/// Create a new [MsgId].
pub fn new(id: u32) -> StatusUpdateSerial {
StatusUpdateSerial(id)
pub fn new(id: u32) -> StatusUpdateId {
StatusUpdateId(id)
}
/// Gets StatusUpdateId as untyped integer.
@@ -92,9 +73,9 @@ impl StatusUpdateSerial {
}
}
impl rusqlite::types::ToSql for StatusUpdateSerial {
impl rusqlite::types::ToSql for StatusUpdateId {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let val = rusqlite::types::Value::Integer(i64::from(self.0));
let val = rusqlite::types::Value::Integer(self.0 as i64);
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
@@ -118,68 +99,48 @@ pub(crate) struct StatusUpdateItem {
summary: Option<String>,
}
/// Update items as passed to the UIs.
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct StatusUpdateItemAndSerial {
#[serde(flatten)]
item: StatusUpdateItem,
serial: StatusUpdateSerial,
max_serial: StatusUpdateSerial,
}
impl Context {
/// check if a file is an acceptable webxdc for sending or receiving.
pub(crate) async fn is_webxdc_file<R>(&self, filename: &str, mut reader: R) -> Result<bool>
where
R: Read + Seek,
{
if !filename.ends_with(WEBXDC_SUFFIX) {
return Ok(false);
}
let size = reader.seek(SeekFrom::End(0))?;
if size > WEBXDC_RECEIVING_LIMIT {
info!(
self,
"{} exceeds receiving limit of {} bytes", &filename, WEBXDC_RECEIVING_LIMIT
);
return Ok(false);
}
reader.seek(SeekFrom::Start(0))?;
let mut archive = match zip::ZipArchive::new(reader) {
Ok(archive) => archive,
Err(_) => {
pub(crate) async fn is_webxdc_file(&self, filename: &str, buf: &[u8]) -> Result<bool> {
if filename.ends_with(WEBXDC_SUFFIX) {
let reader = std::io::Cursor::new(buf);
if let Ok(mut archive) = zip::ZipArchive::new(reader) {
if let Ok(_index_html) = archive.by_name("index.html") {
if buf.len() <= WEBXDC_RECEIVING_LIMIT {
return Ok(true);
} else {
info!(
self,
"{} exceeds receiving limit of {} bytes",
&filename,
WEBXDC_RECEIVING_LIMIT
);
}
} else {
info!(self, "{} misses index.html", &filename);
}
} else {
info!(self, "{} cannot be opened as zip-file", &filename);
return Ok(false);
}
};
if archive.by_name("index.html").is_err() {
info!(self, "{} misses index.html", &filename);
return Ok(false);
}
Ok(true)
Ok(false)
}
/// ensure that a file is an acceptable webxdc for sending
/// (sending has more strict size limits).
pub(crate) async fn ensure_sendable_webxdc_file(&self, path: &PathBuf) -> Result<()> {
let mut file = std::fs::File::open(path)?;
let mut buf = Vec::new();
file.read_to_end(&mut buf)?;
if !self
.is_webxdc_file(path.to_str().unwrap_or_default(), &mut file)
.is_webxdc_file(path.to_str().unwrap_or_default(), &buf)
.await?
{
bail!(
"{} is not a valid webxdc file",
path.to_str().unwrap_or_default()
);
}
let size = file.seek(SeekFrom::End(0))?;
if size > WEBXDC_SENDING_LIMIT {
} else if buf.len() > WEBXDC_SENDING_LIMIT {
bail!(
"webxdc {} exceeds acceptable size of {} bytes",
path.to_str().unwrap_or_default(),
@@ -196,7 +157,7 @@ impl Context {
instance: &mut Message,
update_str: &str,
timestamp: i64,
) -> Result<StatusUpdateSerial> {
) -> Result<StatusUpdateId> {
let update_str = update_str.trim();
if update_str.is_empty() {
bail!("create_status_update_record: empty update.");
@@ -215,7 +176,13 @@ impl Context {
_ => item,
}
} else {
bail!("create_status_update_record: no valid update item.");
// TODO: this fallback (legacy `PAYLOAD`) should be deleted soon, together with the test below
let payload: Value = serde_json::from_str(update_str)?; // checks if input data are valid json
StatusUpdateItem {
payload,
info: None,
summary: None,
}
}
};
@@ -226,7 +193,6 @@ impl Context {
info.as_str(),
SystemMessage::Unknown,
timestamp,
None,
Some(instance),
)
.await?;
@@ -239,7 +205,10 @@ impl Context {
{
instance.param.set(Param::WebxdcSummary, summary);
instance.update_param(self).await;
self.emit_msgs_changed(instance.chat_id, instance.id);
self.emit_event(EventType::MsgsChanged {
chat_id: instance.chat_id,
msg_id: instance.id,
});
}
}
@@ -250,15 +219,14 @@ impl Context {
paramsv![instance.id, serde_json::to_string(&status_update_item)?],
)
.await?;
let status_update_serial = StatusUpdateSerial(u32::try_from(rowid)?);
let status_update_id = StatusUpdateId(u32::try_from(rowid)?);
self.emit_event(EventType::WebxdcStatusUpdate {
msg_id: instance.id,
status_update_serial,
status_update_id,
});
Ok(status_update_serial)
Ok(status_update_id)
}
/// Sends a status update for an webxdc instance.
@@ -282,7 +250,7 @@ impl Context {
let chat = Chat::load_from_db(self, instance.chat_id).await?;
ensure!(chat.can_send(self).await?, "cannot send to {}", chat.id);
let status_update_serial = self
let status_update_id = self
.create_status_update_record(
&mut instance,
update_str,
@@ -311,7 +279,7 @@ impl Context {
Param::Arg,
self.render_webxdc_status_update_object(
instance_msg_id,
Some(status_update_serial),
Some(status_update_id),
)
.await?
.ok_or_else(|| format_err!("Status object expected."))?,
@@ -370,79 +338,24 @@ impl Context {
Ok(())
}
/// Returns status updates as an JSON-array, ready to be consumed by a webxdc.
/// Returns status updates as an JSON-array.
///
/// Example: `[{"serial":1, "max_serial":3, "payload":"any update data"},
/// {"serial":3, "max_serial":3, "payload":"another update data"}]`
/// Updates with serials larger than `last_known_serial` are returned.
/// If no last serial is known, set `last_known_serial` to 0.
/// If no updates are available, an empty JSON-array is returned.
/// Example: `[{"payload":"any update data"},{"payload":"another update data"}]`
/// The updates may be filtered by a given status_update_id;
/// if no updates are available, an empty JSON-array is returned.
pub async fn get_webxdc_status_updates(
&self,
instance_msg_id: MsgId,
last_known_serial: StatusUpdateSerial,
status_update_id: Option<StatusUpdateId>,
) -> Result<String> {
let json = self
.sql
.query_map(
"SELECT update_item, id FROM msgs_status_updates WHERE msg_id=? AND id>? ORDER BY id",
paramsv![instance_msg_id, last_known_serial],
|row| {
let update_item_str = row.get::<_, String>(0)?;
let serial = row.get::<_, StatusUpdateSerial>(1)?;
Ok((update_item_str, serial))
},
|rows| {
let mut rows_copy : Vec<(String, StatusUpdateSerial)> = Vec::new(); // `rows_copy` needed as `rows` cannot be iterated twice.
let mut max_serial = StatusUpdateSerial(0);
for row in rows {
let row = row?;
if row.1 > max_serial {
max_serial = row.1;
}
rows_copy.push(row);
}
let mut json = String::default();
for row in rows_copy {
let (update_item_str, serial) = row;
let update_item = StatusUpdateItemAndSerial
{
item: serde_json::from_str(&*update_item_str)?,
serial,
max_serial,
};
if !json.is_empty() {
json.push_str(",\n");
}
json.push_str(&*serde_json::to_string(&update_item)?);
}
Ok(json)
},
)
.await?;
Ok(format!("[{}]", json))
}
/// Renders JSON-object for status updates as used on the wire.
///
/// Example: `{"updates": [{"payload":"any update data"},
/// {"payload":"another update data"}]}`
/// If `status_update_serial` is set, exactly that update is rendered, otherwise all updates are rendered.
pub(crate) async fn render_webxdc_status_update_object(
&self,
instance_msg_id: MsgId,
status_update_serial: Option<StatusUpdateSerial>,
) -> Result<Option<String>> {
let json = self
.sql
.query_map(
"SELECT update_item FROM msgs_status_updates WHERE msg_id=? AND (1=? OR id=?) ORDER BY id",
"SELECT update_item FROM msgs_status_updates WHERE msg_id=? AND (1=? OR id=?)",
paramsv![
instance_msg_id,
if status_update_serial.is_some() { 0 } else { 1 },
status_update_serial.unwrap_or(StatusUpdateSerial(0))
if status_update_id.is_some() { 0 } else { 1 },
status_update_id.unwrap_or(StatusUpdateId(0))
],
|row| row.get::<_, String>(0),
|rows| {
@@ -458,10 +371,22 @@ impl Context {
},
)
.await?;
if json.is_empty() {
Ok(format!("[{}]", json))
}
/// Render JSON-object for status updates as used on the wire.
pub(crate) async fn render_webxdc_status_update_object(
&self,
instance_msg_id: MsgId,
status_update_id: Option<StatusUpdateId>,
) -> Result<Option<String>> {
let updates_array = self
.get_webxdc_status_updates(instance_msg_id, status_update_id)
.await?;
if updates_array == "[]" {
Ok(None)
} else {
Ok(Some(format!(r#"{{"updates":[{}]}}"#, json)))
Ok(Some(format!(r#"{{"updates":{}}}"#, updates_array)))
}
}
}
@@ -508,21 +433,6 @@ impl Message {
};
let mut 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(manifest) = parse_webxdc_manifest(&bytes).await {
if let Some(min_api) = manifest.min_api {
if min_api > WEBXDC_API_VERSION {
return Ok(Vec::from(
"<!DOCTYPE html>This Webxdc requires a newer Delta Chat version.",
));
}
}
}
}
}
get_blob(&mut archive, name).await
}
@@ -535,16 +445,10 @@ impl Message {
if let Ok(manifest) = parse_webxdc_manifest(&bytes).await {
manifest
} else {
WebxdcManifest {
name: None,
min_api: None,
}
WebxdcManifest { name: None }
}
} else {
WebxdcManifest {
name: None,
min_api: None,
}
WebxdcManifest { name: None }
};
if let Some(ref name) = manifest.name {
@@ -579,11 +483,7 @@ impl Message {
#[cfg(test)]
mod tests {
use std::io::Cursor;
use async_std::fs::File;
use async_std::io::WriteExt;
use super::*;
use crate::chat::{
add_contact_to_chat, create_group_chat, forward_msgs, send_msg, send_text_msg, ChatId,
ProtectionStatus,
@@ -592,8 +492,8 @@ mod tests {
use crate::contact::Contact;
use crate::dc_receive_imf::dc_receive_imf;
use crate::test_utils::TestContext;
use super::*;
use async_std::fs::File;
use async_std::io::WriteExt;
#[allow(clippy::assertions_on_constants)]
#[async_std::test]
@@ -611,35 +511,35 @@ mod tests {
assert!(
!t.is_webxdc_file(
"bad-ext-no-zip.txt",
Cursor::new(include_bytes!("../test-data/message/issue_523.txt"))
include_bytes!("../test-data/message/issue_523.txt")
)
.await?
);
assert!(
!t.is_webxdc_file(
"bad-ext-good-zip.txt",
Cursor::new(include_bytes!("../test-data/webxdc/minimal.xdc"))
include_bytes!("../test-data/webxdc/minimal.xdc")
)
.await?
);
assert!(
!t.is_webxdc_file(
"good-ext-no-zip.xdc",
Cursor::new(include_bytes!("../test-data/message/issue_523.txt"))
include_bytes!("../test-data/message/issue_523.txt")
)
.await?
);
assert!(
!t.is_webxdc_file(
"good-ext-no-index-html.xdc",
Cursor::new(include_bytes!("../test-data/webxdc/no-index-html.xdc"))
include_bytes!("../test-data/webxdc/no-index-html.xdc")
)
.await?
);
assert!(
t.is_webxdc_file(
"good-ext-good-zip.xdc",
Cursor::new(include_bytes!("../test-data/webxdc/minimal.xdc"))
include_bytes!("../test-data/webxdc/minimal.xdc")
)
.await?
);
@@ -735,9 +635,8 @@ mod tests {
.await?;
assert!(!instance.is_forwarded());
assert_eq!(
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":42,"info":"foo","summary":"bar","serial":1,"max_serial":1}]"#
t.get_webxdc_status_updates(instance.id, None).await?,
r#"[{"payload":42,"info":"foo","summary":"bar"}]"#
);
assert_eq!(chat_id.get_msg_cnt(&t).await?, 2); // instance and info
let info = Message::load_from_db(&t, instance.id)
@@ -750,11 +649,7 @@ mod tests {
forward_msgs(&t, &[instance.get_id()], chat_id).await?;
let instance2 = t.get_last_msg_in(chat_id).await;
assert!(instance2.is_forwarded());
assert_eq!(
t.get_webxdc_status_updates(instance2.id, StatusUpdateSerial(0))
.await?,
"[]"
);
assert_eq!(t.get_webxdc_status_updates(instance2.id, None).await?, "[]");
assert_eq!(chat_id.get_msg_cnt(&t).await?, 3); // two instances, only one info
let info = Message::load_from_db(&t, instance2.id)
.await?
@@ -771,6 +666,7 @@ mod tests {
dc_receive_imf(
&t,
include_bytes!("../test-data/message/webxdc_good_extension.eml"),
"INBOX",
false,
)
.await?;
@@ -781,6 +677,7 @@ mod tests {
dc_receive_imf(
&t,
include_bytes!("../test-data/message/webxdc_bad_extension.eml"),
"INBOX",
false,
)
.await?;
@@ -815,8 +712,7 @@ mod tests {
.await
.is_err());
assert_eq!(
bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0))
.await?,
bob.get_webxdc_status_updates(bob_instance.id, None).await?,
"[]"
);
@@ -827,9 +723,8 @@ mod tests {
.await
.is_ok());
assert_eq!(
bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":42,"serial":1,"max_serial":1}]"#
bob.get_webxdc_status_updates(bob_instance.id, None).await?,
r#"[{"payload":42}]"#
);
Ok(())
@@ -851,16 +746,14 @@ mod tests {
t.send_webxdc_status_update(instance.id, r#"{"payload": 42}"#, "descr")
.await?;
assert_eq!(
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":42,"serial":1,"max_serial":1}]"#.to_string()
t.get_webxdc_status_updates(instance.id, None).await?,
r#"[{"payload":42}]"#.to_string()
);
// set_draft(None) deletes the message without the need to simulate network
chat_id.set_draft(&t, None).await?;
assert_eq!(
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
.await?,
t.get_webxdc_status_updates(instance.id, None).await?,
"[]".to_string()
);
assert_eq!(
@@ -879,13 +772,9 @@ mod tests {
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let mut instance = send_webxdc_instance(&t, chat_id).await?;
assert_eq!(
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
.await?,
"[]"
);
assert_eq!(t.get_webxdc_status_updates(instance.id, None).await?, "[]");
let update_id1 = t
let id = t
.create_status_update_record(
&mut instance,
"\n\n{\"payload\": {\"foo\":\"bar\"}}\n",
@@ -893,9 +782,8 @@ mod tests {
)
.await?;
assert_eq!(
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"#
t.get_webxdc_status_updates(instance.id, Some(id)).await?,
r#"[{"payload":{"foo":"bar"}}]"#
);
assert!(t
@@ -907,12 +795,15 @@ mod tests {
.await
.is_err());
assert_eq!(
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"#
t.get_webxdc_status_updates(instance.id, Some(id)).await?,
r#"[{"payload":{"foo":"bar"}}]"#
);
assert_eq!(
t.get_webxdc_status_updates(instance.id, None).await?,
r#"[{"payload":{"foo":"bar"}}]"#
);
let update_id2 = t
let id = t
.create_status_update_record(
&mut instance,
r#"{"payload" : { "foo2":"bar2"}}"#,
@@ -920,20 +811,19 @@ mod tests {
)
.await?;
assert_eq!(
t.get_webxdc_status_updates(instance.id, update_id1).await?,
r#"[{"payload":{"foo2":"bar2"},"serial":2,"max_serial":2}]"#
t.get_webxdc_status_updates(instance.id, Some(id)).await?,
r#"[{"payload":{"foo2":"bar2"}}]"#
);
t.create_status_update_record(&mut instance, r#"{"payload":true}"#, 1640178619)
.await?;
assert_eq!(
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":3},
{"payload":{"foo2":"bar2"},"serial":2,"max_serial":3},
{"payload":true,"serial":3,"max_serial":3}]"#
t.get_webxdc_status_updates(instance.id, None).await?,
r#"[{"payload":{"foo":"bar"}},
{"payload":{"foo2":"bar2"}},
{"payload":true}]"#
);
let _update_id3 = t
let id = t
.create_status_update_record(
&mut instance,
r#"{"payload" : 1, "sender": "that is not used"}"#,
@@ -941,9 +831,17 @@ mod tests {
)
.await?;
assert_eq!(
t.get_webxdc_status_updates(instance.id, update_id2).await?,
r#"[{"payload":true,"serial":3,"max_serial":4},
{"payload":1,"serial":4,"max_serial":4}]"#
t.get_webxdc_status_updates(instance.id, Some(id)).await?,
r#"[{"payload":1}]"#
);
// TODO: legacy `PAYLOAD` support should be deleted soon
let id = t
.create_status_update_record(&mut instance, r#"{"foo" : 1}"#, 1640178619)
.await?;
assert_eq!(
t.get_webxdc_status_updates(instance.id, Some(id)).await?,
r#"[{"payload":{"foo":1}}]"#
);
Ok(())
@@ -975,9 +873,8 @@ mod tests {
t.receive_status_update(instance.id, r#"{"updates":[{"payload":{"foo":"bar"}}]}"#)
.await?;
assert_eq!(
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"#
t.get_webxdc_status_updates(instance.id, None).await?,
r#"[{"payload":{"foo":"bar"}}]"#
);
t.receive_status_update(
@@ -986,11 +883,10 @@ mod tests {
)
.await?;
assert_eq!(
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":3},
{"payload":42,"serial":2,"max_serial":3},
{"payload":23,"serial":3,"max_serial":3}]"#
t.get_webxdc_status_updates(instance.id, None).await?,
r#"[{"payload":{"foo":"bar"}},
{"payload":42},
{"payload":23}]"#
);
t.receive_status_update(
@@ -999,12 +895,11 @@ mod tests {
)
.await?; // ignore members that may be added in the future
assert_eq!(
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":4},
{"payload":42,"serial":2,"max_serial":4},
{"payload":23,"serial":3,"max_serial":4},
{"payload":"ok","serial":4,"max_serial":4}]"#
t.get_webxdc_status_updates(instance.id, None).await?,
r#"[{"payload":{"foo":"bar"}},
{"payload":42},
{"payload":23},
{"payload":"ok"}]"#
);
Ok(())
@@ -1018,8 +913,13 @@ mod tests {
match event {
EventType::WebxdcStatusUpdate {
msg_id,
status_update_serial: _,
status_update_id,
} => {
assert_eq!(
t.get_webxdc_status_updates(msg_id, Some(status_update_id))
.await?,
r#"[{"payload":{"foo":"bar"}}]"#
);
assert_eq!(msg_id, instance_id);
}
_ => unreachable!(),
@@ -1064,9 +964,9 @@ mod tests {
assert!(sent2.payload().contains("descr text"));
assert_eq!(
alice
.get_webxdc_status_updates(alice_instance.id, StatusUpdateSerial(0))
.get_webxdc_status_updates(alice_instance.id, None)
.await?,
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"#
r#"[{"payload":{"foo":"bar"}}]"#
);
alice
@@ -1079,10 +979,10 @@ mod tests {
.unwrap();
assert_eq!(
alice
.get_webxdc_status_updates(alice_instance.id, StatusUpdateSerial(0))
.get_webxdc_status_updates(alice_instance.id, None)
.await?,
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":2},
{"payload":{"snipp":"snapp"},"serial":2,"max_serial":2}]"#
r#"[{"payload":{"foo":"bar"}},
{"payload":{"snipp":"snapp"}}]"#
);
// Bob receives all messages
@@ -1098,9 +998,8 @@ mod tests {
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 1);
assert_eq!(
bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"#
bob.get_webxdc_status_updates(bob_instance.id, None).await?,
r#"[{"payload":{"foo":"bar"}}]"#
);
// Alice has a second device and also receives messages there
@@ -1192,10 +1091,9 @@ mod tests {
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?,
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":2},
{"payload":42,"serial":2,"max_serial":2}]"# // 'info: "i"' ignored as sent in draft mode
bob.get_webxdc_status_updates(bob_instance.id, None).await?,
r#"[{"payload":{"foo":"bar"}},
{"payload":42}]"# // 'info: "i"' ignored as sent in draft mode
);
Ok(())
@@ -1326,38 +1224,6 @@ sth_for_the = "future""#
)
.await?;
assert_eq!(manifest.name, Some("foz".to_string()));
Ok(())
}
#[async_std::test]
async fn test_parse_webxdc_manifest_min_api() -> Result<()> {
let manifest = parse_webxdc_manifest(r#"min_api = 3"#.as_bytes()).await?;
assert_eq!(manifest.min_api, Some(3));
let result = parse_webxdc_manifest(r#"min_api = "1""#.as_bytes()).await;
assert!(result.is_err());
let result = parse_webxdc_manifest(r#"min_api = 1.2"#.as_bytes()).await;
assert!(result.is_err());
Ok(())
}
#[async_std::test]
async fn test_webxdc_min_api_too_large() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?;
let mut instance = create_webxdc_instance(
&t,
"with-min-api-1001.xdc",
include_bytes!("../test-data/webxdc/with-min-api-1001.xdc"),
)
.await?;
send_msg(&t, chat_id, &mut instance).await?;
let instance = t.get_last_msg().await;
let html = instance.get_webxdc_blob(&t, "index.html").await?;
assert!(String::from_utf8_lossy(&*html).contains("requires a newer Delta Chat version"));
Ok(())
}
@@ -1545,9 +1411,9 @@ sth_for_the = "future""#
assert!(info_msg.quoted_message(&alice).await?.is_none());
assert_eq!(
alice
.get_webxdc_status_updates(alice_instance.id, StatusUpdateSerial(0))
.get_webxdc_status_updates(alice_instance.id, None)
.await?,
r#"[{"payload":"sth. else","info":"this appears in-chat","serial":1,"max_serial":1}]"#
r#"[{"payload":"sth. else","info":"this appears in-chat"}]"#
);
// Bob receives all messages
@@ -1565,9 +1431,8 @@ sth_for_the = "future""#
assert_eq!(info_msg.parent(&bob).await?.unwrap().id, bob_instance.id);
assert!(info_msg.quoted_message(&bob).await?.is_none());
assert_eq!(
bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":"sth. else","info":"this appears in-chat","serial":1,"max_serial":1}]"#
bob.get_webxdc_status_updates(bob_instance.id, None).await?,
r#"[{"payload":"sth. else","info":"this appears in-chat"}]"#
);
// Alice has a second device and also receives the info message there
@@ -1590,9 +1455,9 @@ sth_for_the = "future""#
assert!(info_msg.quoted_message(&alice2).await?.is_none());
assert_eq!(
alice2
.get_webxdc_status_updates(alice2_instance.id, StatusUpdateSerial(0))
.get_webxdc_status_updates(alice2_instance.id, None)
.await?,
r#"[{"payload":"sth. else","info":"this appears in-chat","serial":1,"max_serial":1}]"#
r#"[{"payload":"sth. else","info":"this appears in-chat"}]"#
);
Ok(())

View File

@@ -16,7 +16,7 @@ Seen status synchronization | IMAP CONDSTORE extension ([RFC 7162](https://
Authorization | OAuth2 ([RFC 6749](https://tools.ietf.org/html/rfc6749))
End-to-end encryption | [Autocrypt Level 1](https://autocrypt.org/level1.html), OpenPGP ([RFC 4880](https://tools.ietf.org/html/rfc4880)), Security Multiparts for MIME ([RFC 1847](https://tools.ietf.org/html/rfc1847)) and [“Mixed Up” Encryption repairing](https://tools.ietf.org/id/draft-dkg-openpgp-pgpmime-message-mangling-00.html)
Header encryption | [Protected Headers for Cryptographic E-mail](https://datatracker.ietf.org/doc/draft-autocrypt-lamps-protected-headers/)
Configuration assistance | [Autoconfigure](https://web.archive.org/web/20210402044801/https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration) and [Autodiscover](https://technet.microsoft.com/library/bb124251(v=exchg.150).aspx)
Configuration assistance | [Autoconfigure](https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration) and [Autodiscover](https://technet.microsoft.com/library/bb124251(v=exchg.150).aspx)
Messenger functions | [Chat-over-Email](https://github.com/deltachat/deltachat-core-rust/blob/master/spec.md#chat-mail-specification)
Detect mailing list | List-Id ([RFC 2919](https://tools.ietf.org/html/rfc2919)) and Precedence ([RFC 3834](https://tools.ietf.org/html/rfc3834))
User and chat colors | [XEP-0392](https://xmpp.org/extensions/xep-0392.html): Consistent Color Generation

View File

@@ -1,43 +1,43 @@
Return-Path: <bob@example.net>
Return-Path: <anonymous@example.org>
Delivered-To: anonymous@posteo.de
Received: from proxy02.posteo.name ([127.0.0.1])
by dovecot16.posteo.name (Dovecot) with LMTP id GaxcARout2HxiwMAchYRkQ
for <anonymous@posteo.de>; Mon, 13 Dec 2021 12:35:32 +0100
by dovecot16.posteo.name (Dovecot) with LMTP id Cp2uFxP1sWHbCQEAchYRkQ
for <anonymous@posteo.de>; Thu, 09 Dec 2021 13:25:38 +0100
Received: from proxy02.posteo.de ([127.0.0.1])
by proxy02.posteo.name (Dovecot) with LMTP id q/LiCqwqt2FMTQEAGFAyLg
; Mon, 13 Dec 2021 12:35:32 +0100
Received: from mailin05.posteo.de (unknown [10.0.1.5])
by proxy02.posteo.de (Postfix) with ESMTPS id 4JCKFJ1LCLz1214
for <anonymous@posteo.de>; Mon, 13 Dec 2021 12:35:32 +0100 (CET)
Received: from mx03.posteo.de (mailin05.posteo.de [127.0.0.1])
by mailin05.posteo.de (Postfix) with ESMTPS id 1B26420012
for <anonymous@posteo.de>; Mon, 13 Dec 2021 12:35:32 +0100 (CET)
by proxy02.posteo.name (Dovecot) with LMTP id MWsaCwrvsWG0wgEAGFAyLg
; Thu, 09 Dec 2021 13:25:38 +0100
Received: from mailin06.posteo.de (unknown [10.0.1.6])
by proxy02.posteo.de (Postfix) with ESMTPS id 4J8tXy0KkMz120l
for <anonymous@posteo.de>; Thu, 9 Dec 2021 13:25:38 +0100 (CET)
Received: from mx04.posteo.de (mailin06.posteo.de [127.0.0.1])
by mailin06.posteo.de (Postfix) with ESMTPS id F24DE215B8
for <anonymous@posteo.de>; Thu, 9 Dec 2021 13:25:37 +0100 (CET)
X-Virus-Scanned: amavisd-new at posteo.de
X-Spam-Flag: NO
X-Spam-Score: 0.011
X-Spam-Level:
X-Spam-Status: No, score=0.011 tagged_above=-1000 required=7
tests=[HTML_MESSAGE=0.001, T_POSTEO_TLSINY=0.01] autolearn=disabled
X-Posteo-Antispam-Signature: v=1; e=base64; a=aes-256-gcm; d=RzB41PpqvrD+cuxf3UAqQLiXQL4MHazHZcKeOYJw75deIl7zxtrLXqfAZCeq2IPKt/njRRONUbfuvNdvLxg4mBJ0Rnb53wFOKOqtEpTzxYoQff3yqBpGSohr0DBG26PyBHi7ba/7
X-Posteo-Antispam-Signature: v=1; e=base64; a=aes-256-gcm; d=27yedFdXeAzOobR4x685XJ/5e6WQmX8PP5pSnOlGU2a9Ismhk38wb5AS44xh1yeL5PUxla78UEsHwGkPR0IyPRlHWaLMFLd5CJZN3GzFfrj/2CuB+cd1hOLpp9hRmCebc3rchuDr
Authentication-Results: posteo.de; dmarc=none (p=none dis=none) header.from=example.org
X-Posteo-TLS-Received-Status: TLSv1.2
Received: from mail.example.org (mail.example.org [0.0.0.0])
by mx03.posteo.de (Postfix) with ESMTPS id 4JCKFH2ZM7zyx0
for <alice@example.org>; Mon, 13 Dec 2021 12:35:31 +0100 (CET)
Received: from [192.168.1.11] (port=27040 helo=mail.example.org)
by mx04.posteo.de (Postfix) with ESMTPS id 4J8tXx38vRz10yw
for <anonymous@posteo.at>; Thu, 9 Dec 2021 13:25:37 +0100 (CET)
Received: from [192.168.1.11] (port=22105 helo=mail.example.org)
by mail.example.org with esmtps (TLS1.2) tls TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384
(Exim 4.94.2)
(envelope-from <bob@example.net>)
id 1mwjc6-0003hM-2K
for alice@example.org; Mon, 13 Dec 2021 12:35:26 +0100
From: Anonymous_2 <bob@example.net>
To: Anonymous_1 <alice@example.org>
(envelope-from <anonymous@example.org>)
id 1mvIUG-0007VC-2U
for anonymous@posteo.at; Thu, 09 Dec 2021 13:25:24 +0100
From: Anonymous <anonymous@example.org>
To: Anonymous <anonymous@posteo.at>
Subject: Gelesen: Test message
Thread-Topic: Test message
Thread-Index: AQHX8BVZ9B3+kB6CNUCtJ9eQIONSNawwSqpi
Date: Mon, 13 Dec 2021 11:35:26 +0000
Message-ID: <59b1d0c94a8d4834b7ab779a76647d44@mail.example.org>
In-Reply-To: <d5904dc344eeb5deaf9bb44603f0c716@posteo.de>
Thread-Index: AQHX7Dt/+5f88Aokk0KrqG0hbF8dN6wqFvxh
Date: Thu, 9 Dec 2021 12:25:24 +0000
Message-ID: <1711fc3548cd4b2699ccd4fffac17713@anonymous>
In-Reply-To: <75dd051097b02468183707ad0dd62ebd@posteo.de>
Accept-Language: de-AT, de-DE, en-US
Content-Language: de-DE
X-MS-Has-Attach:
@@ -45,29 +45,29 @@ X-MS-TNEF-Correlator:
x-ms-exchange-transport-fromentityheader: Hosted
x-originating-ip: [192.168.120.215]
Content-Type: multipart/report;
boundary="_000_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_";
boundary="_000_1711fc3548cd4b2699ccd4fffac17713anonymous_";
report-type=disposition-notification
MIME-Version: 1.0
--_000_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_
--_000_1711fc3548cd4b2699ccd4fffac17713anonymous_
Content-Type: multipart/alternative;
boundary="_002_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_"
boundary="_002_1711fc3548cd4b2699ccd4fffac17713anonymous_"
--_002_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_
--_002_1711fc3548cd4b2699ccd4fffac17713anonymous_
Content-Type: text/plain; charset="iso-8859-1"
Content-Transfer-Encoding: quoted-printable
Ihre Nachricht
An: Anonymous_2
An: Anonymous
Betreff: Test message
Gesendet: Montag, 13. Dezember 2021 12:33:58 (UTC+01:00) Amsterdam, Berl=
in, Bern, Rom, Stockholm, Wien
Gesendet: Mittwoch, 8. Dezember 2021 14:57:05 (UTC+01:00) Amsterdam, Ber=
lin, Bern, Rom, Stockholm, Wien
wurde am Montag, 13. Dezember 2021 12:34:40 (UTC+01:00) Amsterdam, Berlin,=
Bern, Rom, Stockholm, Wien gelesen.
wurde am Donnerstag, 9. Dezember 2021 13:24:34 (UTC+01:00) Amsterdam, Berl=
in, Bern, Rom, Stockholm, Wien gelesen.
--_002_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_
--_002_1711fc3548cd4b2699ccd4fffac17713anonymous_
Content-Type: text/html; charset="iso-8859-1"
Content-Transfer-Encoding: quoted-printable
@@ -83,26 +83,26 @@ ding-left: 4pt; border-left: #800000 2px solid; } --></style>
<font size=3D"2"><span style=3D"font-size:10pt;">
<div class=3D"PlainText">Ihre Nachricht <br>
<br>
&nbsp;&nbsp; An: Anonymous_2<br>
&nbsp;&nbsp; An: Anonymous<br>
&nbsp;&nbsp; Betreff: Test message<br>
&nbsp;&nbsp; Gesendet: Montag, 13. Dezember 2021 12:33:58 (UTC&#43;01:00) A=
msterdam, Berlin, Bern, Rom, Stockholm, Wien<br>
&nbsp;&nbsp; Gesendet: Mittwoch, 8. Dezember 2021 14:57:05 (UTC&#43;01:00) =
Amsterdam, Berlin, Bern, Rom, Stockholm, Wien<br>
<br>
&nbsp;wurde am Montag, 13. Dezember 2021 12:34:40 (UTC&#43;01:00) Amsterdam=
, Berlin, Bern, Rom, Stockholm, Wien gelesen.</div>
&nbsp;wurde am Donnerstag, 9. Dezember 2021 13:24:34 (UTC&#43;01:00) Amster=
dam, Berlin, Bern, Rom, Stockholm, Wien gelesen.</div>
</span></font>
</body>
</html>
--_002_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_--
--_002_1711fc3548cd4b2699ccd4fffac17713anonymous_--
--_000_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_
--_000_1711fc3548cd4b2699ccd4fffac17713anonymous_
Content-Type: message/disposition-notification
Final-recipient: RFC822; bob@example.net
Final-recipient: RFC822; anonymous@example.org
Disposition: automatic-action/MDN-sent-automatically; displayed
X-MSExch-Correlation-Key: nf7/jgN6Qk+WzsrkY5s9WA==
X-Display-Name: Anonymous_2
X-MSExch-Correlation-Key: coNC5vaCQkiAOjek1v1Uew==
X-Display-Name: Anonymous
--_000_59b1d0c94a8d4834b7ab779a76647d44mailexampleorg_--
--_000_1711fc3548cd4b2699ccd4fffac17713anonymous_--

View File

@@ -1,34 +0,0 @@
Received: We have to put a Received header here. Otherwise, the message would be ignored
because DC thinks it's a draft, and the test fails.
Alternatively, we could configure the Sentobox folder in the test.
MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary="=_3293d145f71bf71c4bb97415536759d6"
Date: Mon, 13 Dec 2021 12:33:58 +0100
From: Anonymous_1 <alice@example.org>
To: Anonymous_2 <bob@example.net>
Subject: Test message
Return-Receipt-To: Anonymous_1 <alice@example.org>
Disposition-Notification-To: Anonymous_1 <alice@example.org>
Message-ID: <d5904dc344eeb5deaf9bb44603f0c716@posteo.de>
X-Sender: alice@example.org
User-Agent: Posteo Webmail
--=_3293d145f71bf71c4bb97415536759d6
Content-Transfer-Encoding: 7bit
Content-Type: text/plain; charset=US-ASCII
This is a test!
Best regards
--=_3293d145f71bf71c4bb97415536759d6
Content-Transfer-Encoding: quoted-printable
Content-Type: text/html; charset=UTF-8
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN">
<html><body style=3D'font-size: 10pt; font-family: Verdana,Geneva,sans-seri=
f'>
This is a test!<br /><br />Best regards
</body></html>
--=_3293d145f71bf71c4bb97415536759d6--