Compare commits

..

1 Commits

Author SHA1 Message Date
Hocuri
4f05de075f Fix two tests (Tokio) 2022-06-29 17:58:32 +02:00
113 changed files with 1223 additions and 5785 deletions

View File

@@ -118,7 +118,7 @@ jobs:
- name: install python
if: ${{ matrix.python }}
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python }}
@@ -131,7 +131,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: build
args: -p deltachat_ffi --features jsonrpc
args: -p deltachat_ffi
- name: run python tests
if: ${{ matrix.python }}
@@ -141,18 +141,3 @@ jobs:
DCC_RS_DEV: ${{ github.workspace }}
working-directory: python
run: tox -e lint,mypy,doc,py3
- name: install pypy
if: ${{ matrix.python }}
uses: actions/setup-python@v4
with:
python-version: 'pypy${{ matrix.python }}'
- name: run pypy tests
if: ${{ matrix.python }}
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
DCC_RS_TARGET: debug
DCC_RS_DEV: ${{ github.workspace }}
working-directory: python
run: tox -e pypy3

View File

@@ -1,45 +0,0 @@
name: JSON-RPC API Test
on:
push:
branches: [master]
pull_request:
branches: [master]
env:
CARGO_TERM_COLOR: always
jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 16.x
uses: actions/setup-node@v1
with:
node-version: 16.x
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- name: Add Rust cache
uses: Swatinem/rust-cache@v1.3.0
- name: npm install
run: |
cd deltachat-jsonrpc/typescript
npm install
- name: Build TypeScript, run Rust tests, generate bindings
run: |
cd deltachat-jsonrpc/typescript
npm run build
- name: Run integration tests
run: |
cd deltachat-jsonrpc/typescript
npm run test
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
- name: Run linter
run: |
cd deltachat-jsonrpc/typescript
npm run prettier:check

View File

@@ -40,17 +40,3 @@ node/old_docs.md
.vscode/
.github/
node/.prettierrc.yml
deltachat-jsonrpc/TODO.md
deltachat-jsonrpc/README.MD
deltachat-jsonrpc/.gitignore
deltachat-jsonrpc/typescript/.gitignore
deltachat-jsonrpc/typescript/.prettierignore
deltachat-jsonrpc/typescript/accounts/
deltachat-jsonrpc/typescript/index.html
deltachat-jsonrpc/typescript/node-demo.js
deltachat-jsonrpc/typescript/report_api_coverage.mjs
deltachat-jsonrpc/typescript/test
deltachat-jsonrpc/typescript/example.ts
.DS_Store

View File

@@ -2,33 +2,12 @@
## Unreleased
### API-Changes
- jsonrpc api over websocket server (basically a new api next to the cffi) #3463
- jsonrpc methods in cffi #3463:
- `dc_jsonrpc_instance_t* dc_jsonrpc_init(dc_accounts_t* account_manager);`
- `void dc_jsonrpc_unref(dc_jsonrpc_instance_t* jsonrpc_instance);`
- `void dc_jsonrpc_request(dc_jsonrpc_instance_t* jsonrpc_instance, char* request);`
- `char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);`
- node: json rpc methods #3463:
- `AccountManager.prototype.startJsonRpcHandler(callback: ((response: string) => void)): void`
- `AccountManager.prototype.jsonRpcRequest(message: string): void`
### Changes
- Implemented "Automatic e-mail address Porting" (AEAP). You can
configure a new address in DC now, and when receivers get messages
they will automatically recognize your moving to a new address. #3385
- added a JSON RPC API, accessible through a WebSocket server, the CFFI bindings and the Node.js bindings #3463
- switch from `async-std` to `tokio` as the async runtime #3449
- upgrade to `pgp@0.8.0` #3467
- add IMAP ID extension support #3468
- configure DeltaChat folder by selecting it, so it is configured even if not LISTed #3371
- build PyPy wheels #6683
- improve default error if NDN does not provide an error #3456
### Fixes
- mailing list: remove square-brackets only for first name #3452
- do not use footers from mailinglists as the contact status #3460
- don't ignore KML parsing errors #3473
## 1.87.0
@@ -43,7 +22,7 @@
- limit the rate of webxdc update sending #3417
### Fixes
- set a default error if NDN does not provide an error #3410
- set a default error if NDN does not provide an error
- python: avoid exceptions when messages/contacts/chats are compared with `None`
- node: wait for the event loop to stop before destroying contexts #3431 #3451
- emit configuration errors via event on failure #3433

View File

@@ -21,7 +21,7 @@ add_custom_command(
PREFIX=${CMAKE_INSTALL_PREFIX}
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
${CARGO} build --release --no-default-features --features jsonrpc
${CARGO} build --release --no-default-features
# Build in `deltachat-ffi` directory instead of using
# `--package deltachat_ffi` to avoid feature resolver version

715
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -20,7 +20,7 @@ deltachat_derive = { path = "./deltachat_derive" }
ansi_term = { version = "0.12.1", optional = true }
anyhow = "1"
async-imap = { git = "https://github.com/async-email/async-imap", branch = "master", default-features = false, features = ["runtime-tokio"] }
async-imap = { version = "0.6", default-features = false, features = ["runtime-tokio"] }
async-native-tls = { version = "0.4", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.5", default-features = false, features = ["smtp-transport", "socks5", "runtime-tokio"] }
trust-dns-resolver = "0.21"
@@ -48,12 +48,12 @@ num-derive = "0.3"
num-traits = "0.2"
once_cell = "1.12.0"
percent-encoding = "2.0"
pgp = { version = "0.8", default-features = false }
pgp = { version = "0.7", default-features = false }
pretty_env_logger = { version = "0.4", optional = true }
quick-xml = "0.23"
r2d2 = "0.8"
r2d2_sqlite = "0.20"
rand = "0.8"
rand = "0.7"
regex = "1.5"
rusqlite = { version = "0.27", features = ["sqlcipher"] }
rust-hsluv = "0.1"
@@ -89,13 +89,11 @@ log = "0.4"
pretty_env_logger = "0.4"
proptest = { version = "1", default-features = false, features = ["std"] }
tempfile = "3"
tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"] }
[workspace]
members = [
"deltachat-ffi",
"deltachat_derive",
"deltachat-jsonrpc"
]
[[example]]

View File

@@ -4,8 +4,8 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::{
config::Config,
context::Context,
dc_receive_imf::dc_receive_imf,
imex::{imex, ImexMode},
receive_imf::receive_imf,
Events,
};
use tempfile::tempdir;
@@ -30,7 +30,7 @@ Hello {i}",
i = i,
i_dec = i - 1,
);
receive_imf(&context, black_box(imf_raw.as_bytes()), false)
dc_receive_imf(&context, black_box(imf_raw.as_bytes()), false)
.await
.unwrap();
}

View File

@@ -16,7 +16,6 @@ crate-type = ["cdylib", "staticlib"]
[dependencies]
deltachat = { path = "../", default-features = false }
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", optional = true }
libc = "0.2"
human-panic = "1"
num-traits = "0.2"
@@ -31,5 +30,4 @@ once_cell = "1.12.0"
default = ["vendored"]
vendored = ["deltachat/vendored"]
nightly = ["deltachat/nightly"]
jsonrpc = ["deltachat-jsonrpc"]

View File

@@ -1,3 +1,4 @@
use std::io::Write;
use std::path::PathBuf;
use std::{env, fs};
@@ -27,9 +28,8 @@ fn main() {
);
fs::create_dir_all(target_path.join("pkgconfig")).unwrap();
fs::write(
target_path.join("pkgconfig").join("deltachat.pc"),
pkg_config.as_bytes(),
)
.unwrap();
fs::File::create(target_path.join("pkgconfig").join("deltachat.pc"))
.unwrap()
.write_all(pkg_config.as_bytes())
.unwrap();
}

View File

@@ -23,7 +23,7 @@ typedef struct _dc_provider dc_provider_t;
typedef struct _dc_event dc_event_t;
typedef struct _dc_event_emitter dc_event_emitter_t;
typedef struct _dc_accounts_event_emitter dc_accounts_event_emitter_t;
typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t;
/**
* @mainpage Getting started
@@ -5178,55 +5178,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*/
/**
* @class dc_jsonrpc_instance_t
*
* Opaque object for using the json rpc api from the cffi bindings.
*/
/**
* Create the jsonrpc instance that is used to call the jsonrpc.
*
* @memberof dc_accounts_t
* @param account_manager The accounts object as created by dc_accounts_new().
* @return Returns the jsonrpc instance, NULL on errors.
* Must be freed using dc_jsonrpc_unref() after usage.
*
*/
dc_jsonrpc_instance_t* dc_jsonrpc_init(dc_accounts_t* account_manager);
/**
* Free a jsonrpc instance.
*
* @memberof dc_jsonrpc_instance_t
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
* If NULL is given, nothing is done and an error is logged.
*/
void dc_jsonrpc_unref(dc_jsonrpc_instance_t* jsonrpc_instance);
/**
* Makes an asynchronous jsonrpc request,
* returns immediately and once the result is ready it can be retrieved via dc_jsonrpc_next_response()
* the jsonrpc specification defines an invocation id that can then be used to match request and response.
*
* @memberof dc_jsonrpc_instance_t
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
* @param request JSON-RPC request as string
*/
void dc_jsonrpc_request(dc_jsonrpc_instance_t* jsonrpc_instance, const char* request);
/**
* Get the next json_rpc response, blocks until there is a new event, so call this in a loop from a thread.
*
* @memberof dc_jsonrpc_instance_t
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
* @return JSON-RPC response as string, must be freed using dc_str_unref() after usage.
* If NULL is returned, the accounts_t belonging to the jsonrpc instance is unref'd and no more events will come;
* in this case, free the jsonrpc instance using dc_jsonrpc_unref().
*/
char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
/**
* @class dc_event_emitter_t
*

View File

@@ -1,4 +1,4 @@
#![warn(unused, clippy::all)]
#![deny(unused, clippy::all)]
#![allow(
non_camel_case_types,
non_snake_case,
@@ -19,7 +19,6 @@ use std::future::Future;
use std::ops::Deref;
use std::ptr;
use std::str::FromStr;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use anyhow::Context as _;
@@ -396,7 +395,7 @@ pub unsafe extern "C" fn dc_get_oauth2_url(
let redirect = to_string_lossy(redirect);
block_on(async move {
match oauth2::get_oauth2_url(ctx, &addr, &redirect)
match oauth2::dc_get_oauth2_url(ctx, &addr, &redirect)
.await
.log_err(ctx, "dc_get_oauth2_url failed")
{
@@ -740,7 +739,7 @@ pub unsafe extern "C" fn dc_preconfigure_keypair(
}
let ctx = &*context;
block_on(async move {
let addr = tools::EmailAddress::new(&to_string_lossy(addr))?;
let addr = dc_tools::EmailAddress::new(&to_string_lossy(addr))?;
let public = key::SignedPublicKey::from_asc(&to_string_lossy(public_data))?.0;
let secret = key::SignedSecretKey::from_asc(&to_string_lossy(secret_data))?.0;
let keypair = key::KeyPair {
@@ -2245,7 +2244,7 @@ pub unsafe extern "C" fn dc_get_securejoin_qr(
Some(ChatId::new(chat_id))
};
block_on(securejoin::get_securejoin_qr(ctx, chat_id))
block_on(securejoin::dc_get_securejoin_qr(ctx, chat_id))
.unwrap_or_else(|_| "".to_string())
.strdup()
}
@@ -2283,7 +2282,7 @@ pub unsafe extern "C" fn dc_join_securejoin(
let ctx = &*context;
block_on(async move {
securejoin::join_securejoin(ctx, &to_string_lossy(qr))
securejoin::dc_join_securejoin(ctx, &to_string_lossy(qr))
.await
.map(|chatid| chatid.to_u32())
.log_err(ctx, "failed dc_join_securejoin() call")
@@ -4109,11 +4108,11 @@ pub unsafe extern "C" fn dc_provider_unref(provider: *mut dc_provider_t) {
/// Reader-writer lock wrapper for accounts manager to guarantee thread safety when using
/// `dc_accounts_t` in multiple threads at once.
pub struct AccountsWrapper {
inner: Arc<RwLock<Accounts>>,
inner: RwLock<Accounts>,
}
impl Deref for AccountsWrapper {
type Target = Arc<RwLock<Accounts>>;
type Target = RwLock<Accounts>;
fn deref(&self) -> &Self::Target {
&self.inner
@@ -4122,7 +4121,7 @@ impl Deref for AccountsWrapper {
impl AccountsWrapper {
fn new(accounts: Accounts) -> Self {
let inner = Arc::new(RwLock::new(accounts));
let inner = RwLock::new(accounts);
Self { inner }
}
}
@@ -4438,89 +4437,3 @@ pub unsafe extern "C" fn dc_accounts_get_next_event(
.map(|ev| Box::into_raw(Box::new(ev)))
.unwrap_or_else(ptr::null_mut)
}
#[cfg(feature = "jsonrpc")]
mod jsonrpc {
use super::*;
use deltachat_jsonrpc::api::DeltaChatApiV0;
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
pub struct dc_jsonrpc_instance_t {
receiver: OutReceiver,
handle: RpcSession<DeltaChatApiV0>,
}
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_init(
account_manager: *mut dc_accounts_t,
api_version: *const libc::c_char,
) -> *mut dc_jsonrpc_instance_t {
if account_manager.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_init()");
return ptr::null_mut();
}
let api_version = to_string_lossy(api_version);
let rpc_api = match api_version.as_str() {
"v0" => {
deltachat_jsonrpc::api::DeltaChatApiV0::from_arc((*account_manager).inner.clone())
}
version => {
eprintln!(
"Error initializing JSON-RPC API: API version {} is not supported.",
version
);
return ptr::null_mut();
}
};
let (request_handle, receiver) = RpcClient::new();
let handle = RpcSession::new(request_handle, rpc_api);
let instance = dc_jsonrpc_instance_t { receiver, handle };
Box::into_raw(Box::new(instance))
}
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_unref(jsonrpc_instance: *mut dc_jsonrpc_instance_t) {
if jsonrpc_instance.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_unref()");
return;
}
Box::from_raw(jsonrpc_instance);
}
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_request(
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
request: *const libc::c_char,
) {
if jsonrpc_instance.is_null() || request.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_request()");
return;
}
let api = &*jsonrpc_instance;
let handle = &api.handle;
let request = to_string_lossy(request);
spawn(async move {
handle.handle_incoming(&request).await;
});
}
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_next_response(
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
) -> *mut libc::c_char {
if jsonrpc_instance.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_next_response()");
return ptr::null_mut();
}
let api = &*jsonrpc_instance;
block_on(api.receiver.recv())
.map(|result| serde_json::to_string(&result).unwrap_or_default().strdup())
.unwrap_or(ptr::null_mut())
}
}

View File

@@ -55,7 +55,7 @@ pub(crate) enum CStringError {
/// # Example
///
/// ```
/// use deltachat::tools::{dc_strdup, OsStrExt};
/// use deltachat::dc_tools::{dc_strdup, OsStrExt};
/// let path = std::path::Path::new("/some/path");
/// let path_c = path.to_c_string().unwrap();
/// unsafe {

View File

@@ -1,3 +0,0 @@
accounts/
.cargo

View File

@@ -1,39 +0,0 @@
[package]
name = "deltachat-jsonrpc"
version = "1.86.0"
description = "DeltaChat JSON-RPC API"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021"
default-run = "deltachat-jsonrpc-server"
license = "MPL-2.0"
[[bin]]
name = "deltachat-jsonrpc-server"
path = "src/webserver.rs"
required-features = ["webserver"]
[dependencies]
anyhow = "1"
deltachat = { path = ".." }
num-traits = "0.2"
serde = { version = "1.0", features = ["derive"] }
tempfile = "3.3.0"
log = "0.4"
async-channel = { version = "1.6.1" }
futures = { version = "0.3.19" }
serde_json = "1.0.75"
yerpc = { version = "^0.3.1", features = ["anyhow_expose"] }
typescript-type-def = { version = "0.5.3", features = ["json_value"] }
tokio = { version = "1.19.2" }
# optional dependencies
axum = { version = "0.5.9", optional = true, features = ["ws"] }
env_logger = { version = "0.9.0", optional = true }
[dev-dependencies]
tokio = { version = "1.19.2", features = ["full", "rt-multi-thread"] }
[features]
default = []
webserver = ["env_logger", "axum", "tokio/full", "yerpc/support-axum"]

View File

@@ -1,123 +0,0 @@
# deltachat-jsonrpc
This crate provides a [JSON-RPC 2.0](https://www.jsonrpc.org/specification) interface to DeltaChat.
The JSON-RPC API is exposed in two fashions:
* A executable that exposes the JSON-RPC API through a [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) server running on localhost.
* The JSON-RPC API can also be called through the [C FFI](../deltachat-ffi). The C FFI needs to be built with the `jsonrpc` feature. It will then expose the functions `dc_jsonrpc_init`, `dc_jsonrpc_request`, `dc_jsonrpc_next_response` and `dc_jsonrpc_unref`. See the docs in the [header file](../deltachat-ffi/deltachat.h) for details.
We also include a JavaScript and TypeScript client for the JSON-RPC API. The source for this is in the [`typescript`](typescript) folder. The client can easily be used with the WebSocket server to build DeltaChat apps for web browsers or Node.js. See the [examples](typescript/example) for details.
## Usage
#### Running the WebSocket server
From within this folder, you can start the WebSocket server with the following command:
```sh
cargo run --features webserver
```
If you want to use the server in a production setup, first build it in release mode:
```sh
cargo build --features webserver --release
```
You will then find the `deltachat-jsonrpc-server` executable in your `target/release` folder.
The executable currently does not support any command-line arguments. By default, once started it will accept WebSocket connections on `ws://localhost:20808/ws`. It will store the persistent configuration and databases in a `./accounts` folder relative to the directory from where it is started.
The server can be configured with environment variables:
|variable|default|description|
|-|-|-|
|`DC_PORT`|`20808`|port to listen on|
|`DC_ACCOUNTS_PATH`|`./accounts`|path to storage directory|
If you are targetting other architectures (like KaiOS or Android), the webserver binary can be cross-compiled easily with [rust-cross](https://github.com/cross-rs/cross):
```sh
cross build --features=webserver --target armv7-linux-androideabi --release
```
#### Using the TypeScript/JavaScript client
The package includes a JavaScript/TypeScript client which is partially auto-generated through the JSON-RPC library used by this crate ([yerpc](https://github.com/Frando/yerpc/)). Find the source in the [`typescript`](typescript) folder.
To use it locally, first install the dependencies and compile the TypeScript code to JavaScript:
```sh
cd typescript
npm install
npm run build
```
The JavaScript client is not yet published on NPM (but will likely be soon). Currently, it is recommended to vendor the bundled build. After running `npm run build` as documented above, there will be a file `dist/deltachat.bundle.js`. This is an ESM module containing all dependencies. Copy this file to your project and import the DeltaChat class.
```typescript
import { DeltaChat } from './deltachat.bundle.js'
const dc = new DeltaChat('ws://localhost:20808/ws')
const accounts = await dc.rpc.getAllAccounts()
console.log('accounts', accounts)
```
A script is included to build autogenerated documentation, which includes all RPC methods:
```sh
cd typescript
npm run docs
```
Then open the [`typescript/docs`](typescript/docs) folder in a web browser.
## Development
#### Running the example app
We include a small demo web application that talks to the WebSocket server. It can be used for testing. Feel invited to expand this.
```sh
cd typescript
npm run build
npm run example:build
npm run example:start
```
Then, open [`http://localhost:8080/example.html`](http://localhost:8080/example.html) in a web browser.
Run `npm run example:dev` to live-rebuild the example app when files changes.
### Testing
The crate includes both a basic Rust smoke test and more featureful integration tests that use the TypeScript client.
#### Rust tests
To run the Rust test, use this command:
```
cargo test
```
#### TypeScript tests
```
cd typescript
npm run test
```
This will build the `deltachat-jsonrpc-server` binary and then run a test suite against the WebSocket server.
The test suite includes some tests that need online connectivity and a way to create test email accounts. To run these tests, talk to DeltaChat developers to get a token for the `testrun.org` service, or use a local instance of [`mailadm`](https://github.com/deltachat/docker-mailadm).
Then, set the `DCC_NEW_TMP_EMAIL` environment variable to your mailadm token before running the tests.
```
DCC_NEW_TMP_EMAIL=https://testrun.org/new_email?t=yourtoken npm run test
```
#### Test Coverage
Running `npm run test` will report test coverage. For the coverage to be accurate the online tests need to be run.
> If you are offline and want to see the coverage results anyway (even though they are inaccurate), you can bypass the errors of the online tests by setting the `COVERAGE_OFFLINE=1` environment variable.
A summary of the coverage will be reported in the terminal after the test run. Open `coverage/index.html` in a web browser for a detailed report.

View File

@@ -1,28 +0,0 @@
# TODO
- [ ] different test type to simulate two devices: to test autocrypt_initiate_key_transfer & autocrypt_continue_key_transfer
## MVP - Websocket server&client
For kaiOS and other experiments, like a deltachat "web" over network from an android phone.
- [ ] coverage for a majority of the API
- [ ] Blobs served
- [ ] Blob upload (for attachments, setting profile-picture, importing backup and so on)
- [ ] other way blobs can be addressed when using websocket vs. jsonrpc over dc-node
- [ ] Web push API? At least some kind of notification hook closure this lib can accept.
### Other Ideas for the Websocket server
- [ ] make sure there can only be one connection at a time to the ws
- why? , it could give problems if its commanded from multiple connections
- [ ] encrypted connection?
- [ ] authenticated connection?
- [ ] Look into unit-testing for the proc macros?
- [ ] proc macro taking over doc comments to generated typescript file
## Desktop Apis
Incomplete todo for desktop api porting, just some remainders for points that might need more work:
- [ ] manual start/stop io functions in the api for context and accounts, so "not syncing all accounts" can still be done in desktop -> webserver should then not do start io on all accounts by default

View File

@@ -1,157 +0,0 @@
use deltachat::{Event, EventType};
use serde::Serialize;
use serde_json::{json, Value};
use typescript_type_def::TypeDef;
pub fn event_to_json_rpc_notification(event: Event) -> Value {
let (field1, field2): (Value, Value) = match &event.typ {
// events with a single string in field1
EventType::Info(txt)
| EventType::SmtpConnected(txt)
| EventType::ImapConnected(txt)
| EventType::SmtpMessageSent(txt)
| EventType::ImapMessageDeleted(txt)
| EventType::ImapMessageMoved(txt)
| EventType::NewBlobFile(txt)
| EventType::DeletedBlobFile(txt)
| EventType::Warning(txt)
| EventType::Error(txt)
| EventType::ErrorSelfNotInGroup(txt) => (json!(txt), Value::Null),
EventType::ImexFileWritten(path) => (json!(path.to_str()), Value::Null),
// single number
EventType::MsgsNoticed(chat_id) | EventType::ChatModified(chat_id) => {
(json!(chat_id), Value::Null)
}
EventType::ImexProgress(progress) => (json!(progress), Value::Null),
// both fields contain numbers
EventType::MsgsChanged { chat_id, msg_id }
| EventType::IncomingMsg { chat_id, msg_id }
| EventType::MsgDelivered { chat_id, msg_id }
| EventType::MsgFailed { chat_id, msg_id }
| EventType::MsgRead { chat_id, msg_id } => (json!(chat_id), json!(msg_id)),
EventType::ChatEphemeralTimerModified { chat_id, timer } => (json!(chat_id), json!(timer)),
EventType::SecurejoinInviterProgress {
contact_id,
progress,
}
| EventType::SecurejoinJoinerProgress {
contact_id,
progress,
} => (json!(contact_id), json!(progress)),
// field 1 number or null
EventType::ContactsChanged(maybe_number) | EventType::LocationChanged(maybe_number) => (
match maybe_number {
Some(number) => json!(number),
None => Value::Null,
},
Value::Null,
),
// number and maybe string
EventType::ConfigureProgress { progress, comment } => (
json!(progress),
match comment {
Some(content) => json!(content),
None => Value::Null,
},
),
EventType::ConnectivityChanged => (Value::Null, Value::Null),
EventType::SelfavatarChanged => (Value::Null, Value::Null),
EventType::WebxdcStatusUpdate {
msg_id,
status_update_serial,
} => (json!(msg_id), json!(status_update_serial)),
};
let id: EventTypeName = event.typ.into();
json!({
"id": id,
"contextId": event.id,
"field1": field1,
"field2": field2
})
}
#[derive(Serialize, TypeDef)]
pub enum EventTypeName {
Info,
SmtpConnected,
ImapConnected,
SmtpMessageSent,
ImapMessageDeleted,
ImapMessageMoved,
NewBlobFile,
DeletedBlobFile,
Warning,
Error,
ErrorSelfNotInGroup,
MsgsChanged,
IncomingMsg,
MsgsNoticed,
MsgDelivered,
MsgFailed,
MsgRead,
ChatModified,
ChatEphemeralTimerModified,
ContactsChanged,
LocationChanged,
ConfigureProgress,
ImexProgress,
ImexFileWritten,
SecurejoinInviterProgress,
SecurejoinJoinerProgress,
ConnectivityChanged,
SelfavatarChanged,
WebxdcStatusUpdate,
}
impl From<EventType> for EventTypeName {
fn from(event: EventType) -> Self {
use EventTypeName::*;
match event {
EventType::Info(_) => Info,
EventType::SmtpConnected(_) => SmtpConnected,
EventType::ImapConnected(_) => ImapConnected,
EventType::SmtpMessageSent(_) => SmtpMessageSent,
EventType::ImapMessageDeleted(_) => ImapMessageDeleted,
EventType::ImapMessageMoved(_) => ImapMessageMoved,
EventType::NewBlobFile(_) => NewBlobFile,
EventType::DeletedBlobFile(_) => DeletedBlobFile,
EventType::Warning(_) => Warning,
EventType::Error(_) => Error,
EventType::ErrorSelfNotInGroup(_) => ErrorSelfNotInGroup,
EventType::MsgsChanged { .. } => MsgsChanged,
EventType::IncomingMsg { .. } => IncomingMsg,
EventType::MsgsNoticed(_) => MsgsNoticed,
EventType::MsgDelivered { .. } => MsgDelivered,
EventType::MsgFailed { .. } => MsgFailed,
EventType::MsgRead { .. } => MsgRead,
EventType::ChatModified(_) => ChatModified,
EventType::ChatEphemeralTimerModified { .. } => ChatEphemeralTimerModified,
EventType::ContactsChanged(_) => ContactsChanged,
EventType::LocationChanged(_) => LocationChanged,
EventType::ConfigureProgress { .. } => ConfigureProgress,
EventType::ImexProgress(_) => ImexProgress,
EventType::ImexFileWritten(_) => ImexFileWritten,
EventType::SecurejoinInviterProgress { .. } => SecurejoinInviterProgress,
EventType::SecurejoinJoinerProgress { .. } => SecurejoinJoinerProgress,
EventType::ConnectivityChanged => ConnectivityChanged,
EventType::SelfavatarChanged => SelfavatarChanged,
EventType::WebxdcStatusUpdate { .. } => WebxdcStatusUpdate,
}
}
}
#[cfg(test)]
#[test]
fn generate_events_ts_types_definition() {
let events = {
let mut buf = Vec::new();
let options = typescript_type_def::DefinitionFileOptions {
root_namespace: None,
..typescript_type_def::DefinitionFileOptions::default()
};
typescript_type_def::write_definition_file::<_, EventTypeName>(&mut buf, options).unwrap();
String::from_utf8(buf).unwrap()
};
std::fs::write("typescript/generated/events.ts", events).unwrap();
}

View File

@@ -1,683 +0,0 @@
use anyhow::{anyhow, bail, Context, Result};
use deltachat::{
chat::{get_chat_media, get_chat_msgs, ChatId},
chatlist::Chatlist,
config::Config,
contact::{may_be_valid_addr, Contact, ContactId},
context::get_info,
message::{Message, MsgId, Viewtype},
provider::get_provider_info,
qr,
webxdc::StatusUpdateSerial,
};
use std::collections::BTreeMap;
use std::sync::Arc;
use std::{collections::HashMap, str::FromStr};
use tokio::sync::RwLock;
use yerpc::rpc;
pub use deltachat::accounts::Accounts;
pub mod events;
pub mod types;
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
use types::account::Account;
use types::chat::FullChat;
use types::chat_list::ChatListEntry;
use types::contact::ContactObject;
use types::message::MessageObject;
use types::provider_info::ProviderInfo;
use types::webxdc::WebxdcMessageInfo;
use self::types::message::MessageViewtype;
#[derive(Clone, Debug)]
pub struct DeltaChatApiV0 {
pub(crate) accounts: Arc<RwLock<Accounts>>,
}
impl DeltaChatApiV0 {
pub fn new(accounts: Accounts) -> Self {
DeltaChatApiV0 {
accounts: Arc::new(RwLock::new(accounts)),
}
}
#[allow(dead_code)]
pub fn from_arc(accounts: Arc<RwLock<Accounts>>) -> Self {
DeltaChatApiV0 { accounts }
}
async fn get_context(&self, id: u32) -> Result<deltachat::context::Context> {
let sc = self
.accounts
.read()
.await
.get_account(id)
.await
.ok_or_else(|| anyhow!("account with id {} not found", id))?;
Ok(sc)
}
}
#[rpc(all_positional, ts_outdir = "typescript/generated")]
impl DeltaChatApiV0 {
// ---------------------------------------------
// Misc top level functions
// ---------------------------------------------
/// Check if an email address is valid.
async fn check_email_validity(&self, email: String) -> bool {
may_be_valid_addr(&email)
}
/// Get general system info.
async fn get_system_info(&self) -> BTreeMap<&'static str, String> {
get_info()
}
// ---------------------------------------------
// Account Management
// ---------------------------------------------
async fn add_account(&self) -> Result<u32> {
self.accounts.write().await.add_account().await
}
async fn remove_account(&self, account_id: u32) -> Result<()> {
self.accounts.write().await.remove_account(account_id).await
}
async fn get_all_account_ids(&self) -> Vec<u32> {
self.accounts.read().await.get_all().await
}
/// Select account id for internally selected state.
/// TODO: Likely this is deprecated as all methods take an account id now.
async fn select_account(&self, id: u32) -> Result<()> {
self.accounts.write().await.select_account(id).await
}
/// Get the selected account id of the internal state..
/// TODO: Likely this is deprecated as all methods take an account id now.
async fn get_selected_account_id(&self) -> Option<u32> {
self.accounts.read().await.get_selected_account_id().await
}
/// Get a list of all configured accounts.
async fn get_all_accounts(&self) -> Result<Vec<Account>> {
let mut accounts = Vec::new();
for id in self.accounts.read().await.get_all().await {
let context_option = self.accounts.read().await.get_account(id).await;
if let Some(ctx) = context_option {
accounts.push(Account::from_context(&ctx, id).await?)
} else {
println!("account with id {} doesn't exist anymore", id);
}
}
Ok(accounts)
}
// ---------------------------------------------
// Methods that work on individual accounts
// ---------------------------------------------
/// Get top-level info for an account.
async fn get_account_info(&self, account_id: u32) -> Result<Account> {
let context_option = self.accounts.read().await.get_account(account_id).await;
if let Some(ctx) = context_option {
Ok(Account::from_context(&ctx, account_id).await?)
} else {
Err(anyhow!(
"account with id {} doesn't exist anymore",
account_id
))
}
}
/// Returns provider for the given domain.
///
/// This function looks up domain in offline database.
///
/// For compatibility, email address can be passed to this function
/// instead of the domain.
async fn get_provider_info(
&self,
account_id: u32,
email: String,
) -> Result<Option<ProviderInfo>> {
let ctx = self.get_context(account_id).await?;
let socks5_enabled = ctx
.get_config_bool(deltachat::config::Config::Socks5Enabled)
.await?;
let provider_info =
get_provider_info(&ctx, email.split('@').last().unwrap_or(""), socks5_enabled).await;
Ok(ProviderInfo::from_dc_type(provider_info))
}
/// Checks if the context is already configured.
async fn is_configured(&self, account_id: u32) -> Result<bool> {
let ctx = self.get_context(account_id).await?;
ctx.is_configured().await
}
/// Get system info for an account.
async fn get_info(&self, account_id: u32) -> Result<BTreeMap<&'static str, String>> {
let ctx = self.get_context(account_id).await?;
ctx.get_info().await
}
async fn set_config(&self, account_id: u32, key: String, value: Option<String>) -> Result<()> {
let ctx = self.get_context(account_id).await?;
set_config(&ctx, &key, value.as_deref()).await
}
async fn batch_set_config(
&self,
account_id: u32,
config: HashMap<String, Option<String>>,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
for (key, value) in config.into_iter() {
set_config(&ctx, &key, value.as_deref())
.await
.with_context(|| format!("Can't set {} to {:?}", key, value))?;
}
Ok(())
}
/// Set configuration values from a QR code. (technically from the URI that is stored in the qrcode)
/// Before this function is called, dc_check_qr() should confirm the type of the
/// QR code is DC_QR_ACCOUNT or DC_QR_WEBRTC_INSTANCE.
///
/// Internally, the function will call dc_set_config() with the appropriate keys,
/// e.g. `addr` and `mail_pw` for DC_QR_ACCOUNT
/// or `webrtc_instance` for DC_QR_WEBRTC_INSTANCE.
async fn set_config_from_qr(&self, account_id: u32, qr_content: String) -> Result<()> {
let ctx = self.get_context(account_id).await?;
qr::set_config_from_qr(&ctx, &qr_content).await
}
async fn get_config(&self, account_id: u32, key: String) -> Result<Option<String>> {
let ctx = self.get_context(account_id).await?;
get_config(&ctx, &key).await
}
async fn batch_get_config(
&self,
account_id: u32,
keys: Vec<String>,
) -> Result<HashMap<String, Option<String>>> {
let ctx = self.get_context(account_id).await?;
let mut result: HashMap<String, Option<String>> = HashMap::new();
for key in keys {
result.insert(key.clone(), get_config(&ctx, &key).await?);
}
Ok(result)
}
/// Configures this account with the currently set parameters.
/// Setup the credential config before calling this.
async fn configure(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.stop_io().await;
let result = ctx.configure().await;
if result.is_err() {
if let Ok(true) = ctx.is_configured().await {
ctx.start_io().await;
}
return result;
}
ctx.start_io().await;
Ok(())
}
/// Signal an ongoing process to stop.
async fn stop_ongoing_process(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.stop_ongoing().await;
Ok(())
}
/// Returns the message IDs of all _fresh_ messages of any chat.
/// Typically used for implementing notification summaries
/// or badge counters e.g. on the app icon.
/// The list is already sorted and starts with the most recent fresh message.
///
/// Messages belonging to muted chats or to the contact requests are not returned;
/// these messages should not be notified
/// and also badge counters should not include these messages.
///
/// To get the number of fresh messages for a single chat, muted or not,
/// use `get_fresh_msg_cnt()`.
async fn get_fresh_msgs(&self, account_id: u32) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
Ok(ctx
.get_fresh_msgs()
.await?
.iter()
.map(|msg_id| msg_id.to_u32())
.collect())
}
/// Get the number of _fresh_ messages in a chat.
/// Typically used to implement a badge with a number in the chatlist.
///
/// If the specified chat is muted,
/// the UI should show the badge counter "less obtrusive",
/// e.g. using "gray" instead of "red" color.
async fn get_fresh_msg_cnt(&self, account_id: u32, chat_id: u32) -> Result<usize> {
let ctx = self.get_context(account_id).await?;
ChatId::new(chat_id).get_fresh_msg_cnt(&ctx).await
}
// ---------------------------------------------
// autocrypt
// ---------------------------------------------
async fn autocrypt_initiate_key_transfer(&self, account_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?;
deltachat::imex::initiate_key_transfer(&ctx).await
}
async fn autocrypt_continue_key_transfer(
&self,
account_id: u32,
message_id: u32,
setup_code: String,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
deltachat::imex::continue_key_transfer(&ctx, MsgId::new(message_id), &setup_code).await
}
// ---------------------------------------------
// chat list
// ---------------------------------------------
async fn get_chatlist_entries(
&self,
account_id: u32,
list_flags: Option<u32>,
query_string: Option<String>,
query_contact_id: Option<u32>,
) -> Result<Vec<ChatListEntry>> {
let ctx = self.get_context(account_id).await?;
let list = Chatlist::try_load(
&ctx,
list_flags.unwrap_or(0) as usize,
query_string.as_deref(),
query_contact_id.map(ContactId::new),
)
.await?;
let mut l: Vec<ChatListEntry> = Vec::with_capacity(list.len());
for i in 0..list.len() {
l.push(ChatListEntry(
list.get_chat_id(i)?.to_u32(),
list.get_msg_id(i)?.unwrap_or_default().to_u32(),
));
}
Ok(l)
}
async fn get_chatlist_items_by_entries(
&self,
account_id: u32,
entries: Vec<ChatListEntry>,
) -> Result<HashMap<u32, ChatListItemFetchResult>> {
// todo custom json deserializer for ChatListEntry?
let ctx = self.get_context(account_id).await?;
let mut result: HashMap<u32, ChatListItemFetchResult> =
HashMap::with_capacity(entries.len());
for entry in entries.iter() {
result.insert(
entry.0,
match get_chat_list_item_by_id(&ctx, entry).await {
Ok(res) => res,
Err(err) => ChatListItemFetchResult::Error {
id: entry.0,
error: format!("{:?}", err),
},
},
);
}
Ok(result)
}
// ---------------------------------------------
// chat
// ---------------------------------------------
async fn chatlist_get_full_chat_by_id(
&self,
account_id: u32,
chat_id: u32,
) -> Result<FullChat> {
let ctx = self.get_context(account_id).await?;
FullChat::try_from_dc_chat_id(&ctx, chat_id).await
}
async fn accept_chat(&self, account_id: u32, chat_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ChatId::new(chat_id).accept(&ctx).await
}
async fn block_chat(&self, account_id: u32, chat_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ChatId::new(chat_id).block(&ctx).await
}
// for now only text messages, because we only used text messages in desktop thusfar
async fn add_device_message(
&self,
account_id: u32,
label: String,
text: String,
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some(text));
let message_id =
deltachat::chat::add_device_msg(&ctx, Some(&label), Some(&mut msg)).await?;
Ok(message_id.to_u32())
}
// ---------------------------------------------
// message list
// ---------------------------------------------
async fn message_list_get_message_ids(
&self,
account_id: u32,
chat_id: u32,
flags: u32,
) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let msg = get_chat_msgs(&ctx, ChatId::new(chat_id), flags).await?;
Ok(msg
.iter()
.filter_map(|chat_item| match chat_item {
deltachat::chat::ChatItem::Message { msg_id } => Some(msg_id.to_u32()),
_ => None,
})
.collect())
}
async fn message_get_message(&self, account_id: u32, message_id: u32) -> Result<MessageObject> {
let ctx = self.get_context(account_id).await?;
MessageObject::from_message_id(&ctx, message_id).await
}
async fn message_get_messages(
&self,
account_id: u32,
message_ids: Vec<u32>,
) -> Result<HashMap<u32, MessageObject>> {
let ctx = self.get_context(account_id).await?;
let mut messages: HashMap<u32, MessageObject> = HashMap::new();
for message_id in message_ids {
messages.insert(
message_id,
MessageObject::from_message_id(&ctx, message_id).await?,
);
}
Ok(messages)
}
// ---------------------------------------------
// contact
// ---------------------------------------------
/// Get a single contact options by ID.
async fn contacts_get_contact(
&self,
account_id: u32,
contact_id: u32,
) -> Result<ContactObject> {
let ctx = self.get_context(account_id).await?;
let contact_id = ContactId::new(contact_id);
ContactObject::try_from_dc_contact(
&ctx,
deltachat::contact::Contact::get_by_id(&ctx, contact_id).await?,
)
.await
}
/// Add a single contact as a result of an explicit user action.
///
/// Returns contact id of the created or existing contact
async fn contacts_create_contact(
&self,
account_id: u32,
email: String,
name: Option<String>,
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
if !may_be_valid_addr(&email) {
bail!(anyhow!(
"provided email address is not a valid email address"
))
}
let contact_id = Contact::create(&ctx, &name.unwrap_or_default(), &email).await?;
Ok(contact_id.to_u32())
}
/// Returns contact id of the created or existing DM chat with that contact
async fn contacts_create_chat_by_contact_id(
&self,
account_id: u32,
contact_id: u32,
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let contact = Contact::get_by_id(&ctx, ContactId::new(contact_id)).await?;
ChatId::create_for_contact(&ctx, contact.id)
.await
.map(|id| id.to_u32())
}
async fn contacts_block(&self, account_id: u32, contact_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
Contact::block(&ctx, ContactId::new(contact_id)).await
}
async fn contacts_unblock(&self, account_id: u32, contact_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
Contact::unblock(&ctx, ContactId::new(contact_id)).await
}
async fn contacts_get_blocked(&self, account_id: u32) -> Result<Vec<ContactObject>> {
let ctx = self.get_context(account_id).await?;
let blocked_ids = Contact::get_all_blocked(&ctx).await?;
let mut contacts: Vec<ContactObject> = Vec::with_capacity(blocked_ids.len());
for id in blocked_ids {
contacts.push(
ContactObject::try_from_dc_contact(
&ctx,
deltachat::contact::Contact::get_by_id(&ctx, id).await?,
)
.await?,
);
}
Ok(contacts)
}
async fn contacts_get_contact_ids(
&self,
account_id: u32,
list_flags: u32,
query: Option<String>,
) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let contacts = Contact::get_all(&ctx, list_flags, query.as_deref()).await?;
Ok(contacts.into_iter().map(|c| c.to_u32()).collect())
}
/// Get a list of contacts.
/// (formerly called getContacts2 in desktop)
async fn contacts_get_contacts(
&self,
account_id: u32,
list_flags: u32,
query: Option<String>,
) -> Result<Vec<ContactObject>> {
let ctx = self.get_context(account_id).await?;
let contact_ids = Contact::get_all(&ctx, list_flags, query.as_deref()).await?;
let mut contacts: Vec<ContactObject> = Vec::with_capacity(contact_ids.len());
for id in contact_ids {
contacts.push(
ContactObject::try_from_dc_contact(
&ctx,
deltachat::contact::Contact::get_by_id(&ctx, id).await?,
)
.await?,
);
}
Ok(contacts)
}
async fn contacts_get_contacts_by_ids(
&self,
account_id: u32,
ids: Vec<u32>,
) -> Result<HashMap<u32, ContactObject>> {
let ctx = self.get_context(account_id).await?;
let mut contacts = HashMap::with_capacity(ids.len());
for id in ids {
contacts.insert(
id,
ContactObject::try_from_dc_contact(
&ctx,
deltachat::contact::Contact::get_by_id(&ctx, ContactId::new(id)).await?,
)
.await?,
);
}
Ok(contacts)
}
// ---------------------------------------------
// chat
// ---------------------------------------------
/// Returns all message IDs of the given types in a chat.
/// Typically used to show a gallery.
///
/// The list is already sorted and starts with the oldest message.
/// Clients should not try to re-sort the list as this would be an expensive action
/// and would result in inconsistencies between clients.
async fn chat_get_media(
&self,
account_id: u32,
chat_id: u32,
message_type: MessageViewtype,
or_message_type2: Option<MessageViewtype>,
or_message_type3: Option<MessageViewtype>,
) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let msg_type = message_type.into();
let or_msg_type2 = or_message_type2.map_or(Viewtype::Unknown, |v| v.into());
let or_msg_type3 = or_message_type3.map_or(Viewtype::Unknown, |v| v.into());
let media = get_chat_media(
&ctx,
ChatId::new(chat_id),
msg_type,
or_msg_type2,
or_msg_type3,
)
.await?;
Ok(media.iter().map(|msg_id| msg_id.to_u32()).collect())
}
// ---------------------------------------------
// webxdc
// ---------------------------------------------
async fn webxdc_send_status_update(
&self,
account_id: u32,
instance_msg_id: u32,
update_str: String,
description: String,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.send_webxdc_status_update(MsgId::new(instance_msg_id), &update_str, &description)
.await
}
async fn webxdc_get_status_updates(
&self,
account_id: u32,
instance_msg_id: u32,
last_known_serial: u32,
) -> Result<String> {
let ctx = self.get_context(account_id).await?;
ctx.get_webxdc_status_updates(
MsgId::new(instance_msg_id),
StatusUpdateSerial::new(last_known_serial),
)
.await
}
/// Get info from a webxdc message
async fn message_get_webxdc_info(
&self,
account_id: u32,
instance_msg_id: u32,
) -> Result<WebxdcMessageInfo> {
let ctx = self.get_context(account_id).await?;
WebxdcMessageInfo::get_for_message(&ctx, MsgId::new(instance_msg_id)).await
}
// ---------------------------------------------
// misc prototyping functions
// that might get removed later again
// ---------------------------------------------
/// Returns the messageid of the sent message
async fn misc_send_text_message(
&self,
account_id: u32,
text: String,
chat_id: u32,
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some(text));
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
Ok(message_id.to_u32())
}
}
// Helper functions (to prevent code duplication)
async fn set_config(
ctx: &deltachat::context::Context,
key: &str,
value: Option<&str>,
) -> Result<(), anyhow::Error> {
if key.starts_with("ui.") {
ctx.set_ui_config(key, value).await
} else {
ctx.set_config(Config::from_str(key).context("unknown key")?, value)
.await
}
}
async fn get_config(
ctx: &deltachat::context::Context,
key: &str,
) -> Result<Option<String>, anyhow::Error> {
if key.starts_with("ui.") {
ctx.get_ui_config(key).await
} else {
ctx.get_config(Config::from_str(key).context("unknown key")?)
.await
}
}

View File

@@ -1,45 +0,0 @@
use anyhow::Result;
use deltachat::config::Config;
use deltachat::contact::{Contact, ContactId};
use serde::Serialize;
use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
#[derive(Serialize, TypeDef)]
#[serde(tag = "type")]
pub enum Account {
#[serde(rename_all = "camelCase")]
Configured {
id: u32,
display_name: Option<String>,
addr: Option<String>,
// size: u32,
profile_image: Option<String>, // TODO: This needs to be converted to work with blob http server.
color: String,
},
#[serde(rename_all = "camelCase")]
Unconfigured { id: u32 },
}
impl Account {
pub async fn from_context(ctx: &deltachat::context::Context, id: u32) -> Result<Self> {
if ctx.is_configured().await? {
let display_name = ctx.get_config(Config::Displayname).await?;
let addr = ctx.get_config(Config::Addr).await?;
let profile_image = ctx.get_config(Config::Selfavatar).await?;
let color = color_int_to_hex_string(
Contact::get_by_id(ctx, ContactId::SELF).await?.get_color(),
);
Ok(Account::Configured {
id,
display_name,
addr,
profile_image,
color,
})
} else {
Ok(Account::Unconfigured { id })
}
}
}

View File

@@ -1,92 +0,0 @@
use anyhow::{anyhow, Result};
use deltachat::chat::get_chat_contacts;
use deltachat::chat::{Chat, ChatId};
use deltachat::contact::{Contact, ContactId};
use deltachat::context::Context;
use num_traits::cast::ToPrimitive;
use serde::Serialize;
use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
use super::contact::ContactObject;
#[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase")]
pub struct FullChat {
id: u32,
name: String,
is_protected: bool,
profile_image: Option<String>, //BLOBS ?
archived: bool,
// subtitle - will be moved to frontend because it uses translation functions
chat_type: u32,
is_unpromoted: bool,
is_self_talk: bool,
contacts: Vec<ContactObject>,
contact_ids: Vec<u32>,
color: String,
fresh_message_counter: usize,
// is_group - please check over chat.type in frontend instead
is_contact_request: bool,
is_device_chat: bool,
self_in_group: bool,
is_muted: bool,
ephemeral_timer: u32, //TODO look if there are more important properties in newer core versions
can_send: bool,
}
impl FullChat {
pub async fn try_from_dc_chat_id(context: &Context, chat_id: u32) -> Result<Self> {
let rust_chat_id = ChatId::new(chat_id);
let chat = Chat::load_from_db(context, rust_chat_id).await?;
let contact_ids = get_chat_contacts(context, rust_chat_id).await?;
let mut contacts = Vec::with_capacity(contact_ids.len());
for contact_id in &contact_ids {
contacts.push(
ContactObject::try_from_dc_contact(
context,
Contact::load_from_db(context, *contact_id).await?,
)
.await?,
)
}
let profile_image = match chat.get_profile_image(context).await? {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
None => None,
};
let color = color_int_to_hex_string(chat.get_color(context).await?);
let fresh_message_counter = rust_chat_id.get_fresh_msg_cnt(context).await?;
let ephemeral_timer = rust_chat_id.get_ephemeral_timer(context).await?.to_u32();
let can_send = chat.can_send(context).await?;
Ok(FullChat {
id: chat_id,
name: chat.name.clone(),
is_protected: chat.is_protected(),
profile_image, //BLOBS ?
archived: chat.get_visibility() == deltachat::chat::ChatVisibility::Archived,
chat_type: chat
.get_type()
.to_u32()
.ok_or_else(|| anyhow!("unknown chat type id"))?, // TODO get rid of this unwrap?
is_unpromoted: chat.is_unpromoted(),
is_self_talk: chat.is_self_talk(),
contacts,
contact_ids: contact_ids.iter().map(|id| id.to_u32()).collect(),
color,
fresh_message_counter,
is_contact_request: chat.is_contact_request(),
is_device_chat: chat.is_device_talk(),
self_in_group: contact_ids.contains(&ContactId::SELF),
is_muted: chat.is_muted(),
ephemeral_timer,
can_send,
})
}
}

View File

@@ -1,126 +0,0 @@
use anyhow::Result;
use deltachat::constants::*;
use deltachat::contact::ContactId;
use deltachat::{
chat::{get_chat_contacts, ChatVisibility},
chatlist::Chatlist,
};
use deltachat::{
chat::{Chat, ChatId},
message::MsgId,
};
use num_traits::cast::ToPrimitive;
use serde::{Deserialize, Serialize};
use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
#[derive(Deserialize, Serialize, TypeDef)]
pub struct ChatListEntry(pub u32, pub u32);
#[derive(Serialize, TypeDef)]
#[serde(tag = "type")]
pub enum ChatListItemFetchResult {
#[serde(rename_all = "camelCase")]
ChatListItem {
id: u32,
name: String,
avatar_path: Option<String>,
color: String,
last_updated: Option<i64>,
summary_text1: String,
summary_text2: String,
summary_status: u32,
is_protected: bool,
is_group: bool,
fresh_message_counter: usize,
is_self_talk: bool,
is_device_talk: bool,
is_sending_location: bool,
is_self_in_group: bool,
is_archived: bool,
is_pinned: bool,
is_muted: bool,
is_contact_request: bool,
/// contact id if this is a dm chat (for view profile entry in context menu)
dm_chat_contact: Option<u32>,
},
ArchiveLink,
#[serde(rename_all = "camelCase")]
Error {
id: u32,
error: String,
},
}
pub(crate) async fn get_chat_list_item_by_id(
ctx: &deltachat::context::Context,
entry: &ChatListEntry,
) -> Result<ChatListItemFetchResult> {
let chat_id = ChatId::new(entry.0);
let last_msgid = match entry.1 {
0 => None,
_ => Some(MsgId::new(entry.1)),
};
if chat_id.is_archived_link() {
return Ok(ChatListItemFetchResult::ArchiveLink);
}
let chat = Chat::load_from_db(ctx, chat_id).await?;
let summary = Chatlist::get_summary2(ctx, chat_id, last_msgid, Some(&chat)).await?;
let summary_text1 = summary.prefix.map_or_else(String::new, |s| s.to_string());
let summary_text2 = summary.text.to_owned();
let visibility = chat.get_visibility();
let avatar_path = chat
.get_profile_image(ctx)
.await?
.map(|path| path.to_str().unwrap_or("invalid/path").to_owned());
let last_updated = match last_msgid {
Some(id) => {
let last_message = deltachat::message::Message::load_from_db(ctx, id).await?;
Some(last_message.get_timestamp() * 1000)
}
None => None,
};
let chat_contacts = get_chat_contacts(ctx, chat_id).await?;
let self_in_group = chat_contacts.contains(&ContactId::SELF);
let dm_chat_contact = if chat.get_type() == Chattype::Single {
chat_contacts.get(0).map(|contact_id| contact_id.to_u32())
} else {
None
};
let fresh_message_counter = chat_id.get_fresh_msg_cnt(ctx).await?;
let color = color_int_to_hex_string(chat.get_color(ctx).await?);
Ok(ChatListItemFetchResult::ChatListItem {
id: chat_id.to_u32(),
name: chat.get_name().to_owned(),
avatar_path,
color,
last_updated,
summary_text1,
summary_text2,
summary_status: summary.state.to_u32().expect("impossible"), // idea and a function to transform the constant to strings? or return string enum
is_protected: chat.is_protected(),
is_group: chat.get_type() == Chattype::Group,
fresh_message_counter,
is_self_talk: chat.is_self_talk(),
is_device_talk: chat.is_device_talk(),
is_self_in_group: self_in_group,
is_sending_location: chat.is_sending_locations(),
is_archived: visibility == ChatVisibility::Archived,
is_pinned: visibility == ChatVisibility::Pinned,
is_muted: chat.is_muted(),
is_contact_request: chat.is_contact_request(),
dm_chat_contact,
})
}

View File

@@ -1,50 +0,0 @@
use anyhow::Result;
use deltachat::contact::VerifiedStatus;
use deltachat::context::Context;
use serde::Serialize;
use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
#[derive(Serialize, TypeDef)]
#[serde(rename = "Contact", rename_all = "camelCase")]
pub struct ContactObject {
address: String,
color: String,
auth_name: String,
status: String,
display_name: String,
id: u32,
name: String,
profile_image: Option<String>, // BLOBS
name_and_addr: String,
is_blocked: bool,
is_verified: bool,
}
impl ContactObject {
pub async fn try_from_dc_contact(
context: &Context,
contact: deltachat::contact::Contact,
) -> Result<Self> {
let profile_image = match contact.get_profile_image(context).await? {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
None => None,
};
let is_verified = contact.is_verified(context).await? == VerifiedStatus::BidirectVerified;
Ok(ContactObject {
address: contact.get_addr().to_owned(),
color: color_int_to_hex_string(contact.get_color()),
auth_name: contact.get_authname().to_owned(),
status: contact.get_status().to_owned(),
display_name: contact.get_display_name().to_owned(),
id: contact.id.to_u32(),
name: contact.get_name().to_owned(),
profile_image, //BLOBS
name_and_addr: contact.get_name_n_addr(),
is_blocked: contact.is_blocked(),
is_verified,
})
}
}

View File

@@ -1,202 +0,0 @@
use anyhow::{anyhow, Result};
use deltachat::contact::Contact;
use deltachat::context::Context;
use deltachat::message::Message;
use deltachat::message::MsgId;
use deltachat::message::Viewtype;
use num_traits::cast::ToPrimitive;
use serde::Deserialize;
use serde::Serialize;
use typescript_type_def::TypeDef;
use super::contact::ContactObject;
#[derive(Serialize, TypeDef)]
#[serde(rename = "Message", rename_all = "camelCase")]
pub struct MessageObject {
id: u32,
chat_id: u32,
from_id: u32,
quoted_text: Option<String>,
quoted_message_id: Option<u32>,
text: Option<String>,
has_location: bool,
has_html: bool,
view_type: MessageViewtype,
state: u32,
timestamp: i64,
sort_timestamp: i64,
received_timestamp: i64,
has_deviating_timestamp: bool,
// summary - use/create another function if you need it
subject: String,
show_padlock: bool,
is_setupmessage: bool,
is_info: bool,
is_forwarded: bool,
duration: i32,
dimensions_height: i32,
dimensions_width: i32,
videochat_type: Option<u32>,
videochat_url: Option<String>,
override_sender_name: Option<String>,
sender: ContactObject,
setup_code_begin: Option<String>,
file: Option<String>,
file_mime: Option<String>,
file_bytes: u64,
file_name: Option<String>,
}
impl MessageObject {
pub async fn from_message_id(context: &Context, message_id: u32) -> Result<Self> {
let msg_id = MsgId::new(message_id);
let message = Message::load_from_db(context, msg_id).await?;
let quoted_message_id = message
.quoted_message(context)
.await?
.map(|m| m.get_id().to_u32());
let sender_contact = Contact::load_from_db(context, message.get_from_id()).await?;
let sender = ContactObject::try_from_dc_contact(context, sender_contact).await?;
let file_bytes = message.get_filebytes(context).await;
let override_sender_name = message.get_override_sender_name();
Ok(MessageObject {
id: message_id,
chat_id: message.get_chat_id().to_u32(),
from_id: message.get_from_id().to_u32(),
quoted_text: message.quoted_text(),
quoted_message_id,
text: message.get_text(),
has_location: message.has_location(),
has_html: message.has_html(),
view_type: message.get_viewtype().into(),
state: message
.get_state()
.to_u32()
.ok_or_else(|| anyhow!("state conversion to number failed"))?,
timestamp: message.get_timestamp(),
sort_timestamp: message.get_sort_timestamp(),
received_timestamp: message.get_received_timestamp(),
has_deviating_timestamp: message.has_deviating_timestamp(),
subject: message.get_subject().to_owned(),
show_padlock: message.get_showpadlock(),
is_setupmessage: message.is_setupmessage(),
is_info: message.is_info(),
is_forwarded: message.is_forwarded(),
duration: message.get_duration(),
dimensions_height: message.get_height(),
dimensions_width: message.get_width(),
videochat_type: match message.get_videochat_type() {
Some(vct) => Some(
vct.to_u32()
.ok_or_else(|| anyhow!("state conversion to number failed"))?,
),
None => None,
},
videochat_url: message.get_videochat_url(),
override_sender_name,
sender,
setup_code_begin: message.get_setupcodebegin(context).await,
file: match message.get_file(context) {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
None => None,
}, //BLOBS
file_mime: message.get_filemime(),
file_bytes,
file_name: message.get_filename(),
})
}
}
#[derive(Serialize, Deserialize, TypeDef)]
#[serde(rename = "Viewtype")]
pub enum MessageViewtype {
Unknown,
/// Text message.
Text,
/// Image message.
/// If the image is an animated GIF, the type `Viewtype.Gif` should be used.
Image,
/// Animated GIF message.
Gif,
/// 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,
/// Message containing an Audio file.
Audio,
/// A voice message that was directly recorded by the user.
/// For all other audio messages, the type `Viewtype.Audio` should be used.
Voice,
/// Video messages.
Video,
/// Message containing any file, eg. a PDF.
File,
/// Message is an invitation to a videochat.
VideochatInvitation,
/// Message is an webxdc instance.
Webxdc,
}
impl From<Viewtype> for MessageViewtype {
fn from(viewtype: Viewtype) -> Self {
match viewtype {
Viewtype::Unknown => MessageViewtype::Unknown,
Viewtype::Text => MessageViewtype::Text,
Viewtype::Image => MessageViewtype::Image,
Viewtype::Gif => MessageViewtype::Gif,
Viewtype::Sticker => MessageViewtype::Sticker,
Viewtype::Audio => MessageViewtype::Audio,
Viewtype::Voice => MessageViewtype::Voice,
Viewtype::Video => MessageViewtype::Video,
Viewtype::File => MessageViewtype::File,
Viewtype::VideochatInvitation => MessageViewtype::VideochatInvitation,
Viewtype::Webxdc => MessageViewtype::Webxdc,
}
}
}
impl From<MessageViewtype> for Viewtype {
fn from(viewtype: MessageViewtype) -> Self {
match viewtype {
MessageViewtype::Unknown => Viewtype::Unknown,
MessageViewtype::Text => Viewtype::Text,
MessageViewtype::Image => Viewtype::Image,
MessageViewtype::Gif => Viewtype::Gif,
MessageViewtype::Sticker => Viewtype::Sticker,
MessageViewtype::Audio => Viewtype::Audio,
MessageViewtype::Voice => Viewtype::Voice,
MessageViewtype::Video => Viewtype::Video,
MessageViewtype::File => Viewtype::File,
MessageViewtype::VideochatInvitation => Viewtype::VideochatInvitation,
MessageViewtype::Webxdc => Viewtype::Webxdc,
}
}
}

View File

@@ -1,19 +0,0 @@
pub mod account;
pub mod chat;
pub mod chat_list;
pub mod contact;
pub mod message;
pub mod provider_info;
pub mod webxdc;
pub fn color_int_to_hex_string(color: u32) -> String {
format!("{:#08x}", color).replace("0x", "#")
}
fn maybe_empty_string_to_option(string: String) -> Option<String> {
if string.is_empty() {
None
} else {
Some(string)
}
}

View File

@@ -1,22 +0,0 @@
use deltachat::provider::Provider;
use num_traits::cast::ToPrimitive;
use serde::Serialize;
use typescript_type_def::TypeDef;
#[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase")]
pub struct ProviderInfo {
pub before_login_hint: String,
pub overview_page: String,
pub status: u32, // in reality this is an enum, but for simlicity and because it gets converted into a number anyway, we use an u32 here.
}
impl ProviderInfo {
pub fn from_dc_type(provider: Option<&Provider>) -> Option<Self> {
provider.map(|p| ProviderInfo {
before_login_hint: p.before_login_hint.to_owned(),
overview_page: p.overview_page.to_owned(),
status: p.status.to_u32().unwrap(),
})
}
}

View File

@@ -1,60 +0,0 @@
use deltachat::{
context::Context,
message::{Message, MsgId},
webxdc::WebxdcInfo,
};
use serde::Serialize;
use typescript_type_def::TypeDef;
use super::maybe_empty_string_to_option;
#[derive(Serialize, TypeDef)]
#[serde(rename = "WebxdcMessageInfo", rename_all = "camelCase")]
pub struct WebxdcMessageInfo {
/// The name of the app.
///
/// Defaults to the filename if not set in the manifest.
name: String,
/// App icon file name.
/// Defaults to an standard icon if nothing is set in the manifest.
///
/// To get the file, use dc_msg_get_webxdc_blob(). (not yet in jsonrpc, use rust api or cffi for it)
///
/// App icons should should be square,
/// the implementations will add round corners etc. as needed.
icon: String,
/// if the Webxdc represents a document, then this is the name of the document
document: Option<String>,
/// short string describing the state of the app,
/// sth. as "2 votes", "Highscore: 123",
/// can be changed by the apps
summary: Option<String>,
/// URL where the source code of the Webxdc and other information can be found;
/// defaults to an empty string.
/// Implementations may offer an menu or a button to open this URL.
source_code_url: Option<String>,
}
impl WebxdcMessageInfo {
pub async fn get_for_message(
context: &Context,
instance_message_id: MsgId,
) -> anyhow::Result<Self> {
let message = Message::load_from_db(context, instance_message_id).await?;
let WebxdcInfo {
name,
icon,
document,
summary,
source_code_url,
} = message.get_webxdc_info(context).await?;
Ok(Self {
name,
icon,
document: maybe_empty_string_to_option(document),
summary: maybe_empty_string_to_option(summary),
source_code_url: maybe_empty_string_to_option(source_code_url),
})
}
}

View File

@@ -1,93 +0,0 @@
pub mod api;
pub use api::events;
pub use api::{Accounts, DeltaChatApiV0};
pub use yerpc;
#[cfg(test)]
mod tests {
use super::api::{Accounts, DeltaChatApiV0};
use async_channel::unbounded;
use futures::StreamExt;
use tempfile::TempDir;
use yerpc::{RpcClient, RpcSession};
#[tokio::test(flavor = "multi_thread")]
async fn basic_json_rpc_functionality() -> anyhow::Result<()> {
let tmp_dir = TempDir::new().unwrap().path().into();
let accounts = Accounts::new(tmp_dir).await?;
let api = DeltaChatApiV0::new(accounts);
let (sender, mut receiver) = unbounded::<String>();
let (client, mut rx) = RpcClient::new();
let session = RpcSession::new(client, api);
tokio::spawn({
async move {
while let Some(message) = rx.next().await {
let message = serde_json::to_string(&message)?;
sender.send(message).await?;
}
let res: Result<(), anyhow::Error> = Ok(());
res
}
});
{
let request = r#"{"jsonrpc":"2.0","method":"add_account","params":[],"id":1}"#;
let response = r#"{"jsonrpc":"2.0","id":1,"result":1}"#;
session.handle_incoming(request).await;
let result = receiver.next().await;
println!("{:?}", result);
assert_eq!(result, Some(response.to_owned()));
}
{
let request = r#"{"jsonrpc":"2.0","method":"get_all_account_ids","params":[],"id":2}"#;
let response = r#"{"jsonrpc":"2.0","id":2,"result":[1]}"#;
session.handle_incoming(request).await;
let result = receiver.next().await;
println!("{:?}", result);
assert_eq!(result, Some(response.to_owned()));
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_batch_set_config() -> anyhow::Result<()> {
let tmp_dir = TempDir::new().unwrap().path().into();
let accounts = Accounts::new(tmp_dir).await?;
let api = DeltaChatApiV0::new(accounts);
let (sender, mut receiver) = unbounded::<String>();
let (client, mut rx) = RpcClient::new();
let session = RpcSession::new(client, api);
tokio::spawn({
async move {
while let Some(message) = rx.next().await {
let message = serde_json::to_string(&message)?;
sender.send(message).await?;
}
let res: Result<(), anyhow::Error> = Ok(());
res
}
});
{
let request = r#"{"jsonrpc":"2.0","method":"add_account","params":[],"id":1}"#;
let response = r#"{"jsonrpc":"2.0","id":1,"result":1}"#;
session.handle_incoming(request).await;
let result = receiver.next().await;
assert_eq!(result, Some(response.to_owned()));
}
{
let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":"","smtp_certificate_checks":"","socks5_enabled":"0","socks5_host":"","socks5_port":"","socks5_user":"","socks5_password":""}]}"#;
let response = r#"{"jsonrpc":"2.0","id":2,"result":null}"#;
session.handle_incoming(request).await;
let result = receiver.next().await;
assert_eq!(result, Some(response.to_owned()));
}
Ok(())
}
}

View File

@@ -1,54 +0,0 @@
use axum::{extract::ws::WebSocketUpgrade, response::Response, routing::get, Extension, Router};
use std::net::SocketAddr;
use std::path::PathBuf;
use yerpc::axum::handle_ws_rpc;
use yerpc::{RpcClient, RpcSession};
mod api;
use api::events::event_to_json_rpc_notification;
use api::{Accounts, DeltaChatApiV0};
const DEFAULT_PORT: u16 = 20808;
#[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<(), std::io::Error> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "./accounts".to_string());
let port = std::env::var("DC_PORT")
.map(|port| port.parse::<u16>().expect("DC_PORT must be a number"))
.unwrap_or(DEFAULT_PORT);
log::info!("Starting with accounts directory `{path}`.");
let accounts = Accounts::new(PathBuf::from(&path)).await.unwrap();
let state = DeltaChatApiV0::new(accounts);
let app = Router::new()
.route("/rpc/v0", get(handler))
.layer(Extension(state.clone()));
tokio::spawn(async move {
state.accounts.read().await.start_io().await;
});
let addr = SocketAddr::from(([127, 0, 0, 1], port));
log::info!("JSON-RPC WebSocket server listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
Ok(())
}
async fn handler(ws: WebSocketUpgrade, Extension(api): Extension<DeltaChatApiV0>) -> Response {
let (client, out_receiver) = RpcClient::new();
let session = RpcSession::new(client.clone(), api.clone());
tokio::spawn(async move {
let events = api.accounts.read().await.get_event_emitter().await;
while let Some(event) = events.recv().await {
let event = event_to_json_rpc_notification(event);
client.send_notification("event", Some(event)).await.ok();
}
});
handle_ws_rpc(ws, out_receiver, session).await
}

View File

@@ -1,8 +0,0 @@
node_modules
dist
test_dist
coverage
yarn.lock
package-lock.json
docs
accounts

View File

@@ -1,6 +0,0 @@
node_modules
accounts
docs
coverage
yarn*
package-lock.json

View File

@@ -1,3 +0,0 @@
coverage
dist
generated

View File

@@ -1 +0,0 @@
export * from "./src/lib.js";

View File

@@ -1,56 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>DeltaChat JSON-RPC example</title>
<style>
body {
font-family: monospace;
background: black;
color: grey;
}
.grid {
display: grid;
grid-template-columns: 3fr 1fr;
grid-template-areas: "a a" "b c";
}
.message {
color: red;
}
#header {
grid-area: a;
color: white;
font-size: 1.2rem;
}
#header a {
color: white;
font-weight: bold;
}
#main {
grid-area: b;
color: green;
}
#main h2,
#main h3 {
color: blue;
}
#side {
grid-area: c;
color: #777;
overflow-y: auto;
}
</style>
<script type="module" src="dist/example.bundle.js"></script>
</head>
<body>
<h1>DeltaChat JSON-RPC example</h1>
<div class="grid">
<div id="header"></div>
<div id="main"></div>
<div id="side"><h2>log</h2></div>
</div>
<p>
Tip: open the dev console and use the client with
<code>window.client</code>
</p>
</body>
</html>

View File

@@ -1,109 +0,0 @@
import { DeltaChat, DeltaChatEvent } from "../deltachat.js";
var SELECTED_ACCOUNT = 0;
window.addEventListener("DOMContentLoaded", (_event) => {
(window as any).selectDeltaAccount = (id: string) => {
SELECTED_ACCOUNT = Number(id);
window.dispatchEvent(new Event("account-changed"));
};
console.log('launch run script...')
run().catch((err) => console.error("run failed", err));
});
async function run() {
const $main = document.getElementById("main")!;
const $side = document.getElementById("side")!;
const $head = document.getElementById("header")!;
const client = new DeltaChat('ws://localhost:20808/ws')
;(window as any).client = client.rpc;
client.on("ALL", event => {
onIncomingEvent(event)
})
window.addEventListener("account-changed", async (_event: Event) => {
listChatsForSelectedAccount();
});
await Promise.all([loadAccountsInHeader(), listChatsForSelectedAccount()]);
async function loadAccountsInHeader() {
console.log('load accounts')
const accounts = await client.rpc.getAllAccounts();
console.log('accounts loaded', accounts)
for (const account of accounts) {
if (account.type === "Configured") {
write(
$head,
`<a href="#" onclick="selectDeltaAccount(${account.id})">
${account.id}: ${account.addr!}
</a>&nbsp;`
);
} else {
write(
$head,
`<a href="#">
${account.id}: (unconfigured)
</a>&nbsp;`
)
}
}
}
async function listChatsForSelectedAccount() {
clear($main);
const selectedAccount = SELECTED_ACCOUNT
const info = await client.rpc.getAccountInfo(selectedAccount);
if (info.type !== "Configured") {
return write($main, "Account is not configured");
}
write($main, `<h2>${info.addr!}</h2>`);
const chats = await client.rpc.getChatlistEntries(
selectedAccount,
0,
null,
null
);
for (const [chatId, _messageId] of chats) {
const chat = await client.rpc.chatlistGetFullChatById(
selectedAccount,
chatId
);
write($main, `<h3>${chat.name}</h3>`);
const messageIds = await client.rpc.messageListGetMessageIds(
selectedAccount,
chatId,
0
);
const messages = await client.rpc.messageGetMessages(
selectedAccount,
messageIds
);
for (const [_messageId, message] of Object.entries(messages)) {
write($main, `<p>${message.text}</p>`);
}
}
}
function onIncomingEvent(event: DeltaChatEvent) {
write(
$side,
`
<p class="message">
[<strong>${event.id}</strong> on account ${event.contextId}]<br>
<em>f1:</em> ${JSON.stringify(event.field1)}<br>
<em>f2:</em> ${JSON.stringify(event.field2)}
</p>`
);
}
}
function write(el: HTMLElement, html: string) {
el.innerHTML += html;
}
function clear(el: HTMLElement) {
el.innerHTML = "";
}

View File

@@ -1,26 +0,0 @@
import { DeltaChat } from "../dist/deltachat.js";
run().catch(console.error);
async function run() {
const delta = new DeltaChat('ws://localhost:20808/ws');
delta.on("event", (event) => {
console.log("event", event.data);
});
const email = process.argv[2]
const password = process.argv[3]
if (!email || !password) throw new Error('USAGE: node node-add-account.js <EMAILADDRESS> <PASSWORD>')
console.log(`creating acccount for ${email}`)
const id = await delta.rpc.addAccount()
console.log(`created account id ${id}`)
await delta.rpc.setConfig(id, "addr", email);
await delta.rpc.setConfig(id, "mail_pw", password);
console.log('configuration updated')
await delta.rpc.configure(id)
console.log('account configured!')
const accounts = await delta.rpc.getAllAccounts();
console.log("accounts", accounts);
console.log("waiting for events...")
}

View File

@@ -1,14 +0,0 @@
import { DeltaChat } from "../dist/deltachat.js";
run().catch(console.error);
async function run() {
const delta = new DeltaChat();
delta.on("event", (event) => {
console.log("event", event.data);
});
const accounts = await delta.rpc.getAllAccounts();
console.log("accounts", accounts);
console.log("waiting for events...")
}

View File

@@ -1,327 +0,0 @@
// AUTO-GENERATED by yerpc-derive
import * as T from "./types.js"
import * as RPC from "./jsonrpc.js"
type RequestMethod = (method: string, params?: RPC.Params) => Promise<unknown>;
type NotificationMethod = (method: string, params?: RPC.Params) => void;
interface Transport {
request: RequestMethod,
notification: NotificationMethod
}
export class RawClient {
constructor(private _transport: Transport) {}
/**
* Check if an email address is valid.
*/
public checkEmailValidity(email: string): Promise<boolean> {
return (this._transport.request('check_email_validity', [email] as RPC.Params)) as Promise<boolean>;
}
/**
* Get general system info.
*/
public getSystemInfo(): Promise<Record<string,string>> {
return (this._transport.request('get_system_info', [] as RPC.Params)) as Promise<Record<string,string>>;
}
public addAccount(): Promise<T.U32> {
return (this._transport.request('add_account', [] as RPC.Params)) as Promise<T.U32>;
}
public removeAccount(accountId: T.U32): Promise<null> {
return (this._transport.request('remove_account', [accountId] as RPC.Params)) as Promise<null>;
}
public getAllAccountIds(): Promise<(T.U32)[]> {
return (this._transport.request('get_all_account_ids', [] as RPC.Params)) as Promise<(T.U32)[]>;
}
/**
* Select account id for internally selected state.
* TODO: Likely this is deprecated as all methods take an account id now.
*/
public selectAccount(id: T.U32): Promise<null> {
return (this._transport.request('select_account', [id] as RPC.Params)) as Promise<null>;
}
/**
* Get the selected account id of the internal state..
* TODO: Likely this is deprecated as all methods take an account id now.
*/
public getSelectedAccountId(): Promise<(T.U32|null)> {
return (this._transport.request('get_selected_account_id', [] as RPC.Params)) as Promise<(T.U32|null)>;
}
/**
* Get a list of all configured accounts.
*/
public getAllAccounts(): Promise<(T.Account)[]> {
return (this._transport.request('get_all_accounts', [] as RPC.Params)) as Promise<(T.Account)[]>;
}
/**
* Get top-level info for an account.
*/
public getAccountInfo(accountId: T.U32): Promise<T.Account> {
return (this._transport.request('get_account_info', [accountId] as RPC.Params)) as Promise<T.Account>;
}
/**
* Returns provider for the given domain.
*
* This function looks up domain in offline database.
*
* For compatibility, email address can be passed to this function
* instead of the domain.
*/
public getProviderInfo(accountId: T.U32, email: string): Promise<(T.ProviderInfo|null)> {
return (this._transport.request('get_provider_info', [accountId, email] as RPC.Params)) as Promise<(T.ProviderInfo|null)>;
}
/**
* Checks if the context is already configured.
*/
public isConfigured(accountId: T.U32): Promise<boolean> {
return (this._transport.request('is_configured', [accountId] as RPC.Params)) as Promise<boolean>;
}
/**
* Get system info for an account.
*/
public getInfo(accountId: T.U32): Promise<Record<string,string>> {
return (this._transport.request('get_info', [accountId] as RPC.Params)) as Promise<Record<string,string>>;
}
public setConfig(accountId: T.U32, key: string, value: (string|null)): Promise<null> {
return (this._transport.request('set_config', [accountId, key, value] as RPC.Params)) as Promise<null>;
}
public batchSetConfig(accountId: T.U32, config: Record<string,(string|null)>): Promise<null> {
return (this._transport.request('batch_set_config', [accountId, config] as RPC.Params)) as Promise<null>;
}
/**
* Set configuration values from a QR code. (technically from the URI that is stored in the qrcode)
* Before this function is called, dc_check_qr() should confirm the type of the
* QR code is DC_QR_ACCOUNT or DC_QR_WEBRTC_INSTANCE.
*
* Internally, the function will call dc_set_config() with the appropriate keys,
* e.g. `addr` and `mail_pw` for DC_QR_ACCOUNT
* or `webrtc_instance` for DC_QR_WEBRTC_INSTANCE.
*/
public setConfigFromQr(accountId: T.U32, qrContent: string): Promise<null> {
return (this._transport.request('set_config_from_qr', [accountId, qrContent] as RPC.Params)) as Promise<null>;
}
public getConfig(accountId: T.U32, key: string): Promise<(string|null)> {
return (this._transport.request('get_config', [accountId, key] as RPC.Params)) as Promise<(string|null)>;
}
public batchGetConfig(accountId: T.U32, keys: (string)[]): Promise<Record<string,(string|null)>> {
return (this._transport.request('batch_get_config', [accountId, keys] as RPC.Params)) as Promise<Record<string,(string|null)>>;
}
/**
* Configures this account with the currently set parameters.
* Setup the credential config before calling this.
*/
public configure(accountId: T.U32): Promise<null> {
return (this._transport.request('configure', [accountId] as RPC.Params)) as Promise<null>;
}
/**
* Signal an ongoing process to stop.
*/
public stopOngoingProcess(accountId: T.U32): Promise<null> {
return (this._transport.request('stop_ongoing_process', [accountId] as RPC.Params)) as Promise<null>;
}
/**
* Returns the message IDs of all _fresh_ messages of any chat.
* Typically used for implementing notification summaries
* or badge counters e.g. on the app icon.
* The list is already sorted and starts with the most recent fresh message.
*
* Messages belonging to muted chats or to the contact requests are not returned;
* these messages should not be notified
* and also badge counters should not include these messages.
*
* To get the number of fresh messages for a single chat, muted or not,
* use `get_fresh_msg_cnt()`.
*/
public getFreshMsgs(accountId: T.U32): Promise<(T.U32)[]> {
return (this._transport.request('get_fresh_msgs', [accountId] as RPC.Params)) as Promise<(T.U32)[]>;
}
/**
* Get the number of _fresh_ messages in a chat.
* Typically used to implement a badge with a number in the chatlist.
*
* If the specified chat is muted,
* the UI should show the badge counter "less obtrusive",
* e.g. using "gray" instead of "red" color.
*/
public getFreshMsgCnt(accountId: T.U32, chatId: T.U32): Promise<T.Usize> {
return (this._transport.request('get_fresh_msg_cnt', [accountId, chatId] as RPC.Params)) as Promise<T.Usize>;
}
public autocryptInitiateKeyTransfer(accountId: T.U32): Promise<string> {
return (this._transport.request('autocrypt_initiate_key_transfer', [accountId] as RPC.Params)) as Promise<string>;
}
public autocryptContinueKeyTransfer(accountId: T.U32, messageId: T.U32, setupCode: string): Promise<null> {
return (this._transport.request('autocrypt_continue_key_transfer', [accountId, messageId, setupCode] as RPC.Params)) as Promise<null>;
}
public getChatlistEntries(accountId: T.U32, listFlags: (T.U32|null), queryString: (string|null), queryContactId: (T.U32|null)): Promise<(T.ChatListEntry)[]> {
return (this._transport.request('get_chatlist_entries', [accountId, listFlags, queryString, queryContactId] as RPC.Params)) as Promise<(T.ChatListEntry)[]>;
}
public getChatlistItemsByEntries(accountId: T.U32, entries: (T.ChatListEntry)[]): Promise<Record<T.U32,T.ChatListItemFetchResult>> {
return (this._transport.request('get_chatlist_items_by_entries', [accountId, entries] as RPC.Params)) as Promise<Record<T.U32,T.ChatListItemFetchResult>>;
}
public chatlistGetFullChatById(accountId: T.U32, chatId: T.U32): Promise<T.FullChat> {
return (this._transport.request('chatlist_get_full_chat_by_id', [accountId, chatId] as RPC.Params)) as Promise<T.FullChat>;
}
public acceptChat(accountId: T.U32, chatId: T.U32): Promise<null> {
return (this._transport.request('accept_chat', [accountId, chatId] as RPC.Params)) as Promise<null>;
}
public blockChat(accountId: T.U32, chatId: T.U32): Promise<null> {
return (this._transport.request('block_chat', [accountId, chatId] as RPC.Params)) as Promise<null>;
}
public addDeviceMessage(accountId: T.U32, label: string, text: string): Promise<T.U32> {
return (this._transport.request('add_device_message', [accountId, label, text] as RPC.Params)) as Promise<T.U32>;
}
public messageListGetMessageIds(accountId: T.U32, chatId: T.U32, flags: T.U32): Promise<(T.U32)[]> {
return (this._transport.request('message_list_get_message_ids', [accountId, chatId, flags] as RPC.Params)) as Promise<(T.U32)[]>;
}
public messageGetMessage(accountId: T.U32, messageId: T.U32): Promise<T.Message> {
return (this._transport.request('message_get_message', [accountId, messageId] as RPC.Params)) as Promise<T.Message>;
}
public messageGetMessages(accountId: T.U32, messageIds: (T.U32)[]): Promise<Record<T.U32,T.Message>> {
return (this._transport.request('message_get_messages', [accountId, messageIds] as RPC.Params)) as Promise<Record<T.U32,T.Message>>;
}
/**
* Get a single contact options by ID.
*/
public contactsGetContact(accountId: T.U32, contactId: T.U32): Promise<T.Contact> {
return (this._transport.request('contacts_get_contact', [accountId, contactId] as RPC.Params)) as Promise<T.Contact>;
}
/**
* Add a single contact as a result of an explicit user action.
*
* Returns contact id of the created or existing contact
*/
public contactsCreateContact(accountId: T.U32, email: string, name: (string|null)): Promise<T.U32> {
return (this._transport.request('contacts_create_contact', [accountId, email, name] as RPC.Params)) as Promise<T.U32>;
}
/**
* Returns contact id of the created or existing DM chat with that contact
*/
public contactsCreateChatByContactId(accountId: T.U32, contactId: T.U32): Promise<T.U32> {
return (this._transport.request('contacts_create_chat_by_contact_id', [accountId, contactId] as RPC.Params)) as Promise<T.U32>;
}
public contactsBlock(accountId: T.U32, contactId: T.U32): Promise<null> {
return (this._transport.request('contacts_block', [accountId, contactId] as RPC.Params)) as Promise<null>;
}
public contactsUnblock(accountId: T.U32, contactId: T.U32): Promise<null> {
return (this._transport.request('contacts_unblock', [accountId, contactId] as RPC.Params)) as Promise<null>;
}
public contactsGetBlocked(accountId: T.U32): Promise<(T.Contact)[]> {
return (this._transport.request('contacts_get_blocked', [accountId] as RPC.Params)) as Promise<(T.Contact)[]>;
}
public contactsGetContactIds(accountId: T.U32, listFlags: T.U32, query: (string|null)): Promise<(T.U32)[]> {
return (this._transport.request('contacts_get_contact_ids', [accountId, listFlags, query] as RPC.Params)) as Promise<(T.U32)[]>;
}
/**
* Get a list of contacts.
* (formerly called getContacts2 in desktop)
*/
public contactsGetContacts(accountId: T.U32, listFlags: T.U32, query: (string|null)): Promise<(T.Contact)[]> {
return (this._transport.request('contacts_get_contacts', [accountId, listFlags, query] as RPC.Params)) as Promise<(T.Contact)[]>;
}
public contactsGetContactsByIds(accountId: T.U32, ids: (T.U32)[]): Promise<Record<T.U32,T.Contact>> {
return (this._transport.request('contacts_get_contacts_by_ids', [accountId, ids] as RPC.Params)) as Promise<Record<T.U32,T.Contact>>;
}
/**
* Returns all message IDs of the given types in a chat.
* Typically used to show a gallery.
*
* The list is already sorted and starts with the oldest message.
* Clients should not try to re-sort the list as this would be an expensive action
* and would result in inconsistencies between clients.
*/
public chatGetMedia(accountId: T.U32, chatId: T.U32, messageType: T.Viewtype, orMessageType2: (T.Viewtype|null), orMessageType3: (T.Viewtype|null)): Promise<(T.U32)[]> {
return (this._transport.request('chat_get_media', [accountId, chatId, messageType, orMessageType2, orMessageType3] as RPC.Params)) as Promise<(T.U32)[]>;
}
public webxdcSendStatusUpdate(accountId: T.U32, instanceMsgId: T.U32, updateStr: string, description: string): Promise<null> {
return (this._transport.request('webxdc_send_status_update', [accountId, instanceMsgId, updateStr, description] as RPC.Params)) as Promise<null>;
}
public webxdcGetStatusUpdates(accountId: T.U32, instanceMsgId: T.U32, lastKnownSerial: T.U32): Promise<string> {
return (this._transport.request('webxdc_get_status_updates', [accountId, instanceMsgId, lastKnownSerial] as RPC.Params)) as Promise<string>;
}
/**
* Get info from a webxdc message
*/
public messageGetWebxdcInfo(accountId: T.U32, instanceMsgId: T.U32): Promise<T.WebxdcMessageInfo> {
return (this._transport.request('message_get_webxdc_info', [accountId, instanceMsgId] as RPC.Params)) as Promise<T.WebxdcMessageInfo>;
}
/**
* Returns the messageid of the sent message
*/
public miscSendTextMessage(accountId: T.U32, text: string, chatId: T.U32): Promise<T.U32> {
return (this._transport.request('misc_send_text_message', [accountId, text, chatId] as RPC.Params)) as Promise<T.U32>;
}
}

View File

@@ -1,3 +0,0 @@
// AUTO-GENERATED by typescript-type-def
export type EventTypeName=("Info"|"SmtpConnected"|"ImapConnected"|"SmtpMessageSent"|"ImapMessageDeleted"|"ImapMessageMoved"|"NewBlobFile"|"DeletedBlobFile"|"Warning"|"Error"|"ErrorSelfNotInGroup"|"MsgsChanged"|"IncomingMsg"|"MsgsNoticed"|"MsgDelivered"|"MsgFailed"|"MsgRead"|"ChatModified"|"ChatEphemeralTimerModified"|"ContactsChanged"|"LocationChanged"|"ConfigureProgress"|"ImexProgress"|"ImexFileWritten"|"SecurejoinInviterProgress"|"SecurejoinJoinerProgress"|"ConnectivityChanged"|"SelfavatarChanged"|"WebxdcStatusUpdate");

View File

@@ -1,10 +0,0 @@
// AUTO-GENERATED by typescript-type-def
export type JSONValue=(null|boolean|number|string|(JSONValue)[]|{[key:string]:JSONValue;});
export type Params=((JSONValue)[]|Record<string,JSONValue>);
export type U32=number;
export type Request={"jsonrpc":"2.0";"method":string;"params"?:Params;"id"?:U32;};
export type I32=number;
export type Error={"code":I32;"message":string;"data"?:JSONValue;};
export type Response={"jsonrpc":"2.0";"id":(U32|null);"result"?:JSONValue;"error"?:Error;};
export type Message=(Request|Response);

View File

@@ -1,97 +0,0 @@
// AUTO-GENERATED by typescript-type-def
export type U32=number;
export type Account=(({"type":"Configured";}&{"id":U32;"displayName":(string|null);"addr":(string|null);"profileImage":(string|null);"color":string;})|({"type":"Unconfigured";}&{"id":U32;}));
export type ProviderInfo={"beforeLoginHint":string;"overviewPage":string;"status":U32;};
export type Usize=number;
export type ChatListEntry=[U32,U32];
export type I64=number;
export type ChatListItemFetchResult=(({"type":"ChatListItem";}&{"id":U32;"name":string;"avatarPath":(string|null);"color":string;"lastUpdated":(I64|null);"summaryText1":string;"summaryText2":string;"summaryStatus":U32;"isProtected":boolean;"isGroup":boolean;"freshMessageCounter":Usize;"isSelfTalk":boolean;"isDeviceTalk":boolean;"isSendingLocation":boolean;"isSelfInGroup":boolean;"isArchived":boolean;"isPinned":boolean;"isMuted":boolean;"isContactRequest":boolean;
/**
* contact id if this is a dm chat (for view profile entry in context menu)
*/
"dmChatContact":(U32|null);})|{"type":"ArchiveLink";}|({"type":"Error";}&{"id":U32;"error":string;}));
export type Contact={"address":string;"color":string;"authName":string;"status":string;"displayName":string;"id":U32;"name":string;"profileImage":(string|null);"nameAndAddr":string;"isBlocked":boolean;"isVerified":boolean;};
export type FullChat={"id":U32;"name":string;"isProtected":boolean;"profileImage":(string|null);"archived":boolean;"chatType":U32;"isUnpromoted":boolean;"isSelfTalk":boolean;"contacts":(Contact)[];"contactIds":(U32)[];"color":string;"freshMessageCounter":Usize;"isContactRequest":boolean;"isDeviceChat":boolean;"selfInGroup":boolean;"isMuted":boolean;"ephemeralTimer":U32;"canSend":boolean;};
export type Viewtype=("Unknown"|
/**
* Text message.
*/
"Text"|
/**
* Image message.
* If the image is an animated GIF, the type `Viewtype.Gif` should be used.
*/
"Image"|
/**
* Animated GIF message.
*/
"Gif"|
/**
* 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"|
/**
* Message containing an Audio file.
*/
"Audio"|
/**
* A voice message that was directly recorded by the user.
* For all other audio messages, the type `Viewtype.Audio` should be used.
*/
"Voice"|
/**
* Video messages.
*/
"Video"|
/**
* Message containing any file, eg. a PDF.
*/
"File"|
/**
* Message is an invitation to a videochat.
*/
"VideochatInvitation"|
/**
* Message is an webxdc instance.
*/
"Webxdc");
export type I32=number;
export type U64=number;
export type Message={"id":U32;"chatId":U32;"fromId":U32;"quotedText":(string|null);"quotedMessageId":(U32|null);"text":(string|null);"hasLocation":boolean;"hasHtml":boolean;"viewType":Viewtype;"state":U32;"timestamp":I64;"sortTimestamp":I64;"receivedTimestamp":I64;"hasDeviatingTimestamp":boolean;"subject":string;"showPadlock":boolean;"isSetupmessage":boolean;"isInfo":boolean;"isForwarded":boolean;"duration":I32;"dimensionsHeight":I32;"dimensionsWidth":I32;"videochatType":(U32|null);"videochatUrl":(string|null);"overrideSenderName":(string|null);"sender":Contact;"setupCodeBegin":(string|null);"file":(string|null);"fileMime":(string|null);"fileBytes":U64;"fileName":(string|null);};
export type WebxdcMessageInfo={
/**
* The name of the app.
*
* Defaults to the filename if not set in the manifest.
*/
"name":string;
/**
* App icon file name.
* Defaults to an standard icon if nothing is set in the manifest.
*
* To get the file, use dc_msg_get_webxdc_blob(). (not yet in jsonrpc, use rust api or cffi for it)
*
* App icons should should be square,
* the implementations will add round corners etc. as needed.
*/
"icon":string;
/**
* if the Webxdc represents a document, then this is the name of the document
*/
"document":(string|null);
/**
* short string describing the state of the app,
* sth. as "2 votes", "Highscore: 123",
* can be changed by the apps
*/
"summary":(string|null);
/**
* URL where the source code of the Webxdc and other information can be found;
* defaults to an empty string.
* Implementations may offer an menu or a button to open this URL.
*/
"sourceCodeUrl":(string|null);};
export type __AllTyps=[string,boolean,Record<string,string>,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],U32,Account,U32,string,(ProviderInfo|null),U32,boolean,U32,Record<string,string>,U32,string,(string|null),null,U32,Record<string,(string|null)>,null,U32,string,null,U32,string,(string|null),U32,(string)[],Record<string,(string|null)>,U32,null,U32,null,U32,(U32)[],U32,U32,Usize,U32,string,U32,U32,string,null,U32,(U32|null),(string|null),(U32|null),(ChatListEntry)[],U32,(ChatListEntry)[],Record<U32,ChatListItemFetchResult>,U32,U32,FullChat,U32,U32,null,U32,U32,null,U32,string,string,U32,U32,U32,U32,(U32)[],U32,U32,Message,U32,(U32)[],Record<U32,Message>,U32,U32,Contact,U32,string,(string|null),U32,U32,U32,U32,U32,U32,null,U32,U32,null,U32,(Contact)[],U32,U32,(string|null),(U32)[],U32,U32,(string|null),(Contact)[],U32,(U32)[],Record<U32,Contact>,U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,string,U32,U32];

View File

@@ -1,51 +0,0 @@
{
"name": "deltachat-jsonrpc-client",
"version": "0.1.0",
"main": "dist/deltachat.js",
"types": "dist/deltachat.d.ts",
"type": "module",
"author": "Delta Chat Developers (ML) <delta@codespeak.net>",
"license": "MPL-2.0",
"scripts": {
"prettier:check": "prettier --check **.ts",
"prettier:fix": "prettier --write **.ts",
"generate-bindings": "cargo test",
"build": "run-s generate-bindings build:tsc build:bundle",
"build:tsc": "tsc",
"build:bundle": "esbuild --format=esm --bundle dist/deltachat.js --outfile=dist/deltachat.bundle.js",
"example": "run-s build example:build example:start",
"example:build": "esbuild --bundle dist/example/example.js --outfile=dist/example.bundle.js",
"example:start": "http-server .",
"example:dev": "esbuild example/example.ts --bundle --outfile=dist/example.bundle.js --servedir=.",
"test": "run-s test:prepare test:run-coverage test:report-coverage",
"test:prepare": "cargo build --features webserver --bin deltachat-jsonrpc-server",
"test:run": "mocha dist/test",
"test:run-coverage": "COVERAGE=1 NODE_OPTIONS=--enable-source-maps c8 --include 'dist/*' -r text -r html -r json mocha dist/test",
"test:report-coverage": "node report_api_coverage.mjs",
"docs": "typedoc --out docs deltachat.ts"
},
"dependencies": {
"isomorphic-ws": "^4.0.1",
"tiny-emitter": "git+https://github.com/Simon-Laux/tiny-emitter.git",
"yerpc": "^0.3.3"
},
"devDependencies": {
"@types/chai": "^4.2.21",
"@types/chai-as-promised": "^7.1.5",
"@types/mocha": "^9.0.0",
"@types/node-fetch": "^2.5.7",
"@types/ws": "^7.2.4",
"c8": "^7.10.0",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"esbuild": "^0.14.11",
"http-server": "^14.1.1",
"mocha": "^9.1.1",
"node-fetch": "^2.6.1",
"npm-run-all": "^4.1.5",
"prettier": "^2.6.2",
"typedoc": "^0.23.2",
"typescript": "^4.5.5",
"ws": "^8.5.0"
}
}

View File

@@ -1,28 +0,0 @@
import { readFileSync } from "fs";
// only checks for the coverge of the api functions in bindings.ts for now
const generatedFile = "typescript/generated/client.ts";
const json = JSON.parse(readFileSync("./coverage/coverage-final.json"));
const jsonCoverage =
json[Object.keys(json).find((k) => k.includes(generatedFile))];
const fnMap = Object.keys(jsonCoverage.fnMap).map(
(key) => jsonCoverage.fnMap[key]
);
const htmlCoverage = readFileSync(
"./coverage/" + generatedFile + ".html",
"utf8"
);
const uncoveredLines = htmlCoverage
.split("\n")
.filter((line) => line.includes(`"function not covered"`));
const uncoveredFunctions = uncoveredLines.map(
(line) => />([\w_]+)\(/.exec(line)[1]
);
console.log(
"\nUncovered api functions:\n" +
uncoveredFunctions
.map((uF) => fnMap.find(({ name }) => name === uF))
.map(
({ name, line }) => `.${name.padEnd(40)} (${generatedFile}:${line})`
)
.join("\n")
);

View File

@@ -1,77 +0,0 @@
import * as T from "../generated/types.js";
import * as RPC from "../generated/jsonrpc.js";
import { RawClient } from "../generated/client.js";
import { EventTypeName } from "../generated/events.js";
import { WebsocketTransport, BaseTransport, Request } from "yerpc";
import { TinyEmitter } from "tiny-emitter";
export type DeltaChatEvent = {
id: EventTypeName;
contextId: number;
field1: any;
field2: any;
};
export type Events = Record<
EventTypeName | "ALL",
(event: DeltaChatEvent) => void
>;
export class BaseDeltaChat<
Transport extends BaseTransport<any>
> extends TinyEmitter<Events> {
rpc: RawClient;
account?: T.Account;
private contextEmitters: TinyEmitter<Events>[] = [];
constructor(public transport: Transport) {
super();
this.rpc = new RawClient(this.transport);
this.transport.on("request", (request: Request) => {
const method = request.method;
if (method === "event") {
const event = request.params! as DeltaChatEvent;
this.emit(event.id, event);
this.emit("ALL", event);
if (this.contextEmitters[event.contextId]) {
this.contextEmitters[event.contextId].emit(event.id, event);
this.contextEmitters[event.contextId].emit("ALL", event);
}
}
});
}
async listAccounts(): Promise<T.Account[]> {
return await this.rpc.getAllAccounts();
}
getContextEvents(account_id: number) {
if (this.contextEmitters[account_id]) {
return this.contextEmitters[account_id];
} else {
this.contextEmitters[account_id] = new TinyEmitter();
return this.contextEmitters[account_id];
}
}
}
export type Opts = {
url: string;
};
export const DEFAULT_OPTS: Opts = {
url: "ws://localhost:20808/ws",
};
export class DeltaChat extends BaseDeltaChat<WebsocketTransport> {
opts: Opts;
close() {
this.transport.close();
}
constructor(opts?: Opts | string) {
if (typeof opts === "string") opts = { url: opts };
if (opts) opts = { ...DEFAULT_OPTS, ...opts };
else opts = { ...DEFAULT_OPTS };
const transport = new WebsocketTransport(opts.url)
super(transport);
this.opts = opts;
}
}

View File

@@ -1,6 +0,0 @@
export * as RPC from "../generated/jsonrpc.js";
export * as T from "../generated/types.js";
export * from "../generated/events.js";
export { RawClient } from "../generated/client.js";
export * from "./client.js";
export * as yerpc from "yerpc";

View File

@@ -1,154 +0,0 @@
import { strictEqual } from "assert";
import chai, { assert, expect } from "chai";
import chaiAsPromised from "chai-as-promised";
chai.use(chaiAsPromised);
import { DeltaChat } from "../deltachat.js";
import {
RpcServerHandle,
startServer,
} from "./test_base.js";
describe("basic tests", () => {
let serverHandle: RpcServerHandle;
let dc: DeltaChat;
before(async () => {
serverHandle = await startServer();
// make sure server is up by the time we continue
await new Promise((res) => setTimeout(res, 100));
dc = new DeltaChat(serverHandle.url)
// dc.on("ALL", (event) => {
//console.log("event", event);
// });
});
after(async () => {
dc && dc.close();
await serverHandle.close();
});
it("check email address validity", async () => {
const validAddresses = [
"email@example.com",
"36aa165ae3406424e0c61af17700f397cad3fe8ab83d682d0bddf3338a5dd52e@yggmail@yggmail",
];
const invalidAddresses = ["email@", "example.com", "emai221"];
expect(
await Promise.all(
validAddresses.map((email) => dc.rpc.checkEmailValidity(email))
)
).to.not.contain(false);
expect(
await Promise.all(
invalidAddresses.map((email) => dc.rpc.checkEmailValidity(email))
)
).to.not.contain(true);
});
it("system info", async () => {
const systemInfo = await dc.rpc.getSystemInfo();
expect(systemInfo).to.contain.keys([
"arch",
"num_cpus",
"deltachat_core_version",
"sqlite_version",
]);
});
describe("account managment", () => {
it("should create account", async () => {
const res = await dc.rpc.addAccount();
assert((await dc.rpc.getAllAccountIds()).length === 1);
});
it("should remove the account again", async () => {
await dc.rpc.removeAccount((await dc.rpc.getAllAccountIds())[0]);
assert((await dc.rpc.getAllAccountIds()).length === 0);
});
it("should create multiple accounts", async () => {
await dc.rpc.addAccount();
await dc.rpc.addAccount();
await dc.rpc.addAccount();
await dc.rpc.addAccount();
assert((await dc.rpc.getAllAccountIds()).length === 4);
});
});
describe("contact managment", function () {
let accountId: number;
before(async () => {
accountId = await dc.rpc.addAccount();
});
it("should block and unblock contact", async function () {
const contactId = await dc.rpc.contactsCreateContact(
accountId,
"example@delta.chat",
null
);
expect((await dc.rpc.contactsGetContact(accountId, contactId)).isBlocked).to.be
.false;
await dc.rpc.contactsBlock(accountId, contactId);
expect((await dc.rpc.contactsGetContact(accountId, contactId)).isBlocked).to.be
.true;
expect(await dc.rpc.contactsGetBlocked(accountId)).to.have.length(1);
await dc.rpc.contactsUnblock(accountId, contactId);
expect((await dc.rpc.contactsGetContact(accountId, contactId)).isBlocked).to.be
.false;
expect(await dc.rpc.contactsGetBlocked(accountId)).to.have.length(0);
});
});
describe("configuration", function () {
let accountId: number;
before(async () => {
accountId = await dc.rpc.addAccount();
});
it("set and retrive", async function () {
await dc.rpc.setConfig(accountId, "addr", "valid@email");
assert((await dc.rpc.getConfig(accountId, "addr")) == "valid@email");
});
it("set invalid key should throw", async function () {
await expect(dc.rpc.setConfig(accountId, "invalid_key", "some value")).to.be
.eventually.rejected;
});
it("get invalid key should throw", async function () {
await expect(dc.rpc.getConfig(accountId, "invalid_key")).to.be.eventually
.rejected;
});
it("set and retrive ui.*", async function () {
await dc.rpc.setConfig(accountId, "ui.chat_bg", "color:red");
assert((await dc.rpc.getConfig(accountId, "ui.chat_bg")) == "color:red");
});
it("set and retrive (batch)", async function () {
const config = { addr: "valid@email", mail_pw: "1234" };
await dc.rpc.batchSetConfig(accountId, config);
const retrieved = await dc.rpc.batchGetConfig(accountId, Object.keys(config));
expect(retrieved).to.deep.equal(config);
});
it("set and retrive ui.* (batch)", async function () {
const config = {
"ui.chat_bg": "color:green",
"ui.enter_key_sends": "true",
};
await dc.rpc.batchSetConfig(accountId, config);
const retrieved = await dc.rpc.batchGetConfig(accountId, Object.keys(config));
expect(retrieved).to.deep.equal(config);
});
it("set and retrive mixed(ui and core) (batch)", async function () {
const config = {
"ui.chat_bg": "color:yellow",
"ui.enter_key_sends": "false",
addr: "valid2@email",
mail_pw: "123456",
};
await dc.rpc.batchSetConfig(accountId, config);
const retrieved = await dc.rpc.batchGetConfig(accountId, Object.keys(config));
expect(retrieved).to.deep.equal(config);
});
});
});

View File

@@ -1,202 +0,0 @@
import { assert, expect } from "chai";
import { DeltaChat, DeltaChatEvent, EventTypeName } from "../deltachat.js";
import {
RpcServerHandle,
createTempUser,
startServer,
} from "./test_base.js";
const EVENT_TIMEOUT = 20000
describe("online tests", function () {
let serverHandle: RpcServerHandle;
let dc: DeltaChat;
let account1: { email: string; password: string };
let account2: { email: string; password: string };
let accountId1: number, accountId2: number;
before(async function () {
this.timeout(12000)
if (!process.env.DCC_NEW_TMP_EMAIL) {
if (process.env.COVERAGE && !process.env.COVERAGE_OFFLINE) {
console.error(
"CAN NOT RUN COVERAGE correctly: Missing DCC_NEW_TMP_EMAIL environment variable!\n\n",
"You can set COVERAGE_OFFLINE=1 to circumvent this check and skip the online tests, but those coverage results will be wrong, because some functions can only be tested in the online test"
);
process.exit(1);
}
console.log(
"Missing DCC_NEW_TMP_EMAIL environment variable!, skip intergration tests"
);
this.skip();
}
serverHandle = await startServer();
dc = new DeltaChat(serverHandle.url)
dc.on("ALL", ({ id, contextId }) => {
if (id !== "Info") console.log(contextId, id);
});
account1 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
if (!account1 || !account1.email || !account1.password) {
console.log(
"We didn't got back an account from the api, skip intergration tests"
);
this.skip();
}
account2 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
if (!account2 || !account2.email || !account2.password) {
console.log(
"We didn't got back an account2 from the api, skip intergration tests"
);
this.skip();
}
});
after(async () => {
dc && dc.close();
serverHandle && (await serverHandle.close());
});
let accountsConfigured = false;
it("configure test accounts", async function () {
this.timeout(40000);
accountId1 = await dc.rpc.addAccount();
await dc.rpc.setConfig(accountId1, "addr", account1.email);
await dc.rpc.setConfig(accountId1, "mail_pw", account1.password);
await dc.rpc.configure(accountId1);
accountId2 = await dc.rpc.addAccount();
await dc.rpc.batchSetConfig(accountId2, {
addr: account2.email,
mail_pw: account2.password,
});
await dc.rpc.configure(accountId2)
accountsConfigured = true;
});
it("send and recieve text message", async function () {
if (!accountsConfigured) {
this.skip();
}
this.timeout(15000);
const contactId = await dc.rpc.contactsCreateContact(
accountId1,
account2.email,
null
);
const chatId = await dc.rpc.contactsCreateChatByContactId(accountId1, contactId);
const eventPromise = Promise.race([
waitForEvent(dc, "MsgsChanged", accountId2),
waitForEvent(dc, "IncomingMsg", accountId2),
]);
await dc.rpc.miscSendTextMessage(accountId1, "Hello", chatId);
const { field1: chatIdOnAccountB } = await eventPromise;
await dc.rpc.acceptChat(accountId2, chatIdOnAccountB);
const messageList = await dc.rpc.messageListGetMessageIds(
accountId2,
chatIdOnAccountB,
0
);
expect(messageList).have.length(1);
const message = await dc.rpc.messageGetMessage(accountId2, messageList[0]);
expect(message.text).equal("Hello");
});
it("send and recieve text message roundtrip, encrypted on answer onwards", async function () {
if (!accountsConfigured) {
this.skip();
}
this.timeout(10000);
// send message from A to B
const contactId = await dc.rpc.contactsCreateContact(
accountId1,
account2.email,
null
);
const chatId = await dc.rpc.contactsCreateChatByContactId(accountId1, contactId);
const eventPromise = Promise.race([
waitForEvent(dc, "MsgsChanged", accountId2),
waitForEvent(dc, "IncomingMsg", accountId2),
]);
dc.rpc.miscSendTextMessage(accountId1, "Hello2", chatId);
// wait for message from A
console.log("wait for message from A");
const event = await eventPromise;
const { field1: chatIdOnAccountB } = event;
await dc.rpc.acceptChat(accountId2, chatIdOnAccountB);
const messageList = await dc.rpc.messageListGetMessageIds(
accountId2,
chatIdOnAccountB,
0
);
const message = await dc.rpc.messageGetMessage(
accountId2,
messageList.reverse()[0]
);
expect(message.text).equal("Hello2");
// Send message back from B to A
const eventPromise2 = Promise.race([
waitForEvent(dc, "MsgsChanged", accountId1),
waitForEvent(dc, "IncomingMsg", accountId1),
]);
dc.rpc.miscSendTextMessage(accountId2, "super secret message", chatId);
// Check if answer arives at A and if it is encrypted
await eventPromise2;
const messageId = (
await dc.rpc.messageListGetMessageIds(accountId1, chatId, 0)
).reverse()[0];
const message2 = await dc.rpc.messageGetMessage(accountId1, messageId);
expect(message2.text).equal("super secret message");
expect(message2.showPadlock).equal(true);
});
it("get provider info for example.com", async () => {
const acc = await dc.rpc.addAccount();
const info = await dc.rpc.getProviderInfo(acc, "example.com");
expect(info).to.be.not.null;
expect(info?.overviewPage).to.equal(
"https://providers.delta.chat/example-com"
);
expect(info?.status).to.equal(3);
});
it("get provider info - domain and email should give same result", async () => {
const acc = await dc.rpc.addAccount();
const info_domain = await dc.rpc.getProviderInfo(acc, "example.com");
const info_email = await dc.rpc.getProviderInfo(acc, "hi@example.com");
expect(info_email).to.deep.equal(info_domain);
});
});
async function waitForEvent(
dc: DeltaChat,
eventType: EventTypeName,
accountId: number,
timeout: number = EVENT_TIMEOUT
): Promise<DeltaChatEvent> {
return new Promise((resolve, reject) => {
const rejectTimeout = setTimeout(
() => reject(new Error('Timeout reached before event came in')),
timeout
)
const callback = (event: DeltaChatEvent) => {
if (event.contextId == accountId) {
dc.off(eventType, callback);
clearTimeout(rejectTimeout)
resolve(event);
}
};
dc.on(eventType, callback);
});
}

View File

@@ -1,94 +0,0 @@
import { tmpdir } from "os";
import { join, resolve } from "path";
import { mkdtemp, rm } from "fs/promises";
import { existsSync } from "fs";
import { spawn, exec } from "child_process";
import fetch from "node-fetch";
export const RPC_SERVER_PORT = 20808;
export type RpcServerHandle = {
url: string,
close: () => Promise<void>
}
export async function startServer(port: number = RPC_SERVER_PORT): Promise<RpcServerHandle> {
const tmpDir = await mkdtemp(join(tmpdir(), "deltachat-jsonrpc-test"));
const pathToServerBinary = resolve(join(await getTargetDir(), "debug/deltachat-jsonrpc-server"));
console.log('using server binary: ' + pathToServerBinary);
if (!existsSync(pathToServerBinary)) {
throw new Error(
"server executable does not exist, you need to build it first" +
"\nserver executable not found at " +
pathToServerBinary
);
}
const server = spawn(pathToServerBinary, {
cwd: tmpDir,
env: {
RUST_LOG: process.env.RUST_LOG || "info",
DC_PORT: '' + port
},
});
let shouldClose = false;
server.on("exit", () => {
if (shouldClose) {
return;
}
throw new Error("Server quit");
});
server.stderr.pipe(process.stderr);
server.stdout.pipe(process.stdout)
const url = `ws://localhost:${port}/ws`
return {
url,
close: async () => {
shouldClose = true;
if (!server.kill()) {
console.log("server termination failed");
}
await rm(tmpDir, { recursive: true });
},
};
}
export async function createTempUser(url: string) {
const response = await fetch(url, {
method: "POST",
headers: {
"cache-control": "no-cache",
},
});
if (!response.ok) throw new Error('Received invalid response')
return response.json();
}
function getTargetDir(): Promise<string> {
return new Promise((resolve, reject) => {
exec(
"cargo metadata --no-deps --format-version 1",
(error, stdout, _stderr) => {
if (error) {
console.log("error", error);
reject(error);
} else {
try {
const json = JSON.parse(stdout);
resolve(json.target_directory);
} catch (error) {
console.log("json error", error);
reject(error);
}
}
}
);
});
}

View File

@@ -1,20 +0,0 @@
{
"compilerOptions": {
"alwaysStrict": true,
"strict": true,
"sourceMap": true,
"strictNullChecks": true,
"rootDir": ".",
"outDir": "dist",
"lib": ["ES2017", "dom"],
"target": "ES2017",
"module": "es2020",
"declaration": true,
"esModuleInterop": true,
"moduleResolution": "node",
"noImplicitAny": true,
"isolatedModules": true
},
"include": ["*.ts", "example/*.ts", "test/*.ts"],
"compileOnSave": false
}

View File

@@ -11,49 +11,26 @@ Changes to the UIs
Changes in the core
-------------------
- [x] We have one primary self address and any number of secondary self addresses. `is_self_addr()` checks all of them.
- DONE: We have one primary self address and any number of secondary self addresses. `is_self_addr()` checks all of them.
- [x] If the user does a reconfigure and changes the email address, the previous address is added as a secondary self address.
- DONE: If the user does a reconfigure and changes the email address, the previous address is added as a secondary self address.
- don't forget to deduplicate secondary self addresses in case the user switches back and forth between addresses).
- The key stays the same.
- [x] No changes for 1:1 chats, there simply is a new one. (This works since, contrary to group messages, messages sent to a 1:1 chat are not assigned to the group chat but always to the 1:1 chat with the sender. So it's not a problem that the new messages might be put into the old chat if they are a reply to a message there.)
- No changes for 1:1 chats, there simply is a new one. (This works since, contrary to group messages, messages sent to a 1:1 chat are not assigned to the group chat but always to the 1:1 chat with the sender. So it's not a problem that the new messages might be put into the old chat if they are a reply to a message there.)
- [ ] When sending a message: If any of the secondary self addrs is in the chat's member list, remove it locally (because we just transitioned away from it). We add a log message for this (alternatively, a system message in the chat would be more visible).
- When sending a message: If any of the secondary self addrs is in the chat's member list, remove it locally (because we just transitioned away from it). We add a log message for this (alternatively, a system message in the chat would be more visible).
- [x] ([#3385](https://github.com/deltachat/deltachat-core-rust/pull/3385)) When receiving a message: If the key exists, but belongs to another address (we may want to benchmark this)
- When receiving a message: If the key exists, but belongs to another address (we may want to benchmark this)
AND there is a `Chat-Version` header\
AND the message is signed correctly
AND the From address is (also) in the encrypted (and therefore signed) headers <sup>[[1]](#myfootnote1)</sup>\
AND the message timestamp is newer than the contact's `lastseen` (to prevent changing the address back when messages arrive out of order) (this condition is not that important since we will have eventual consistency even without it):
Replace the contact in _all_ groups, possibly deduplicate the members list, and add a system message to all of these chats.
- Note that we can't simply compare the keys byte-by-byte, since the UID may have changed, or the sender may have rotated the key and signed the new key with the old one.
<a name="myfootnote1">[1]</a>: Without this check, an attacker could replay a message from Alice to Bob. Then Bob's device would do an AEAP transition from Alice's to the attacker's address, allowing for easier phishing.
<details>
<summary>More details about this</summary>
Suppose Alice sends a message to Evil (or to a group with both Evil and Bob). Evil then forwards the message to Bob, changing the From and To headers (and if necessary Message-Id) and replacing `addr=alice@example.org;` in the autocrypt header with `addr=evil@example.org;`.
Then Bob's device sees that there is a message which is signed by Alice's key and comes from Evil's address and would do the AEAP transition, i.e. replace Alice with Evil in all groups and show a message "Alice changed their address from alice@example.org to evil@example.org". Disadvantages for Evil are that Bob's message will be shown on Alice's device, possibly creating confusion/suspicion, and that the usual "Setup changed for..." message will be shown the next time Evil sends a message (because Evil doesn't know Alice's private key).
Possible mitigations:
- if we make the AEAP device message sth. like "Automatically removed alice@example.org and added evil@example.org", then this will create more suspicion, making the phishing harder (we didn't talk about what what the wording should be at all yet).
- Add something similar to replay protection to our Autocrypt implementation. This could be done e.g. by adding a second `From` header to the protected headers. If it's present, the receiver then requires it to be the same as the outer `From`, and if it's not present, we don't do AEAP --> **That's what we implemented**
Note that usually a mail is signed by a key that has a UID matching the from address.
That's not mandatory for Autocrypt (and in fact, we just keep the old UID when changing the self address, so with AEAP the UID will actually be different than the from address sometimes)
https://autocrypt.org/level1.html#openpgp-based-key-data says:
> The content of the user id packet is only decorative
</details>
### Notes:
- We treat protected and non-protected chats the same
@@ -120,8 +97,3 @@ Other
-----
- The user is responsible that messages to the old address arrive at the new address, for example by configuring the old provider to forward all emails to the new one.
Notes during implementing
========================
- As far as I understand the code, unencrypted messages are unsigned. So, the transition only works if both sides have the other side's key.

View File

@@ -12,6 +12,8 @@ use deltachat::chatlist::*;
use deltachat::constants::*;
use deltachat::contact::*;
use deltachat::context::*;
use deltachat::dc_receive_imf::*;
use deltachat::dc_tools::*;
use deltachat::download::DownloadState;
use deltachat::imex::*;
use deltachat::location;
@@ -19,9 +21,7 @@ use deltachat::log::LogExt;
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::receive_imf::*;
use deltachat::sql;
use deltachat::tools::*;
use deltachat::{config, provider};
use tokio::fs;
@@ -96,10 +96,10 @@ async fn reset_tables(context: &Context, bits: i32) {
}
async fn poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<()> {
let data = read_file(context, filename).await?;
let data = dc_read_file(context, filename).await?;
if let Err(err) = receive_imf(context, &data, false).await {
println!("receive_imf errored: {:?}", err);
if let Err(err) = dc_receive_imf(context, &data, false).await {
println!("dc_receive_imf errored: {:?}", err);
}
Ok(())
}
@@ -128,7 +128,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
}
real_spec = rs.unwrap();
}
if let Some(suffix) = get_filesuffix_lc(&real_spec) {
if let Some(suffix) = dc_get_filesuffix_lc(&real_spec) {
if suffix == "eml" && poke_eml_file(context, &real_spec).await.is_ok() {
read_cnt += 1
}
@@ -187,7 +187,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
DownloadState::Failure => " [⬇ Download failed]",
};
let temp2 = timestamp_to_str(msg.get_timestamp());
let temp2 = dc_timestamp_to_str(msg.get_timestamp());
let msgtext = msg.get_text();
println!(
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{}{}{} [{}]",
@@ -215,14 +215,6 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
msg.get_videochat_url().unwrap_or_default(),
msg.get_videochat_type().unwrap_or_default()
)
} else if msg.get_viewtype() == Viewtype::Webxdc {
match msg.get_webxdc_info(context).await {
Ok(info) => format!(
"[WEBXDC: {}, icon={}, document={}, summary={}, source_code_url={}]",
info.name, info.icon, info.document, info.summary, info.source_code_url
),
Err(err) => format!("[get_webxdc_info() failed: {}]", err),
}
} else {
"".to_string()
},
@@ -601,7 +593,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
_ => "",
}
};
let timestr = timestamp_to_str(summary.timestamp);
let timestr = dc_timestamp_to_str(summary.timestamp);
println!(
"{}{}{} [{}]{}",
summary
@@ -814,7 +806,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!(
"Loc#{}: {}: lat={} lng={} acc={} Chat#{} Contact#{} {} {}",
location.location_id,
timestamp_to_str(location.timestamp),
dc_timestamp_to_str(location.timestamp),
location.latitude,
location.longitude,
location.accuracy,
@@ -1237,8 +1229,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"fileinfo" => {
ensure!(!arg1.is_empty(), "Argument <file> missing.");
if let Ok(buf) = read_file(&context, &arg1).await {
let (width, height) = get_filemeta(&buf)?;
if let Ok(buf) = dc_read_file(&context, &arg1).await {
let (width, height) = dc_get_filemeta(&buf)?;
println!("width={}, height={}", width, height);
} else {
bail!("Command failed.");

View File

@@ -409,7 +409,7 @@ async fn handle_cmd(
"oauth2" => {
if let Some(addr) = ctx.get_config(config::Config::Addr).await? {
let oauth2_url =
get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?;
dc_get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?;
if oauth2_url.is_none() {
println!("OAuth2 not available for {}.", &addr);
} else {
@@ -426,7 +426,7 @@ async fn handle_cmd(
"getqr" | "getbadqr" => {
ctx.start_io().await;
let group = arg1.parse::<u32>().ok().map(ChatId::new);
let mut qr = get_securejoin_qr(&ctx, group).await?;
let mut qr = dc_get_securejoin_qr(&ctx, group).await?;
if !qr.is_empty() {
if arg0 == "getbadqr" && qr.len() > 40 {
qr.replace_range(12..22, "0000000000")
@@ -457,7 +457,7 @@ async fn handle_cmd(
"joinqr" => {
ctx.start_io().await;
if !arg0.is_empty() {
join_securejoin(&ctx, arg1).await?;
dc_join_securejoin(&ctx, arg1).await?;
}
}
"exit" | "quit" => return Ok(ExitResult::Exit),

View File

@@ -117,7 +117,7 @@ $ fnm install 17 --arch x64
$ fnm use 17
$ node -p process.arch
# result should be x64
$ rustup target add x86_64-apple-darwin
$ cd deltachat-core-rust && rustup target add x86_64-apple-darwin && cd -
$ git apply patches/m1_build_use_x86_64.patch
$ CARGO_BUILD_TARGET=x86_64-apple-darwin npm run build
$ npm run test

View File

@@ -19,11 +19,10 @@ interface NativeAccount {}
export class AccountManager extends EventEmitter {
dcn_accounts: NativeAccount
accountDir: string
jsonRpcStarted = false
constructor(cwd: string, os = 'deltachat-node') {
super()
debug('DeltaChat constructor')
super()
this.accountDir = cwd
this.dcn_accounts = binding.dcn_accounts_new(os, this.accountDir)
@@ -115,31 +114,6 @@ export class AccountManager extends EventEmitter {
debug('Started event handler')
}
startJsonRpcHandler(callback: ((response: string) => void) | null, apiVersion: string = "v0") {
if (this.dcn_accounts === null) {
throw new Error('dcn_account is null')
}
if (!callback) {
throw new Error('no callback set')
}
if (this.jsonRpcStarted) {
throw new Error('jsonrpc was started already')
}
binding.dcn_accounts_start_jsonrpc(this.dcn_accounts, apiVersion, callback.bind(this))
debug('Started JSON-RPC handler')
this.jsonRpcStarted = true
}
jsonRpcRequest(message: string) {
if (!this.jsonRpcStarted) {
throw new Error(
'jsonrpc is not active, start it with startJsonRpcHandler first'
)
}
binding.dcn_json_rpc_request(this.dcn_accounts, message)
}
startIO() {
binding.dcn_accounts_start_io(this.dcn_accounts)
}

View File

@@ -9,7 +9,7 @@ const buildArgs = [
'build',
'--release',
'--features',
'vendored,jsonrpc',
'vendored',
'-p',
'deltachat_ffi'
]

View File

@@ -1,28 +0,0 @@
const { default: dc } = require('./dist')
const ac = new dc('test1233490')
console.log("[1]");
ac.startJsonRpcHandler(console.log)
console.log("[2]");
console.log(
ac.jsonRpcRequest(
JSON.stringify({
jsonrpc: '2.0',
method: 'get_all_account_ids',
params: [],
id: 2,
})
)
)
console.log("[3]");
setTimeout(() => {
console.log("[4]");
ac.close() // This segfaults -> TODO Findout why?
console.log('still living')
}, 1000)

View File

@@ -1,50 +0,0 @@
const { default: dc } = require('./dist')
const ac = new dc('test1233490')
console.log('[1]')
ac.startJsonRpcHandler(console.log)
console.log('[2]')
console.log(
ac.jsonRpcRequest(
JSON.stringify({
jsonrpc: '2.0',
method: 'batch_set_config',
id: 3,
params: [
69,
{
addr: '',
mail_user: '',
mail_pw: '',
mail_server: '',
mail_port: '',
mail_security: '',
imap_certificate_checks: '',
send_user: '',
send_pw: '',
send_server: '',
send_port: '',
send_security: '',
smtp_certificate_checks: '',
socks5_enabled: '0',
socks5_host: '',
socks5_port: '',
socks5_user: '',
socks5_password: '',
},
],
})
)
)
console.log('[3]')
setTimeout(() => {
console.log('[4]')
ac.close() // This segfaults -> TODO Findout why?
console.log('still living')
}, 1000)

View File

@@ -34,9 +34,6 @@ typedef struct dcn_accounts_t {
dc_accounts_t* dc_accounts;
napi_threadsafe_function threadsafe_event_handler;
uv_thread_t event_handler_thread;
napi_threadsafe_function threadsafe_jsonrpc_handler;
uv_thread_t jsonrpc_thread;
dc_jsonrpc_instance_t* jsonrpc_instance;
int gc;
} dcn_accounts_t;
@@ -101,6 +98,14 @@ static void finalize_provider(napi_env env, void* data, void* hint) {
}
}
static void finalize_account(napi_env env, void* data, void* hint) {
if (data) {
dc_accounts_t* dcn_accounts = (dc_accounts_t*)data;
//TRACE("cleaning up provider");
dc_accounts_unref(dcn_accounts);
}
}
/**
* Helpers.
*/
@@ -2927,12 +2932,6 @@ NAPI_METHOD(dcn_accounts_unref) {
uv_thread_join(&dcn_accounts->event_handler_thread);
dcn_accounts->event_handler_thread = 0;
}
if (dcn_accounts->jsonrpc_instance) {
dc_jsonrpc_request(dcn_accounts->jsonrpc_instance, "{}");
uv_thread_join(&dcn_accounts->jsonrpc_thread);
dc_jsonrpc_unref(dcn_accounts->jsonrpc_instance);
dcn_accounts->jsonrpc_instance = NULL;
}
dc_accounts_unref(dcn_accounts->dc_accounts);
dcn_accounts->dc_accounts = NULL;
@@ -3091,6 +3090,8 @@ static void accounts_event_handler_thread_func(void* arg)
{
dcn_accounts_t* dcn_accounts = (dcn_accounts_t*)arg;
TRACE("event_handler_thread_func starting");
dc_accounts_event_emitter_t * dc_accounts_event_emitter = dc_accounts_get_event_emitter(dcn_accounts->dc_accounts);
@@ -3180,7 +3181,7 @@ static void call_accounts_js_event_handler(napi_env env, napi_value js_callback,
if (status != napi_ok) {
napi_throw_error(env, NULL, "Unable to create argv[3] for event_handler arguments");
}
dc_str_unref(data2_string);
free(data2_string);
} else {
status = napi_create_int32(env, dc_event_get_data2_int(dc_event), &argv[3]);
if (status != napi_ok) {
@@ -3241,125 +3242,6 @@ NAPI_METHOD(dcn_accounts_start_event_handler) {
NAPI_RETURN_UNDEFINED();
}
// JSON RPC
static void accounts_jsonrpc_thread_func(void* arg)
{
dcn_accounts_t* dcn_accounts = (dcn_accounts_t*)arg;
TRACE("accounts_jsonrpc_thread_func starting");
char* response;
while (true) {
response = dc_jsonrpc_next_response(dcn_accounts->jsonrpc_instance);
if (response == NULL) {
// done or broken
break;
}
if (!dcn_accounts->threadsafe_jsonrpc_handler) {
TRACE("threadsafe_jsonrpc_handler not set, bailing");
break;
}
// Don't process events if we're being garbage collected!
if (dcn_accounts->gc == 1) {
TRACE("dc_accounts has been destroyed, bailing");
break;
}
napi_status status = napi_call_threadsafe_function(dcn_accounts->threadsafe_jsonrpc_handler, response, napi_tsfn_blocking);
if (status == napi_closing) {
TRACE("JS function got released, bailing");
break;
}
}
TRACE("accounts_jsonrpc_thread_func ended");
napi_release_threadsafe_function(dcn_accounts->threadsafe_jsonrpc_handler, napi_tsfn_release);
}
static void call_accounts_js_jsonrpc_handler(napi_env env, napi_value js_callback, void* _context, void* data)
{
char* response = (char*)data;
napi_value global;
napi_status status = napi_get_global(env, &global);
if (status != napi_ok) {
napi_throw_error(env, NULL, "Unable to get global");
}
napi_value argv[1];
if (response != 0) {
status = napi_create_string_utf8(env, response, NAPI_AUTO_LENGTH, &argv[0]);
} else {
status = napi_create_string_utf8(env, "", NAPI_AUTO_LENGTH, &argv[0]);
}
if (status != napi_ok) {
napi_throw_error(env, NULL, "Unable to create argv for js jsonrpc_handler arguments");
}
dc_str_unref(response);
TRACE("calling back into js");
napi_value result;
status = napi_call_function(
env,
global,
js_callback,
1,
argv,
&result);
if (status != napi_ok) {
TRACE("Unable to call jsonrpc_handler callback2");
const napi_extended_error_info* error_result;
NAPI_STATUS_THROWS(napi_get_last_error_info(env, &error_result));
}
}
NAPI_METHOD(dcn_accounts_start_jsonrpc) {
NAPI_ARGV(3);
NAPI_DCN_ACCOUNTS();
NAPI_ARGV_UTF8_MALLOC(api_version, 1);
napi_value callback = argv[2];
TRACE("calling..");
napi_value async_resource_name;
NAPI_STATUS_THROWS(napi_create_string_utf8(env, "dc_accounts_jsonrpc_callback", NAPI_AUTO_LENGTH, &async_resource_name));
TRACE("creating threadsafe function..");
NAPI_STATUS_THROWS(napi_create_threadsafe_function(
env,
callback,
0,
async_resource_name,
1000, // max_queue_size
1,
NULL,
NULL,
NULL,
call_accounts_js_jsonrpc_handler,
&dcn_accounts->threadsafe_jsonrpc_handler));
TRACE("done");
dcn_accounts->gc = 0;
dcn_accounts->jsonrpc_instance = dc_jsonrpc_init(dcn_accounts->dc_accounts, api_version);
TRACE("creating uv thread..");
uv_thread_create(&dcn_accounts->jsonrpc_thread, accounts_jsonrpc_thread_func, dcn_accounts);
NAPI_RETURN_UNDEFINED();
}
NAPI_METHOD(dcn_json_rpc_request) {
NAPI_ARGV(2);
NAPI_DCN_ACCOUNTS();
if (!dcn_accounts->jsonrpc_instance) {
const char* msg = "dcn_accounts->jsonrpc_instance is null, have you called dcn_accounts_start_jsonrpc()?";
NAPI_STATUS_THROWS(napi_throw_type_error(env, NULL, msg));
}
NAPI_ARGV_UTF8_MALLOC(request, 1);
dc_jsonrpc_request(dcn_accounts->jsonrpc_instance, request);
free(request);
NAPI_RETURN_UNDEFINED();
}
NAPI_INIT() {
/**
@@ -3630,9 +3512,4 @@ NAPI_INIT() {
NAPI_EXPORT_FUNCTION(dcn_send_webxdc_status_update);
NAPI_EXPORT_FUNCTION(dcn_get_webxdc_status_updates);
NAPI_EXPORT_FUNCTION(dcn_msg_get_webxdc_blob);
/** jsonrpc **/
NAPI_EXPORT_FUNCTION(dcn_accounts_start_jsonrpc);
NAPI_EXPORT_FUNCTION(dcn_json_rpc_request);
}

View File

@@ -23,7 +23,7 @@
dcn_accounts_t* dcn_accounts; \
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dcn_accounts)); \
if (!dcn_accounts) { \
const char* msg = "Provided dcn_acounts is null"; \
const char* msg = "Provided dnc_acounts is null"; \
NAPI_STATUS_THROWS(napi_throw_type_error(env, NULL, msg)); \
} \
if (!dcn_accounts->dc_accounts) { \

View File

@@ -2,7 +2,7 @@
import DeltaChat, { Message } from '../dist'
import binding from '../binding'
import { deepEqual, deepStrictEqual, strictEqual } from 'assert'
import { strictEqual } from 'assert'
import chai, { expect } from 'chai'
import chaiAsPromised from 'chai-as-promised'
import { EventId2EventName, C } from '../dist/constants'
@@ -84,95 +84,6 @@ describe('static tests', function () {
})
})
describe('JSON RPC', function () {
it('smoketest', async function () {
const { dc } = DeltaChat.newTemporary()
let promise_resolve
const promise = new Promise((res, _rej) => {
promise_resolve = res
})
dc.startJsonRpcHandler(promise_resolve)
dc.jsonRpcRequest(
JSON.stringify({
jsonrpc: '2.0',
method: 'get_all_account_ids',
params: [],
id: 2,
})
)
deepStrictEqual(
{
jsonrpc: '2.0',
id: 2,
result: [1],
},
JSON.parse(await promise)
)
dc.close()
})
it('basic test', async function () {
const { dc } = DeltaChat.newTemporary()
const promises = {}
dc.startJsonRpcHandler((msg) => {
const response = JSON.parse(msg)
promises[response.id](response)
delete promises[response.id]
})
const call = (request) => {
dc.jsonRpcRequest(JSON.stringify(request))
return new Promise((res, _rej) => {
promises[request.id] = res
})
}
deepStrictEqual(
{
jsonrpc: '2.0',
id: 2,
result: [1],
},
await call({
jsonrpc: '2.0',
method: 'get_all_account_ids',
params: [],
id: 2,
})
)
deepStrictEqual(
{
jsonrpc: '2.0',
id: 3,
result: 2,
},
await call({
jsonrpc: '2.0',
method: 'add_account',
params: [],
id: 3,
})
)
deepStrictEqual(
{
jsonrpc: '2.0',
id: 4,
result: [1, 2],
},
await call({
jsonrpc: '2.0',
method: 'get_all_account_ids',
params: [],
id: 4,
})
)
dc.close()
})
})
describe('Basic offline Tests', function () {
it('opens a context', async function () {
const { dc, context } = DeltaChat.newTemporary()

View File

@@ -1,6 +1,5 @@
{
"dependencies": {
"@deltachat/jsonrpc-client": "file:deltachat-jsonrpc/typescript",
"debug": "^4.1.1",
"napi-macros": "^2.0.0",
"node-gyp-build": "^4.1.0"

View File

@@ -33,7 +33,7 @@ mkdir -p $TOXWORKDIR
# Note that the independent remote_tests_python step does all kinds of
# live-testing already.
unset DCC_NEW_TMP_EMAIL
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,pypy37,pypy38,pypy39,auditwheels
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,auditwheels
popd

View File

@@ -63,7 +63,7 @@ def main():
parser = ArgumentParser(prog="set_core_version")
parser.add_argument("newversion")
toml_list = ["Cargo.toml", "deltachat-ffi/Cargo.toml", "deltachat-jsonrpc/Cargo.toml"]
toml_list = ["Cargo.toml", "deltachat-ffi/Cargo.toml"]
try:
opts = parser.parse_args()
except SystemExit:

18
spec.md
View File

@@ -1,6 +1,6 @@
# chat-mail specification
Version: 0.34.0
Version: 0.33.0
Status: In-progress
Format: [Semantic Line Breaks](https://sembr.org/)
@@ -474,20 +474,4 @@ as the sending time of the message as indicated by its Date header,
or the time of first receipt if that date is in the future or unavailable.
# Transitioning to a new e-mail address (AEAP)
When receiving a message:
- If the key exists, but belongs to another address
- AND there is a `Chat-Version` header
- AND the message is signed correctly
- AND the From address is (also) in the encrypted (and therefore signed) headers
- AND the message timestamp is newer than the contact's `lastseen`
(to prevent changing the address back when messages arrive out of order)
(this condition is not that important
since we will have eventual consistency even without it):
Replace the contact in _all_ groups,
possibly deduplicate the members list,
and add a system message to all of these chats.
Copyright © 2017-2021 Delta Chat contributors.

View File

@@ -724,14 +724,19 @@ mod tests {
let t = TestContext::new().await;
let avatar_src = t.dir.path().join("avatar.jpg");
let avatar_bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
fs::write(&avatar_src, avatar_bytes).await.unwrap();
File::create(&avatar_src)
.await
.unwrap()
.write_all(avatar_bytes)
.await
.unwrap();
let avatar_blob = t.get_blobdir().join("avatar.jpg");
assert!(!avatar_blob.exists());
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
.await
.unwrap();
assert!(avatar_blob.exists());
assert!(fs::metadata(&avatar_blob).await.unwrap().len() < avatar_bytes.len() as u64);
assert!(tokio::fs::metadata(&avatar_blob).await.unwrap().len() < avatar_bytes.len() as u64);
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
@@ -761,7 +766,10 @@ mod tests {
async fn test_selfavatar_in_blobdir() {
let t = TestContext::new().await;
let avatar_src = t.get_blobdir().join("avatar.png");
fs::write(&avatar_src, test_utils::AVATAR_900x900_BYTES)
File::create(&avatar_src)
.await
.unwrap()
.write_all(test_utils::AVATAR_900x900_BYTES)
.await
.unwrap();
@@ -784,7 +792,12 @@ mod tests {
let t = TestContext::new().await;
let avatar_src = t.dir.path().join("avatar.png");
let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png");
fs::write(&avatar_src, avatar_bytes).await.unwrap();
File::create(&avatar_src)
.await
.unwrap()
.write_all(avatar_bytes)
.await
.unwrap();
let avatar_blob = t.get_blobdir().join("avatar.png");
assert!(!avatar_blob.exists());
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
@@ -792,7 +805,7 @@ mod tests {
.unwrap();
assert!(avatar_blob.exists());
assert_eq!(
fs::metadata(&avatar_blob).await.unwrap().len(),
tokio::fs::metadata(&avatar_blob).await.unwrap().len(),
avatar_bytes.len() as u64
);
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
@@ -963,7 +976,7 @@ mod tests {
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
let file = t.get_blobdir().join("anyfile.dat");
fs::write(&file, b"bla").await?;
File::create(&file).await?.write_all("bla".as_ref()).await?;
let mut msg = Message::new(Viewtype::File);
msg.set_file(file.to_str().unwrap(), None);
let prepared_id = chat::prepare_msg(&t, chat_id, &mut msg).await?;
@@ -983,7 +996,7 @@ mod tests {
assert_ne!(t.get_blobdir().to_str(), t.dir.path().to_str());
let file = t.dir.path().join("anyfile.dat");
fs::write(&file, b"bla").await?;
File::create(&file).await?.write_all("bla".as_ref()).await?;
let mut msg = Message::new(Viewtype::File);
msg.set_file(file.to_str().unwrap(), None);
assert!(chat::prepare_msg(&t, chat_id, &mut msg).await.is_err());

View File

@@ -20,6 +20,12 @@ use crate::constants::{
};
use crate::contact::{Contact, ContactId, Origin, VerifiedStatus};
use crate::context::Context;
use crate::dc_receive_imf::ReceivedMsg;
use crate::dc_tools::{
dc_create_id, dc_create_outgoing_rfc724_mid, dc_create_smeared_timestamp,
dc_create_smeared_timestamps, dc_get_abs_path, dc_gm2local_offset, improve_single_line_input,
time, IsNoneOrEmpty,
};
use crate::ephemeral::Timer as EphemeralTimer;
use crate::events::EventType;
use crate::html::new_html_mimepart;
@@ -28,14 +34,9 @@ use crate::mimefactory::MimeFactory;
use crate::mimeparser::SystemMessage;
use crate::param::{Param, Params};
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::receive_imf::ReceivedMsg;
use crate::scheduler::InterruptInfo;
use crate::smtp::send_msg_to_smtp;
use crate::stock_str;
use crate::tools::{
create_id, create_outgoing_rfc724_mid, create_smeared_timestamp, create_smeared_timestamps,
get_abs_path, gm2local_offset, improve_single_line_input, time, IsNoneOrEmpty,
};
use crate::webxdc::WEBXDC_SUFFIX;
use crate::{location, sql};
@@ -232,7 +233,7 @@ impl ChatId {
grpname,
grpid,
create_blocked,
create_smeared_timestamp(context).await,
dc_create_smeared_timestamp(context).await,
create_protected,
param.unwrap_or_default(),
],
@@ -434,7 +435,7 @@ impl ChatId {
self,
&msg_text,
cmd,
create_smeared_timestamp(context).await,
dc_create_smeared_timestamp(context).await,
None,
None,
None,
@@ -1133,7 +1134,7 @@ impl Chat {
pub async fn get_profile_image(&self, context: &Context) -> Result<Option<PathBuf>> {
if let Some(image_rel) = self.param.get(Param::ProfileImage) {
if !image_rel.is_empty() {
return Ok(Some(get_abs_path(context, image_rel)));
return Ok(Some(dc_get_abs_path(context, image_rel)));
}
} else if self.typ == Chattype::Single {
let contacts = get_chat_contacts(context, self.id).await?;
@@ -1144,7 +1145,7 @@ impl Chat {
}
} else if self.typ == Chattype::Broadcast {
if let Ok(image_rel) = get_broadcast_icon(context).await {
return Ok(Some(get_abs_path(context, image_rel)));
return Ok(Some(dc_get_abs_path(context, image_rel)));
}
}
Ok(None)
@@ -1270,7 +1271,7 @@ impl Chat {
Chattype::Group => Some(self.grpid.as_str()),
_ => None,
};
create_outgoing_rfc724_mid(grpid, &from)
dc_create_outgoing_rfc724_mid(grpid, &from)
};
if self.typ == Chattype::Single {
@@ -1747,7 +1748,7 @@ impl ChatIdBlocked {
_ => (),
}
let created_timestamp = create_smeared_timestamp(context).await;
let created_timestamp = dc_create_smeared_timestamp(context).await;
let chat_id = context
.sql
.transaction(move |transaction| {
@@ -1900,7 +1901,7 @@ async fn prepare_msg_common(
context,
msg,
update_msg_id,
create_smeared_timestamp(context).await,
dc_create_smeared_timestamp(context).await,
)
.await?;
msg.chat_id = chat_id;
@@ -1994,7 +1995,7 @@ async fn prepare_send_msg(
chat_id: ChatId,
msg: &mut Message,
) -> Result<Option<i64>> {
// prepare_msg() leaves the message state to OutPreparing, we
// dc_prepare_msg() leaves the message state to OutPreparing, we
// only have to change the state to OutPending in this case.
// Otherwise we still have to prepare the message, which will set
// the state to OutPending.
@@ -2178,7 +2179,7 @@ pub async fn send_videochat_invitation(context: &Context, chat_id: ChatId) -> Re
bail!("webrtc_instance not set");
};
let instance = Message::create_webrtc_instance(&instance, &create_id());
let instance = Message::create_webrtc_instance(&instance, &dc_create_id());
let mut msg = Message::new(Viewtype::VideochatInvitation);
msg.param.set(Param::WebrtcRoom, &instance);
@@ -2241,7 +2242,7 @@ pub async fn get_chat_msgs(
let mut ret = Vec::new();
let mut last_day = 0;
let cnv_to_local = gm2local_offset();
let cnv_to_local = dc_gm2local_offset();
for (ts, curr_id) in sorted_rows {
if (flags & DC_GCM_ADDDAYMARKER) != 0 {
@@ -2537,7 +2538,7 @@ pub async fn create_group_chat(
let chat_name = improve_single_line_input(chat_name);
ensure!(!chat_name.is_empty(), "Invalid chat name");
let grpid = create_id();
let grpid = dc_create_id();
let row_id = context
.sql
@@ -2549,7 +2550,7 @@ pub async fn create_group_chat(
Chattype::Group,
chat_name,
grpid,
create_smeared_timestamp(context).await,
dc_create_smeared_timestamp(context).await,
],
)
.await?;
@@ -2596,7 +2597,7 @@ async fn find_unused_broadcast_list_name(context: &Context) -> Result<String> {
/// Creates a new broadcast list.
pub async fn create_broadcast_list(context: &Context) -> Result<ChatId> {
let chat_name = find_unused_broadcast_list_name(context).await?;
let grpid = create_id();
let grpid = dc_create_id();
let row_id = context
.sql
.insert(
@@ -2607,7 +2608,7 @@ pub async fn create_broadcast_list(context: &Context) -> Result<ChatId> {
Chattype::Broadcast,
chat_name,
grpid,
create_smeared_timestamp(context).await,
dc_create_smeared_timestamp(context).await,
],
)
.await?;
@@ -3048,7 +3049,7 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
chat_id.unarchive_if_not_muted(context).await?;
if let Ok(mut chat) = Chat::load_from_db(context, chat_id).await {
ensure!(chat.can_send(context).await?, "cannot send to {}", chat_id);
curr_timestamp = create_smeared_timestamps(context, msg_ids.len()).await;
curr_timestamp = dc_create_smeared_timestamps(context, msg_ids.len()).await;
let ids = context
.sql
.query_map(
@@ -3243,12 +3244,12 @@ pub async fn add_device_msg_with_importance(
if let Some(msg) = msg {
chat_id = ChatId::get_for_contact(context, ContactId::DEVICE).await?;
let rfc724_mid = create_outgoing_rfc724_mid(None, "@device");
let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device");
msg.try_calc_and_set_dimensions(context).await.ok();
prepare_msg_blob(context, msg).await?;
chat_id.unarchive_if_not_muted(context).await?;
let timestamp_sent = create_smeared_timestamp(context).await;
let timestamp_sent = dc_create_smeared_timestamp(context).await;
// makes sure, the added message is the last one,
// even if the date is wrong (useful esp. when warning about bad dates)
@@ -3381,7 +3382,7 @@ pub(crate) async fn add_info_msg_with_cmd(
parent: Option<&Message>,
from_id: Option<ContactId>,
) -> Result<MsgId> {
let rfc724_mid = create_outgoing_rfc724_mid(None, "@device");
let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device");
let ephemeral_timer = chat_id.get_ephemeral_timer(context).await?;
let mut param = Params::new();
@@ -3458,11 +3459,13 @@ pub(crate) async fn update_msg_text_and_timestamp(
mod tests {
use super::*;
use crate::chatlist::{get_archived_cnt, Chatlist};
use crate::chatlist::{dc_get_archived_cnt, Chatlist};
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS};
use crate::contact::Contact;
use crate::receive_imf::receive_imf;
use crate::dc_receive_imf::dc_receive_imf;
use crate::test_utils::TestContext;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_chat_info() {
@@ -4243,7 +4246,7 @@ mod tests {
let t = TestContext::new_alice().await;
async fn msg_from_bob(t: &TestContext, num: u32) -> Result<()> {
receive_imf(
dc_receive_imf(
t,
format!(
"From: bob@example.net\n\
@@ -4266,17 +4269,17 @@ mod tests {
let chat_id = t.get_last_msg().await.get_chat_id();
chat_id.accept(&t).await?;
chat_id.set_visibility(&t, ChatVisibility::Archived).await?;
assert_eq!(get_archived_cnt(&t).await?, 1);
assert_eq!(dc_get_archived_cnt(&t).await?, 1);
// not muted chat is unarchived on receiving a message
msg_from_bob(&t, 2).await?;
assert_eq!(get_archived_cnt(&t).await?, 0);
assert_eq!(dc_get_archived_cnt(&t).await?, 0);
// forever muted chat is not unarchived on receiving a message
chat_id.set_visibility(&t, ChatVisibility::Archived).await?;
set_muted(&t, chat_id, MuteDuration::Forever).await?;
msg_from_bob(&t, 3).await?;
assert_eq!(get_archived_cnt(&t).await?, 1);
assert_eq!(dc_get_archived_cnt(&t).await?, 1);
// otherwise muted chat is not unarchived on receiving a message
set_muted(
@@ -4290,7 +4293,7 @@ mod tests {
)
.await?;
msg_from_bob(&t, 4).await?;
assert_eq!(get_archived_cnt(&t).await?, 1);
assert_eq!(dc_get_archived_cnt(&t).await?, 1);
// expired mute will unarchive the chat
set_muted(
@@ -4304,19 +4307,19 @@ mod tests {
)
.await?;
msg_from_bob(&t, 5).await?;
assert_eq!(get_archived_cnt(&t).await?, 0);
assert_eq!(dc_get_archived_cnt(&t).await?, 0);
// no unarchiving on sending to muted chat or on adding info messages to muted chat
chat_id.set_visibility(&t, ChatVisibility::Archived).await?;
set_muted(&t, chat_id, MuteDuration::Forever).await?;
send_text_msg(&t, chat_id, "out".to_string()).await?;
add_info_msg(&t, chat_id, "info", time()).await?;
assert_eq!(get_archived_cnt(&t).await?, 1);
assert_eq!(dc_get_archived_cnt(&t).await?, 1);
// finally, unarchive on sending to not muted chat
set_muted(&t, chat_id, MuteDuration::NotMuted).await?;
send_text_msg(&t, chat_id, "out2".to_string()).await?;
assert_eq!(get_archived_cnt(&t).await?, 0);
assert_eq!(dc_get_archived_cnt(&t).await?, 0);
Ok(())
}
@@ -4703,7 +4706,7 @@ mod tests {
assert_eq!(msg.match_indices("Gr.").count(), 1);
// Bob receives this message, he may detect group by `References:`- or `Chat-Group:`-header
receive_imf(&bob, msg.as_bytes(), false).await.unwrap();
dc_receive_imf(&bob, msg.as_bytes(), false).await.unwrap();
let msg = bob.get_last_msg().await;
let bob_chat = Chat::load_from_db(&bob, msg.chat_id).await?;
@@ -4722,7 +4725,7 @@ mod tests {
assert_eq!(msg.match_indices("Chat-").count(), 0);
// Alice receives this message - she can still detect the group by the `References:`-header
receive_imf(&alice, msg.as_bytes(), false).await.unwrap();
dc_receive_imf(&alice, msg.as_bytes(), false).await.unwrap();
let msg = alice.get_last_msg().await;
assert_eq!(msg.chat_id, alice_chat_id);
assert_eq!(msg.text, Some("ho!".to_string()));
@@ -4735,7 +4738,7 @@ mod tests {
let t = TestContext::new_alice().await;
let chat = t.create_chat_with_contact("bob", "bob@example.org").await;
receive_imf(
dc_receive_imf(
&t,
b"From: bob@example.org\n\
To: alice@example.org\n\
@@ -4782,7 +4785,7 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await?;
assert_eq!(chats.len(), 0);
receive_imf(
dc_receive_imf(
&t,
b"From: bob@example.org\n\
To: alice@example.org\n\
@@ -4829,7 +4832,7 @@ mod tests {
async fn test_contact_request_archive() -> Result<()> {
let t = TestContext::new_alice().await;
receive_imf(
dc_receive_imf(
&t,
b"From: bob@example.org\n\
To: alice@example.org\n\
@@ -4846,7 +4849,7 @@ mod tests {
assert_eq!(chats.len(), 1);
let chat_id = chats.get_chat_id(0)?;
assert!(Chat::load_from_db(&t, chat_id).await?.is_contact_request());
assert_eq!(get_archived_cnt(&t).await?, 0);
assert_eq!(dc_get_archived_cnt(&t).await?, 0);
// archive request without accepting or blocking
chat_id.set_visibility(&t, ChatVisibility::Archived).await?;
@@ -4855,7 +4858,7 @@ mod tests {
assert_eq!(chats.len(), 1);
let chat_id = chats.get_chat_id(0)?;
assert!(chat_id.is_archived_link());
assert_eq!(get_archived_cnt(&t).await?, 1);
assert_eq!(dc_get_archived_cnt(&t).await?, 1);
let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None).await?;
assert_eq!(chats.len(), 1);
@@ -4876,7 +4879,7 @@ mod tests {
.unwrap();
// Alice receives a classic (non-chat) message from Bob.
receive_imf(
dc_receive_imf(
&alice,
b"From: bob@example.org\n\
To: alice@example.org\n\
@@ -4933,7 +4936,7 @@ mod tests {
let bob_chat = bob.create_chat(&alice).await;
let file = alice.get_blobdir().join(filename);
tokio::fs::write(&file, bytes).await?;
File::create(&file).await?.write_all(bytes).await?;
let mut msg = Message::new(Viewtype::Sticker);
msg.set_file(file.to_str().unwrap(), None);
@@ -4998,7 +5001,7 @@ mod tests {
let file_name = "sticker.jpg";
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
let file = alice.get_blobdir().join(file_name);
tokio::fs::write(&file, bytes).await?;
File::create(&file).await?.write_all(bytes).await?;
let mut msg = Message::new(Viewtype::Sticker);
msg.set_file(file.to_str().unwrap(), None);

View File

@@ -240,7 +240,7 @@ impl Chatlist {
ids
};
if add_archived_link_item && get_archived_cnt(context).await? > 0 {
if add_archived_link_item && dc_get_archived_cnt(context).await? > 0 {
if ids.is_empty() && flag_add_alldone_hint {
ids.push((DC_CHAT_ID_ALLDONE_HINT, None));
}
@@ -355,7 +355,7 @@ impl Chatlist {
}
/// Returns the number of archived chats
pub async fn get_archived_cnt(context: &Context) -> Result<usize> {
pub async fn dc_get_archived_cnt(context: &Context) -> Result<usize> {
let count = context
.sql
.count(
@@ -371,8 +371,8 @@ mod tests {
use super::*;
use crate::chat::{create_group_chat, get_chat_contacts, ProtectionStatus};
use crate::dc_receive_imf::dc_receive_imf;
use crate::message::Viewtype;
use crate::receive_imf::receive_imf;
use crate::stock_str::StockMessage;
use crate::test_utils::TestContext;
@@ -493,7 +493,7 @@ mod tests {
let t = TestContext::new_alice().await;
// receive a one-to-one-message
receive_imf(
dc_receive_imf(
&t,
b"From: Bob Authname <bob@example.org>\n\
To: alice@example.org\n\
@@ -553,7 +553,7 @@ mod tests {
let t = TestContext::new_alice().await;
// receive a one-to-one-message without authname set
receive_imf(
dc_receive_imf(
&t,
b"From: bob@example.org\n\
To: alice@example.org\n\

View File

@@ -8,10 +8,10 @@ use crate::blob::BlobObject;
use crate::constants::DC_VERSION_STR;
use crate::contact::addr_cmp;
use crate::context::Context;
use crate::dc_tools::{dc_get_abs_path, improve_single_line_input, EmailAddress};
use crate::events::EventType;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::provider::{get_provider_by_id, Provider};
use crate::tools::{get_abs_path, improve_single_line_input, EmailAddress};
/// The available configuration keys.
#[derive(
@@ -196,7 +196,7 @@ impl Context {
let value = match key {
Config::Selfavatar => {
let rel_path = self.sql.get_raw_config(key).await?;
rel_path.map(|p| get_abs_path(self, &p).to_string_lossy().into_owned())
rel_path.map(|p| dc_get_abs_path(self, &p).to_string_lossy().into_owned())
}
Config::SysVersion => Some((&*DC_VERSION_STR).clone()),
Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)),
@@ -437,8 +437,9 @@ mod tests {
use std::string::ToString;
use crate::constants;
use crate::dc_receive_imf::dc_receive_imf;
use crate::test_utils::TestContext;
use crate::test_utils::TestContextManager;
use num_traits::FromPrimitive;
#[test]
@@ -556,4 +557,68 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_change_primary_self_addr() -> Result<()> {
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;
let bob = tcm.bob().await;
// Alice sends a message to Bob
let alice_bob_chat = alice.create_chat(&bob).await;
let sent = alice.send_text(alice_bob_chat.id, "Hi").await;
let bob_msg = bob.recv_msg(&sent).await;
bob_msg.chat_id.accept(&bob).await?;
assert_eq!(bob_msg.text.unwrap(), "Hi");
// Alice changes her self address and reconfigures
// (ensure_secret_key_exists() is called during configure)
alice
.set_primary_self_addr("alice@someotherdomain.xyz")
.await?;
crate::e2ee::ensure_secret_key_exists(&alice).await?;
assert_eq!(
alice.get_primary_self_addr().await?,
"alice@someotherdomain.xyz"
);
// Bob sends a message to Alice, encrypting to her previous key
let sent = bob.send_text(bob_msg.chat_id, "hi back").await;
// Alice set up message forwarding so that she still receives
// the message with her new address
let alice_msg = alice.recv_msg(&sent).await;
assert_eq!(alice_msg.text, Some("hi back".to_string()));
assert_eq!(alice_msg.get_showpadlock(), true);
assert_eq!(alice_msg.chat_id, alice_bob_chat.id);
// Even if Bob sends a message to Alice without In-Reply-To,
// it's still assigned to the 1:1 chat with Bob and not to
// a group (without secondary addresses, an ad-hoc group
// would be created)
dc_receive_imf(
&alice,
b"From: bob@example.net
To: alice@example.org
Chat-Version: 1.0
Message-ID: <456@example.com>
Message w/out In-Reply-To
",
false,
)
.await?;
let alice_msg = alice.get_last_msg().await;
assert_eq!(
alice_msg.text,
Some("Message w/out In-Reply-To".to_string())
);
assert_eq!(alice_msg.get_showpadlock(), false);
assert_eq!(alice_msg.chat_id, alice_bob_chat.id);
Ok(())
}
}

View File

@@ -13,16 +13,16 @@ use tokio::task;
use crate::config::Config;
use crate::context::Context;
use crate::dc_tools::{time, EmailAddress};
use crate::imap::Imap;
use crate::job;
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam, Socks5Config};
use crate::message::{Message, Viewtype};
use crate::oauth2::get_oauth2_addr;
use crate::oauth2::dc_get_oauth2_addr;
use crate::provider::{Protocol, Socket, UsernamePattern};
use crate::scheduler::InterruptInfo;
use crate::smtp::Smtp;
use crate::stock_str;
use crate::tools::{time, EmailAddress};
use crate::{chat, e2ee, provider};
use auto_mozilla::moz_autoconfigure;
@@ -154,9 +154,9 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
// IMAP and SMTP or not at all.
if param.imap.oauth2 && !socks5_enabled {
// the used oauth2 addr may differ, check this.
// if get_oauth2_addr() is not available in the oauth2 implementation, just use the given one.
// if dc_get_oauth2_addr() is not available in the oauth2 implementation, just use the given one.
progress!(ctx, 10);
if let Some(oauth2_addr) = get_oauth2_addr(ctx, &param.addr, &param.imap.password)
if let Some(oauth2_addr) = dc_get_oauth2_addr(ctx, &param.addr, &param.imap.password)
.await?
.and_then(|e| e.parse().ok())
{
@@ -416,9 +416,6 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
let create_mvbox = ctx.should_watch_mvbox().await?;
// Send client ID as soon as possible before doing anything else.
imap.determine_capabilities(ctx).await?;
imap.configure_folders(ctx, create_mvbox).await?;
imap.select_with_uidvalidity(ctx, "INBOX")

View File

@@ -16,6 +16,7 @@ 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::context::Context;
use crate::dc_tools::{dc_get_abs_path, improve_single_line_input, EmailAddress};
use crate::events::EventType;
use crate::key::{DcKey, SignedPublicKey};
use crate::login_param::LoginParam;
@@ -24,7 +25,6 @@ use crate::mimeparser::AvatarAction;
use crate::param::{Param, Params};
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::sql::{self, params_iter};
use crate::tools::{get_abs_path, improve_single_line_input, EmailAddress};
use crate::{chat, stock_str};
/// Contact ID, including reserved IDs.
@@ -38,7 +38,7 @@ impl ContactId {
pub const UNDEFINED: ContactId = ContactId::new(0);
/// The owner of the account.
///
/// The email-address is set by `set_config` using "addr".
/// 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);
@@ -142,7 +142,7 @@ pub struct Contact {
/// E-Mail-Address of the contact. It is recommended to use `Contact::get_addr` to access this field.
addr: String,
/// Blocked state. Use contact_is_blocked to access this field.
/// Blocked state. Use dc_contact_is_blocked to access this field.
pub blocked: bool,
/// Time when the contact was seen last time, Unix time in seconds.
@@ -212,13 +212,13 @@ pub enum Origin {
/// address is in our address book
AddressBook = 0x80000,
/// set on Alice's side for contacts like Bob that have scanned the QR code offered by her. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling contact_is_verified() !
/// set on Alice's side for contacts like Bob that have scanned the QR code offered by her. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling dc_contact_is_verified() !
SecurejoinInvited = 0x0100_0000,
/// set on Bob's side for contacts scanned and verified from a QR code. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling contact_is_verified() !
/// set on Bob's side for contacts scanned and verified from a QR code. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling dc_contact_is_verified() !
SecurejoinJoined = 0x0200_0000,
/// contact added mannually by create_contact(), this should be the largest origin as otherwise the user cannot modify the names
/// contact added mannually by dc_create_contact(), this should be the largest origin as otherwise the user cannot modify the names
ManuallyCreated = 0x0400_0000,
}
@@ -344,7 +344,7 @@ impl Contact {
/// We assume, the contact name, if any, is entered by the user and is used "as is" therefore,
/// normalize() is *not* called for the name. If the contact is blocked, it is unblocked.
///
/// To add a number of contacts, see `add_address_book()` which is much faster for adding
/// To add a number of contacts, see `dc_add_address_book()` which is much faster for adding
/// a bunch of addresses.
///
/// May result in a `#DC_EVENT_CONTACTS_CHANGED` event.
@@ -384,10 +384,10 @@ impl Contact {
/// Check if an e-mail address belongs to a known and unblocked contact.
///
/// Known and unblocked contacts will be returned by `get_contacts()`.
/// Known and unblocked contacts will be returned by `dc_get_contacts()`.
///
/// To validate an e-mail address independently of the contact database
/// use `may_be_valid_addr()`.
/// use `dc_may_be_valid_addr()`.
pub async fn lookup_id_by_addr(
context: &Context,
addr: &str,
@@ -676,7 +676,7 @@ impl Contact {
/// Returns known and unblocked contacts.
///
/// To get information about a single contact, see get_contact().
/// To get information about a single contact, see dc_get_contact().
///
/// `listflags` is a combination of flags:
/// - if the flag DC_GCL_ADD_SELF is set, SELF is added to the list unless filtered by other parameters
@@ -970,11 +970,11 @@ impl Contact {
bail!("Could not delete contact with ongoing chats");
}
/// Get a single contact object. For a list, see eg. get_contacts().
/// Get a single contact object. For a list, see eg. dc_get_contacts().
///
/// For contact ContactId::SELF (1), the function returns sth.
/// like "Me" in the selected language and the email address
/// defined by set_config().
/// defined by dc_set_config().
pub async fn get_by_id(context: &Context, contact_id: ContactId) -> Result<Contact> {
let contact = Contact::load_from_db(context, contact_id).await?;
@@ -1063,7 +1063,7 @@ impl Contact {
/// Get the contact's profile image.
/// This is the image set by each remote user on their own
/// using set_config(context, "selfavatar", image).
/// 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 let Some(p) = context.get_config(Config::Selfavatar).await? {
@@ -1071,7 +1071,7 @@ impl Contact {
}
} else if let Some(image_rel) = self.param.get(Param::ProfileImage) {
if !image_rel.is_empty() {
return Ok(Some(get_abs_path(context, image_rel)));
return Ok(Some(dc_get_abs_path(context, image_rel)));
}
}
Ok(None)
@@ -1438,12 +1438,15 @@ fn split_address_book(book: &str) -> Vec<(&str, &str)> {
#[cfg(test)]
mod tests {
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
use super::*;
use crate::chat::{get_chat_contacts, send_text_msg, Chat};
use crate::chatlist::Chatlist;
use crate::dc_receive_imf::dc_receive_imf;
use crate::message::Message;
use crate::receive_imf::receive_imf;
use crate::test_utils::{self, TestContext};
#[test]
@@ -1687,7 +1690,7 @@ mod tests {
let t = TestContext::new_alice().await;
// first message creates contact and one-to-one-chat without name set
receive_imf(
dc_receive_imf(
&t,
b"From: f@example.org\n\
To: alice@example.org\n\
@@ -1716,7 +1719,7 @@ mod tests {
assert_eq!(contacts.len(), 1);
// second message inits the name
receive_imf(
dc_receive_imf(
&t,
b"From: Flobbyfoo <f@example.org>\n\
To: alice@example.org\n\
@@ -1744,7 +1747,7 @@ mod tests {
assert_eq!(contacts.len(), 1);
// third message changes the name
receive_imf(
dc_receive_imf(
&t,
b"From: Foo Flobby <f@example.org>\n\
To: alice@example.org\n\
@@ -2197,7 +2200,10 @@ CCCB 5AA9 F6E1 141C 9431
assert_eq!(alice1.get_config(Config::Selfavatar).await?, None);
let avatar_src = alice1.get_blobdir().join("avatar.png");
tokio::fs::write(&avatar_src, test_utils::AVATAR_900x900_BYTES).await?;
File::create(&avatar_src)
.await?
.write_all(test_utils::AVATAR_900x900_BYTES)
.await?;
alice1
.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
@@ -2260,7 +2266,7 @@ Chat-Version: 1.0
Date: Sun, 22 Mar 2020 22:37:55 +0000
Hi."#;
receive_imf(&alice, mime, false).await?;
dc_receive_imf(&alice, mime, false).await?;
let msg = alice.get_last_msg().await;
let timestamp = msg.get_timestamp();

View File

@@ -15,6 +15,7 @@ use crate::chat::{get_chat_cnt, ChatId};
use crate::config::Config;
use crate::constants::DC_VERSION_STR;
use crate::contact::Contact;
use crate::dc_tools::{duration_to_str, time};
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::key::{DcKey, SignedPublicKey};
use crate::login_param::LoginParam;
@@ -23,7 +24,6 @@ use crate::quota::QuotaInfo;
use crate::ratelimit::Ratelimit;
use crate::scheduler::Scheduler;
use crate::sql::Sql;
use crate::tools::{duration_to_str, time};
#[derive(Clone, Debug)]
pub struct Context {
@@ -61,11 +61,6 @@ pub struct InnerContext {
/// Set to `None` if quota was never tried to load.
pub(crate) quota: RwLock<Option<QuotaInfo>>,
/// Server ID response if ID capability is supported
/// and the server returned non-NIL on the inbox connection.
/// <https://datatracker.ietf.org/doc/html/rfc2971>
pub(crate) server_id: RwLock<Option<HashMap<String, String>>>,
pub(crate) last_full_folder_scan: Mutex<Option<Instant>>,
/// ID for this `Context` in the current process.
@@ -195,7 +190,6 @@ impl Context {
scheduler: RwLock::new(None),
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 3.0)), // Allow to send 3 messages immediately, no more than once every 20 seconds.
quota: RwLock::new(None),
server_id: RwLock::new(None),
creation_time: std::time::SystemTime::now(),
last_full_folder_scan: Mutex::new(None),
last_error: std::sync::RwLock::new("".to_string()),
@@ -435,11 +429,6 @@ impl Context {
res.insert("socks5_enabled", socks5_enabled.to_string());
res.insert("entered_account_settings", l.to_string());
res.insert("used_account_settings", l2.to_string());
let server_id = self.server_id.read().await;
res.insert("imap_server_id", format!("{:?}", server_id));
drop(server_id);
res.insert("secondary_addrs", secondary_addrs);
res.insert(
"fetch_existing_msgs",
@@ -680,10 +669,10 @@ mod tests {
get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat, ChatId, MuteDuration,
};
use crate::contact::ContactId;
use crate::dc_receive_imf::dc_receive_imf;
use crate::dc_tools::dc_create_outgoing_rfc724_mid;
use crate::message::{Message, Viewtype};
use crate::receive_imf::receive_imf;
use crate::test_utils::TestContext;
use crate::tools::create_outgoing_rfc724_mid;
use anyhow::Context as _;
use std::time::Duration;
use strum::IntoEnumIterator;
@@ -722,10 +711,10 @@ mod tests {
\n\
hello\n",
contact.get_addr(),
create_outgoing_rfc724_mid(None, contact.get_addr())
dc_create_outgoing_rfc724_mid(None, contact.get_addr())
);
println!("{}", msg);
receive_imf(t, msg.as_bytes(), false).await.unwrap();
dc_receive_imf(t, msg.as_bytes(), false).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -18,6 +18,7 @@ use crate::contact::{
may_be_valid_addr, normalize_name, Contact, ContactId, Origin, VerifiedStatus,
};
use crate::context::Context;
use crate::dc_tools::{dc_create_id, dc_extract_grpid_from_rfc724_mid, dc_smeared_time};
use crate::download::DownloadState;
use crate::ephemeral::{stock_ephemeral_timer_changed, Timer as EphemeralTimer};
use crate::events::EventType;
@@ -36,7 +37,6 @@ use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus};
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
use crate::sql;
use crate::stock_str;
use crate::tools::{create_id, extract_grpid_from_rfc724_mid, smeared_time};
/// This is the struct that is returned after receiving one email (aka MIME message).
///
@@ -59,7 +59,7 @@ pub struct ReceivedMsg {
///
/// This method returns errors on a failure to parse the mail or extract Message-ID. It's only used
/// for tests and REPL tool, not actual message reception pipeline.
pub async fn receive_imf(
pub async fn dc_receive_imf(
context: &Context,
imf_raw: &[u8],
seen: bool,
@@ -69,8 +69,8 @@ pub async fn receive_imf(
.headers
.get_header_value(HeaderDef::MessageId)
.and_then(|msgid| parse_message_id(&msgid).ok())
.unwrap_or_else(create_id);
receive_imf_inner(context, &rfc724_mid, imf_raw, seen, None, false).await
.unwrap_or_else(dc_create_id);
dc_receive_imf_inner(context, &rfc724_mid, imf_raw, seen, None, false).await
}
/// Receive a message and add it to the database.
@@ -87,7 +87,7 @@ pub async fn receive_imf(
///
/// If `is_partial_download` is set, it contains the full message size in bytes.
/// Do not confuse that with `replace_partial_download` that will be set when the full message is loaded later.
pub(crate) async fn receive_imf_inner(
pub(crate) async fn dc_receive_imf_inner(
context: &Context,
rfc724_mid: &str,
imf_raw: &[u8],
@@ -98,14 +98,14 @@ pub(crate) async fn receive_imf_inner(
info!(context, "Receiving message, seen={}...", seen);
if std::env::var(crate::DCC_MIME_DEBUG).unwrap_or_default() == "2" {
info!(context, "receive_imf: incoming message mime-body:");
info!(context, "dc_receive_imf: incoming message mime-body:");
println!("{}", String::from_utf8_lossy(imf_raw));
}
let mut mime_parser =
match MimeMessage::from_bytes_with_partial(context, imf_raw, is_partial_download).await {
Err(err) => {
warn!(context, "receive_imf: can't parse MIME: {}", err);
warn!(context, "dc_receive_imf: can't parse MIME: {}", err);
return Ok(None);
}
Ok(mime_parser) => mime_parser,
@@ -113,7 +113,7 @@ pub(crate) async fn receive_imf_inner(
// we can not add even an empty record if we have no info whatsoever
if !mime_parser.has_headers() {
warn!(context, "receive_imf: no headers found");
warn!(context, "dc_receive_imf: no headers found");
return Ok(None);
}
@@ -158,7 +158,7 @@ pub(crate) async fn receive_imf_inner(
let incoming = from_id != ContactId::SELF;
let to_ids = add_or_lookup_contacts_by_address_list(
let to_ids = dc_add_or_lookup_contacts_by_address_list(
context,
&mime_parser.recipients,
if !incoming {
@@ -172,7 +172,7 @@ pub(crate) async fn receive_imf_inner(
)
.await?;
let rcvd_timestamp = smeared_time(context).await;
let rcvd_timestamp = dc_smeared_time(context).await;
let sent_timestamp = mime_parser
.get_header(HeaderDef::Date)
.and_then(|value| mailparse::dateparse(value).ok())
@@ -346,13 +346,13 @@ pub(crate) async fn receive_imf_inner(
///
/// Also returns whether it is blocked or not and its origin.
///
/// * `prevent_rename`: passed through to `add_or_lookup_contacts_by_address_list()`
/// * `prevent_rename`: passed through to `dc_add_or_lookup_contacts_by_address_list()`
pub async fn from_field_to_contact_id(
context: &Context,
from_address_list: &[SingleInfo],
prevent_rename: bool,
) -> Result<(ContactId, bool, Origin)> {
let from_ids = add_or_lookup_contacts_by_address_list(
let from_ids = dc_add_or_lookup_contacts_by_address_list(
context,
from_address_list,
Origin::IncomingUnknownFrom,
@@ -1271,7 +1271,7 @@ async fn calc_sort_timestamp(
}
}
Ok(min(sort_timestamp, smeared_time(context).await))
Ok(min(sort_timestamp, dc_smeared_time(context).await))
}
async fn lookup_chat_by_reply(
@@ -1885,7 +1885,7 @@ fn extract_grpid(mime_parser: &MimeMessage, headerdef: HeaderDef) -> Option<&str
.split(',')
.map(str::trim)
.filter(|part| !part.is_empty());
parts.filter_map(extract_grpid_from_rfc724_mid).next()
parts.filter_map(dc_extract_grpid_from_rfc724_mid).next()
}
/// Creates ad-hoc group and returns chat ID on success.
@@ -2162,7 +2162,7 @@ pub(crate) async fn get_prefetch_parent_message(
/// mailing lists: In some mailing lists, many users write from the same address but with different
/// display names. We don't want the display name to change everytime the user gets a new email from
/// a mailing list.
async fn add_or_lookup_contacts_by_address_list(
async fn dc_add_or_lookup_contacts_by_address_list(
context: &Context,
address_list: &[SingleInfo],
origin: Origin,
@@ -2205,7 +2205,9 @@ async fn add_or_lookup_contact_by_addr(
#[cfg(test)]
mod tests {
use tokio::fs;
use tokio::fs::{self, File};
use tokio::io::AsyncWriteExt;
use super::*;
@@ -2292,15 +2294,17 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0);
receive_imf(&t, MSGRMSG, false).await.unwrap();
dc_receive_imf(&t, MSGRMSG, false).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
receive_imf(&t, ONETOONE_NOREPLY_MAIL, false).await.unwrap();
dc_receive_imf(&t, ONETOONE_NOREPLY_MAIL, false)
.await
.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
receive_imf(&t, GRP_MAIL, false).await.unwrap();
dc_receive_imf(&t, GRP_MAIL, false).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
}
@@ -2309,7 +2313,7 @@ mod tests {
async fn test_adhoc_group_show_accepted_contact_unknown() {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("1")).await.unwrap();
receive_imf(&t, GRP_MAIL, false).await.unwrap();
dc_receive_imf(&t, GRP_MAIL, false).await.unwrap();
// adhoc-group with unknown contacts with show_emails=accepted is ignored for unknown contacts
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
@@ -2321,7 +2325,7 @@ mod tests {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("1")).await.unwrap();
Contact::create(&t, "Bob", "bob@example.com").await.unwrap();
receive_imf(&t, GRP_MAIL, false).await.unwrap();
dc_receive_imf(&t, GRP_MAIL, false).await.unwrap();
// adhoc-group with known contacts with show_emails=accepted is still ignored for known contacts
// (and existent chat is required)
@@ -2335,7 +2339,7 @@ mod tests {
t.set_config(Config::ShowEmails, Some("1")).await.unwrap();
// accept Bob by accepting a delta-message from Bob
receive_imf(&t, MSGRMSG, false).await.unwrap();
dc_receive_imf(&t, MSGRMSG, false).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let chat_id = chats.get_chat_id(0).unwrap();
@@ -2350,12 +2354,14 @@ mod tests {
assert_eq!(chat::get_chat_msgs(&t, chat_id, 0).await.unwrap().len(), 1);
// receive a non-delta-message from Bob, shows up because of the show_emails setting
receive_imf(&t, ONETOONE_NOREPLY_MAIL, false).await.unwrap();
dc_receive_imf(&t, ONETOONE_NOREPLY_MAIL, false)
.await
.unwrap();
assert_eq!(chat::get_chat_msgs(&t, chat_id, 0).await.unwrap().len(), 2);
// let Bob create an adhoc-group by a non-delta-message, shows up because of the show_emails setting
receive_imf(&t, GRP_MAIL, false).await.unwrap();
dc_receive_imf(&t, GRP_MAIL, false).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 2);
let chat_id = chats.get_chat_id(0).unwrap();
@@ -2369,7 +2375,7 @@ mod tests {
async fn test_adhoc_group_show_all() {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
receive_imf(&t, GRP_MAIL, false).await.unwrap();
dc_receive_imf(&t, GRP_MAIL, false).await.unwrap();
// adhoc-group with unknown contacts with show_emails=all will show up in a single chat
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
@@ -2417,7 +2423,7 @@ mod tests {
);
// send a message to group with bob
receive_imf(
dc_receive_imf(
&t,
format!(
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
@@ -2446,7 +2452,7 @@ mod tests {
assert!(group.get_visibility() == ChatVisibility::Normal);
// bob sends a read receipt to the group
receive_imf(
dc_receive_imf(
&t,
format!(
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
@@ -2510,7 +2516,7 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert!(chats.get_msg_id(0).is_err());
receive_imf(
dc_receive_imf(
context,
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
To: bob@example.com\n\
@@ -2537,7 +2543,7 @@ mod tests {
.await
.unwrap();
let chat_id = ChatId::create_for_contact(&t, contact_id).await.unwrap();
receive_imf(
dc_receive_imf(
&t,
b"From: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= <foobar@example.com>\n\
To: alice@example.org\n\
@@ -2576,7 +2582,7 @@ mod tests {
.unwrap()
.0;
receive_imf(
dc_receive_imf(
&t,
b"From: Foobar <foobar@example.com>\n\
To: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= alice@example.org\n\
@@ -2618,7 +2624,7 @@ mod tests {
.unwrap()
.0;
receive_imf(
dc_receive_imf(
&t,
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
From: Foobar <foobar@example.com>\n\
@@ -2647,7 +2653,7 @@ mod tests {
"shenauithz@testrun.org",
"Mr.un2NYERi1RM.lbQ5F9q-QyJ@tiscali.it",
include_bytes!("../test-data/message/tiscali_ndn.eml"),
Some("Delivery to at least one recipient failed."),
Some("Non-Delivery-Notification without further details."),
)
.await;
}
@@ -2735,7 +2741,7 @@ mod tests {
let t = TestContext::new().await;
t.configure_addr(self_addr).await;
receive_imf(
dc_receive_imf(
&t,
format!(
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
@@ -2770,7 +2776,7 @@ mod tests {
.await
.unwrap());
receive_imf(&t, raw_ndn, false).await.unwrap();
dc_receive_imf(&t, raw_ndn, false).await.unwrap();
let msg = Message::load_from_db(&t, msg_id).await.unwrap();
assert_eq!(msg.state, MessageState::OutFailed);
@@ -2783,7 +2789,7 @@ mod tests {
let t = TestContext::new().await;
t.configure_addr("alice@gmail.com").await;
receive_imf(
dc_receive_imf(
&t,
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
From: alice@gmail.com\n\
@@ -2805,7 +2811,7 @@ mod tests {
let msg_id = chats.get_msg_id(0)?.unwrap();
let raw = include_bytes!("../test-data/message/gmail_ndn_group.eml");
receive_imf(&t, raw, false).await?;
dc_receive_imf(&t, raw, false).await?;
let msg = Message::load_from_db(&t, msg_id).await?;
@@ -2832,7 +2838,7 @@ mod tests {
.set_config(Config::ShowEmails, Some("2"))
.await
.unwrap();
receive_imf(context, imf_raw, false).await.unwrap();
dc_receive_imf(context, imf_raw, false).await.unwrap();
let chats = Chatlist::try_load(context, 0, None, None).await.unwrap();
let msg_id = chats.get_msg_id(0).unwrap().unwrap();
Message::load_from_db(context, msg_id).await.unwrap()
@@ -2876,7 +2882,7 @@ mod tests {
let t = TestContext::new_alice().await;
t.ctx.set_config(Config::ShowEmails, Some("2")).await?;
receive_imf(&t.ctx, GH_MAILINGLIST, false).await?;
dc_receive_imf(&t.ctx, GH_MAILINGLIST, false).await?;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await?;
assert_eq!(chats.len(), 1);
@@ -2890,7 +2896,7 @@ mod tests {
assert_eq!(chat.name, "deltachat/deltachat-core-rust");
assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).await?.len(), 1);
receive_imf(&t.ctx, GH_MAILINGLIST2.as_bytes(), false).await?;
dc_receive_imf(&t.ctx, GH_MAILINGLIST2.as_bytes(), false).await?;
let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await?;
assert!(!chat.can_send(&t.ctx).await?);
@@ -2945,7 +2951,7 @@ mod tests {
.set_config(Config::ShowEmails, Some("2"))
.await
.unwrap();
receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap();
dc_receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
let chat_id = chats.get_chat_id(0).unwrap();
chat_id.accept(&t).await.unwrap();
@@ -2976,7 +2982,7 @@ mod tests {
Hello mailinglist!\r\n"
));
receive_imf(&t.ctx, DC_MAILINGLIST2, false).await?;
dc_receive_imf(&t.ctx, DC_MAILINGLIST2, false).await?;
let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await?;
assert!(chat.can_send(&t.ctx).await?);
@@ -2988,7 +2994,7 @@ Hello mailinglist!\r\n"
async fn test_other_device_writes_to_mailinglist() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await?;
receive_imf(&t, DC_MAILINGLIST, false).await.unwrap();
dc_receive_imf(&t, DC_MAILINGLIST, false).await.unwrap();
let first_msg = t.get_last_msg().await;
let first_chat = Chat::load_from_db(&t, first_msg.chat_id).await?;
assert_eq!(
@@ -3012,7 +3018,7 @@ Hello mailinglist!\r\n"
(first_chat.id, false, Blocked::Request)
);
receive_imf(
dc_receive_imf(
&t,
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
From: Alice <alice@example.org>\n\
@@ -3042,7 +3048,7 @@ Hello mailinglist!\r\n"
.await
.unwrap();
receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap();
dc_receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let chat_id = chats.get_chat_id(0).unwrap();
@@ -3055,7 +3061,9 @@ Hello mailinglist!\r\n"
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0); // Test that the message disappeared
receive_imf(&t.ctx, DC_MAILINGLIST2, false).await.unwrap();
dc_receive_imf(&t.ctx, DC_MAILINGLIST2, false)
.await
.unwrap();
// Test that the mailing list stays disappeared
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
@@ -3071,7 +3079,7 @@ Hello mailinglist!\r\n"
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
receive_imf(&t, DC_MAILINGLIST, false).await.unwrap();
dc_receive_imf(&t, DC_MAILINGLIST, false).await.unwrap();
let blocked = Contact::get_all_blocked(&t).await.unwrap();
assert_eq!(blocked.len(), 0);
@@ -3091,7 +3099,9 @@ Hello mailinglist!\r\n"
let blocked = Contact::get_all_blocked(&t).await.unwrap();
assert_eq!(blocked.len(), 0);
receive_imf(&t.ctx, DC_MAILINGLIST2, false).await.unwrap();
dc_receive_imf(&t.ctx, DC_MAILINGLIST2, false)
.await
.unwrap();
let msg = t.get_last_msg().await;
let msgs = chat::get_chat_msgs(&t, msg.chat_id, 0).await.unwrap();
assert_eq!(msgs.len(), 2);
@@ -3105,7 +3115,7 @@ Hello mailinglist!\r\n"
.await
.unwrap();
receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap();
dc_receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap();
let msg = t.get_last_msg().await;
let chat_id = msg.get_chat_id();
@@ -3118,7 +3128,9 @@ Hello mailinglist!\r\n"
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0).await.unwrap();
assert_eq!(msgs.len(), 1); // ...and contains 1 message
receive_imf(&t.ctx, DC_MAILINGLIST2, false).await.unwrap();
dc_receive_imf(&t.ctx, DC_MAILINGLIST2, false)
.await
.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1); // Test that the new mailing list message got into the same chat
@@ -3136,7 +3148,7 @@ Hello mailinglist!\r\n"
.await
.unwrap();
receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap();
dc_receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap();
let msg = t.get_last_msg().await;
let chat_id = msg.get_chat_id();
@@ -3146,7 +3158,9 @@ Hello mailinglist!\r\n"
assert_eq!(chats.len(), 1); // Test that the message is shown
assert!(!chat_id.is_special());
receive_imf(&t.ctx, DC_MAILINGLIST2, false).await.unwrap();
dc_receive_imf(&t.ctx, DC_MAILINGLIST2, false)
.await
.unwrap();
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0).await.unwrap();
assert_eq!(msgs.len(), 2);
@@ -3158,7 +3172,7 @@ Hello mailinglist!\r\n"
async fn test_mailing_list_multiple_names_in_subject() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await?;
receive_imf(
dc_receive_imf(
&t,
b"From: Foo Bar <foo@bar.org>\n\
To: deltachat/deltachat-core-rust <deltachat-core-rust@noreply.github.com>\n\
@@ -3185,7 +3199,7 @@ Hello mailinglist!\r\n"
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
// test mailing lists not having a `ListId:`-header
receive_imf(
dc_receive_imf(
&t,
b"From: Foo Bar <foo@bar.org>\n\
To: deltachat/deltachat-core-rust <deltachat-core-rust@noreply.github.com>\n\
@@ -3210,7 +3224,7 @@ Hello mailinglist!\r\n"
// receive another message with no sender name but the same address,
// make sure this lands in the same chat
receive_imf(
dc_receive_imf(
&t,
b"From: Nu Bar <nu@bar.org>\n\
To: deltachat/deltachat-core-rust <deltachat-core-rust@noreply.github.com>\n\
@@ -3233,7 +3247,7 @@ Hello mailinglist!\r\n"
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
receive_imf(
dc_receive_imf(
&t,
b"To: alice <alice@example.org>\n\
Subject: =?utf-8?Q?How=20early=20megacities=20emerged=20from=20Cambodia=E2=80=99s=20jungles?=\n\
@@ -3263,7 +3277,7 @@ Hello mailinglist!\r\n"
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
receive_imf(
dc_receive_imf(
&t,
include_bytes!("../test-data/message/mailinglist_dhl.eml"),
false,
@@ -3288,7 +3302,7 @@ Hello mailinglist!\r\n"
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
receive_imf(
dc_receive_imf(
&t,
include_bytes!("../test-data/message/mailinglist_dpd.eml"),
false,
@@ -3313,7 +3327,7 @@ Hello mailinglist!\r\n"
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await?;
receive_imf(
dc_receive_imf(
&t,
include_bytes!("../test-data/message/mailinglist_xt_local_microsoft.eml"),
false,
@@ -3324,7 +3338,7 @@ Hello mailinglist!\r\n"
assert_eq!(chat.grpid, "96540.xt.local");
assert_eq!(chat.name, "Microsoft Store");
receive_imf(
dc_receive_imf(
&t,
include_bytes!("../test-data/message/mailinglist_xt_local_spiegel.eml"),
false,
@@ -3343,7 +3357,7 @@ Hello mailinglist!\r\n"
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await?;
receive_imf(
dc_receive_imf(
&t,
include_bytes!("../test-data/message/mailinglist_xing.eml"),
false,
@@ -3364,7 +3378,7 @@ Hello mailinglist!\r\n"
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await?;
receive_imf(
dc_receive_imf(
&t,
include_bytes!("../test-data/message/mailinglist_ttline.eml"),
false,
@@ -3390,7 +3404,7 @@ Hello mailinglist!\r\n"
// if the user-edited text contains html.
// this footer should not become a text-message in delta chat
// (otherwise every second mail might be the same footer)
receive_imf(
dc_receive_imf(
&t,
include_bytes!("../test-data/message/mailinglist_with_mimepart_footer.eml"),
false,
@@ -3416,7 +3430,7 @@ Hello mailinglist!\r\n"
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
receive_imf(
dc_receive_imf(
&t,
include_bytes!("../test-data/message/mailinglist_with_mimepart_footer_signed.eml"),
false,
@@ -3441,7 +3455,7 @@ Hello mailinglist!\r\n"
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
receive_imf(&t, GH_MAILINGLIST, false).await.unwrap();
dc_receive_imf(&t, GH_MAILINGLIST, false).await.unwrap();
let chat_id = t.get_last_msg().await.chat_id;
chat_id.accept(&t).await.unwrap();
@@ -3449,7 +3463,7 @@ Hello mailinglist!\r\n"
assert!(chat.can_send(&t).await.unwrap());
let imf_raw = format!("In-Reply-To: 3333@example.org\n{}", GH_MAILINGLIST2);
receive_imf(&t, imf_raw.as_bytes(), false).await.unwrap();
dc_receive_imf(&t, imf_raw.as_bytes(), false).await.unwrap();
assert_eq!(
t.get_last_msg().await.in_reply_to.unwrap(),
@@ -3497,7 +3511,7 @@ Hello mailinglist!\r\n"
.set_config(Config::ShowEmails, Some("2"))
.await
.unwrap();
receive_imf(
dc_receive_imf(
&t,
format!(
"Subject: Re: [deltachat/deltachat-core-rust] DC is the best repo on GitHub!
@@ -3569,7 +3583,7 @@ YEAAAAAA!.
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
receive_imf(
dc_receive_imf(
&t,
include_bytes!("../test-data/message/many_images_amazon_via_apple_mail.eml"),
false,
@@ -3591,7 +3605,7 @@ YEAAAAAA!.
t.configure_addr("bob@example.com").await;
// Receive message from Alice about group "foo".
receive_imf(
dc_receive_imf(
&t,
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
From: alice@example.org\n\
@@ -3610,7 +3624,7 @@ YEAAAAAA!.
.unwrap();
// Receive reply from Charlie without group ID but with In-Reply-To header.
receive_imf(
dc_receive_imf(
&t,
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
From: charlie@example.net\n\
@@ -3656,7 +3670,7 @@ YEAAAAAA!.
t.configure_addr("bob@example.com").await;
// Receive message from Alice about group "foo".
receive_imf(
dc_receive_imf(
&t,
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
From: alice@example.org\n\
@@ -3676,7 +3690,7 @@ YEAAAAAA!.
// Receive a classic MUA reply from Alice.
// It is assigned to the group chat.
receive_imf(
dc_receive_imf(
&t,
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
From: alice@example.org\n\
@@ -3700,7 +3714,7 @@ YEAAAAAA!.
// Receive a Delta Chat reply from Alice.
// It is assigned to group chat, because it has a group ID.
receive_imf(
dc_receive_imf(
&t,
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
From: alice@example.org\n\
@@ -3728,7 +3742,7 @@ YEAAAAAA!.
// It is assigned to 1:1 chat, because it has no group ID,
// which means it was created using "reply privately" feature.
// Normally it contains a quote, but it should not matter.
receive_imf(
dc_receive_imf(
&t,
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
From: alice@example.org\n\
@@ -3851,7 +3865,7 @@ YEAAAAAA!.
.set_config(Config::ShowEmails, Some("2"))
.await
.unwrap();
receive_imf(&alice, claire_request.as_bytes(), false)
dc_receive_imf(&alice, claire_request.as_bytes(), false)
.await
.unwrap();
@@ -3874,7 +3888,7 @@ YEAAAAAA!.
.set_config(Config::ShowEmails, Some("2"))
.await
.unwrap();
receive_imf(&claire, claire_request.as_bytes(), false)
dc_receive_imf(&claire, claire_request.as_bytes(), false)
.await
.unwrap();
@@ -3904,7 +3918,7 @@ YEAAAAAA!.
// Check that Alice gets the message in the same chat.
let request = alice.get_last_msg().await;
receive_imf(&alice, reply, false).await.unwrap();
dc_receive_imf(&alice, reply, false).await.unwrap();
let answer = alice.get_last_msg().await;
assert_eq!(answer.get_subject(), "Re: i have a question");
assert!(answer.get_text().unwrap().contains("the version is 1.0"));
@@ -3927,7 +3941,7 @@ YEAAAAAA!.
// Check that Claire also gets the message in the same chat.
let request = claire.get_last_msg().await;
receive_imf(&claire, reply, false).await.unwrap();
dc_receive_imf(&claire, reply, false).await.unwrap();
let answer = claire.get_last_msg().await;
assert_eq!(answer.get_subject(), "Re: i have a question");
assert!(answer.get_text().unwrap().contains("the version is 1.0"));
@@ -3987,7 +4001,7 @@ YEAAAAAA!.
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
println!("\n========= Receive a message ==========");
receive_imf(
dc_receive_imf(
&t,
b"From: Nu Bar <nu@bar.org>\n\
To: alice@example.org, bob@example.org\n\
@@ -4012,7 +4026,7 @@ YEAAAAAA!.
assert_eq!(msgs.len(), 0);
println!("\n========= Receive a message that is a reply to the deleted message ==========");
receive_imf(
dc_receive_imf(
&t,
b"From: Nu Bar <nu@bar.org>\n\
To: alice@example.org, bob@example.org\n\
@@ -4037,7 +4051,7 @@ YEAAAAAA!.
// be shown in the self-chat.
let t = TestContext::new_alice().await;
receive_imf(
dc_receive_imf(
&t,
b"Bcc: alice@example.org
Received: from [127.0.0.1]
@@ -4068,7 +4082,7 @@ Message content",
.unwrap();
// Alice downloads outgoing classic email.
receive_imf(
dc_receive_imf(
&alice,
b"Received: from [127.0.0.1]
Subject: Subj
@@ -4125,18 +4139,18 @@ Message content
--
Second signature";
receive_imf(&alice, first_message, false).await?;
dc_receive_imf(&alice, first_message, false).await?;
let contact = Contact::load_from_db(&alice, bob_contact_id).await?;
assert_eq!(contact.get_status(), "First signature");
assert_eq!(contact.get_display_name(), "Bob1");
receive_imf(&alice, second_message, false).await?;
dc_receive_imf(&alice, second_message, false).await?;
let contact = Contact::load_from_db(&alice, bob_contact_id).await?;
assert_eq!(contact.get_status(), "Second signature");
assert_eq!(contact.get_display_name(), "Bob2");
// Duplicate message, should be ignored
receive_imf(&alice, first_message, false).await?;
dc_receive_imf(&alice, first_message, false).await?;
// No change because last message is duplicate of the first.
let contact = Contact::load_from_db(&alice, bob_contact_id).await?;
@@ -4157,7 +4171,7 @@ Second signature";
assert_eq!(bob.get_status(), "");
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 0);
receive_imf(
dc_receive_imf(
&t,
b"From: Bob <bob@example.net>
To: Alice <alice@example.org>
@@ -4175,7 +4189,7 @@ Original signature",
let bob = Contact::load_from_db(&t, bob_id).await?;
assert_eq!(bob.get_status(), "Original signature");
receive_imf(
dc_receive_imf(
&t,
b"From: Bob <bob@example.net>
Sender: ml@example.net
@@ -4197,7 +4211,7 @@ Tap here to unsubscribe ...",
let bob = Contact::load_from_db(&t, bob_id).await?;
assert_eq!(bob.get_status(), "Original signature");
receive_imf(
dc_receive_imf(
&t,
b"From: Bob <bob@example.net>
To: Alice <alice@example.org>
@@ -4225,7 +4239,7 @@ Original signature updated",
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
receive_imf(
dc_receive_imf(
&t,
format!(
r#"Received: from mout.gmx.net (mout.gmx.net [212.227.17.22])
@@ -4267,7 +4281,7 @@ Message-ID: <Gr.eJ_llQIXf0K.buxmrnMmG0Y@gmx.de>"
assert_eq!(group_chat.typ, Chattype::Group);
assert_eq!(group_chat.name, "single reply-to");
receive_imf(
dc_receive_imf(
&t,
format!(
r#"Subject: Re: single reply-to
@@ -4311,7 +4325,7 @@ Private reply"#,
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
receive_imf(
dc_receive_imf(
&t,
format!(
r#"Received: from mout.gmx.net (mout.gmx.net [212.227.17.22])
@@ -4357,7 +4371,7 @@ Message-ID: <Gr.iy1KCE2y65_.mH2TM52miv9@testrun.org>"
assert_eq!(group_chat.typ, Chattype::Group);
assert_eq!(group_chat.name, "single reply-to");
receive_imf(
dc_receive_imf(
&t,
format!(
r#"Subject: =?utf-8?q?Re=3A_single_reply-to?=
@@ -4405,7 +4419,7 @@ Sent with my Delta Chat Messenger: https://delta.chat
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
receive_imf(
dc_receive_imf(
&t,
format!(
r#"Received: from mout.gmx.net (mout.gmx.net [212.227.17.22])
@@ -4447,7 +4461,7 @@ Message-ID: <Gr.eJ_llQIXf0K.buxmrnMmG0Y@gmx.de>"
assert_eq!(group_chat.name, "single reply-to");
// =============== Receive another outgoing message and check that it is put into the same chat ===============
receive_imf(
dc_receive_imf(
&t,
format!(
r#"Received: from mout.gmx.net (mout.gmx.net [212.227.17.22])
@@ -4478,7 +4492,7 @@ Outgoing reply to all"#,
assert_eq!(reply.chat_id, group_msg.chat_id);
// =============== Receive an incoming message and check that it is put into the same chat ===============
receive_imf(
dc_receive_imf(
&t,
br#"Received: from mout.gmx.net (mout.gmx.net [212.227.17.22])
Subject: In subj
@@ -4531,15 +4545,15 @@ Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Second thread."#;
// Alice receives two classic emails from Claire.
receive_imf(&alice, first_thread_mime, false).await?;
dc_receive_imf(&alice, first_thread_mime, false).await?;
let alice_first_msg = alice.get_last_msg().await;
receive_imf(&alice, second_thread_mime, false).await?;
dc_receive_imf(&alice, second_thread_mime, false).await?;
let alice_second_msg = alice.get_last_msg().await;
// Bob receives the same two emails.
receive_imf(&bob, first_thread_mime, false).await?;
dc_receive_imf(&bob, first_thread_mime, false).await?;
let bob_first_msg = bob.get_last_msg().await;
receive_imf(&bob, second_thread_mime, false).await?;
dc_receive_imf(&bob, second_thread_mime, false).await?;
let bob_second_msg = bob.get_last_msg().await;
// Messages go to separate chats both for Alice and Bob.
@@ -4612,7 +4626,7 @@ Second thread."#;
let mdn_body = rendered_mdn.message;
// Alice receives the read receipt.
receive_imf(&alice, mdn_body.as_bytes(), false).await?;
dc_receive_imf(&alice, mdn_body.as_bytes(), false).await?;
// Chat should not pop up in the chatlist.
let chats = Chatlist::try_load(&alice, 0, None, None).await?;
@@ -4626,7 +4640,7 @@ Second thread."#;
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await?;
receive_imf(
dc_receive_imf(
&t,
include_bytes!("../test-data/message/gmx-forward.eml"),
false,
@@ -4645,7 +4659,7 @@ Second thread."#;
async fn test_incoming_contact_request() -> Result<()> {
let t = TestContext::new_alice().await;
receive_imf(&t, MSGRMSG, false).await?;
dc_receive_imf(&t, MSGRMSG, false).await?;
let msg = t.get_last_msg().await;
let chat = chat::Chat::load_from_db(&t, msg.chat_id).await?;
assert!(chat.is_contact_request());
@@ -4678,7 +4692,7 @@ From: Bob <bob@example.net>
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
First."#;
receive_imf(&t, mime, false).await?;
dc_receive_imf(&t, mime, false).await?;
let first = t.get_last_msg().await;
let mime = br#"Subject: Second
Message-ID: second@example.net
@@ -4687,7 +4701,7 @@ From: Bob <bob@example.net>
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
First."#;
receive_imf(&t, mime, false).await?;
dc_receive_imf(&t, mime, false).await?;
let second = t.get_last_msg().await;
let mime = br#"Subject: Third
Message-ID: third@example.net
@@ -4696,7 +4710,7 @@ From: Bob <bob@example.net>
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
First."#;
receive_imf(&t, mime, false).await?;
dc_receive_imf(&t, mime, false).await?;
let third = t.get_last_msg().await;
let mime = br#"Subject: Message with references.
@@ -4747,7 +4761,7 @@ Message with references."#;
// Alice sends a message to Bob using Thunderbird.
let raw = include_bytes!("../test-data/message/rfc1847_encapsulation.eml");
receive_imf(&bob, raw, false).await?;
dc_receive_imf(&bob, raw, false).await?;
let msg = bob.get_last_msg().await;
assert!(msg.get_showpadlock());
@@ -4761,8 +4775,8 @@ Message with references."#;
let mime = include_bytes!("../test-data/message/invalid_email_to.eml");
// receive_imf should not fail on this mail with invalid To: field
receive_imf(&alice, mime, false).await?;
// dc_receive_imf should not fail on this mail with invalid To: field
dc_receive_imf(&alice, mime, false).await?;
Ok(())
}
@@ -4773,7 +4787,7 @@ Message with references."#;
t.set_config(Config::ShowEmails, Some("2")).await?;
// Alice creates a 2-person-group with Bob
receive_imf(
dc_receive_imf(
&t,
br#"Subject: =?utf-8?q?Januar_13-19?=
Chat-Group-ID: qetqsutor7a
@@ -4797,7 +4811,7 @@ Hi, I created a group"#,
assert_eq!(msg_out.in_reply_to, None);
// Bob replies from a different address
receive_imf(
dc_receive_imf(
&t,
b"Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
@@ -4849,7 +4863,10 @@ Reply from different address
] {
let attachment = alice.blobdir.join(filename_sent);
let content = format!("File content of {}", filename_sent);
tokio::fs::write(&attachment, content.as_bytes()).await?;
File::create(&attachment)
.await?
.write_all(content.as_bytes())
.await?;
let mut msg_alice = Message::new(Viewtype::File);
msg_alice.set_file(attachment.to_str().unwrap(), None);

View File

@@ -30,7 +30,7 @@ use crate::stock_str;
/// Shortens a string to a specified length and adds "[...]" to the
/// end of the shortened string.
#[allow(clippy::indexing_slicing)]
pub(crate) fn truncate(buf: &str, approx_chars: usize) -> Cow<str> {
pub(crate) fn dc_truncate(buf: &str, approx_chars: usize) -> Cow<str> {
let count = buf.chars().count();
if count > approx_chars + DC_ELLIPSIS.len() {
let end_pos = buf
@@ -53,7 +53,7 @@ pub(crate) fn truncate(buf: &str, approx_chars: usize) -> Cow<str> {
* date/time tools
******************************************************************************/
pub fn timestamp_to_str(wanted: i64) -> String {
pub fn dc_timestamp_to_str(wanted: i64) -> String {
let ts = Local.timestamp(wanted, 0);
ts.format("%Y.%m.%d %H:%M:%S").to_string()
}
@@ -66,7 +66,7 @@ pub fn duration_to_str(duration: Duration) -> String {
format!("{}h {}m {}s", h, m, s)
}
pub(crate) fn gm2local_offset() -> i64 {
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();
@@ -90,7 +90,7 @@ pub(crate) const MAX_SECONDS_TO_LEND_FROM_FUTURE: i64 = 5;
/// Returns the current smeared timestamp,
///
/// The returned timestamp MUST NOT be sent out.
pub(crate) async fn smeared_time(context: &Context) -> i64 {
pub(crate) async fn dc_smeared_time(context: &Context) -> i64 {
let mut now = time();
let ts = *context.last_smeared_timestamp.read().await;
if ts >= now {
@@ -101,7 +101,7 @@ pub(crate) async fn smeared_time(context: &Context) -> i64 {
}
/// Returns a timestamp that is guaranteed to be unique.
pub(crate) async fn create_smeared_timestamp(context: &Context) -> i64 {
pub(crate) async fn dc_create_smeared_timestamp(context: &Context) -> i64 {
let now = time();
let mut ret = now;
@@ -120,7 +120,7 @@ pub(crate) async fn create_smeared_timestamp(context: &Context) -> i64 {
// creates `count` timestamps that are guaranteed to be unique.
// the frist created timestamps is returned directly,
// get the other timestamps just by adding 1..count-1
pub(crate) async fn create_smeared_timestamps(context: &Context, count: usize) -> i64 {
pub(crate) async fn dc_create_smeared_timestamps(context: &Context, count: usize) -> i64 {
let now = time();
let count = count as i64;
let mut start = now + min(count, MAX_SECONDS_TO_LEND_FROM_FUTURE) - count;
@@ -204,7 +204,7 @@ async fn maybe_warn_on_outdated(context: &Context, now: i64, approx_compile_time
/// - for OUTGOING messages this ID is written to the header as `Chat-Group-ID:` and is added to the message ID as Gr.<grpid>.<random>@<random>
/// - for INCOMING messages, the ID is taken from the Chat-Group-ID-header or from the Message-ID in the In-Reply-To: or References:-Header
/// - the group-id should be a string with the characters [a-zA-Z0-9\-_]
pub(crate) fn create_id() -> String {
pub(crate) fn dc_create_id() -> String {
// ThreadRng implements CryptoRng trait and is supposed to be cryptographically secure.
let mut rng = thread_rng();
@@ -223,14 +223,14 @@ pub(crate) fn create_id() -> String {
/// - this function is called for all outgoing messages.
/// - the message ID should be globally unique
/// - do not add a counter or any private data as this leaks information unncessarily
pub(crate) fn create_outgoing_rfc724_mid(grpid: Option<&str>, from_addr: &str) -> String {
pub(crate) fn dc_create_outgoing_rfc724_mid(grpid: Option<&str>, from_addr: &str) -> String {
let hostname = from_addr
.find('@')
.and_then(|k| from_addr.get(k..))
.unwrap_or("@nohost");
match grpid {
Some(grpid) => format!("Gr.{}.{}{}", grpid, create_id(), hostname),
None => format!("Mr.{}.{}{}", create_id(), create_id(), hostname),
Some(grpid) => format!("Gr.{}.{}{}", grpid, dc_create_id(), hostname),
None => format!("Mr.{}.{}{}", dc_create_id(), dc_create_id(), hostname),
}
}
@@ -240,7 +240,7 @@ pub(crate) fn create_outgoing_rfc724_mid(grpid: Option<&str>, from_addr: &str) -
///
/// * `mid` - A string that holds the message id. Leading/Trailing <>
/// characters are automatically stripped.
pub(crate) fn extract_grpid_from_rfc724_mid(mid: &str) -> Option<&str> {
pub(crate) fn dc_extract_grpid_from_rfc724_mid(mid: &str) -> Option<&str> {
let mid = mid.trim_start_matches('<').trim_end_matches('>');
if mid.len() < 9 || !mid.starts_with("Gr.") {
@@ -260,14 +260,14 @@ pub(crate) fn extract_grpid_from_rfc724_mid(mid: &str) -> Option<&str> {
}
// the returned suffix is lower-case
pub fn get_filesuffix_lc(path_filename: impl AsRef<str>) -> Option<String> {
pub fn dc_get_filesuffix_lc(path_filename: impl AsRef<str>) -> Option<String> {
Path::new(path_filename.as_ref())
.extension()
.map(|p| p.to_string_lossy().to_lowercase())
}
/// Returns the `(width, height)` of the given image buffer.
pub fn get_filemeta(buf: &[u8]) -> Result<(u32, u32), Error> {
pub fn dc_get_filemeta(buf: &[u8]) -> Result<(u32, u32), Error> {
let image = image::io::Reader::new(Cursor::new(buf)).with_guessed_format()?;
let dimensions = image.into_dimensions()?;
Ok(dimensions)
@@ -277,7 +277,7 @@ pub fn get_filemeta(buf: &[u8]) -> Result<(u32, u32), Error> {
///
/// If `path` starts with "$BLOBDIR", replaces it with the blobdir path.
/// Otherwise, returns path as is.
pub(crate) fn get_abs_path(context: &Context, path: impl AsRef<Path>) -> PathBuf {
pub(crate) fn dc_get_abs_path(context: &Context, path: impl AsRef<Path>) -> PathBuf {
let p: &Path = path.as_ref();
if let Ok(p) = p.strip_prefix("$BLOBDIR") {
context.get_blobdir().join(p)
@@ -286,16 +286,16 @@ pub(crate) fn get_abs_path(context: &Context, path: impl AsRef<Path>) -> PathBuf
}
}
pub(crate) async fn get_filebytes(context: &Context, path: impl AsRef<Path>) -> u64 {
let path_abs = get_abs_path(context, &path);
pub(crate) async fn dc_get_filebytes(context: &Context, path: impl AsRef<Path>) -> u64 {
let path_abs = dc_get_abs_path(context, &path);
match fs::metadata(&path_abs).await {
Ok(meta) => meta.len() as u64,
Err(_err) => 0,
}
}
pub(crate) async fn delete_file(context: &Context, path: impl AsRef<Path>) -> bool {
let path_abs = get_abs_path(context, &path);
pub(crate) async fn dc_delete_file(context: &Context, path: impl AsRef<Path>) -> bool {
let path_abs = dc_get_abs_path(context, &path);
if !path_abs.exists() {
return false;
}
@@ -321,14 +321,14 @@ pub(crate) async fn delete_file(context: &Context, path: impl AsRef<Path>) -> bo
}
}
pub async fn delete_files_in_dir(context: &Context, path: impl AsRef<Path>) {
pub async fn dc_delete_files_in_dir(context: &Context, path: impl AsRef<Path>) {
match tokio::fs::read_dir(path).await {
Ok(read_dir) => {
let mut read_dir = tokio_stream::wrappers::ReadDirStream::new(read_dir);
while let Some(entry) = read_dir.next().await {
match entry {
Ok(file) => {
delete_file(context, file.file_name()).await;
dc_delete_file(context, file.file_name()).await;
}
Err(e) => warn!(context, "Could not read file to delete: {}", e),
}
@@ -339,11 +339,11 @@ pub async fn delete_files_in_dir(context: &Context, path: impl AsRef<Path>) {
}
}
pub(crate) async fn create_folder(
pub(crate) async fn dc_create_folder(
context: &Context,
path: impl AsRef<Path>,
) -> Result<(), io::Error> {
let path_abs = get_abs_path(context, &path);
let path_abs = dc_get_abs_path(context, &path);
if !path_abs.exists() {
match fs::create_dir_all(path_abs).await {
Ok(_) => Ok(()),
@@ -363,12 +363,12 @@ pub(crate) async fn create_folder(
}
/// Write a the given content to provied file path.
pub(crate) async fn write_file(
pub(crate) async fn dc_write_file(
context: &Context,
path: impl AsRef<Path>,
buf: &[u8],
) -> Result<(), io::Error> {
let path_abs = get_abs_path(context, &path);
let path_abs = dc_get_abs_path(context, &path);
fs::write(&path_abs, buf).await.map_err(|err| {
warn!(
context,
@@ -381,8 +381,8 @@ pub(crate) async fn write_file(
})
}
pub async fn read_file<P: AsRef<Path>>(context: &Context, path: P) -> Result<Vec<u8>, Error> {
let path_abs = get_abs_path(context, &path);
pub async fn dc_read_file<P: AsRef<Path>>(context: &Context, path: P) -> Result<Vec<u8>, Error> {
let path_abs = dc_get_abs_path(context, &path);
match fs::read(&path_abs).await {
Ok(bytes) => Ok(bytes),
@@ -398,8 +398,8 @@ pub async fn read_file<P: AsRef<Path>>(context: &Context, path: P) -> Result<Vec
}
}
pub async fn open_file<P: AsRef<Path>>(context: &Context, path: P) -> Result<fs::File, Error> {
let path_abs = get_abs_path(context, &path);
pub async fn dc_open_file<P: AsRef<Path>>(context: &Context, path: P) -> Result<fs::File, Error> {
let path_abs = dc_get_abs_path(context, &path);
match fs::File::open(&path_abs).await {
Ok(bytes) => Ok(bytes),
@@ -415,12 +415,12 @@ pub async fn open_file<P: AsRef<Path>>(context: &Context, path: P) -> Result<fs:
}
}
pub fn open_file_std<P: AsRef<std::path::Path>>(
pub fn dc_open_file_std<P: AsRef<std::path::Path>>(
context: &Context,
path: P,
) -> Result<std::fs::File, Error> {
let p: PathBuf = path.as_ref().into();
let path_abs = get_abs_path(context, p);
let path_abs = dc_get_abs_path(context, p);
match std::fs::File::open(&path_abs) {
Ok(bytes) => Ok(bytes),
@@ -450,7 +450,7 @@ pub(crate) fn time() -> i64 {
/// # Example
///
/// ```
/// use deltachat::tools::EmailAddress;
/// use deltachat::dc_tools::EmailAddress;
/// let email = match EmailAddress::new("someone@example.com") {
/// Ok(addr) => addr,
/// Err(e) => panic!("Error parsing address, error was {}", e),
@@ -619,7 +619,8 @@ mod tests {
use super::*;
use crate::{
config::Config, message::get_msg_info, receive_imf::receive_imf, test_utils::TestContext,
config::Config, dc_receive_imf::dc_receive_imf, message::get_msg_info,
test_utils::TestContext,
};
#[test]
@@ -688,7 +689,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();
receive_imf(&t, raw, false).await.unwrap();
dc_receive_imf(&t, raw, false).await.unwrap();
let msg = t.get_last_msg().await;
let msg_info = get_msg_info(&t, msg.id).await.unwrap();
@@ -705,108 +706,108 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
}
#[test]
fn test_truncate_1() {
fn test_dc_truncate_1() {
let s = "this is a little test string";
assert_eq!(truncate(s, 16), "this is a [...]");
assert_eq!(dc_truncate(s, 16), "this is a [...]");
}
#[test]
fn test_truncate_2() {
assert_eq!(truncate("1234", 2), "1234");
fn test_dc_truncate_2() {
assert_eq!(dc_truncate("1234", 2), "1234");
}
#[test]
fn test_truncate_3() {
assert_eq!(truncate("1234567", 1), "1[...]");
fn test_dc_truncate_3() {
assert_eq!(dc_truncate("1234567", 1), "1[...]");
}
#[test]
fn test_truncate_4() {
assert_eq!(truncate("123456", 4), "123456");
fn test_dc_truncate_4() {
assert_eq!(dc_truncate("123456", 4), "123456");
}
#[test]
fn test_truncate_edge() {
assert_eq!(truncate("", 4), "");
fn test_dc_truncate_edge() {
assert_eq!(dc_truncate("", 4), "");
assert_eq!(truncate("\n hello \n world", 4), "\n [...]");
assert_eq!(dc_truncate("\n hello \n world", 4), "\n [...]");
assert_eq!(truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 1), "𐠈[...]");
assert_eq!(truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 0), "[...]");
assert_eq!(dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 1), "𐠈[...]");
assert_eq!(dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 0), "[...]");
// 9 characters, so no truncation
assert_eq!(truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠", 6), "𑒀ὐ¢🜀\u{1e01b}A a🟠",);
assert_eq!(dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠", 6), "𑒀ὐ¢🜀\u{1e01b}A a🟠",);
// 12 characters, truncation
assert_eq!(
truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠bcd", 6),
dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠bcd", 6),
"𑒀ὐ¢🜀\u{1e01b}A[...]",
);
}
#[test]
fn test_create_id() {
let buf = create_id();
fn test_dc_create_id() {
let buf = dc_create_id();
assert_eq!(buf.len(), 11);
}
#[test]
fn test_create_id_invalid_chars() {
fn test_dc_create_id_invalid_chars() {
for _ in 1..1000 {
let buf = create_id();
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_extract_grpid_from_rfc724_mid() {
fn test_dc_extract_grpid_from_rfc724_mid() {
// Should return None if we pass invalid mid
let mid = "foobar";
let grpid = extract_grpid_from_rfc724_mid(mid);
let grpid = dc_extract_grpid_from_rfc724_mid(mid);
assert_eq!(grpid, None);
// Should return None if grpid has a length which is not 11 or 16
let mid = "Gr.12345678.morerandom@domain.de";
let grpid = extract_grpid_from_rfc724_mid(mid);
let grpid = dc_extract_grpid_from_rfc724_mid(mid);
assert_eq!(grpid, None);
// Should return extracted grpid for grpid with length of 11
let mid = "Gr.12345678901.morerandom@domain.de";
let grpid = extract_grpid_from_rfc724_mid(mid);
let grpid = dc_extract_grpid_from_rfc724_mid(mid);
assert_eq!(grpid, Some("12345678901"));
// Should return extracted grpid for grpid with length of 11
let mid = "Gr.1234567890123456.morerandom@domain.de";
let grpid = extract_grpid_from_rfc724_mid(mid);
let grpid = dc_extract_grpid_from_rfc724_mid(mid);
assert_eq!(grpid, Some("1234567890123456"));
// Should return extracted grpid for grpid with length of 11
let mid = "<Gr.12345678901.morerandom@domain.de>";
let grpid = extract_grpid_from_rfc724_mid(mid);
let grpid = dc_extract_grpid_from_rfc724_mid(mid);
assert_eq!(grpid, Some("12345678901"));
// Should return extracted grpid for grpid with length of 11
let mid = "<Gr.1234567890123456.morerandom@domain.de>";
let grpid = extract_grpid_from_rfc724_mid(mid);
let grpid = dc_extract_grpid_from_rfc724_mid(mid);
assert_eq!(grpid, Some("1234567890123456"));
}
#[test]
fn test_create_outgoing_rfc724_mid() {
fn test_dc_create_outgoing_rfc724_mid() {
// create a normal message-id
let mid = create_outgoing_rfc724_mid(None, "foo@bar.de");
let mid = dc_create_outgoing_rfc724_mid(None, "foo@bar.de");
assert!(mid.starts_with("Mr."));
assert!(mid.ends_with("bar.de"));
assert!(extract_grpid_from_rfc724_mid(mid.as_str()).is_none());
assert!(dc_extract_grpid_from_rfc724_mid(mid.as_str()).is_none());
// create a message-id containing a group-id
let grpid = create_id();
let mid = create_outgoing_rfc724_mid(Some(&grpid), "foo@bar.de");
let grpid = dc_create_id();
let mid = dc_create_outgoing_rfc724_mid(Some(&grpid), "foo@bar.de");
assert!(mid.starts_with("Gr."));
assert!(mid.ends_with("bar.de"));
assert_eq!(
extract_grpid_from_rfc724_mid(mid.as_str()),
dc_extract_grpid_from_rfc724_mid(mid.as_str()),
Some(grpid.as_str())
);
}
@@ -852,11 +853,11 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
proptest! {
#[test]
fn test_truncate(
fn test_dc_truncate(
buf: String,
approx_chars in 0..100usize
) {
let res = truncate(&buf, approx_chars);
let res = dc_truncate(&buf, approx_chars);
let el_len = 5;
let l = res.chars().count();
assert!(
@@ -876,7 +877,7 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
async fn test_file_handling() {
let t = TestContext::new().await;
let context = &t;
macro_rules! file_exist {
macro_rules! dc_file_exist {
($ctx:expr, $fname:expr) => {
$ctx.get_blobdir()
.join(Path::new($fname).file_name().unwrap())
@@ -884,13 +885,13 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
};
}
assert!(!delete_file(context, "$BLOBDIR/lkqwjelqkwlje").await);
assert!(write_file(context, "$BLOBDIR/foobar", b"content")
assert!(!dc_delete_file(context, "$BLOBDIR/lkqwjelqkwlje").await);
assert!(dc_write_file(context, "$BLOBDIR/foobar", b"content")
.await
.is_ok());
assert!(file_exist!(context, "$BLOBDIR/foobar"));
assert!(!file_exist!(context, "$BLOBDIR/foobarx"));
assert_eq!(get_filebytes(context, "$BLOBDIR/foobar").await, 7);
assert!(dc_file_exist!(context, "$BLOBDIR/foobar"));
assert!(!dc_file_exist!(context, "$BLOBDIR/foobarx"));
assert_eq!(dc_get_filebytes(context, "$BLOBDIR/foobar").await, 7);
let abs_path = context
.get_blobdir()
@@ -898,31 +899,31 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
.to_string_lossy()
.to_string();
assert!(file_exist!(context, &abs_path));
assert!(dc_file_exist!(context, &abs_path));
assert!(delete_file(context, "$BLOBDIR/foobar").await);
assert!(create_folder(context, "$BLOBDIR/foobar-folder")
assert!(dc_delete_file(context, "$BLOBDIR/foobar").await);
assert!(dc_create_folder(context, "$BLOBDIR/foobar-folder")
.await
.is_ok());
assert!(file_exist!(context, "$BLOBDIR/foobar-folder"));
assert!(!delete_file(context, "$BLOBDIR/foobar-folder").await);
assert!(dc_file_exist!(context, "$BLOBDIR/foobar-folder"));
assert!(!dc_delete_file(context, "$BLOBDIR/foobar-folder").await);
let fn0 = "$BLOBDIR/data.data";
assert!(write_file(context, &fn0, b"content").await.is_ok());
assert!(dc_write_file(context, &fn0, b"content").await.is_ok());
assert!(delete_file(context, &fn0).await);
assert!(!file_exist!(context, &fn0));
assert!(dc_delete_file(context, &fn0).await);
assert!(!dc_file_exist!(context, &fn0));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_smeared_timestamp() {
let t = TestContext::new().await;
assert_ne!(
create_smeared_timestamp(&t).await,
create_smeared_timestamp(&t).await
dc_create_smeared_timestamp(&t).await,
dc_create_smeared_timestamp(&t).await
);
assert!(
create_smeared_timestamp(&t).await
dc_create_smeared_timestamp(&t).await
>= SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
@@ -934,13 +935,13 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
async fn test_create_smeared_timestamps() {
let t = TestContext::new().await;
let count = MAX_SECONDS_TO_LEND_FROM_FUTURE - 1;
let start = create_smeared_timestamps(&t, count as usize).await;
let next = smeared_time(&t).await;
let start = dc_create_smeared_timestamps(&t, count as usize).await;
let next = dc_smeared_time(&t).await;
assert!((start + count - 1) < next);
let count = MAX_SECONDS_TO_LEND_FROM_FUTURE + 30;
let start = create_smeared_timestamps(&t, count as usize).await;
let next = smeared_time(&t).await;
let start = dc_create_smeared_timestamps(&t, count as usize).await;
let next = dc_smeared_time(&t).await;
assert!((start + count - 1) < next);
}
@@ -979,17 +980,17 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
#[test]
fn test_get_filemeta() {
let (w, h) = get_filemeta(test_utils::AVATAR_900x900_BYTES).unwrap();
let (w, h) = dc_get_filemeta(test_utils::AVATAR_900x900_BYTES).unwrap();
assert_eq!(w, 900);
assert_eq!(h, 900);
let data = include_bytes!("../test-data/image/avatar1000x1000.jpg");
let (w, h) = get_filemeta(data).unwrap();
let (w, h) = dc_get_filemeta(data).unwrap();
assert_eq!(w, 1000);
assert_eq!(h, 1000);
let data = include_bytes!("../test-data/image/image100x50.gif");
let (w, h) = get_filemeta(data).unwrap();
let (w, h) = dc_get_filemeta(data).unwrap();
assert_eq!(w, 100);
assert_eq!(h, 50);
}

View File

@@ -7,12 +7,12 @@ use std::collections::BTreeMap;
use crate::config::Config;
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::mimeparser::{MimeMessage, Part};
use crate::param::Params;
use crate::tools::time;
use crate::{job_try, stock_str, EventType};
use std::cmp::max;
@@ -257,9 +257,9 @@ mod tests {
use num_traits::FromPrimitive;
use crate::chat::send_msg;
use crate::dc_receive_imf::dc_receive_imf_inner;
use crate::ephemeral::Timer;
use crate::message::Viewtype;
use crate::receive_imf::receive_imf_inner;
use crate::test_utils::TestContext;
use super::*;
@@ -342,7 +342,7 @@ mod tests {
Date: Sun, 22 Mar 2020 22:37:57 +0000\
Content-Type: text/plain";
receive_imf_inner(
dc_receive_imf_inner(
&t,
"Mr.12345678901@example.com",
header.as_bytes(),
@@ -359,7 +359,7 @@ mod tests {
.unwrap()
.contains(&stock_str::partial_download_msg_body(&t, 100000).await));
receive_imf_inner(
dc_receive_imf_inner(
&t,
"Mr.12345678901@example.com",
format!("{}\n\n100k text...", header).as_bytes(),
@@ -388,7 +388,7 @@ mod tests {
.await?;
// download message from bob partially, this must not change the ephemeral timer
receive_imf_inner(
dc_receive_imf_inner(
&t,
"first@example.org",
b"From: Bob <bob@example.org>\n\

View File

@@ -8,13 +8,11 @@ use num_traits::FromPrimitive;
use crate::aheader::{Aheader, EncryptPreference};
use crate::config::Config;
use crate::contact::addr_cmp;
use crate::context::Context;
use crate::headerdef::HeaderDef;
use crate::headerdef::HeaderDefMap;
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
use crate::keyring::Keyring;
use crate::log::LogExt;
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::pgp;
@@ -133,56 +131,6 @@ impl EncryptHelper {
}
}
/// Applies Autocrypt header to Autocrypt peer state and saves it into the database.
///
/// If we already know this fingerprint from another contact's peerstate, return that
/// peerstate in order to make AEAP work, but don't save it into the db yet.
///
/// Returns updated peerstate.
pub(crate) async fn get_autocrypt_peerstate(
context: &Context,
from: &str,
autocrypt_header: Option<&Aheader>,
message_time: i64,
) -> Result<Option<Peerstate>> {
let mut peerstate;
// Apply Autocrypt header
if let Some(header) = autocrypt_header {
// The "from_nongossiped_fingerprint" part is for AEAP:
// If we know this fingerprint from another addr,
// we may want to do a transition from this other addr
// (and keep its peerstate)
peerstate = Peerstate::from_nongossiped_fingerprint_or_addr(
context,
&header.public_key.fingerprint(),
from,
)
.await?;
if let Some(ref mut peerstate) = peerstate {
if addr_cmp(&peerstate.addr, from) {
peerstate.apply_header(header, message_time);
peerstate.save_to_db(&context.sql, false).await?;
}
// If `peerstate.addr` and `from` differ, this means that
// someone is using the same key but a different addr, probably
// because they made an AEAP transition.
// But we don't know if that's legit until we checked the
// signatures, so wait until then with writing anything
// to the database.
} else {
let p = Peerstate::from_header(header, message_time);
p.save_to_db(&context.sql, true).await?;
peerstate = Some(p);
}
} else {
peerstate = Peerstate::from_addr(context, from).await?;
}
Ok(peerstate)
}
/// Tries to decrypt a message, but only if it is structured as an
/// Autocrypt message.
///
@@ -192,42 +140,10 @@ pub(crate) async fn get_autocrypt_peerstate(
/// If the message is wrongly signed, this will still return the decrypted
/// message but the HashSet will be empty.
pub async fn try_decrypt(
context: &Context,
mail: &ParsedMail<'_>,
decryption_info: &DecryptionInfo,
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
// Possibly perform decryption
let public_keyring_for_validate = keyring_from_peerstate(&decryption_info.peerstate);
let context = context;
let encrypted_data_part = match get_autocrypt_mime(mail)
.or_else(|| get_mixed_up_mime(mail))
.or_else(|| get_attachment_mime(mail))
{
None => {
// not an autocrypt mime message, abort and ignore
return Ok(None);
}
Some(res) => res,
};
info!(context, "Detected Autocrypt-mime message");
let private_keyring: Keyring<SignedSecretKey> = Keyring::new_self(context)
.await
.context("failed to get own keyring")?;
decrypt_part(
encrypted_data_part,
private_keyring,
public_keyring_for_validate,
)
.await
}
pub async fn create_decryption_info(
context: &Context,
mail: &ParsedMail<'_>,
message_time: i64,
) -> Result<DecryptionInfo> {
) -> Result<(Option<Vec<u8>>, HashSet<Fingerprint>)> {
let from = mail
.headers
.get_header(HeaderDef::From_)
@@ -236,34 +152,56 @@ pub async fn create_decryption_info(
.map(|from| from.addr)
.unwrap_or_default();
let autocrypt_header = Aheader::from_headers(&from, &mail.headers)
.ok_or_log_msg(context, "Failed to parse Autocrypt header")
.flatten();
let mut peerstate = Peerstate::from_addr(context, &from).await?;
let peerstate =
get_autocrypt_peerstate(context, &from, autocrypt_header.as_ref(), message_time).await?;
// Apply Autocrypt header
match Aheader::from_headers(&from, &mail.headers) {
Ok(Some(ref header)) => {
if let Some(ref mut peerstate) = peerstate {
peerstate.apply_header(header, message_time);
peerstate.save_to_db(&context.sql, false).await?;
} else {
let p = Peerstate::from_header(header, message_time);
p.save_to_db(&context.sql, true).await?;
peerstate = Some(p);
}
}
Ok(None) => {}
Err(err) => warn!(context, "Failed to parse Autocrypt header: {}", err),
}
Ok(DecryptionInfo {
from,
autocrypt_header,
peerstate,
message_time,
})
}
// Possibly perform decryption
let mut public_keyring_for_validate: Keyring<SignedPublicKey> = Keyring::new();
#[derive(Debug)]
pub struct DecryptionInfo {
/// The From address. This is the address from the unnencrypted, outer
/// From header.
pub from: String,
pub autocrypt_header: Option<Aheader>,
/// The peerstate that will be used to validate the signatures
pub peerstate: Option<Peerstate>,
/// The timestamp when the message was sent.
/// If this is older than the peerstate's last_seen, this probably
/// means out-of-order message arrival, We don't modify the
/// peerstate in this case.
pub message_time: i64,
if let Some(ref mut peerstate) = peerstate {
peerstate
.handle_fingerprint_change(context, message_time)
.await?;
if let Some(key) = &peerstate.public_key {
public_keyring_for_validate.add(key.clone());
} else if let Some(key) = &peerstate.gossip_key {
public_keyring_for_validate.add(key.clone());
}
}
let (out_mail, signatures) =
match decrypt_if_autocrypt_message(context, mail, public_keyring_for_validate).await? {
Some((out_mail, signatures)) => (Some(out_mail), signatures),
None => (None, Default::default()),
};
if let Some(mut peerstate) = peerstate {
// If message is not encrypted and it is not a read receipt, degrade encryption.
if out_mail.is_none()
&& message_time > peerstate.last_seen_autocrypt
&& !contains_report(mail)
{
peerstate.degrade_encryption(message_time);
peerstate.save_to_db(&context.sql, false).await?;
}
}
Ok((out_mail, signatures))
}
/// Returns a reference to the encrypted payload of a valid PGP/MIME message.
@@ -345,16 +283,32 @@ fn get_attachment_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMai
}
}
fn keyring_from_peerstate(peerstate: &Option<Peerstate>) -> Keyring<SignedPublicKey> {
let mut public_keyring_for_validate: Keyring<SignedPublicKey> = Keyring::new();
if let Some(ref peerstate) = *peerstate {
if let Some(key) = &peerstate.public_key {
public_keyring_for_validate.add(key.clone());
} else if let Some(key) = &peerstate.gossip_key {
public_keyring_for_validate.add(key.clone());
async fn decrypt_if_autocrypt_message(
context: &Context,
mail: &ParsedMail<'_>,
public_keyring_for_validate: Keyring<SignedPublicKey>,
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
let encrypted_data_part = match get_autocrypt_mime(mail)
.or_else(|| get_mixed_up_mime(mail))
.or_else(|| get_attachment_mime(mail))
{
None => {
// not an autocrypt mime message, abort and ignore
return Ok(None);
}
}
public_keyring_for_validate
Some(res) => res,
};
info!(context, "Detected Autocrypt-mime message");
let private_keyring: Keyring<SignedSecretKey> = Keyring::new_self(context)
.await
.context("failed to get own keyring")?;
decrypt_part(
encrypted_data_part,
private_keyring,
public_keyring_for_validate,
)
.await
}
/// Validates signatures of Multipart/Signed message part, as defined in RFC 1847.
@@ -403,7 +357,7 @@ async fn decrypt_part(
return Ok(Some((content, valid_detached_signatures)));
} else {
// If the message was wrongly or not signed, still return the plain text.
// The caller has to check if the signatures set is empty then.
// The caller has to check the signatures then.
return Ok(Some((plain, ret_valid_signatures)));
}
@@ -426,6 +380,18 @@ fn has_decrypted_pgp_armor(input: &[u8]) -> bool {
false
}
/// Checks if a MIME structure contains a multipart/report part.
///
/// As reports are often unencrypted, we do not reset the Autocrypt header in
/// this case.
///
/// However, Delta Chat itself has no problem with encrypted multipart/report
/// parts and MUAs should be encouraged to encrpyt multipart/reports as well so
/// that we could use the normal Autocrypt processing.
fn contains_report(mail: &ParsedMail<'_>) -> bool {
mail.ctype.mimetype == "multipart/report"
}
/// Ensures a private key exists for the configured user.
///
/// Normally the private key is generated when the first message is
@@ -445,10 +411,10 @@ pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
#[cfg(test)]
mod tests {
use crate::chat;
use crate::dc_receive_imf::dc_receive_imf;
use crate::message::{Message, Viewtype};
use crate::param::Param;
use crate::peerstate::ToSave;
use crate::receive_imf::receive_imf;
use crate::test_utils::{bob_keypair, TestContext};
use super::*;
@@ -680,7 +646,7 @@ Sent with my Delta Chat Messenger: https://delta.chat";
assert!(get_attachment_mime(&mail).is_some());
let bob = TestContext::new_bob().await;
receive_imf(&bob, attachment_mime, false).await?;
dc_receive_imf(&bob, attachment_mime, false).await?;
let msg = bob.get_last_msg().await;
assert_eq!(msg.text.as_deref(), Some("Hello from Thunderbird!"));

View File

@@ -70,6 +70,7 @@ use crate::chat::{send_msg, ChatId};
use crate::constants::{DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH};
use crate::contact::ContactId;
use crate::context::Context;
use crate::dc_tools::{duration_to_str, time};
use crate::download::MIN_DELETE_SERVER_AFTER;
use crate::events::EventType;
use crate::log::LogExt;
@@ -77,7 +78,6 @@ use crate::message::{Message, MessageState, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::sql::{self, params_iter};
use crate::stock_str;
use crate::tools::{duration_to_str, time};
use std::cmp::max;
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
@@ -339,7 +339,7 @@ pub(crate) async fn delete_expired_messages(context: &Context, now: i64) -> Resu
.sql
.execute(
// If you change which information is removed here, also change MsgId::trash() and
// which information receive_imf::add_parts() still adds to the db if the chat_id is TRASH
// which information dc_receive_imf::add_parts() still adds to the db if the chat_id is TRASH
r#"
UPDATE msgs
SET
@@ -572,13 +572,13 @@ pub(crate) async fn start_ephemeral_timers(context: &Context) -> Result<()> {
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::receive_imf::receive_imf;
use crate::test_utils::TestContext;
use crate::tools::MAX_SECONDS_TO_LEND_FROM_FUTURE;
use crate::{
chat::{self, Chat, ChatItem},
tools::IsNoneOrEmpty,
dc_tools::IsNoneOrEmpty,
};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -1101,7 +1101,7 @@ mod tests {
let alice = TestContext::new_alice().await;
// Message with Message-ID <first@example.com> and no timer is received.
receive_imf(
dc_receive_imf(
&alice,
b"From: Bob <bob@example.com>\n\
To: Alice <alice@example.org>\n\
@@ -1120,7 +1120,7 @@ mod tests {
assert_eq!(chat_id.get_ephemeral_timer(&alice).await?, Timer::Disabled);
// Message with Message-ID <second@example.com> is received.
receive_imf(
dc_receive_imf(
&alice,
b"From: Bob <bob@example.com>\n\
To: Alice <alice@example.org>\n\
@@ -1155,7 +1155,7 @@ mod tests {
//
// The message also contains a quote of the first message to test that only References:
// header and not In-Reply-To: is consulted by the rollback protection.
receive_imf(
dc_receive_imf(
&alice,
b"From: Bob <bob@example.com>\n\
To: Alice <alice@example.org>\n\

View File

@@ -280,8 +280,8 @@ mod tests {
use crate::chat::forward_msgs;
use crate::config::Config;
use crate::contact::ContactId;
use crate::dc_receive_imf::dc_receive_imf;
use crate::message::{MessengerMessage, Viewtype};
use crate::receive_imf::receive_imf;
use crate::test_utils::TestContext;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -440,7 +440,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");
receive_imf(&alice, raw, false).await.unwrap();
dc_receive_imf(&alice, raw, false).await.unwrap();
let msg = alice.get_last_msg_in(chat.get_id()).await;
assert_ne!(msg.get_from_id(), ContactId::SELF);
assert_eq!(msg.is_dc_message, MessengerMessage::No);
@@ -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");
receive_imf(&alice, raw, false).await.unwrap();
dc_receive_imf(&alice, raw, false).await.unwrap();
let msg = alice.get_last_msg_in(chat.get_id()).await;
// forward the message to saved-messages,
@@ -551,7 +551,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
async fn test_cp1252_html() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await?;
receive_imf(
dc_receive_imf(
&t,
include_bytes!("../test-data/message/cp1252-html.eml"),
false,

View File

@@ -25,6 +25,10 @@ use crate::constants::{
};
use crate::contact::{normalize_name, Contact, ContactId, Modifier, Origin};
use crate::context::Context;
use crate::dc_receive_imf::{
dc_receive_imf_inner, from_field_to_contact_id, get_prefetch_parent_message, ReceivedMsg,
};
use crate::dc_tools::dc_create_id;
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::job;
@@ -33,16 +37,12 @@ use crate::login_param::{
};
use crate::message::{self, Message, MessageState, MessengerMessage, MsgId, Viewtype};
use crate::mimeparser;
use crate::oauth2::get_oauth2_access_token;
use crate::oauth2::dc_get_oauth2_access_token;
use crate::provider::Socket;
use crate::receive_imf::{
from_field_to_contact_id, get_prefetch_parent_message, receive_imf_inner, ReceivedMsg,
};
use crate::scheduler::connectivity::ConnectivityStore;
use crate::scheduler::InterruptInfo;
use crate::sql;
use crate::stock_str;
use crate::tools::create_id;
mod client;
mod idle;
@@ -390,7 +390,7 @@ impl Imap {
let login_res = if oauth2 {
let addr: &str = config.addr.as_ref();
let token = get_oauth2_access_token(context, addr, imap_pw, true)
let token = dc_get_oauth2_access_token(context, addr, imap_pw, true)
.await?
.context("IMAP could not get OAUTH token")?;
let auth = OAuth2 {
@@ -451,9 +451,7 @@ impl Imap {
}
/// Determine server capabilities if not done yet.
///
/// If server supports ID capability, send our client ID.
pub(crate) async fn determine_capabilities(&mut self, context: &Context) -> Result<()> {
async fn determine_capabilities(&mut self) -> Result<()> {
if self.capabilities_determined {
return Ok(());
}
@@ -465,12 +463,6 @@ impl Imap {
.capabilities()
.await
.context("CAPABILITY command error")?;
if caps.has_str("ID") {
let server_id = session.id([("name", Some("Delta Chat"))]).await?;
info!(context, "Server ID: {:?}", server_id);
let mut lock = context.server_id.write().await;
*lock = server_id;
}
self.config.can_idle = caps.has_str("IDLE");
self.config.can_move = caps.has_str("MOVE");
self.config.can_check_quota = caps.has_str("QUOTA");
@@ -489,8 +481,8 @@ impl Imap {
return Err(err);
}
self.determine_capabilities(context).await?;
self.ensure_configured_folders(context, true).await?;
self.determine_capabilities().await?;
Ok(())
}
@@ -795,7 +787,7 @@ impl Imap {
};
// Get the Message-ID or generate a fake one to identify the message in the database.
let message_id = prefetch_get_message_id(&headers).unwrap_or_else(create_id);
let message_id = prefetch_get_message_id(&headers).unwrap_or_else(dc_create_id);
let target = match target_folder(context, folder, is_spam_folder, &headers).await? {
Some(config) => match context.get_config(config).await? {
@@ -883,8 +875,8 @@ impl Imap {
received_msgs.extend(received_msgs_2);
// determine which uid_next to use to update to
// receive_imf() returns an `Err` value only on recoverable errors, otherwise it just logs an error.
// `largest_uid_processed` is the largest uid where receive_imf() did NOT return an error.
// dc_receive_imf() returns an `Err` value only on recoverable errors, otherwise it just logs an error.
// `largest_uid_processed` is the largest uid where dc_receive_imf() did NOT return an error.
// So: Update the uid_next to the largest uid that did NOT recoverably fail. Not perfect because if there was
// another message afterwards that succeeded, we will not retry. The upside is that we will not retry an infinite amount of times.
@@ -1439,7 +1431,7 @@ impl Imap {
continue;
}
// XXX put flags into a set and pass them to receive_imf
// XXX put flags into a set and pass them to dc_receive_imf
let context = context.clone();
// safe, as we checked above that there is a body.
@@ -1457,7 +1449,7 @@ impl Imap {
);
""
};
match receive_imf_inner(
match dc_receive_imf_inner(
&context,
rfc724_mid,
body,
@@ -1474,7 +1466,7 @@ impl Imap {
last_uid = Some(server_uid)
}
Err(err) => {
warn!(context, "receive_imf error: {:#}", err);
warn!(context, "dc_receive_imf error: {:#}", err);
}
};
}
@@ -1569,59 +1561,6 @@ impl Imap {
self.configure_folders(context, create_mvbox).await
}
/// Attempts to configure mvbox.
///
/// Tries to find any folder in the given list of `folders`. If none is found, tries to create
/// any of them in the same order. This method does not use LIST command to ensure that
/// configuration works even if mailbox lookup is forbidden via Access Control List (see
/// <https://datatracker.ietf.org/doc/html/rfc4314>).
///
/// Returns first found or created folder name.
async fn configure_mvbox<'a>(
&mut self,
context: &Context,
folders: &[&'a str],
create_mvbox: bool,
) -> Result<Option<&'a str>> {
// Close currently selected folder if needed.
// We are going to select folders using low-level EXAMINE operations below.
self.select_folder(context, None).await?;
let session = self
.session
.as_mut()
.context("no IMAP connection established")?;
for folder in folders {
info!(context, "Looking for MVBOX-folder \"{}\"...", &folder);
let res = session.examine(&folder).await;
if res.is_ok() {
info!(
context,
"MVBOX-folder {:?} successfully selected, using it.", &folder
);
session.close().await?;
return Ok(Some(folder));
}
}
if create_mvbox {
for folder in folders {
match session.create(&folder).await {
Ok(_) => {
info!(context, "MVBOX-folder {} created.", &folder);
return Ok(Some(folder));
}
Err(err) => {
warn!(context, "Cannot create MVBOX-folder {:?}: {}", &folder, err);
}
}
}
}
Ok(None)
}
pub async fn configure_folders(&mut self, context: &Context, create_mvbox: bool) -> Result<()> {
let session = self
.session
@@ -1634,7 +1573,9 @@ impl Imap {
.context("list_folders failed")?;
let mut delimiter = ".".to_string();
let mut delimiter_is_default = true;
let mut mvbox_folder = None;
let mut folder_configs = BTreeMap::new();
let mut fallback_folder = get_fallback_folder(&delimiter);
while let Some(folder) = folders.next().await {
let folder = folder?;
@@ -1644,13 +1585,22 @@ impl Imap {
if let Some(d) = folder.delimiter() {
if delimiter_is_default && !d.is_empty() && delimiter != d {
delimiter = d.to_string();
fallback_folder = get_fallback_folder(&delimiter);
delimiter_is_default = false;
}
}
let folder_meaning = get_folder_meaning(&folder);
let folder_name_meaning = get_folder_meaning_by_name(folder.name());
if let Some(config) = folder_meaning.to_config() {
if folder.name() == "DeltaChat" {
// Always takes precedence
mvbox_folder = Some(folder.name().to_string());
} else if folder.name() == fallback_folder {
// only set if none has been already set
if mvbox_folder.is_none() {
mvbox_folder = Some(folder.name().to_string());
}
} else if let Some(config) = folder_meaning.to_config() {
// Always takes precedence
folder_configs.insert(config, folder.name().to_string());
} else if let Some(config) = folder_name_meaning.to_config() {
@@ -1664,17 +1614,47 @@ impl Imap {
info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
let fallback_folder = format!("INBOX{}DeltaChat", delimiter);
let mvbox_folder = self
.configure_mvbox(context, &["DeltaChat", &fallback_folder], create_mvbox)
.await
.context("failed to configure mvbox")?;
if mvbox_folder.is_none() && create_mvbox {
info!(context, "Creating MVBOX-folder \"DeltaChat\"...",);
match session.create("DeltaChat").await {
Ok(_) => {
mvbox_folder = Some("DeltaChat".into());
info!(context, "MVBOX-folder created.",);
}
Err(err) => {
warn!(
context,
"Cannot create MVBOX-folder, trying to create INBOX subfolder. ({})", err
);
match session.create(&fallback_folder).await {
Ok(_) => {
mvbox_folder = Some(fallback_folder);
info!(
context,
"MVBOX-folder created as INBOX subfolder. ({})", err
);
}
Err(err) => {
warn!(context, "Cannot create MVBOX-folder. ({})", err);
}
}
}
}
// SUBSCRIBE is needed to make the folder visible to the LSUB command
// that may be used by other MUAs to list folders.
// for the LIST command, the folder is always visible.
if let Some(ref mvbox) = mvbox_folder {
if let Err(err) = session.subscribe(mvbox).await {
warn!(context, "could not subscribe to {:?}: {:?}", mvbox, err);
}
}
}
context
.set_config(Config::ConfiguredInboxFolder, Some("INBOX"))
.await?;
if let Some(mvbox_folder) = mvbox_folder {
info!(context, "Setting MVBOX FOLDER TO {}", &mvbox_folder);
if let Some(ref mvbox_folder) = mvbox_folder {
context
.set_config(Config::ConfiguredMvboxFolder, Some(mvbox_folder))
.await?;
@@ -1737,11 +1717,11 @@ async fn should_move_out_of_spam(
// the SecureJoin header. So, we always move chat messages out of Spam.
// Two possibilities to change this would be:
// 1. Remove the `&& !context.is_spam_folder(folder).await?` check from
// `fetch_new_messages()`, and then let `receive_imf()` check
// `fetch_new_messages()`, and then let `dc_receive_imf()` check
// if it's a spam message and should be hidden.
// 2. Or add a flag to the ChatVersion header that this is a securejoin
// request, and return `true` here only if the message has this flag.
// `receive_imf()` can then check if the securejoin request is valid.
// `dc_receive_imf()` can then check if the securejoin request is valid.
return Ok(true);
}
@@ -2064,6 +2044,10 @@ pub(crate) async fn prefetch_should_download(
Ok(should_download)
}
fn get_fallback_folder(delimiter: &str) -> String {
format!("INBOX{}DeltaChat", delimiter)
}
/// Marks messages in `msgs` table as seen, searching for them by UID.
///
/// Returns updated chat ID if any message was marked as seen.

View File

@@ -11,7 +11,7 @@ use async_smtp::ServerAddress;
use tokio::net::{self, TcpStream};
use super::session::Session;
use crate::login_param::{build_tls, Socks5Config};
use crate::login_param::{dc_build_tls, Socks5Config};
use super::session::SessionStream;
@@ -67,7 +67,7 @@ impl Client {
strict_tls: bool,
) -> Result<Self> {
let stream = TcpStream::connect(addr).await?;
let tls = build_tls(strict_tls);
let tls = dc_build_tls(strict_tls);
let tls_stream: Box<dyn SessionStream> = Box::new(tls.connect(domain, stream).await?);
let mut client = ImapClient::new(tls_stream);
@@ -108,7 +108,7 @@ impl Client {
.await?,
);
let tls = build_tls(strict_tls);
let tls = dc_build_tls(strict_tls);
let tls_stream: Box<dyn SessionStream> =
Box::new(tls.connect(target_addr.host.clone(), socks5_stream).await?);
let mut client = ImapClient::new(tls_stream);
@@ -151,7 +151,7 @@ impl Client {
Ok(self)
} else {
let Client { mut inner, .. } = self;
let tls = build_tls(strict_tls);
let tls = dc_build_tls(strict_tls);
inner.run_command_and_check_ok("STARTTLS", None).await?;
let stream = inner.into_inner();

View File

@@ -17,6 +17,10 @@ use crate::chat::{self, delete_and_reset_all_device_msgs, ChatId};
use crate::config::Config;
use crate::contact::ContactId;
use crate::context::Context;
use crate::dc_tools::{
dc_create_folder, dc_delete_file, dc_get_filesuffix_lc, dc_open_file_std, dc_read_file,
dc_write_file, time, EmailAddress,
};
use crate::e2ee;
use crate::events::EventType;
use crate::key::{self, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey};
@@ -27,10 +31,6 @@ use crate::param::Param;
use crate::pgp;
use crate::sql;
use crate::stock_str;
use crate::tools::{
create_folder, delete_file, get_filesuffix_lc, open_file_std, read_file, time, write_file,
EmailAddress,
};
// Name of the database file in the backup.
const DBFILE_BACKUP_NAME: &str = "dc_database_backup.sqlite";
@@ -58,7 +58,7 @@ pub enum ImexMode {
ExportBackup = 11,
/// `path` is the file (not: directory) to import. The file is normally
/// created by DC_IMEX_EXPORT_BACKUP and detected by imex_has_backup(). Importing a backup
/// created by DC_IMEX_EXPORT_BACKUP and detected by dc_imex_has_backup(). Importing a backup
/// is only possible as long as the context is not configured or used in another way.
ImportBackup = 12,
}
@@ -290,7 +290,7 @@ pub async fn continue_key_transfer(
);
if let Some(filename) = msg.get_file(context) {
let file = open_file_std(context, filename)?;
let file = dc_open_file_std(context, filename)?;
let sc = normalize_setup_code(setup_code);
let armored_key = decrypt_setup_file(&sc, file).await?;
set_self_key(context, &armored_key, true, true).await?;
@@ -393,7 +393,7 @@ async fn imex_inner(
if e2ee::ensure_secret_key_exists(context).await.is_err() {
bail!("Cannot create private key or private key not available.");
} else {
create_folder(context, &path).await?;
dc_create_folder(context, &path).await?;
}
}
@@ -499,7 +499,7 @@ async fn import_backup(
fn get_next_backup_path(folder: &Path, backup_time: i64) -> Result<(PathBuf, PathBuf, PathBuf)> {
let folder = PathBuf::from(folder);
let stem = chrono::NaiveDateTime::from_timestamp(backup_time, 0)
// Don't change this file name format, in `dc_imex_has_backup` we use string comparison to determine which backup is newer:
// Don't change this file name format, in has_backup() we use string comparison to determine which backup is newer:
.format("delta-chat-backup-%Y-%m-%d")
.to_string();
@@ -577,7 +577,7 @@ struct DeleteOnDrop(PathBuf);
impl Drop for DeleteOnDrop {
fn drop(&mut self) {
let file = self.0.clone();
// Not using `tools::delete_file` here because it would send a DeletedBlobFile event
// Not using dc_delete_file() here because it would send a DeletedBlobFile event
// Hack to avoid panic in nested runtime calls of tokio
std::fs::remove_file(file).ok();
}
@@ -650,7 +650,7 @@ async fn import_self_keys(context: &Context, dir: &Path) -> Result<()> {
let entry_fn = entry.file_name();
let name_f = entry_fn.to_string_lossy();
let path_plus_name = dir.join(&entry_fn);
match get_filesuffix_lc(&name_f) {
match dc_get_filesuffix_lc(&name_f) {
Some(suffix) => {
if suffix != "asc" {
continue;
@@ -672,7 +672,7 @@ async fn import_self_keys(context: &Context, dir: &Path) -> Result<()> {
path_plus_name.display()
);
match read_file(context, &path_plus_name).await {
match dc_read_file(context, &path_plus_name).await {
Ok(buf) => {
let armored = std::string::String::from_utf8_lossy(&buf);
if let Err(err) = set_self_key(context, &armored, set_default, false).await {
@@ -775,10 +775,10 @@ where
key.key_id(),
file_name.display()
);
delete_file(context, &file_name).await;
dc_delete_file(context, &file_name).await;
let content = key.to_asc(None).into_bytes();
let res = write_file(context, &file_name, &content).await;
let res = dc_write_file(context, &file_name, &content).await;
if res.is_err() {
error!(context, "Cannot write key to {}", file_name.display());
} else {

View File

@@ -9,10 +9,10 @@ use deltachat_derive::{FromSql, ToSql};
use rand::{thread_rng, Rng};
use crate::context::Context;
use crate::dc_tools::time;
use crate::imap::Imap;
use crate::param::Params;
use crate::scheduler::InterruptInfo;
use crate::tools::time;
// results in ~3 weeks for the last backoff timespan
const JOB_RETRIES: u32 = 17;

View File

@@ -16,7 +16,7 @@ use tokio::runtime::Handle;
use crate::config::Config;
use crate::constants::KeyGenType;
use crate::context::Context;
use crate::tools::{time, EmailAddress};
use crate::dc_tools::{time, EmailAddress};
// Re-export key types
pub use crate::pgp::KeyPair;
@@ -40,7 +40,7 @@ pub trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
/// Create a key from a base64 string.
fn from_base64(data: &str) -> Result<Self::KeyType> {
// strip newlines and other whitespace
let cleaned: String = data.split_whitespace().collect();
let cleaned: String = data.trim().split_whitespace().collect();
let bytes = base64::decode(cleaned.as_bytes())?;
Self::from_slice(&bytes)
}

View File

@@ -2,7 +2,7 @@
#![recursion_limit = "256"]
#![forbid(unsafe_code)]
#![warn(
#![deny(
unused,
clippy::correctness,
missing_debug_implementations,
@@ -17,8 +17,7 @@
clippy::match_bool,
clippy::eval_order_dependence,
clippy::bool_assert_comparison,
clippy::manual_split_once,
clippy::format_push_string
clippy::manual_split_once
)]
#[macro_use]
@@ -98,8 +97,8 @@ pub mod plaintext;
mod ratelimit;
pub mod summary;
pub mod receive_imf;
pub mod tools;
pub mod dc_receive_imf;
pub mod dc_tools;
pub mod accounts;
@@ -108,5 +107,3 @@ pub const DCC_MIME_DEBUG: &str = "DCC_MIME_DEBUG";
#[cfg(test)]
mod test_utils;
#[cfg(test)]
mod tests;

View File

@@ -11,11 +11,11 @@ use tokio::time::timeout;
use crate::chat::{self, ChatId};
use crate::contact::ContactId;
use crate::context::Context;
use crate::dc_tools::{duration_to_str, time};
use crate::events::EventType;
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::stock_str;
use crate::tools::{duration_to_str, time};
/// Location record
#[derive(Debug, Clone, Default)]
@@ -63,7 +63,7 @@ impl Kml {
Default::default()
}
pub fn parse(to_parse: &[u8]) -> Result<Self> {
pub fn parse(context: &Context, to_parse: &[u8]) -> Result<Self> {
ensure!(to_parse.len() <= 1024 * 1024, "kml-file is too large");
let mut reader = quick_xml::Reader::from_reader(to_parse);
@@ -75,16 +75,19 @@ impl Kml {
let mut buf = Vec::new();
loop {
match reader.read_event(&mut buf).with_context(|| {
format!(
"location parsing error at position {}",
reader.buffer_position()
)
})? {
quick_xml::events::Event::Start(ref e) => kml.starttag_cb(e, &reader),
quick_xml::events::Event::End(ref e) => kml.endtag_cb(e),
quick_xml::events::Event::Text(ref e) => kml.text_cb(e, &reader),
quick_xml::events::Event::Eof => break,
match reader.read_event(&mut buf) {
Ok(quick_xml::events::Event::Start(ref e)) => kml.starttag_cb(e, &reader),
Ok(quick_xml::events::Event::End(ref e)) => kml.endtag_cb(e),
Ok(quick_xml::events::Event::Text(ref e)) => kml.text_cb(e, &reader),
Err(e) => {
error!(
context,
"Location parsing: Error at position {}: {:?}",
reader.buffer_position(),
e
);
}
Ok(quick_xml::events::Event::Eof) => break,
_ => (),
}
buf.clear();
@@ -725,15 +728,17 @@ mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::receive_imf::receive_imf;
use crate::dc_receive_imf::dc_receive_imf;
use crate::test_utils::TestContext;
#[test]
fn test_kml_parse() {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_kml_parse() {
let context = TestContext::new().await;
let xml =
b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n<Document addr=\"user@example.org\">\n<Placemark><Timestamp><when>2019-03-06T21:09:57Z</when></Timestamp><Point><coordinates accuracy=\"32.000000\">9.423110,53.790302</coordinates></Point></Placemark>\n<PlaceMARK>\n<Timestamp><WHEN > \n\t2018-12-13T22:11:12Z\t</WHEN></Timestamp><Point><coordinates aCCuracy=\"2.500000\"> 19.423110 \t , \n 63.790302\n </coordinates></Point></PlaceMARK>\n</Document>\n</kml>";
let kml = Kml::parse(xml).expect("parsing failed");
let kml = Kml::parse(&context.ctx, xml).expect("parsing failed");
assert!(kml.addr.is_some());
assert_eq!(kml.addr.as_ref().unwrap(), "user@example.org",);
@@ -758,18 +763,13 @@ mod tests {
assert_eq!(locations_ref[1].timestamp, 1544739072);
}
#[test]
fn test_kml_parse_error() {
let xml = b"<?><xmlversi\"\"\">?</document>";
assert!(Kml::parse(xml).is_err());
}
#[test]
fn test_get_message_kml() {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_message_kml() {
let context = TestContext::new().await;
let timestamp = 1598490000;
let xml = get_message_kml(timestamp, 51.423723f64, 8.552556f64);
let kml = Kml::parse(xml.as_bytes()).expect("parsing failed");
let kml = Kml::parse(&context.ctx, xml.as_bytes()).expect("parsing failed");
let locations_ref = &kml.locations;
assert_eq!(locations_ref.len(), 1);
@@ -795,7 +795,7 @@ mod tests {
async fn receive_location_kml() -> Result<()> {
let alice = TestContext::new_alice().await;
receive_imf(
dc_receive_imf(
&alice,
br#"Subject: Hello
Message-ID: hello@example.net
@@ -812,7 +812,7 @@ Text message."#,
let received_msg = alice.get_last_msg().await;
assert_eq!(received_msg.text.unwrap(), "Text message.");
receive_imf(
dc_receive_imf(
&alice,
br#"Subject: locations
MIME-Version: 1.0

View File

@@ -393,7 +393,7 @@ static LETSENCRYPT_ROOT: Lazy<Certificate> = Lazy::new(|| {
.unwrap()
});
pub fn build_tls(strict_tls: bool) -> async_native_tls::TlsConnector {
pub fn dc_build_tls(strict_tls: bool) -> async_native_tls::TlsConnector {
let tls_builder =
async_native_tls::TlsConnector::new().add_root_certificate(LETSENCRYPT_ROOT.clone());
@@ -462,8 +462,8 @@ mod tests {
async fn test_build_tls() -> Result<()> {
// we are using some additional root certificates.
// make sure, they do not break construction of TlsConnector
let _ = build_tls(true);
let _ = build_tls(false);
let _ = dc_build_tls(true);
let _ = dc_build_tls(false);
Ok(())
}
}

View File

@@ -15,6 +15,10 @@ use crate::constants::{
};
use crate::contact::{Contact, ContactId, 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::events::EventType;
@@ -26,10 +30,6 @@ use crate::scheduler::InterruptInfo;
use crate::sql;
use crate::stock_str;
use crate::summary::Summary;
use crate::tools::{
create_smeared_timestamp, get_filebytes, get_filemeta, gm2local_offset, read_file, time,
timestamp_to_str, truncate,
};
/// Message ID, including reserved IDs.
///
@@ -94,7 +94,7 @@ impl MsgId {
.sql
.execute(
// If you change which information is removed here, also change delete_expired_messages() and
// which information receive_imf::add_parts() still adds to the db if the chat_id is TRASH
// which information dc_receive_imf::add_parts() still adds to the db if the chat_id is TRASH
r#"
UPDATE msgs
SET
@@ -232,6 +232,10 @@ impl Default for MessengerMessage {
/// An object representing a single message in memory.
/// The message object is not updated.
/// If you want an update, you have to recreate the object.
///
/// to check if a mail was sent, use dc_msg_is_sent()
/// approx. max. length returned by dc_msg_get_text()
/// approx. max. length returned by dc_get_msg_info()
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Message {
pub(crate) id: MsgId,
@@ -391,8 +395,8 @@ impl Message {
self.param.set_int(Param::Width, 0);
self.param.set_int(Param::Height, 0);
if let Ok(buf) = read_file(context, path_and_filename).await {
if let Ok((width, height)) = get_filemeta(&buf) {
if let Ok(buf) = dc_read_file(context, path_and_filename).await {
if let Ok((width, height)) = dc_get_filemeta(&buf) {
self.param.set_int(Param::Width, width as i32);
self.param.set_int(Param::Height, height as i32);
}
@@ -408,7 +412,7 @@ impl Message {
}
/// Check if a message has a location bound to it.
/// These messages are also returned by get_locations()
/// These messages are also returned by dc_get_locations()
/// and the UI may decide to display a special icon beside such messages,
///
/// @memberof Message
@@ -423,7 +427,7 @@ impl Message {
/// at a position different from the self-location.
/// You should not call this function
/// if you want to bind the current self-location to a message;
/// this is done by set_location() and send_locations_to_chat().
/// 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.
@@ -493,7 +497,7 @@ impl Message {
pub async fn get_filebytes(&self, context: &Context) -> u64 {
match self.param.get_path(Param::File, context) {
Ok(Some(path)) => get_filebytes(context, &path).await,
Ok(Some(path)) => dc_get_filebytes(context, &path).await,
Ok(None) => 0,
Err(_) => 0,
}
@@ -553,9 +557,9 @@ impl Message {
Ok(Summary::new(context, self, chat, contact.as_ref()).await)
}
// It's a little unfortunate that the UI has to first call `dc_msg_get_override_sender_name` and then if it was `NULL`, call
// `dc_contact_get_display_name` but this was the best solution:
// - We could load a Contact struct from the db here to call `dc_get_display_name` instead of returning `None`, but then we had a db
// It's a little unfortunate that the UI has to first call dc_msg_get_override_sender_name() and then if it was NULL, call
// dc_contact_get_display_name() but this was the best solution:
// - We could load a Contact struct from the db here to call get_display_name() instead of returning None, but then we had a db
// call everytime (and this fn is called a lot while the user is scrolling through a group), so performance would be bad
// - We could pass both a Contact struct and a Message struct in the FFI, but at least on Android we would need to handle raw
// C-data in the Java code (i.e. a `long` storing a C pointer)
@@ -568,14 +572,14 @@ impl Message {
}
// Exposing this function over the ffi instead of get_override_sender_name() would mean that at least Android Java code has
// to handle raw C-data (as it is done for msg_get_summary())
// to handle raw C-data (as it is done for dc_msg_get_summary())
pub fn get_sender_name(&self, contact: &Contact) -> String {
self.get_override_sender_name()
.unwrap_or_else(|| contact.get_display_name().to_string())
}
pub fn has_deviating_timestamp(&self) -> bool {
let cnv_to_local = gm2local_offset();
let cnv_to_local = dc_gm2local_offset();
let sort_timestamp = self.get_sort_timestamp() as i64 + cnv_to_local;
let send_timestamp = self.get_timestamp() as i64 + cnv_to_local;
@@ -632,7 +636,7 @@ impl Message {
}
if let Some(filename) = self.get_file(context) {
if let Ok(ref buf) = read_file(context, filename).await {
if let Ok(ref buf) = dc_read_file(context, filename).await {
if let Ok((typ, headers, _)) = split_armored_data(buf) {
if typ == pgp::armor::BlockType::Message {
return headers.get(crate::pgp::HEADER_SETUPCODE).cloned();
@@ -1000,9 +1004,9 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
return Ok(ret);
}
let rawtxt = rawtxt.unwrap_or_default();
let rawtxt = truncate(rawtxt.trim(), DC_DESIRED_TEXT_LEN);
let rawtxt = dc_truncate(rawtxt.trim(), DC_DESIRED_TEXT_LEN);
let fts = timestamp_to_str(msg.get_timestamp());
let fts = dc_timestamp_to_str(msg.get_timestamp());
ret += &format!("Sent: {}", fts);
let name = Contact::load_from_db(context, msg.from_id)
@@ -1014,7 +1018,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
ret += "\n";
if msg.from_id != ContactId::SELF {
let s = timestamp_to_str(if 0 != msg.timestamp_rcvd {
let s = dc_timestamp_to_str(if 0 != msg.timestamp_rcvd {
msg.timestamp_rcvd
} else {
msg.timestamp_sort
@@ -1028,7 +1032,10 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
}
if msg.ephemeral_timestamp != 0 {
ret += &format!("Expires: {}\n", timestamp_to_str(msg.ephemeral_timestamp));
ret += &format!(
"Expires: {}\n",
dc_timestamp_to_str(msg.ephemeral_timestamp)
);
}
if msg.from_id == ContactId::INFO || msg.to_id == ContactId::INFO {
@@ -1051,7 +1058,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
.await
{
for (contact_id, ts) in rows {
let fts = timestamp_to_str(ts);
let fts = dc_timestamp_to_str(ts);
ret += &format!("Read: {}", fts);
let name = Contact::load_from_db(context, contact_id)
@@ -1087,7 +1094,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
}
if let Some(path) = msg.get_file(context) {
let bytes = get_filebytes(context, &path).await;
let bytes = dc_get_filebytes(context, &path).await;
ret += &format!("\nFile: {}, {}, bytes\n", path.display(), bytes);
}
@@ -1201,7 +1208,7 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> {
/// Get the raw mime-headers of the given message.
/// Raw headers are saved for incoming messages
/// only if `set_config(context, "save_mime_headers", "1")`
/// only if `dc_set_config(context, "save_mime_headers", "1")`
/// was called before.
///
/// Returns an empty vector if there are no headers saved for the given message,
@@ -1533,7 +1540,7 @@ pub async fn handle_mdn(
pub(crate) async fn handle_ndn(
context: &Context,
failed: &FailureReport,
error: Option<String>,
error: &str,
) -> Result<()> {
if failed.rfc724_mid.is_empty() {
return Ok(());
@@ -1564,18 +1571,10 @@ pub(crate) async fn handle_ndn(
)
.await?;
let error = if let Some(error) = error {
error
} else if let Some(failed_recipient) = &failed.failed_recipient {
format!("Delivery to {} failed.", failed_recipient).clone()
} else {
"Delivery to at least one recipient failed.".to_string()
};
let mut first = true;
for msg in msgs.into_iter() {
let (msg_id, chat_id, chat_type) = msg?;
set_msg_failed(context, msg_id, &error).await;
set_msg_failed(context, msg_id, error).await;
if first {
// Add only one info msg for all failed messages
ndn_maybe_add_info_msg(context, failed, chat_id, chat_type).await?;
@@ -1607,7 +1606,7 @@ async fn ndn_maybe_add_info_msg(
context,
chat_id,
&text,
create_smeared_timestamp(context).await,
dc_create_smeared_timestamp(context).await,
)
.await?;
context.emit_event(EventType::ChatModified(chat_id));
@@ -1637,7 +1636,7 @@ pub async fn get_unblocked_msg_cnt(context: &Context) -> usize {
{
Ok(res) => res,
Err(err) => {
error!(context, "get_unblocked_msg_cnt() failed. {}", err);
error!(context, "dc_get_unblocked_msg_cnt() failed. {}", err);
0
}
}
@@ -1657,7 +1656,7 @@ pub async fn get_request_msg_cnt(context: &Context) -> usize {
{
Ok(res) => res,
Err(err) => {
error!(context, "get_request_msg_cnt() failed. {}", err);
error!(context, "dc_get_request_msg_cnt() failed. {}", err);
0
}
}
@@ -1754,7 +1753,8 @@ 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().
/// The text of the message is set using dc_msg_set_text()
/// and retrieved with dc_msg_get_text().
Text = 10,
/// Image message.
@@ -1835,7 +1835,7 @@ mod tests {
use crate::chat::{marknoticed_chat, ChatItem};
use crate::chatlist::Chatlist;
use crate::receive_imf::receive_imf;
use crate::dc_receive_imf::dc_receive_imf;
use crate::test_utils as test;
use crate::test_utils::TestContext;
@@ -2037,7 +2037,7 @@ mod tests {
async fn test_get_chat_id() {
// Alice receives a message that pops up as a contact request
let alice = TestContext::new_alice().await;
receive_imf(
dc_receive_imf(
&alice,
b"From: Bob <bob@example.com>\n\
To: alice@example.org\n\
@@ -2239,7 +2239,7 @@ mod tests {
let alice = TestContext::new_alice().await;
// Alice receives a message from Bob the bot.
receive_imf(
dc_receive_imf(
&alice,
b"From: Bob <bob@example.com>\n\
To: alice@example.org\n\
@@ -2257,7 +2257,7 @@ mod tests {
assert!(msg.is_bot());
// Alice receives a message from Bob who is not the bot anymore.
receive_imf(
dc_receive_imf(
&alice,
b"From: Bob <bob@example.com>\n\
To: alice@example.org\n\

View File

@@ -13,6 +13,11 @@ use crate::config::Config;
use crate::constants::{Chattype, DC_FROM_HANDSHAKE};
use crate::contact::Contact;
use crate::context::{get_version_str, Context};
use crate::dc_tools::IsNoneOrEmpty;
use crate::dc_tools::{
dc_create_outgoing_rfc724_mid, dc_create_smeared_timestamp, dc_get_filebytes,
remove_subject_prefix, time,
};
use crate::e2ee::EncryptHelper;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::format_flowed::{format_flowed, format_flowed_quote};
@@ -24,11 +29,6 @@ use crate::param::Param;
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::simplify::escape_message_footer_marks;
use crate::stock_str;
use crate::tools::IsNoneOrEmpty;
use crate::tools::{
create_outgoing_rfc724_mid, create_smeared_timestamp, get_filebytes, remove_subject_prefix,
time,
};
// attachments of 25 mb brutto should work on the majority of providers
// (brutto examples: web.de=50, 1&1=40, t-online.de=32, gmail=25, posteo=50, yahoo=25, all-inkl=100).
@@ -243,7 +243,7 @@ impl<'a> MimeFactory<'a> {
.get_config(Config::Selfstatus)
.await?
.unwrap_or_default();
let timestamp = create_smeared_timestamp(context).await;
let timestamp = dc_create_smeared_timestamp(context).await;
let res = MimeFactory::<'a> {
from_addr,
@@ -496,9 +496,9 @@ 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>.
let from_header = Header::new_with_value("From".into(), vec![from]).unwrap();
headers.unprotected.push(from_header.clone());
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());
@@ -533,7 +533,7 @@ impl<'a> MimeFactory<'a> {
let rfc724_mid = match self.loaded {
Loaded::Message { .. } => self.msg.rfc724_mid.clone(),
Loaded::Mdn { .. } => create_outgoing_rfc724_mid(None, &self.from_addr),
Loaded::Mdn { .. } => dc_create_outgoing_rfc724_mid(None, &self.from_addr),
};
let rfc724_mid_headervalue = render_rfc724_mid(&rfc724_mid);
@@ -666,8 +666,6 @@ impl<'a> MimeFactory<'a> {
};
let outer_message = if is_encrypted {
headers.protected.push(from_header);
// Store protected headers in the inner message.
let message = headers
.protected
@@ -1403,7 +1401,7 @@ fn recipients_contain_addr(recipients: &[(String, String)], addr: &str) -> bool
async fn is_file_size_okay(context: &Context, msg: &Message) -> Result<bool> {
match msg.param.get_path(Param::File, context)? {
Some(path) => {
let bytes = get_filebytes(context, &path).await;
let bytes = dc_get_filebytes(context, &path).await;
Ok(bytes <= UPPER_LIMIT_FILE_SIZE)
}
None => Ok(false),
@@ -1454,6 +1452,8 @@ fn maybe_encode_words(words: &str) -> String {
#[cfg(test)]
mod tests {
use mailparse::{addrparse_header, MailHeaderMap};
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
use crate::chat::ChatId;
use crate::chat::{
@@ -1462,8 +1462,8 @@ mod tests {
};
use crate::chatlist::Chatlist;
use crate::contact::Origin;
use crate::dc_receive_imf::dc_receive_imf;
use crate::mimeparser::MimeMessage;
use crate::receive_imf::receive_imf;
use crate::test_utils::{get_chat_msg, TestContext};
use super::*;
@@ -1660,7 +1660,7 @@ mod tests {
async fn test_subject_mdn() {
// 5. Receive an mdn (read receipt) and make sure the mdn's subject is not used
let t = TestContext::new_alice().await;
receive_imf(
dc_receive_imf(
&t,
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
From: alice@example.org\n\
@@ -1748,7 +1748,7 @@ mod tests {
let subject = send_msg_get_subject(&t, group_id, None).await?;
assert_eq!(subject, "Re: groupname");
receive_imf(
dc_receive_imf(
&t,
format!(
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
@@ -1863,7 +1863,7 @@ mod tests {
}
if message_arrives_inbetween {
receive_imf(
dc_receive_imf(
&t,
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
From: Bob <bob@example.com>\n\
@@ -1897,7 +1897,7 @@ mod tests {
.await
.unwrap();
receive_imf(context, imf_raw, false).await.unwrap();
dc_receive_imf(context, imf_raw, false).await.unwrap();
let chats = Chatlist::try_load(context, 0, None, None).await.unwrap();
@@ -2016,7 +2016,7 @@ mod tests {
let file = t.dir.path().join("avatar.png");
let bytes = include_bytes!("../test-data/image/avatar64x64.png");
tokio::fs::write(&file, bytes).await?;
File::create(&file).await?.write_all(bytes).await?;
t.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
.await?;

View File

@@ -13,8 +13,9 @@ 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_cmp, addr_normalize, ContactId};
use crate::contact::{addr_normalize, ContactId};
use crate::context::Context;
use crate::dc_tools::{dc_get_filemeta, dc_truncate, parse_receive_headers};
use crate::dehtml::dehtml;
use crate::e2ee;
use crate::events::EventType;
@@ -28,7 +29,6 @@ use crate::peerstate::Peerstate;
use crate::simplify::{simplify, SimplifiedText};
use crate::stock_str;
use crate::sync::SyncItems;
use crate::tools::{get_filemeta, parse_receive_headers, truncate};
/// A parsed MIME message.
///
@@ -47,9 +47,6 @@ pub struct MimeMessage {
/// Addresses are normalized and lowercased:
pub recipients: Vec<SingleInfo>,
pub from: Vec<SingleInfo>,
/// Whether the From address was repeated in the signed part
/// (and we know that the signer intended to send from this address)
pub from_is_signed: bool,
pub list_post: Option<String>,
pub chat_disposition_notification_to: Option<SingleInfo>,
pub decrypting_failed: bool,
@@ -219,94 +216,67 @@ impl MimeMessage {
// Memory location for a possible decrypted message.
let mut mail_raw = Vec::new();
let mut gossiped_addr = Default::default();
let mut from_is_signed = false;
let mut decryption_info =
e2ee::create_decryption_info(context, &mail, message_time).await?;
// `signatures` is non-empty exactly if the message was encrypted and correctly signed.
let (mail, signatures, warn_empty_signature) =
match e2ee::try_decrypt(context, &mail, &decryption_info).await {
Ok(Some((raw, signatures))) => {
// Encrypted, but maybe unsigned message. Only if
// `signatures` set is non-empty, it is a valid
// autocrypt message.
match e2ee::try_decrypt(context, &mail, message_time).await {
Ok((raw, signatures)) => {
if let Some(raw) = raw {
// Encrypted, but maybe unsigned message. Only if
// `signatures` set is non-empty, it is a valid
// autocrypt message.
mail_raw = raw;
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(context, "decrypted message mime-body:");
println!("{}", String::from_utf8_lossy(&mail_raw));
}
// Handle any gossip headers if the mail was encrypted. See section
// "3.6 Key Gossip" of <https://autocrypt.org/autocrypt-spec-1.1.0.pdf>
// but only if the mail was correctly signed:
if !signatures.is_empty() {
let gossip_headers =
decrypted_mail.headers.get_all_values("Autocrypt-Gossip");
gossiped_addr =
update_gossip_peerstates(context, message_time, &mail, gossip_headers)
.await?;
}
// let known protected headers from the decrypted
// part override the unencrypted top-level
// Signature was checked for original From, so we
// do not allow overriding it.
let mut signed_from = Vec::new();
// We do not want to allow unencrypted subject in encrypted emails because the user might falsely think that the subject is safe.
// See <https://github.com/deltachat/deltachat-core-rust/issues/1790>.
headers.remove("subject");
MimeMessage::merge_headers(
context,
&mut headers,
&mut recipients,
&mut signed_from,
&mut list_post,
&mut chat_disposition_notification_to,
&decrypted_mail.headers,
);
if let Some(signed_from) = signed_from.first() {
if let Some(from) = from.first() {
if addr_cmp(&signed_from.addr, &from.addr) {
from_is_signed = true;
} else {
// There is a From: header in the encrypted &
// signed part, but it doesn't match the outer one.
// This _might_ be because the sender's mail server
// replaced the sending address, e.g. in a mailing list.
// Or it's because someone is doing some replay attack
// - OTOH, I can't come up with an attack scenario
// where this would be useful.
warn!(
context,
"From header in signed part does't match the outer one"
);
}
mail_raw = raw;
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(context, "decrypted message mime-body:");
println!("{}", String::from_utf8_lossy(&mail_raw));
}
}
(Ok(decrypted_mail), signatures, true)
}
Ok(None) => {
// Message was not encrypted.
// If it is not a read receipt, degrade encryption.
if let Some(peerstate) = &mut decryption_info.peerstate {
if message_time > peerstate.last_seen_autocrypt
&& mail.ctype.mimetype != "multipart/report"
{
peerstate.degrade_encryption(message_time);
peerstate.save_to_db(&context.sql, false).await?;
// Handle any gossip headers if the mail was encrypted. See section
// "3.6 Key Gossip" of <https://autocrypt.org/autocrypt-spec-1.1.0.pdf>
// but only if the mail was correctly signed:
if !signatures.is_empty() {
let gossip_headers =
decrypted_mail.headers.get_all_values("Autocrypt-Gossip");
gossiped_addr = update_gossip_peerstates(
context,
message_time,
&mail,
gossip_headers,
)
.await?;
}
// let known protected headers from the decrypted
// part override the unencrypted top-level
// Signature was checked for original From, so we
// do not allow overriding it.
let mut throwaway_from = from.clone();
// We do not want to allow unencrypted subject in encrypted emails because the user might falsely think that the subject is safe.
// See <https://github.com/deltachat/deltachat-core-rust/issues/1790>.
headers.remove("subject");
MimeMessage::merge_headers(
context,
&mut headers,
&mut recipients,
&mut throwaway_from,
&mut list_post,
&mut chat_disposition_notification_to,
&decrypted_mail.headers,
);
(Ok(decrypted_mail), signatures, true)
} else {
// Message was not encrypted
(Ok(mail), signatures, false)
}
(Ok(mail), HashSet::new(), false)
}
Err(err) => {
warn!(context, "decryption failed: {}", err);
(Err(err), HashSet::new(), true)
(Err(err), Default::default(), true)
}
};
@@ -316,7 +286,6 @@ impl MimeMessage {
recipients,
list_post,
from,
from_is_signed,
chat_disposition_notification_to,
decrypting_failed: mail.is_err(),
@@ -380,13 +349,6 @@ impl MimeMessage {
parser.decoded_data = mail_raw;
}
crate::peerstate::maybe_do_aeap_transition(context, &mut decryption_info, &parser).await?;
if let Some(peerstate) = decryption_info.peerstate {
peerstate
.handle_fingerprint_change(context, message_time)
.await?;
}
Ok(parser)
}
@@ -1017,7 +979,7 @@ impl MimeMessage {
> DC_DESIRED_TEXT_LEN + DC_ELLIPSIS.len()
{
self.is_mime_modified = true;
truncate(&*simplified_txt, DC_DESIRED_TEXT_LEN).to_string()
dc_truncate(&*simplified_txt, DC_DESIRED_TEXT_LEN).to_string()
} else {
simplified_txt
};
@@ -1076,7 +1038,7 @@ impl MimeMessage {
// XXX what if somebody sends eg an "location-highlights.kml"
// attachment unrelated to location streaming?
if filename.starts_with("location") || filename.starts_with("message") {
let parsed = location::Kml::parse(decoded_data)
let parsed = location::Kml::parse(context, decoded_data)
.map_err(|err| {
warn!(context, "failed to parse kml part: {}", err);
})
@@ -1128,7 +1090,7 @@ impl MimeMessage {
/* create and register Mime part referencing the new Blob object */
let mut part = Part::default();
if mime_type.type_() == mime::IMAGE {
if let Ok((width, height)) = get_filemeta(decoded_data) {
if let Ok((width, height)) = dc_get_filemeta(decoded_data) {
part.param.set_int(Param::Width, width as i32);
part.param.set_int(Param::Height, height as i32);
}
@@ -1375,7 +1337,7 @@ impl MimeMessage {
/// Some providers like GMX and Yahoo do not send standard NDNs (Non Delivery notifications).
/// If you improve heuristics here you might also have to change prefetch_should_download() in imap/mod.rs.
/// Also you should add a test in receive_imf.rs (there already are lots of test_parse_ndn_* tests).
/// Also you should add a test in dc_receive_imf.rs (there already are lots of test_parse_ndn_* tests).
#[allow(clippy::indexing_slicing)]
async fn heuristically_parse_ndn(&mut self, context: &Context) {
let maybe_ndn = if let Some(from) = self.get_header(HeaderDef::From_) {
@@ -1439,11 +1401,11 @@ impl MimeMessage {
}
if let Some(failure_report) = &self.failure_report {
let error = parts
.iter()
.find(|p| p.typ == Viewtype::Text)
.map(|p| p.msg.clone());
if let Err(e) = message::handle_ndn(context, failure_report, error).await {
let error = parts.iter().find(|p| p.typ == Viewtype::Text).map_or_else(
|| "Non-Delivery-Notification without further details.".to_string(),
|p| p.msg.clone(),
);
if let Err(e) = message::handle_ndn(context, failure_report, &error).await {
warn!(context, "Could not handle ndn: {}", e);
}
}
@@ -1771,8 +1733,8 @@ mod tests {
chatlist::Chatlist,
config::Config,
constants::Blocked,
dc_receive_imf::dc_receive_imf,
message::{Message, MessageState, MessengerMessage},
receive_imf::receive_imf,
test_utils::TestContext,
};
use mailparse::ParsedMail;
@@ -1845,7 +1807,7 @@ mod tests {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mimeparser_crash() {
async fn test_dc_mimeparser_crash() {
let context = TestContext::new().await;
let raw = include_bytes!("../test-data/message/issue_523.txt");
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
@@ -2945,7 +2907,7 @@ On 2020-10-25, Bob wrote:
async fn test_add_subj_to_multimedia_msg() {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
receive_imf(
dc_receive_imf(
&t.ctx,
include_bytes!("../test-data/message/subj_with_multimedia_msg.eml"),
false,
@@ -3096,7 +3058,7 @@ Subject: ...
Some quote.
"###;
receive_imf(&t, raw, false).await?;
dc_receive_imf(&t, raw, false).await?;
// Delta Chat generates In-Reply-To with a starting tab when Message-ID is too long.
let raw = br###"In-Reply-To:
@@ -3113,7 +3075,7 @@ Subject: ...
Some reply
"###;
receive_imf(&t, raw, false).await?;
dc_receive_imf(&t, raw, false).await?;
let msg = t.get_last_msg().await;
assert_eq!(msg.get_text().unwrap(), "Some reply");
@@ -3141,13 +3103,13 @@ Message.
"###;
// Bob receives message.
receive_imf(&bob, raw, false).await?;
dc_receive_imf(&bob, raw, 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.
receive_imf(&alice, raw, false).await?;
dc_receive_imf(&alice, raw, 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());
@@ -3160,7 +3122,7 @@ Message.
let alice = TestContext::new_alice().await;
// Alice receives BCC-self copy of a message sent to Bob.
receive_imf(
dc_receive_imf(
&alice,
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
From: alice@example.org\n\
@@ -3181,7 +3143,7 @@ Message.
// Due to a bug in the old version running on the other device, Alice receives a read
// receipt from self.
receive_imf(
dc_receive_imf(
&alice,
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
From: alice@example.org\n\
@@ -3232,7 +3194,7 @@ Message.
let original =
include_bytes!("../test-data/message/ms_exchange_report_original_message.eml");
receive_imf(&t, original, false).await?;
dc_receive_imf(&t, original, false).await?;
let original_msg_id = t.get_last_msg().await.id;
// 1. Test mimeparser directly
@@ -3247,7 +3209,7 @@ Message.
assert!(mimeparser.mdn_reports[0].additional_message_ids.is_empty());
// 2. Test that marking the original msg as read works
receive_imf(&t, mdn, false).await?;
dc_receive_imf(&t, mdn, false).await?;
assert_eq!(
original_msg_id.get_state(&t).await?,

View File

@@ -8,9 +8,9 @@ use serde::Deserialize;
use crate::config::Config;
use crate::context::Context;
use crate::dc_tools::time;
use crate::provider;
use crate::provider::Oauth2Authorizer;
use crate::tools::time;
const OAUTH2_GMAIL: Oauth2 = Oauth2 {
// see <https://developers.google.com/identity/protocols/OAuth2InstalledApp>
@@ -53,7 +53,7 @@ struct Response {
scope: Option<String>,
}
pub async fn get_oauth2_url(
pub async fn dc_get_oauth2_url(
context: &Context,
addr: &str,
redirect_uri: &str,
@@ -73,7 +73,7 @@ pub async fn get_oauth2_url(
}
}
pub async fn get_oauth2_access_token(
pub async fn dc_get_oauth2_access_token(
context: &Context,
addr: &str,
code: &str,
@@ -224,7 +224,11 @@ pub async fn get_oauth2_access_token(
}
}
pub async fn get_oauth2_addr(context: &Context, addr: &str, code: &str) -> Result<Option<String>> {
pub async fn dc_get_oauth2_addr(
context: &Context,
addr: &str,
code: &str,
) -> Result<Option<String>> {
let socks5_enabled = context.get_config_bool(Config::Socks5Enabled).await?;
let oauth2 = match Oauth2::from_address(context, addr, socks5_enabled).await {
Some(o) => o,
@@ -234,11 +238,13 @@ pub async fn get_oauth2_addr(context: &Context, addr: &str, code: &str) -> Resul
return Ok(None);
}
if let Some(access_token) = get_oauth2_access_token(context, addr, code, false).await? {
if let Some(access_token) = dc_get_oauth2_access_token(context, addr, code, false).await? {
let addr_out = oauth2.get_addr(context, &access_token).await;
if addr_out.is_none() {
// regenerate
if let Some(access_token) = get_oauth2_access_token(context, addr, code, true).await? {
if let Some(access_token) =
dc_get_oauth2_access_token(context, addr, code, true).await?
{
Ok(oauth2.get_addr(context, &access_token).await)
} else {
Ok(None)
@@ -398,31 +404,33 @@ mod tests {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_oauth2_addr() {
async fn test_dc_get_oauth2_addr() {
let ctx = TestContext::new().await;
let addr = "dignifiedquire@gmail.com";
let code = "fail";
let res = get_oauth2_addr(&ctx.ctx, addr, code).await.unwrap();
let res = dc_get_oauth2_addr(&ctx.ctx, addr, code).await.unwrap();
// this should fail as it is an invalid password
assert_eq!(res, None);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_oauth2_url() {
async fn test_dc_get_oauth2_url() {
let ctx = TestContext::new().await;
let addr = "dignifiedquire@gmail.com";
let redirect_uri = "chat.delta:/com.b44t.messenger";
let res = get_oauth2_url(&ctx.ctx, addr, redirect_uri).await.unwrap();
let res = dc_get_oauth2_url(&ctx.ctx, addr, redirect_uri)
.await
.unwrap();
assert_eq!(res, Some("https://accounts.google.com/o/oauth2/auth?client_id=959970109878%2D4mvtgf6feshskf7695nfln6002mom908%2Eapps%2Egoogleusercontent%2Ecom&redirect_uri=chat%2Edelta%3A%2Fcom%2Eb44t%2Emessenger&response_type=code&scope=https%3A%2F%2Fmail.google.com%2F%20email&access_type=offline".into()));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_oauth2_token() {
async fn test_dc_get_oauth2_token() {
let ctx = TestContext::new().await;
let addr = "dignifiedquire@gmail.com";
let code = "fail";
let res = get_oauth2_access_token(&ctx.ctx, addr, code, false)
let res = dc_get_oauth2_access_token(&ctx.ctx, addr, code, false)
.await
.unwrap();
// this should fail as it is an invalid password

View File

@@ -4,10 +4,8 @@ use std::collections::HashSet;
use std::fmt;
use crate::aheader::{Aheader, EncryptPreference};
use crate::chat::{self, is_contact_in_chat, Chat};
use crate::chat::{self};
use crate::chatlist::Chatlist;
use crate::constants::Chattype;
use crate::contact::{addr_cmp, Contact, Origin};
use crate::context::Context;
use crate::events::EventType;
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
@@ -15,7 +13,7 @@ use crate::message::Message;
use crate::mimeparser::SystemMessage;
use crate::sql::Sql;
use crate::stock_str;
use anyhow::{Context as _, Result};
use anyhow::{bail, Result};
use num_traits::FromPrimitive;
#[derive(Debug)]
@@ -146,41 +144,26 @@ impl Peerstate {
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
verified_key, verified_key_fingerprint \
FROM acpeerstates \
WHERE addr=? COLLATE NOCASE LIMIT 1;";
WHERE addr=? COLLATE NOCASE;";
Self::from_stmt(context, query, paramsv![addr]).await
}
pub async fn from_fingerprint(
context: &Context,
_sql: &Sql,
fingerprint: &Fingerprint,
) -> Result<Option<Peerstate>> {
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
verified_key, verified_key_fingerprint \
FROM acpeerstates \
WHERE public_key_fingerprint=? \
OR gossip_key_fingerprint=? \
ORDER BY public_key_fingerprint=? DESC LIMIT 1;";
WHERE public_key_fingerprint=? COLLATE NOCASE \
OR gossip_key_fingerprint=? COLLATE NOCASE \
ORDER BY public_key_fingerprint=? DESC;";
let fp = fingerprint.hex();
Self::from_stmt(context, query, paramsv![fp, fp, fp]).await
}
pub async fn from_nongossiped_fingerprint_or_addr(
context: &Context,
fingerprint: &Fingerprint,
addr: &str,
) -> Result<Option<Peerstate>> {
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
verified_key, verified_key_fingerprint \
FROM acpeerstates \
WHERE public_key_fingerprint=? \
OR addr=? COLLATE NOCASE \
ORDER BY public_key_fingerprint=? DESC, last_seen DESC LIMIT 1;";
let fp = fingerprint.hex();
Self::from_stmt(context, query, paramsv![fp, addr, fp]).await
}
async fn from_stmt(
context: &Context,
query: &str,
@@ -237,10 +220,6 @@ impl Peerstate {
Ok(peerstate)
}
/// Re-calculate `self.public_key_fingerprint` and `self.gossip_key_fingerprint`.
/// If one of them was changed, `self.fingerprint_changed` is set to `true`.
///
/// Call this after you changed `self.public_key` or `self.gossip_key`.
pub fn recalc_fingerprint(&mut self) {
if let Some(ref public_key) = self.public_key {
let old_public_fingerprint = self.public_key_fingerprint.take();
@@ -282,8 +261,61 @@ impl Peerstate {
self.to_save = Some(ToSave::All);
}
/// Adds a warning to the chat corresponding to peerstate if fingerprint has changed.
pub(crate) async fn handle_fingerprint_change(
&self,
context: &Context,
timestamp: i64,
) -> Result<()> {
if context.is_self_addr(&self.addr).await? {
// Do not try to search all the chats with self.
return Ok(());
}
if self.fingerprint_changed {
if let Some(contact_id) = context
.sql
.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 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,
None,
)
.await?;
context.emit_event(EventType::ChatModified(*chat_id));
}
} else {
bail!("contact with peerstate.addr {:?} not found", &self.addr);
}
}
Ok(())
}
pub fn apply_header(&mut self, header: &Aheader, message_time: i64) {
if !addr_cmp(&self.addr, &header.addr) {
if self.addr.to_lowercase() != header.addr.to_lowercase() {
return;
}
@@ -489,175 +521,6 @@ impl Peerstate {
false
}
}
/// Add an info message to all the chats with this contact, informing about
/// a [`PeerstateChange`].
///
/// Also, in the case of an address change (AEAP), replace the old address
/// with the new address in all chats.
async fn handle_setup_change(
&self,
context: &Context,
timestamp: i64,
change: PeerstateChange,
) -> Result<()> {
if context.is_self_addr(&self.addr).await? {
// Do not try to search all the chats with self.
return Ok(());
}
let contact_id = context
.sql
.query_get_value(
"SELECT id FROM contacts WHERE addr=? COLLATE NOCASE;",
paramsv![self.addr],
)
.await?
.with_context(|| format!("contact with peerstate.addr {:?} not found", &self.addr))?;
let chats = Chatlist::try_load(context, 0, None, Some(contact_id)).await?;
for (chat_id, msg_id) in chats.iter() {
let msg = match &change {
PeerstateChange::FingerprintChange => {
stock_str::contact_setup_changed(context, self.addr.clone()).await
}
PeerstateChange::Aeap(new_addr) => {
let old_contact = Contact::load_from_db(context, contact_id).await?;
stock_str::aeap_addr_changed(
context,
old_contact.get_display_name(),
&self.addr,
new_addr,
)
.await
}
};
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,
None,
)
.await?;
if let PeerstateChange::Aeap(new_addr) = &change {
let chat = Chat::load_from_db(context, *chat_id).await?;
if chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast {
chat::remove_from_chat_contacts_table(context, *chat_id, contact_id).await?;
let (new_contact_id, _) =
Contact::add_or_lookup(context, "", new_addr, Origin::IncomingUnknownFrom)
.await?;
if !is_contact_in_chat(context, *chat_id, new_contact_id).await? {
chat::add_to_chat_contacts_table(context, *chat_id, new_contact_id).await?;
}
context.emit_event(EventType::ChatModified(*chat_id));
}
}
}
Ok(())
}
/// Adds a warning to all the chats corresponding to peerstate if fingerprint has changed.
pub(crate) async fn handle_fingerprint_change(
&self,
context: &Context,
timestamp: i64,
) -> Result<()> {
if self.fingerprint_changed {
self.handle_setup_change(context, timestamp, PeerstateChange::FingerprintChange)
.await?;
}
Ok(())
}
}
/// Do an AEAP transition, if necessary.
/// AEAP stands for "Automatic Email Address Porting."
///
/// In `drafts/aeap_mvp.md` there is a "big picture" overview over AEAP.
pub async fn maybe_do_aeap_transition(
context: &Context,
info: &mut crate::e2ee::DecryptionInfo,
mime_parser: &crate::mimeparser::MimeMessage,
) -> Result<()> {
if let Some(peerstate) = &mut info.peerstate {
if let Some(from) = mime_parser.from.first() {
// If the from addr is different from the peerstate address we know,
// we may want to do an AEAP transition.
if !addr_cmp(&peerstate.addr, &from.addr)
// Check if it's a chat message; we do this to avoid
// some accidental transitions if someone writes from multiple
// addresses with an MUA.
&& mime_parser.has_chat_version()
// Check if the message is signed correctly.
// If it's not signed correctly, the whole autocrypt header will be mostly
// ignored anyway and the message shown as not encrypted, so we don't
// have to handle this case.
&& !mime_parser.signatures.is_empty()
// Check if the From: address was also in the signed part of the email.
// Without this check, an attacker could replay a message from Alice
// to Bob. Then Bob's device would do an AEAP transition from Alice's
// to the attacker's address, allowing for easier phishing.
&& mime_parser.from_is_signed
&& info.message_time > peerstate.last_seen
{
// Add an info messages to all chats with this contact
//
peerstate
.handle_setup_change(
context,
info.message_time,
PeerstateChange::Aeap(info.from.clone()),
)
.await?;
peerstate.addr = info.from.clone();
let header = info.autocrypt_header.as_ref().context(
"Internal error: Tried to do an AEAP transition without an autocrypt header??",
)?;
peerstate.apply_header(header, info.message_time);
peerstate.to_save = Some(ToSave::All);
// We don't know whether a peerstate with this address already existed, or a
// new one should be created, so just try both create=false and create=true,
// and if this fails, create=true, one will succeed (this is a very cold path,
// so performance doesn't really matter).
peerstate.save_to_db(&context.sql, true).await?;
peerstate.save_to_db(&context.sql, false).await?;
}
}
}
Ok(())
}
enum PeerstateChange {
/// The contact's public key fingerprint changed, likely because
/// the contact uses a new device and didn't transfer their key.
FingerprintChange,
/// The contact changed their address to the given new address
/// (Automatic Email Address Porting).
Aeap(String),
}
/// Removes duplicate peerstates from `acpeerstates` database table.
@@ -725,10 +588,11 @@ mod tests {
// clear to_save, as that is not persissted
peerstate.to_save = None;
assert_eq!(peerstate, peerstate_new);
let peerstate_new2 = Peerstate::from_fingerprint(&ctx.ctx, &pub_key.fingerprint())
.await
.expect("failed to load peerstate from db")
.expect("no peerstate found in the database");
let peerstate_new2 =
Peerstate::from_fingerprint(&ctx.ctx, &ctx.ctx.sql, &pub_key.fingerprint())
.await
.expect("failed to load peerstate from db")
.expect("no peerstate found in the database");
assert_eq!(peerstate, peerstate_new2);
}

View File

@@ -18,9 +18,9 @@ use rand::{thread_rng, CryptoRng, Rng};
use tokio::runtime::Handle;
use crate::constants::KeyGenType;
use crate::dc_tools::EmailAddress;
use crate::key::{DcKey, Fingerprint};
use crate::keyring::Keyring;
use crate::tools::EmailAddress;
pub const HEADER_AUTOCRYPT: &str = "autocrypt-prefer-encrypt";
pub const HEADER_SETUPCODE: &str = "passphrase-begin";
@@ -386,7 +386,6 @@ mod tests {
use super::*;
use crate::test_utils::{alice_keypair, bob_keypair};
use once_cell::sync::Lazy;
use tokio::sync::OnceCell;
#[test]
fn test_split_armored_data_1() {
@@ -457,50 +456,37 @@ mod tests {
/// Initialised [TestKeys] for tests.
static KEYS: Lazy<TestKeys> = Lazy::new(TestKeys::new);
static CTEXT_SIGNED: OnceCell<String> = OnceCell::const_new();
static CTEXT_UNSIGNED: OnceCell<String> = OnceCell::const_new();
/// A cyphertext encrypted to Alice & Bob, signed by Alice.
async fn ctext_signed() -> &'static String {
CTEXT_SIGNED
.get_or_init(|| async {
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_public.clone());
keyring.add(KEYS.bob_public.clone());
pk_encrypt(CLEARTEXT, keyring, Some(KEYS.alice_secret.clone()))
.await
.unwrap()
})
.await
}
static CTEXT_SIGNED: Lazy<String> = Lazy::new(|| {
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_public.clone());
keyring.add(KEYS.bob_public.clone());
futures_lite::future::block_on(pk_encrypt(
CLEARTEXT,
keyring,
Some(KEYS.alice_secret.clone()),
))
.unwrap()
});
/// A cyphertext encrypted to Alice & Bob, not signed.
async fn ctext_unsigned() -> &'static String {
CTEXT_UNSIGNED
.get_or_init(|| async {
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_public.clone());
keyring.add(KEYS.bob_public.clone());
pk_encrypt(CLEARTEXT, keyring, None).await.unwrap()
})
.await
}
static CTEXT_UNSIGNED: Lazy<String> = Lazy::new(|| {
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_public.clone());
keyring.add(KEYS.bob_public.clone());
futures_lite::future::block_on(pk_encrypt(CLEARTEXT, keyring, None)).unwrap()
});
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_encrypt_signed() {
assert!(!ctext_signed().await.is_empty());
assert!(ctext_signed()
.await
.starts_with("-----BEGIN PGP MESSAGE-----"));
assert!(!CTEXT_SIGNED.is_empty());
assert!(CTEXT_SIGNED.starts_with("-----BEGIN PGP MESSAGE-----"));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_encrypt_unsigned() {
assert!(!ctext_unsigned().await.is_empty());
assert!(ctext_unsigned()
.await
.starts_with("-----BEGIN PGP MESSAGE-----"));
assert!(!CTEXT_UNSIGNED.is_empty());
assert!(CTEXT_UNSIGNED.starts_with("-----BEGIN PGP MESSAGE-----"));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -511,7 +497,7 @@ mod tests {
let mut sig_check_keyring: Keyring<SignedPublicKey> = Keyring::new();
sig_check_keyring.add(KEYS.alice_public.clone());
let (plain, valid_signatures) = pk_decrypt(
ctext_signed().await.as_bytes().to_vec(),
CTEXT_SIGNED.as_bytes().to_vec(),
decrypt_keyring,
&sig_check_keyring,
)
@@ -526,7 +512,7 @@ mod tests {
let mut sig_check_keyring = Keyring::new();
sig_check_keyring.add(KEYS.alice_public.clone());
let (plain, valid_signatures) = pk_decrypt(
ctext_signed().await.as_bytes().to_vec(),
CTEXT_SIGNED.as_bytes().to_vec(),
decrypt_keyring,
&sig_check_keyring,
)
@@ -541,13 +527,10 @@ mod tests {
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_secret.clone());
let empty_keyring = Keyring::new();
let (plain, valid_signatures) = pk_decrypt(
ctext_signed().await.as_bytes().to_vec(),
keyring,
&empty_keyring,
)
.await
.unwrap();
let (plain, valid_signatures) =
pk_decrypt(CTEXT_SIGNED.as_bytes().to_vec(), keyring, &empty_keyring)
.await
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
@@ -560,7 +543,7 @@ mod tests {
let mut sig_check_keyring = Keyring::new();
sig_check_keyring.add(KEYS.bob_public.clone());
let (plain, valid_signatures) = pk_decrypt(
ctext_signed().await.as_bytes().to_vec(),
CTEXT_SIGNED.as_bytes().to_vec(),
decrypt_keyring,
&sig_check_keyring,
)
@@ -576,7 +559,7 @@ mod tests {
decrypt_keyring.add(KEYS.bob_secret.clone());
let sig_check_keyring = Keyring::new();
let (plain, valid_signatures) = pk_decrypt(
ctext_unsigned().await.as_bytes().to_vec(),
CTEXT_UNSIGNED.as_bytes().to_vec(),
decrypt_keyring,
&sig_check_keyring,
)

View File

@@ -192,8 +192,8 @@ mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::dc_tools::time;
use crate::test_utils::TestContext;
use crate::tools::time;
use chrono::NaiveDate;
#[test]

View File

@@ -11,11 +11,11 @@ use crate::config::Config;
use crate::constants::Blocked;
use crate::contact::{addr_normalize, may_be_valid_addr, Contact, ContactId, Origin};
use crate::context::Context;
use crate::dc_tools::time;
use crate::key::Fingerprint;
use crate::message::Message;
use crate::peerstate::Peerstate;
use crate::token;
use crate::tools::time;
const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase
const DCACCOUNT_SCHEME: &str = "DCACCOUNT:";
@@ -201,7 +201,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
};
// retrieve known state for this fingerprint
let peerstate = Peerstate::from_fingerprint(context, &fingerprint)
let peerstate = Peerstate::from_fingerprint(context, &context.sql, &fingerprint)
.await
.context("Can't load peerstate")?;
@@ -561,7 +561,7 @@ mod tests {
use crate::chat::{create_group_chat, ProtectionStatus};
use crate::key::DcKey;
use crate::peerstate::ToSave;
use crate::securejoin::get_securejoin_qr;
use crate::securejoin::dc_get_securejoin_qr;
use crate::test_utils::{alice_keypair, TestContext};
use anyhow::Result;
@@ -889,7 +889,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_withdraw_verifycontact() -> Result<()> {
let alice = TestContext::new_alice().await;
let qr = get_securejoin_qr(&alice, None).await?;
let qr = dc_get_securejoin_qr(&alice, None).await?;
// scanning own verify-contact code offers withdrawing
assert!(matches!(
@@ -924,7 +924,7 @@ mod tests {
async fn test_withdraw_verifygroup() -> Result<()> {
let alice = TestContext::new_alice().await;
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
let qr = get_securejoin_qr(&alice, Some(chat_id)).await?;
let qr = dc_get_securejoin_qr(&alice, Some(chat_id)).await?;
// scanning own verify-group code offers withdrawing
if let Qr::WithdrawVerifyGroup { grpname, .. } = check_qr(&alice, &qr).await? {

View File

@@ -32,7 +32,7 @@ async fn generate_join_group_qr_code(context: &Context, chat_id: ChatId) -> Resu
inner_generate_secure_join_qr_code(
&stock_str::secure_join_group_qr_description(context, &chat).await,
&securejoin::get_securejoin_qr(context, Some(chat_id)).await?,
&securejoin::dc_get_securejoin_qr(context, Some(chat_id)).await?,
&color_int_to_hex_string(chat.get_color(context).await?),
avatar,
chat.get_name().chars().next().unwrap_or('#'),
@@ -57,7 +57,7 @@ async fn generate_verification_qr(context: &Context) -> Result<String> {
inner_generate_secure_join_qr_code(
&stock_str::setup_contact_qr_description(context, &displayname, contact.get_addr()).await,
&securejoin::get_securejoin_qr(context, None).await?,
&securejoin::dc_get_securejoin_qr(context, None).await?,
&color_int_to_hex_string(contact.get_color()),
avatar,
displayname.chars().next().unwrap_or('#'),

View File

@@ -7,12 +7,12 @@ use std::collections::BTreeMap;
use crate::chat::add_device_msg_with_importance;
use crate::config::Config;
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::param::Params;
use crate::tools::time;
use crate::{job, stock_str, EventType};
/// warn about a nearly full mailbox after this usage percentage is reached.

View File

@@ -6,6 +6,8 @@ use tokio::task;
use crate::config::Config;
use crate::context::Context;
use crate::dc_tools::time;
use crate::dc_tools::{duration_to_str, maybe_add_time_based_warnings};
use crate::ephemeral::{self, delete_expired_imap_messages};
use crate::imap::Imap;
use crate::job;
@@ -13,8 +15,6 @@ use crate::location;
use crate::log::LogExt;
use crate::smtp::{send_smtp_messages, Smtp};
use crate::sql;
use crate::tools::time;
use crate::tools::{duration_to_str, maybe_add_time_based_warnings};
use self::connectivity::ConnectivityStore;

View File

@@ -3,13 +3,13 @@ use std::{ops::Deref, sync::Arc};
use tokio::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,
};
use crate::tools::time;
use crate::{config::Config, scheduler::Scheduler, stock_str, tools};
use crate::{config::Config, dc_tools, scheduler::Scheduler, stock_str};
use crate::{context::Context, log::LogExt};
use anyhow::{anyhow, Result};
use humansize::{file_size_opts, FileSize};
@@ -449,7 +449,7 @@ impl Context {
// [======67%===== ]
// =============================================================================================
let domain = tools::EmailAddress::new(&self.get_primary_self_addr().await?)?.domain;
let domain = dc_tools::EmailAddress::new(&self.get_primary_self_addr().await?)?.domain;
ret += &format!(
"<h3>{}</h3><ul>",
stock_str::storage_on_domain(self, domain).await

View File

@@ -11,6 +11,7 @@ use crate::config::Config;
use crate::constants::Blocked;
use crate::contact::{Contact, ContactId, 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;
@@ -22,7 +23,6 @@ use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus, ToS
use crate::qr::check_qr;
use crate::stock_str;
use crate::token;
use crate::tools::time;
mod bob;
mod bobstate;
@@ -51,7 +51,7 @@ macro_rules! inviter_progress {
///
/// With `group` set to `None` this generates a setup-contact QR code, with `group` set to a
/// [`ChatId`] generates a join-group QR code for the given chat.
pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Result<String> {
pub async fn dc_get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Result<String> {
/*=======================================================
==== Alice - the inviter side ====
==== Step 1 in "Setup verified contact" protocol ====
@@ -143,7 +143,7 @@ async fn get_self_fingerprint(context: &Context) -> Option<Fingerprint> {
/// for more details.
///
/// The function returns immediately and the handshake will run in background.
pub async fn join_securejoin(context: &Context, qr: &str) -> Result<ChatId> {
pub async fn dc_join_securejoin(context: &Context, qr: &str) -> Result<ChatId> {
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.
@@ -238,10 +238,10 @@ async fn fingerprint_equals_sender(
/// What to do with a Secure-Join handshake message after it was handled.
///
/// This status is returned to [`receive_imf`] which will use it to decide what to do
/// This status is returned to [`dc_receive_imf`] which will use it to decide what to do
/// next with this incoming setup-contact/secure-join handshake message.
///
/// [`receive_imf`]: crate::receive_imf::receive_imf
/// [`dc_receive_imf`]: crate::dc_receive_imf::dc_receive_imf
pub(crate) enum HandshakeMessage {
/// The message has been fully handled and should be removed/delete.
///
@@ -633,7 +633,9 @@ async fn could_not_establish_secure_connection(
}
async fn mark_peer_as_verified(context: &Context, fingerprint: &Fingerprint) -> Result<(), Error> {
if let Some(ref mut peerstate) = Peerstate::from_fingerprint(context, fingerprint).await? {
if let Some(ref mut peerstate) =
Peerstate::from_fingerprint(context, &context.sql, fingerprint).await?
{
if peerstate.set_verified(
PeerstateKeyType::PublicKey,
fingerprint,
@@ -690,8 +692,8 @@ mod tests {
use crate::chat::ProtectionStatus;
use crate::chatlist::Chatlist;
use crate::constants::{Chattype, DC_GCM_ADDDAYMARKER};
use crate::dc_receive_imf::dc_receive_imf;
use crate::peerstate::Peerstate;
use crate::receive_imf::receive_imf;
use crate::test_utils::{TestContext, TestContextManager};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -712,10 +714,10 @@ mod tests {
);
// Step 1: Generate QR-code, ChatId(0) indicates setup-contact
let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap();
let qr = dc_get_securejoin_qr(&alice.ctx, None).await.unwrap();
// Step 2: Bob scans QR-code, sends vc-request
join_securejoin(&bob.ctx, &qr).await.unwrap();
dc_join_securejoin(&bob.ctx, &qr).await.unwrap();
assert_eq!(
Chatlist::try_load(&bob, 0, None, None).await.unwrap().len(),
1
@@ -904,7 +906,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_setup_contact_bad_qr() {
let bob = TestContext::new_bob().await;
let ret = join_securejoin(&bob.ctx, "not a qr code").await;
let ret = dc_join_securejoin(&bob.ctx, "not a qr code").await;
assert!(ret.is_err());
}
@@ -934,10 +936,10 @@ mod tests {
peerstate.save_to_db(&bob.ctx.sql, true).await?;
// Step 1: Generate QR-code, ChatId(0) indicates setup-contact
let qr = get_securejoin_qr(&alice.ctx, None).await?;
let qr = dc_get_securejoin_qr(&alice.ctx, None).await?;
// Step 2+4: Bob scans QR-code, sends vc-request-with-auth, skipping vc-request
join_securejoin(&bob.ctx, &qr).await.unwrap();
dc_join_securejoin(&bob.ctx, &qr).await.unwrap();
// Check Bob emitted the JoinerProgress event.
let event = bob
@@ -1041,7 +1043,7 @@ mod tests {
// 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";
let claire_id = join_securejoin(&bob, qr_stale).await?;
let claire_id = dc_join_securejoin(&bob, qr_stale).await?;
let chat = Chat::load_from_db(&bob, claire_id).await?;
assert!(!claire_id.is_special());
assert_eq!(chat.typ, Chattype::Single);
@@ -1049,8 +1051,8 @@ mod tests {
// subsequent scans shall abort existing ones or run concurrently -
// but they must not fail as otherwise the whole qr scanning becomes unusable until restart.
let qr = get_securejoin_qr(&alice, None).await?;
let alice_id = join_securejoin(&bob, &qr).await?;
let qr = dc_get_securejoin_qr(&alice, None).await?;
let alice_id = dc_join_securejoin(&bob, &qr).await?;
let chat = Chat::load_from_db(&bob, alice_id).await?;
assert!(!alice_id.is_special());
assert_eq!(chat.typ, Chattype::Single);
@@ -1078,12 +1080,12 @@ mod tests {
chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat").await?;
// Step 1: Generate QR-code, secure-join implied by chatid
let qr = get_securejoin_qr(&alice.ctx, Some(alice_chatid))
let qr = dc_get_securejoin_qr(&alice.ctx, Some(alice_chatid))
.await
.unwrap();
// Step 2: Bob scans QR-code, sends vg-request
let bob_chatid = join_securejoin(&bob.ctx, &qr).await?;
let bob_chatid = 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;
@@ -1301,11 +1303,11 @@ Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
First thread."#;
receive_imf(&alice, mime, false).await?;
dc_receive_imf(&alice, mime, false).await?;
let msg = alice.get_last_msg().await;
let chat_id = msg.chat_id;
assert!(get_securejoin_qr(&alice, Some(chat_id)).await.is_err());
assert!(dc_get_securejoin_qr(&alice, Some(chat_id)).await.is_err());
Ok(())
}
}

View File

@@ -9,9 +9,9 @@ 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::tools::time;
use crate::{chat, stock_str};
use super::bobstate::{BobHandshakeStage, BobState};
@@ -33,7 +33,7 @@ use super::HandshakeMessage;
pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Result<ChatId> {
// 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
// receive_imf.
// dc_receive_imf.
let hidden = match invite {
QrInvite::Contact { .. } => Blocked::Not,
QrInvite::Group { .. } => Blocked::Yes,

View File

@@ -14,12 +14,12 @@ use crate::config::Config;
use crate::contact::{Contact, ContactId};
use crate::events::EventType;
use crate::login_param::{
build_tls, CertificateChecks, LoginParam, ServerLoginParam, Socks5Config,
dc_build_tls, CertificateChecks, LoginParam, ServerLoginParam, Socks5Config,
};
use crate::message::Message;
use crate::message::{self, MsgId};
use crate::mimefactory::MimeFactory;
use crate::oauth2::get_oauth2_access_token;
use crate::oauth2::dc_get_oauth2_access_token;
use crate::provider::Socket;
use crate::sql;
use crate::{context::Context, scheduler::connectivity::ConnectivityStore};
@@ -140,13 +140,13 @@ impl Smtp {
CertificateChecks::AcceptInvalidCertificates
| CertificateChecks::AcceptInvalidCertificates2 => false,
};
let tls_config = build_tls(strict_tls);
let tls_config = dc_build_tls(strict_tls);
let tls_parameters = ClientTlsParameters::new(domain.to_string(), tls_config);
let (creds, mechanism) = if lp.oauth2 {
// oauth2
let send_pw = &lp.password;
let access_token = get_oauth2_access_token(context, addr, send_pw, false).await?;
let access_token = dc_get_oauth2_access_token(context, addr, send_pw, false).await?;
if access_token.is_none() {
bail!("SMTP OAuth 2 error {}", addr);
}
@@ -434,7 +434,7 @@ pub(crate) async fn send_msg_to_smtp(
.collect::<Vec<_>>();
// If there is a msg-id and it does not exist in the db, cancel sending. this happens if
// delete_msgs() was called before the generated mime was sent out.
// dc_delete_msgs() was called before the generated mime was sent out.
if !message::exists(context, msg_id)
.await
.with_context(|| format!("failed to check message {} existence", msg_id))?

Some files were not shown because too many files have changed in this diff Show More