Compare commits

..

14 Commits

Author SHA1 Message Date
holger krekel
6e16ea5f8b intermediate commit 2019-08-23 13:30:09 +02:00
holger krekel
2fd4b09cbc rustify several things around the precheck function 2019-08-22 21:49:47 +02:00
holger krekel
157d68246f Merge branch 'master' into cleanup/jobsend 2019-08-21 09:34:46 +02:00
holger krekel
6b044dd85a make python tests run the mvbox thread 2019-08-21 07:39:41 +02:00
holger krekel
9227c4b55c - simplify inbox-connection-guards and returns
- make dc_msg_exists safe
2019-08-20 11:15:28 +02:00
holger krekel
287ccb15ba address @dignifiedquire Default suggestion 2019-08-20 10:40:27 +02:00
holger krekel
f47e637519 cargo fmt 2019-08-20 09:31:05 +02:00
holger krekel
9633f251e0 deindent/cleanup set_mdnsent and delete_msg return code handling 2019-08-20 09:26:57 +02:00
holger krekel
e319f51342 Merge branch 'master' into cleanup/jobsend 2019-08-20 09:08:21 +02:00
holger krekel
55b62b0b7d cargo fmt 2019-08-20 09:03:36 +02:00
holger krekel
37d20bfb5c select_folder -> bool 2019-08-20 08:59:33 +02:00
holger krekel
6441ceeedc streamline imap mv() wrt to return codes, follow C more closely and add comments about missing dest_uid setting from imap 2019-08-20 08:36:27 +02:00
holger krekel
7641bb4d9a implement ImapResult enum like it was in C, and simplify logic a bit (with @r10s on the side) 2019-08-19 23:21:54 +02:00
holger krekel
67594b52f9 rustify job send 2019-08-19 22:13:49 +02:00
95 changed files with 11818 additions and 17006 deletions

View File

@@ -1,4 +1,5 @@
version: 2.1
executors:
default:
docker:
@@ -12,7 +13,7 @@ restore-workspace: &restore-workspace
restore-cache: &restore-cache
restore_cache:
keys:
- cargo-v2-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
- cargo-v1-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
- repo-source-{{ .Branch }}-{{ .Revision }}
commands:
@@ -23,9 +24,20 @@ commands:
steps:
- *restore-workspace
- *restore-cache
- setup_remote_docker:
docker_layer_caching: true
# TODO: move into image
- run:
name: Install Docker client
command: |
set -x
VER="18.09.2"
curl -L -o /tmp/docker-$VER.tgz https://download.docker.com/linux/static/stable/x86_64/docker-$VER.tgz
tar -xz -C /tmp -f /tmp/docker-$VER.tgz
mv /tmp/docker/* /usr/bin
- run:
name: Test (<< parameters.target >>)
command: TARGET=<< parameters.target >> ci_scripts/run-rust-test.sh
command: TARGET=<< parameters.target >> ci/run.sh
no_output_timeout: 15m
jobs:
@@ -41,7 +53,7 @@ jobs:
command: cargo generate-lockfile
- restore_cache:
keys:
- cargo-v2-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
- cargo-v1-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
- run: rustup install $(cat rust-toolchain)
- run: rustup default $(cat rust-toolchain)
- run: rustup component add --toolchain $(cat rust-toolchain) rustfmt
@@ -50,14 +62,13 @@ jobs:
- run: cargo fetch
- run: rustc +stable --version
- run: rustc +$(cat rust-toolchain) --version
# make sure this git repo doesn't grow too big
- run: git gc
- run: rm -rf .git
- persist_to_workspace:
root: /mnt
paths:
- crate
- save_cache:
key: cargo-v2-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
key: cargo-v1-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
paths:
- "~/.cargo"
- "~/.rustup"
@@ -92,7 +103,7 @@ jobs:
- run: cargo fetch
- run:
name: Test
command: TARGET=x86_64-apple-darwin ci_scripts/run-rust-test.sh
command: TARGET=x86_64-apple-darwin ci/run.sh
test_x86_64-unknown-linux-gnu:
executor: default
@@ -114,18 +125,18 @@ jobs:
build_test_docs_wheel:
docker:
- image: deltachat/coredeps
environment:
TESTS: 1
DOCS: 1
working_directory: /mnt/crate
machine: True
steps:
- *restore-workspace
- *restore-cache
- checkout
# - run: docker pull deltachat/doxygen
- run: docker pull deltachat/coredeps
- run:
name: build docs, run tests and build wheels
command: ci_scripts/run-python.sh
command: ci_scripts/ci_run.sh
environment:
TESTS: 1
DOCS: 1
- run:
name: copying docs and wheels to workspace
command: |
@@ -133,6 +144,7 @@ jobs:
# cp -av docs workspace/c-docs
cp -av python/.docker-tox/wheelhouse workspace/
cp -av python/doc/_build/ workspace/py-docs
- persist_to_workspace:
root: workspace
paths:
@@ -141,12 +153,11 @@ jobs:
- wheelhouse
upload_docs_wheels:
machine: true
machine: True
steps:
- checkout
- attach_workspace:
at: workspace
- run: pyenv global 3.5.2
- run: ls -laR workspace
- run: ci_scripts/ci_upload.sh workspace/py-docs workspace/wheelhouse
@@ -165,16 +176,15 @@ workflows:
test:
jobs:
- cargo_fetch
- build_test_docs_wheel:
requires:
- cargo_fetch
- build_test_docs_wheel
- upload_docs_wheels:
requires:
- build_test_docs_wheel
- cargo_fetch
- rustfmt:
requires:
- cargo_fetch
- clippy:
requires:
- cargo_fetch

4
.gitattributes vendored
View File

@@ -2,10 +2,6 @@
# ensures this even if the user has not set core.autocrlf.
* text=auto
# This directory contains email messages verbatim, and changing CRLF to
# LF will corrupt them.
test-data/* text=false
# binary files should be detected by git, however, to be sure, you can add them here explicitly
*.png binary
*.jpg binary

4
.gitignore vendored
View File

@@ -18,7 +18,3 @@ __pycache__
python/src/deltachat/capi*.so
python/liveconfig*
# ignore doxgen generated files
deltachat-ffi/html
deltachat-ffi/xml

655
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,10 @@ authors = ["dignifiedquire <dignifiedquire@gmail.com>"]
edition = "2018"
license = "MPL"
[build-dependencies]
cc = "1.0.35"
pkg-config = "0.3"
[dependencies]
deltachat_derive = { path = "./deltachat_derive" }
libc = "0.2.51"
@@ -19,8 +23,8 @@ num-derive = "0.2.5"
num-traits = "0.2.6"
native-tls = "0.2.3"
lettre = "0.9.0"
imap = { git = "https://github.com/jonhoo/rust-imap", rev = "281d2eb8ab50dc656ceff2ae749ca5045f334e15" }
mmime = { git = "https://github.com/dignifiedquire/mmime", rev = "bccd2c2" }
imap = "1.0.1"
mmime = "0.1.0"
base64 = "0.10"
charset = "0.1"
percent-encoding = "2.0"
@@ -31,7 +35,7 @@ failure = "0.1.5"
failure_derive = "0.1.5"
# TODO: make optional
rustyline = "4.1.0"
lazy_static = "1.4.0"
lazy_static = "1.3.0"
regex = "1.1.6"
rusqlite = { version = "0.20", features = ["bundled"] }
r2d2_sqlite = "0.12.0"
@@ -45,15 +49,11 @@ itertools = "0.8.0"
image-meta = "0.1.0"
quick-xml = "0.15.0"
escaper = "0.1.0"
bitflags = "1.1.0"
jetscii = "0.4.4"
debug_stub_derive = "0.3.0"
[dev-dependencies]
tempfile = "3.0"
pretty_assertions = "0.6.1"
pretty_env_logger = "0.3.0"
proptest = "0.9.4"
[workspace]
members = [

38
build.rs Normal file
View File

@@ -0,0 +1,38 @@
extern crate cc;
fn link_static(lib: &str) {
println!("cargo:rustc-link-lib=static={}", lib);
}
fn link_framework(fw: &str) {
println!("cargo:rustc-link-lib=framework={}", fw);
}
fn add_search_path(p: &str) {
println!("cargo:rustc-link-search={}", p);
}
fn build_tools() {
let mut config = cc::Build::new();
config.file("misc.c").compile("libtools.a");
println!("rerun-if-changed=build.rs");
println!("rerun-if-changed=misc.h");
println!("rerun-if-changed=misc.c");
}
fn main() {
build_tools();
add_search_path("/usr/local/lib");
let target = std::env::var("TARGET").unwrap();
if target.contains("-apple") || target.contains("-darwin") {
link_framework("CoreFoundation");
link_framework("CoreServices");
link_framework("Security");
}
// local tools
link_static("tools");
}

View File

@@ -39,4 +39,8 @@ fi
# Run all the test configurations:
$CARGO_CMD $CARGO_SUBCMD $OPT
$CARGO_CMD $CARGO_SUBCMD $OPT_RELEASE
$CARGO_CMD $CARGO_SUBCMD $OPT_RELEASE_IGNORED
# Build the ffi lib
$CARGO_CMD $CARGO_SUBCMD $OPT_FFI_RELEASE

22
ci_scripts/ci_run.sh Executable file
View File

@@ -0,0 +1,22 @@
# perform CI jobs on PRs and after merges to master.
# triggered from .circleci/config.yml
set -e -x
export BRANCH=${CIRCLE_BRANCH:-test7}
# run doxygen on c-source (needed by later doc-generation steps).
# XXX modifies the host filesystem docs/xml and docs/html directories
# XXX which you can then only remove with sudo as they belong to root
# XXX we don't do doxygen doc generation with Rust anymore, needs to be
# substituted with rust-docs
#if [ -n "$DOCS" ] ; then
# docker run --rm -it -v $PWD:/mnt -w /mnt/docs deltachat/doxygen doxygen
#fi
# run everything else inside docker (TESTS, DOCS, WHEELS)
docker run -e DCC_PY_LIVECONFIG -e BRANCH -e TESTS -e DOCS \
--rm -it -v $(pwd):/mnt -w /mnt \
deltachat/coredeps ci_scripts/run_all.sh

View File

@@ -34,7 +34,7 @@ echo -----------------------
# Bundle external shared libraries into the wheels
pushd $WHEELHOUSEDIR
pip3 install devpi-client
pip install devpi-client
devpi use https://m.devpi.net
devpi login dc --password $DEVPI_LOGIN

View File

@@ -36,21 +36,19 @@ if [ -n "$TESTS" ]; then
rm -rf src/deltachat/__pycache__
export PYTHONDONTWRITEBYTECODE=1
# run tox. The circle-ci project env-var-setting DCC_PY_LIVECONFIG
# allows running of "liveconfig" tests but for speed reasons
# we run them only for the highest python version we support
# run tox
# XXX we don't run liveconfig tests because they hang sometimes
# see https://github.com/deltachat/deltachat-core-rust/issues/331
# unset DCC_PY_LIVECONFIG
tox --workdir "$TOXWORKDIR" -e py37
unset DCC_PY_LIVECONFIG
tox --workdir "$TOXWORKDIR" -p4 -e lint,py35,py36,doc
tox --workdir "$TOXWORKDIR" -e auditwheels
tox --workdir "$TOXWORKDIR" -e lint,py27,py35,py36,py37,auditwheels
popd
fi
# if [ -n "$DOCS" ]; then
# echo -----------------------
# echo generating python docs
# echo -----------------------
# (cd python && tox --workdir "$TOXWORKDIR" -e doc)
# fi
if [ -n "$DOCS" ]; then
echo -----------------------
echo generating python docs
echo -----------------------
(cd python && tox --workdir "$TOXWORKDIR" -e doc)
fi

View File

@@ -16,7 +16,6 @@ crate-type = ["cdylib", "staticlib"]
[dependencies]
deltachat = { path = "../", default-features = false }
deltachat-provider-overview = { git = "https://github.com/deltachat/provider-overview", rev = "366b41a7503973e4ffac3aa5173b419f2f03c211" }
libc = "0.2"
human-panic = "1.0.1"
num-traits = "0.2.6"

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -1,7 +0,0 @@
/* the code snippet frame, defaults to white which tends to get badly readable in combination with explaining text around */
div.fragment {
background-color: #e0e0e0;
border: 0;
padding: 1em;
}

View File

@@ -1,10 +1 @@
# Delta Chat C Interface
## Documentation
To generate the C Interface documentation,
call doxygen in the `deltachat-ffi` directory
and browse the `html` subdirectory.
If thinks work,
the documentation is also available online at <https://c.delta.chat>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,92 +0,0 @@
extern crate deltachat_provider_overview;
use std::ptr;
use deltachat::dc_tools::{as_str, StrExt};
use deltachat_provider_overview::StatusState;
#[no_mangle]
pub type dc_provider_t = deltachat_provider_overview::Provider;
#[no_mangle]
pub unsafe extern "C" fn dc_provider_new_from_domain(
domain: *const libc::c_char,
) -> *const dc_provider_t {
match deltachat_provider_overview::get_provider_info(as_str(domain)) {
Some(provider) => provider,
None => ptr::null(),
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_new_from_email(
email: *const libc::c_char,
) -> *const dc_provider_t {
let domain = deltachat_provider_overview::get_domain_from_email(as_str(email));
match deltachat_provider_overview::get_provider_info(domain) {
Some(provider) => provider,
None => ptr::null(),
}
}
macro_rules! null_guard {
($context:tt) => {
if $context.is_null() {
return ptr::null_mut() as *mut libc::c_char;
}
};
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_overview_page(
provider: *const dc_provider_t,
) -> *mut libc::c_char {
null_guard!(provider);
format!(
"{}/{}",
deltachat_provider_overview::PROVIDER_OVERVIEW_URL,
(*provider).overview_page
)
.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_name(provider: *const dc_provider_t) -> *mut libc::c_char {
null_guard!(provider);
(*provider).name.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_markdown(
provider: *const dc_provider_t,
) -> *mut libc::c_char {
null_guard!(provider);
(*provider).markdown.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_status_date(
provider: *const dc_provider_t,
) -> *mut libc::c_char {
null_guard!(provider);
(*provider).status.date.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_status(provider: *const dc_provider_t) -> u32 {
if provider.is_null() {
return 0;
}
match (*provider).status.state {
StatusState::OK => 1,
StatusState::PREPARATION => 2,
StatusState::BROKEN => 3,
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_unref(_provider: *const dc_provider_t) {
()
}
// TODO expose general provider overview url?

View File

@@ -36,7 +36,7 @@ pub fn from_sql_derive(input: TokenStream) -> TokenStream {
impl rusqlite::types::FromSql for #name {
fn column_result(col: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
let inner = rusqlite::types::FromSql::column_result(col)?;
Ok(num_traits::FromPrimitive::from_i64(inner).unwrap_or_default())
num_traits::FromPrimitive::from_i64(inner).ok_or(rusqlite::types::FromSqlError::InvalidType)
}
}
};

View File

@@ -1,5 +1,4 @@
use std::ffi::CString;
use std::ptr;
use std::str::FromStr;
use deltachat::chat::{self, Chat};
@@ -10,27 +9,28 @@ use deltachat::constants::*;
use deltachat::contact::*;
use deltachat::context::*;
use deltachat::dc_imex::*;
use deltachat::dc_location::*;
use deltachat::dc_receive_imf::*;
use deltachat::dc_tools::*;
use deltachat::error::Error;
use deltachat::job::*;
use deltachat::location;
use deltachat::lot::LotState;
use deltachat::message::*;
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::sql;
use deltachat::types::*;
use deltachat::x::*;
use deltachat::Event;
use num_traits::FromPrimitive;
/// Reset database tables. This function is called from Core cmdline.
/// Argument is a bitmask, executing single or multiple actions in one call.
/// e.g. bitmask 7 triggers actions definded with bits 1, 2 and 4.
pub unsafe fn dc_reset_tables(context: &Context, bits: i32) -> i32 {
info!(context, "Resetting tables ({})...", bits);
info!(context, 0, "Resetting tables ({})...", bits);
if 0 != bits & 1 {
sql::execute(context, &context.sql, "DELETE FROM jobs;", params![]).unwrap();
info!(context, "(1) Jobs reset.");
info!(context, 0, "(1) Jobs reset.");
}
if 0 != bits & 2 {
sql::execute(
@@ -40,11 +40,11 @@ pub unsafe fn dc_reset_tables(context: &Context, bits: i32) -> i32 {
params![],
)
.unwrap();
info!(context, "(2) Peerstates reset.");
info!(context, 0, "(2) Peerstates reset.");
}
if 0 != bits & 4 {
sql::execute(context, &context.sql, "DELETE FROM keypairs;", params![]).unwrap();
info!(context, "(4) Private keypairs reset.");
info!(context, 0, "(4) Private keypairs reset.");
}
if 0 != bits & 8 {
sql::execute(
@@ -83,13 +83,10 @@ pub unsafe fn dc_reset_tables(context: &Context, bits: i32) -> i32 {
)
.unwrap();
sql::execute(context, &context.sql, "DELETE FROM leftgrps;", params![]).unwrap();
info!(context, "(8) Rest but server config reset.");
info!(context, 0, "(8) Rest but server config reset.");
}
context.call_cb(Event::MsgsChanged {
chat_id: 0,
msg_id: 0,
});
context.call_cb(Event::MSGS_CHANGED, 0, 0);
1
}
@@ -97,8 +94,8 @@ pub unsafe fn dc_reset_tables(context: &Context, bits: i32) -> i32 {
unsafe fn dc_poke_eml_file(context: &Context, filename: *const libc::c_char) -> libc::c_int {
/* mainly for testing, may be called by dc_import_spec() */
let mut success: libc::c_int = 0i32;
let mut data: *mut libc::c_char = ptr::null_mut();
let mut data_bytes = 0;
let mut data: *mut libc::c_char = 0 as *mut libc::c_char;
let mut data_bytes: size_t = 0;
if !(dc_read_file(
context,
filename,
@@ -124,14 +121,14 @@ unsafe fn dc_poke_eml_file(context: &Context, filename: *const libc::c_char) ->
/// @return 1=success, 0=error.
unsafe fn poke_spec(context: &Context, spec: *const libc::c_char) -> libc::c_int {
if !context.sql.is_open() {
error!(context, "Import: Database not opened.");
error!(context, 0, "Import: Database not opened.");
return 0;
}
let ok_to_continue;
let mut success: libc::c_int = 0;
let real_spec: *mut libc::c_char;
let mut suffix: *mut libc::c_char = ptr::null_mut();
let mut suffix: *mut libc::c_char = 0 as *mut libc::c_char;
let mut read_cnt: libc::c_int = 0;
/* if `spec` is given, remember it for later usage; if it is not given, try to use the last one */
@@ -145,7 +142,7 @@ unsafe fn poke_spec(context: &Context, spec: *const libc::c_char) -> libc::c_int
} else {
let rs = context.sql.get_config(context, "import_spec");
if rs.is_none() {
error!(context, "Import: No file or folder given.");
error!(context, 0, "Import: No file or folder given.");
ok_to_continue = false;
} else {
ok_to_continue = true;
@@ -168,6 +165,7 @@ unsafe fn poke_spec(context: &Context, spec: *const libc::c_char) -> libc::c_int
if dir.is_err() {
error!(
context,
0,
"Import: Cannot open directory \"{}\".",
as_str(real_spec),
);
@@ -183,7 +181,7 @@ unsafe fn poke_spec(context: &Context, spec: *const libc::c_char) -> libc::c_int
let name = name_f.to_string_lossy();
if name.ends_with(".eml") {
let path_plus_name = format!("{}/{}", as_str(real_spec), name);
info!(context, "Import: {}", path_plus_name);
info!(context, 0, "Import: {}", path_plus_name);
let path_plus_name_c = CString::yolo(path_plus_name);
if 0 != dc_poke_eml_file(context, path_plus_name_c.as_ptr()) {
read_cnt += 1
@@ -196,15 +194,13 @@ unsafe fn poke_spec(context: &Context, spec: *const libc::c_char) -> libc::c_int
if ok_to_continue2 {
info!(
context,
0,
"Import: {} items read from \"{}\".",
read_cnt,
as_str(real_spec)
);
if read_cnt > 0 {
context.call_cb(Event::MsgsChanged {
chat_id: 0,
msg_id: 0,
});
context.call_cb(Event::MSGS_CHANGED, 0 as uintptr_t, 0 as uintptr_t);
}
success = 1
}
@@ -231,10 +227,11 @@ unsafe fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
let msgtext = dc_msg_get_text(msg);
info!(
context,
0,
"{}#{}{}{}: {} (Contact#{}): {} {}{}{}{} [{}]",
prefix.as_ref(),
dc_msg_get_id(msg) as libc::c_int,
if dc_msg_get_showpadlock(msg) {
if 0 != dc_msg_get_showpadlock(msg) {
"🔒"
} else {
""
@@ -253,7 +250,11 @@ unsafe fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
} else {
"[FRESH]"
},
if dc_msg_is_info(msg) { "[INFO]" } else { "" },
if 0 != dc_msg_is_info(msg) {
"[INFO]"
} else {
""
},
statestr,
&temp2,
);
@@ -266,6 +267,7 @@ unsafe fn log_msglist(context: &Context, msglist: &Vec<u32>) -> Result<(), Error
if msg_id == 9 as libc::c_uint {
info!(
context,
0,
"--------------------------------------------------------------------------------"
);
@@ -273,7 +275,7 @@ unsafe fn log_msglist(context: &Context, msglist: &Vec<u32>) -> Result<(), Error
} else if msg_id > 0 {
if lines_out == 0 {
info!(
context,
context, 0,
"--------------------------------------------------------------------------------",
);
lines_out += 1
@@ -285,7 +287,7 @@ unsafe fn log_msglist(context: &Context, msglist: &Vec<u32>) -> Result<(), Error
if lines_out > 0 {
info!(
context,
"--------------------------------------------------------------------------------"
0, "--------------------------------------------------------------------------------"
);
}
Ok(())
@@ -302,7 +304,7 @@ unsafe fn log_contactlist(context: &Context, contacts: &Vec<u32>) {
if let Ok(contact) = Contact::get_by_id(context, contact_id) {
let name = contact.get_name();
let addr = contact.get_addr();
let verified_state = contact.is_verified(context);
let verified_state = contact.is_verified();
let verified_str = if VerifiedStatus::Unverified != verified_state {
if verified_state == VerifiedStatus::BidirectVerified {
" √√"
@@ -334,11 +336,17 @@ unsafe fn log_contactlist(context: &Context, contacts: &Vec<u32>) {
);
}
info!(context, "Contact#{}: {}{}", contact_id, line, line2);
info!(context, 0, "Contact#{}: {}{}", contact_id, line, line2);
}
}
}
static mut S_IS_AUTH: libc::c_int = 0;
pub unsafe fn dc_cmdline_skip_auth() {
S_IS_AUTH = 1;
}
fn chat_prefix(chat: &Chat) -> &'static str {
chat.typ.into()
}
@@ -419,7 +427,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
send <text>\n\
send-garbage\n\
sendimage <file> [<text>]\n\
sendfile <file> [<text>]\n\
sendfile <file>\n\
draft [<text>]\n\
listmedia\n\
archive <chat-id>\n\
@@ -452,6 +460,28 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
============================================="
),
},
"auth" => {
if 0 == S_IS_AUTH {
let is_pw = context
.get_config(config::Config::MailPw)
.unwrap_or_default();
if arg1 == is_pw {
S_IS_AUTH = 1;
} else {
println!("Bad password.");
}
} else {
println!("Already authorized.");
}
}
"open" => {
ensure!(!arg1.is_empty(), "Argument <file> missing");
dc_close(context);
ensure!(dc_open(context, arg1, None), "Open failed");
}
"close" => {
dc_close(context);
}
"initiate-key-transfer" => {
let setup_code = dc_initiate_key_transfer(context);
if !setup_code.is_null() {
@@ -469,7 +499,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
let msg_id: u32 = arg1.parse()?;
let msg = dc_get_msg(context, msg_id)?;
if dc_msg_is_setupmessage(&msg) {
let setupcodebegin = dc_msg_get_setupcodebegin(context, &msg);
let setupcodebegin = dc_msg_get_setupcodebegin(&msg);
println!(
"The setup code for setup message Msg#{} starts with: {}",
msg_id,
@@ -485,7 +515,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
!arg1.is_empty() && !arg2.is_empty(),
"Arguments <msg-id> <setup-code> expected"
);
if !dc_continue_key_transfer(context, arg1.parse()?, arg2_c) {
if 0 == dc_continue_key_transfer(context, arg1.parse()?, arg2_c) {
bail!("Continue key transfer failed");
}
}
@@ -496,28 +526,32 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
}
}
"export-backup" => {
dc_imex(context, 11, Some(context.get_blobdir()), ptr::null());
dc_imex(context, 11, context.get_blobdir(), 0 as *const libc::c_char);
}
"import-backup" => {
ensure!(!arg1.is_empty(), "Argument <backup-file> missing.");
dc_imex(context, 12, Some(arg1), ptr::null());
dc_imex(context, 12, arg1_c, 0 as *const libc::c_char);
}
"export-keys" => {
dc_imex(context, 1, Some(context.get_blobdir()), ptr::null());
dc_imex(context, 1, context.get_blobdir(), 0 as *const libc::c_char);
}
"import-keys" => {
dc_imex(context, 2, Some(context.get_blobdir()), ptr::null());
dc_imex(context, 2, context.get_blobdir(), 0 as *const libc::c_char);
}
"export-setup" => {
let setup_code = dc_create_setup_code(context);
let file_name = context.get_blobdir().join("autocrypt-setup-message.html");
let file_name: *mut libc::c_char = dc_mprintf(
b"%s/autocrypt-setup-message.html\x00" as *const u8 as *const libc::c_char,
context.get_blobdir(),
);
let file_content = dc_render_setup_file(context, &setup_code)?;
std::fs::write(&file_name, file_content)?;
std::fs::write(as_str(file_name), file_content)?;
println!(
"Setup message written to: {}\nSetup code: {}",
file_name.display(),
as_str(file_name),
&setup_code,
);
free(file_name as *mut libc::c_void);
}
"poke" => {
ensure!(0 != poke_spec(context, arg1_c), "Poke failed");
@@ -544,7 +578,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
println!("{}={:?}", key, val);
}
"info" => {
println!("{:#?}", context.get_info());
println!("{}", to_string(dc_get_info(context)));
}
"maybenetwork" => {
maybe_network(context);
@@ -564,16 +598,17 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
let cnt = chatlist.len();
if cnt > 0 {
info!(
context,
context, 0,
"================================================================================"
);
for i in (0..cnt).rev() {
let chat = Chat::load_from_db(context, chatlist.get_chat_id(i))?;
let temp_subtitle = chat.get_subtitle(context);
let temp_subtitle = chat.get_subtitle();
let temp_name = chat.get_name();
info!(
context,
0,
"{}#{}: {} [{}] [{} fresh]",
chat_prefix(&chat),
chat.get_id(),
@@ -581,7 +616,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
temp_subtitle,
chat::get_fresh_msg_cnt(context, chat.get_id()),
);
let lot = chatlist.get_summary(context, i, Some(&chat));
let lot = chatlist.get_summary(i, Some(&chat));
let statestr = if chat.is_archived() {
" [Archived]"
} else {
@@ -598,6 +633,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
let text2 = lot.get_text2();
info!(
context,
0,
"{}{}{}{} [{}]{}",
text1.unwrap_or(""),
if text1.is_some() { ": " } else { "" },
@@ -611,13 +647,13 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
},
);
info!(
context,
context, 0,
"================================================================================"
);
}
}
if location::is_sending_locations_to_chat(context, 0) {
info!(context, "Location streaming enabled.");
if dc_is_sending_locations_to_chat(context, 0 as uint32_t) {
info!(context, 0, "Location streaming enabled.");
}
println!("{} chats", cnt);
}
@@ -636,10 +672,11 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
let sel_chat = sel_chat.as_ref().unwrap();
let msglist = chat::get_chat_msgs(context, sel_chat.get_id(), 0x1, 0);
let temp2 = sel_chat.get_subtitle(context);
let temp2 = sel_chat.get_subtitle();
let temp_name = sel_chat.get_name();
info!(
context,
0,
"{}#{}: {} [{}]{}",
chat_prefix(sel_chat),
sel_chat.get_id(),
@@ -652,7 +689,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
},
);
log_msglist(context, &msglist)?;
if let Some(draft) = chat::get_draft(context, sel_chat.get_id())? {
if let Ok(draft) = chat::get_draft(context, sel_chat.get_id()) {
log_msg(context, "Draft", &draft);
}
@@ -665,7 +702,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
"createchat" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id: libc::c_int = arg1.parse()?;
let chat_id = chat::create_by_contact_id(context, contact_id as u32)?;
let chat_id = chat::create_by_contact_id(context, contact_id as uint32_t)?;
println!("Single#{} created successfully.", chat_id,);
}
@@ -694,10 +731,10 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id_0: libc::c_int = arg1.parse()?;
if chat::add_contact_to_chat(
if 0 != chat::add_contact_to_chat(
context,
sel_chat.as_ref().unwrap().get_id(),
contact_id_0 as u32,
contact_id_0 as uint32_t,
) {
println!("Contact added to chat.");
} else {
@@ -711,7 +748,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
chat::remove_contact_from_chat(
context,
sel_chat.as_ref().unwrap().get_id(),
contact_id_1 as u32,
contact_id_1 as uint32_t,
)?;
println!("Contact added to chat.");
@@ -735,23 +772,20 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
ensure!(sel_chat.is_some(), "No chat selected.");
let contacts = chat::get_chat_contacts(context, sel_chat.as_ref().unwrap().get_id());
info!(context, "Memberlist:");
info!(context, 0, "Memberlist:");
log_contactlist(context, &contacts);
println!(
"{} contacts\nLocation streaming: {}",
contacts.len(),
location::is_sending_locations_to_chat(
context,
sel_chat.as_ref().unwrap().get_id()
),
dc_is_sending_locations_to_chat(context, sel_chat.as_ref().unwrap().get_id()),
);
}
"getlocations" => {
ensure!(sel_chat.is_some(), "No chat selected.");
let contact_id = arg1.parse().unwrap_or_default();
let locations = location::get_range(
let locations = dc_get_locations(
context,
sel_chat.as_ref().unwrap().get_id(),
contact_id,
@@ -763,6 +797,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
let marker = location.marker.as_ref().unwrap_or(&default_marker);
info!(
context,
0,
"Loc#{}: {}: lat={} lng={} acc={} Chat#{} Contact#{} Msg#{} {}",
location.location_id,
dc_timestamp_to_str(location.timestamp),
@@ -776,7 +811,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
);
}
if locations.is_empty() {
info!(context, "No locations.");
info!(context, 0, "No locations.");
}
}
"sendlocations" => {
@@ -784,7 +819,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
ensure!(!arg1.is_empty(), "No timeout given.");
let seconds = arg1.parse()?;
location::send_locations_to_chat(context, sel_chat.as_ref().unwrap().get_id(), seconds);
dc_send_locations_to_chat(context, sel_chat.as_ref().unwrap().get_id(), seconds);
println!(
"Locations will be sent to Chat#{} for {} seconds. Use 'setlocation <lat> <lng>' to play around.",
sel_chat.as_ref().unwrap().get_id(),
@@ -799,7 +834,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
let latitude = arg1.parse()?;
let longitude = arg2.parse()?;
let continue_streaming = location::set(context, latitude, longitude, 0.);
let continue_streaming = dc_set_location(context, latitude, longitude, 0.);
if 0 != continue_streaming {
println!("Success, streaming should be continued.");
} else {
@@ -807,7 +842,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
}
}
"dellocations" => {
location::delete_all(context)?;
dc_delete_all_locations(context);
}
"send" => {
ensure!(sel_chat.is_some(), "No chat selected.");
@@ -823,17 +858,18 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
}
"sendimage" | "sendfile" => {
ensure!(sel_chat.is_some(), "No chat selected.");
ensure!(!arg1.is_empty(), "No file given.");
ensure!(!arg1.is_empty() && !arg2.is_empty(), "No file given.");
let mut msg = dc_msg_new(if arg0 == "sendimage" {
Viewtype::Image
} else {
Viewtype::File
});
dc_msg_set_file(&mut msg, arg1_c, ptr::null());
if !arg2.is_empty() {
dc_msg_set_text(&mut msg, arg2_c);
}
let mut msg = dc_msg_new(
context,
if arg0 == "sendimage" {
Viewtype::Image
} else {
Viewtype::File
},
);
dc_msg_set_file(&mut msg, arg1_c, 0 as *const libc::c_char);
dc_msg_set_text(&mut msg, arg2_c);
chat::send_msg(context, sel_chat.as_ref().unwrap().get_id(), &mut msg)?;
}
"listmsgs" => {
@@ -845,7 +881,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
0 as libc::c_uint
};
let msglist = context.search_msgs(chat, arg1);
let msglist = dc_search_msgs(context, chat, arg1_c);
log_msglist(context, &msglist)?;
println!("{} messages.", msglist.len());
@@ -854,7 +890,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
ensure!(sel_chat.is_some(), "No chat selected.");
if !arg1.is_empty() {
let mut draft = dc_msg_new(Viewtype::Text);
let mut draft = dc_msg_new(context, Viewtype::Text);
dc_msg_set_text(&mut draft, arg1_c);
chat::set_draft(
context,
@@ -908,7 +944,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
println!("{}", as_str(res));
}
"listfresh" => {
let msglist = context.get_fresh_msgs();
let msglist = dc_get_fresh_msgs(context);
log_msglist(context, &msglist)?;
print!("{} fresh messages.", msglist.len());
@@ -1014,17 +1050,16 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
res.get_text2()
);
}
// TODO: implement this again, unclear how to match this through though, without writing a parser.
// "event" => {
// ensure!(!arg1.is_empty(), "Argument <id> missing.");
// let event = arg1.parse()?;
// let event = Event::from_u32(event).ok_or(format_err!("Event::from_u32({})", event))?;
// let r = context.call_cb(event, 0 as libc::uintptr_t, 0 as libc::uintptr_t);
// println!(
// "Sending event {:?}({}), received value {}.",
// event, event as usize, r as libc::c_int,
// );
// }
"event" => {
ensure!(!arg1.is_empty(), "Argument <id> missing.");
let event = arg1.parse()?;
let event = Event::from_u32(event).ok_or(format_err!("Event::from_u32({})", event))?;
let r = context.call_cb(event, 0 as uintptr_t, 0 as uintptr_t);
println!(
"Sending event {:?}({}), received value {}.",
event, event as usize, r as libc::c_int,
);
}
"fileinfo" => {
ensure!(!arg1.is_empty(), "Argument <file> missing.");

View File

@@ -14,21 +14,19 @@ extern crate lazy_static;
extern crate rusqlite;
use std::borrow::Cow::{self, Borrowed, Owned};
use std::io::{self, Write};
use std::path::Path;
use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use deltachat::config;
use deltachat::configure::*;
use deltachat::constants::*;
use deltachat::context::*;
use deltachat::dc_securejoin::*;
use deltachat::dc_tools::*;
use deltachat::job::*;
use deltachat::oauth2::*;
use deltachat::securejoin::*;
use deltachat::types::*;
use deltachat::x::*;
use deltachat::Event;
use rustyline::completion::{Completer, FilenameCompleter, Pair};
use rustyline::config::OutputStreamType;
use rustyline::error::ReadlineError;
@@ -43,75 +41,96 @@ use self::cmdline::*;
// Event Handler
fn receive_event(_context: &Context, event: Event) -> libc::uintptr_t {
unsafe extern "C" fn receive_event(
_context: &Context,
event: Event,
data1: uintptr_t,
data2: uintptr_t,
) -> uintptr_t {
match event {
Event::GetString { .. } => {}
Event::Info(msg) => {
Event::GET_STRING => {}
Event::INFO => {
/* do not show the event as this would fill the screen */
println!("{}", msg);
println!("{}", to_string(data2 as *const _),);
}
Event::SmtpConnected(msg) => {
println!("[DC_EVENT_SMTP_CONNECTED] {}", msg);
Event::SMTP_CONNECTED => {
println!("[DC_EVENT_SMTP_CONNECTED] {}", to_string(data2 as *const _));
}
Event::ImapConnected(msg) => {
println!("[DC_EVENT_IMAP_CONNECTED] {}", msg);
Event::IMAP_CONNECTED => {
println!("[DC_EVENT_IMAP_CONNECTED] {}", to_string(data2 as *const _),);
}
Event::SmtpMessageSent(msg) => {
println!("[DC_EVENT_SMTP_MESSAGE_SENT] {}", msg);
}
Event::Warning(msg) => {
println!("[Warning] {}", msg);
}
Event::Error(msg) => {
println!("\x1b[31m[DC_EVENT_ERROR] {}\x1b[0m", msg);
}
Event::ErrorNetwork(msg) => {
println!("\x1b[31m[DC_EVENT_ERROR_NETWORK] msg={}\x1b[0m", msg);
}
Event::ErrorSelfNotInGroup(msg) => {
println!("\x1b[31m[DC_EVENT_ERROR_SELF_NOT_IN_GROUP] {}\x1b[0m", msg);
}
Event::MsgsChanged { chat_id, msg_id } => {
print!(
"\x1b[33m{{Received DC_EVENT_MSGS_CHANGED(chat_id={}, msg_id={})}}\n\x1b[0m",
chat_id, msg_id,
Event::SMTP_MESSAGE_SENT => {
println!(
"[DC_EVENT_SMTP_MESSAGE_SENT] {}",
to_string(data2 as *const _),
);
}
Event::ContactsChanged(_) => {
Event::WARNING => {
println!("[Warning] {}", to_string(data2 as *const _),);
}
Event::ERROR => {
println!(
"\x1b[31m[DC_EVENT_ERROR] {}\x1b[0m",
to_string(data2 as *const _),
);
}
Event::ERROR_NETWORK => {
println!(
"\x1b[31m[DC_EVENT_ERROR_NETWORK] first={}, msg={}\x1b[0m",
data1 as usize,
to_string(data2 as *const _),
);
}
Event::ERROR_SELF_NOT_IN_GROUP => {
println!(
"\x1b[31m[DC_EVENT_ERROR_SELF_NOT_IN_GROUP] {}\x1b[0m",
to_string(data2 as *const _),
);
}
Event::MSGS_CHANGED => {
print!(
"\x1b[33m{{Received DC_EVENT_MSGS_CHANGED({}, {})}}\n\x1b[0m",
data1 as usize, data2 as usize,
);
}
Event::CONTACTS_CHANGED => {
print!("\x1b[33m{{Received DC_EVENT_CONTACTS_CHANGED()}}\n\x1b[0m");
}
Event::LocationChanged(contact) => {
Event::LOCATION_CHANGED => {
print!(
"\x1b[33m{{Received DC_EVENT_LOCATION_CHANGED(contact={:?})}}\n\x1b[0m",
contact,
"\x1b[33m{{Received DC_EVENT_LOCATION_CHANGED(contact={})}}\n\x1b[0m",
data1 as usize,
);
}
Event::ConfigureProgress(progress) => {
Event::CONFIGURE_PROGRESS => {
print!(
"\x1b[33m{{Received DC_EVENT_CONFIGURE_PROGRESS({} ‰)}}\n\x1b[0m",
progress,
data1 as usize,
);
}
Event::ImexProgress(progress) => {
Event::IMEX_PROGRESS => {
print!(
"\x1b[33m{{Received DC_EVENT_IMEX_PROGRESS({} ‰)}}\n\x1b[0m",
progress,
data1 as usize,
);
}
Event::ImexFileWritten(file) => {
Event::IMEX_FILE_WRITTEN => {
print!(
"\x1b[33m{{Received DC_EVENT_IMEX_FILE_WRITTEN({})}}\n\x1b[0m",
file.display()
to_string(data1 as *const _)
);
}
Event::ChatModified(chat) => {
Event::CHAT_MODIFIED => {
print!(
"\x1b[33m{{Received DC_EVENT_CHAT_MODIFIED({})}}\n\x1b[0m",
chat
data1 as usize,
);
}
_ => {
print!("\x1b[33m{{Received {:?}}}\n\x1b[0m", event);
print!(
"\x1b[33m{{Received {:?}({}, {})}}\n\x1b[0m",
event, data1 as usize, data2 as usize,
);
}
}
@@ -365,15 +384,21 @@ impl Highlighter for DcHelper {
impl Helper for DcHelper {}
fn main_0(args: Vec<String>) -> Result<(), failure::Error> {
if args.len() < 2 {
let mut context = dc_context_new(
Some(receive_event),
0 as *mut libc::c_void,
Some("CLI".into()),
);
unsafe { dc_cmdline_skip_auth() };
if args.len() == 2 {
if unsafe { !dc_open(&mut context, &args[1], None) } {
println!("Error: Cannot open {}.", args[0],);
}
} else if args.len() != 1 {
println!("Error: Bad arguments, expected [db-name].");
return Err(format_err!("No db-name specified"));
}
let context = Context::new(
Box::new(receive_event),
"CLI".into(),
Path::new(&args[1]).to_path_buf(),
)?;
println!("Delta Chat Core is awaiting your commands.");
@@ -494,27 +519,30 @@ unsafe fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult
}
"getqr" | "getbadqr" => {
start_threads(ctx.clone());
if let Some(mut qr) =
dc_get_securejoin_qr(&ctx.read().unwrap(), arg1.parse().unwrap_or_default())
{
if !qr.is_empty() {
if arg0 == "getbadqr" && qr.len() > 40 {
qr.replace_range(12..22, "0000000000")
let qrstr =
dc_get_securejoin_qr(&ctx.read().unwrap(), arg1.parse().unwrap_or_default());
if !qrstr.is_null() && 0 != *qrstr.offset(0isize) as libc::c_int {
if arg0 == "getbadqr" && strlen(qrstr) > 40 {
let mut i: libc::c_int = 12i32;
while i < 22i32 {
*qrstr.offset(i as isize) = '0' as i32 as libc::c_char;
i += 1
}
println!("{}", qr);
let output = Command::new("qrencode")
.args(&["-t", "ansiutf8", qr.as_str(), "-o", "-"])
.output()
.expect("failed to execute process");
io::stdout().write_all(&output.stdout).unwrap();
io::stderr().write_all(&output.stderr).unwrap();
}
println!("{}", to_string(qrstr as *const _));
let syscmd = dc_mprintf(
b"qrencode -t ansiutf8 \"%s\" -o -\x00" as *const u8 as *const libc::c_char,
qrstr,
);
system(syscmd);
free(syscmd as *mut libc::c_void);
}
free(qrstr as *mut libc::c_void);
}
"joinqr" => {
start_threads(ctx.clone());
if !arg0.is_empty() {
dc_join_securejoin(&ctx.read().unwrap(), arg1);
dc_join_securejoin(&ctx.read().unwrap(), arg1_c);
}
}
"exit" | "quit" => return Ok(ExitResult::Exit),

View File

@@ -1,5 +1,6 @@
extern crate deltachat;
use std::ffi::CStr;
use std::sync::{Arc, RwLock};
use std::{thread, time};
use tempfile::tempdir;
@@ -8,43 +9,42 @@ use deltachat::chat;
use deltachat::chatlist::*;
use deltachat::config;
use deltachat::configure::*;
use deltachat::constants::Event;
use deltachat::contact::*;
use deltachat::context::*;
use deltachat::job::{
perform_imap_fetch, perform_imap_idle, perform_imap_jobs, perform_smtp_idle, perform_smtp_jobs,
};
use deltachat::Event;
fn cb(_ctx: &Context, event: Event) -> usize {
print!("[{:?}]", event);
extern "C" fn cb(_ctx: &Context, event: Event, data1: usize, data2: usize) -> usize {
println!("[{:?}]", event);
match event {
Event::ConfigureProgress(progress) => {
print!(" progress: {}\n", progress);
Event::CONFIGURE_PROGRESS => {
println!(" progress: {}", data1);
0
}
Event::Info(msg) | Event::Warning(msg) | Event::Error(msg) | Event::ErrorNetwork(msg) => {
print!(" {}\n", msg);
0
}
_ => {
print!("\n");
Event::INFO | Event::WARNING | Event::ERROR | Event::ERROR_NETWORK => {
println!(
" {}",
unsafe { CStr::from_ptr(data2 as *const _) }
.to_str()
.unwrap()
);
0
}
_ => 0,
}
}
fn main() {
unsafe {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
println!("creating database {:?}", dbfile);
let ctx =
Context::new(Box::new(cb), "FakeOs".into(), dbfile).expect("Failed to create context");
let ctx = dc_context_new(Some(cb), std::ptr::null_mut(), None);
let running = Arc::new(RwLock::new(true));
let info = ctx.get_info();
let info = dc_get_info(&ctx);
let info_s = CStr::from_ptr(info);
let duration = time::Duration::from_millis(4000);
println!("info: {:#?}", info);
println!("info: {}", info_s.to_str().unwrap());
let ctx = Arc::new(ctx);
let ctx1 = ctx.clone();
@@ -73,6 +73,13 @@ fn main() {
}
});
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
println!("opening database {:?}", dbfile);
assert!(dc_open(&ctx, dbfile.to_str().unwrap(), None));
println!("configuring");
let args = std::env::args().collect::<Vec<String>>();
assert_eq!(args.len(), 2, "missing password");
@@ -94,7 +101,7 @@ fn main() {
let chats = Chatlist::try_load(&ctx, 0, None, None).unwrap();
for i in 0..chats.len() {
let summary = chats.get_summary(&ctx, 0, None);
let summary = chats.get_summary(0, None);
let text1 = summary.get_text1();
let text2 = summary.get_text2();
println!("chat: {} - {:?} - {:?}", i, text1, text2,);
@@ -123,5 +130,6 @@ fn main() {
t2.join().unwrap();
println!("closing");
dc_close(&ctx);
}
}

52
misc.c Normal file
View File

@@ -0,0 +1,52 @@
#include <stdlib.h>
#include <stdarg.h>
#include <ctype.h>
#include <string.h>
#include <stdio.h>
#include "misc.h"
static char* internal_dc_strdup(const char* s) /* strdup(NULL) is undefined, save_strdup(NULL) returns an empty string in this case */
{
char* ret = NULL;
if (s) {
if ((ret=strdup(s))==NULL) {
exit(16); /* cannot allocate (little) memory, unrecoverable error */
}
}
else {
if ((ret=(char*)calloc(1, 1))==NULL) {
exit(17); /* cannot allocate little memory, unrecoverable error */
}
}
return ret;
}
char* dc_mprintf(const char* format, ...)
{
char testbuf[1];
char* buf = NULL;
int char_cnt_without_zero = 0;
va_list argp;
va_list argp_copy;
va_start(argp, format);
va_copy(argp_copy, argp);
char_cnt_without_zero = vsnprintf(testbuf, 0, format, argp);
va_end(argp);
if (char_cnt_without_zero < 0) {
va_end(argp_copy);
return internal_dc_strdup("ErrFmt");
}
buf = malloc(char_cnt_without_zero+2 /* +1 would be enough, however, protect against off-by-one-errors */);
if (buf==NULL) {
va_end(argp_copy);
return internal_dc_strdup("ErrMem");
}
vsnprintf(buf, char_cnt_without_zero+1, format, argp_copy);
va_end(argp_copy);
return buf;
}

1
misc.h Normal file
View File

@@ -0,0 +1 @@
char* dc_mprintf (const char* format, ...); /* The result must be free()'d. */

View File

@@ -1,7 +0,0 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 679506fe9ac59df773f8cfa800fdab5f0a32fe49d2ab370394000a1aa5bc2a72 # shrinks to buf = "%0A"

View File

@@ -1,9 +0,0 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc c310754465ee0261807b96fa9bcc4861ff9aa286e94667524b5960c69f9b6620 # shrinks to buf = "", approx_chars = 0, do_unwrap = false
cc 5fd8d730b0a9cdf7308ce58818ca9aefc0255c9ba2a0878944fc48d43a67315b # shrinks to buf = "𑒀ὐ¢🜀\u{1e01b}A a🟠", approx_chars = 0, do_unwrap = false
cc c6a0029a54137a4b9efc9ef2ea6d9a7dd1d60d1c937bb472b66a174618ba8013 # shrinks to buf = "𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ ", approx_chars = 0, do_unwrap = false

View File

@@ -65,17 +65,17 @@ Afterwards ``which python`` tells you that it comes out of the "venv"
directory that contains all python install artifacts. Let's first
install test tools::
pip install pytest pytest-timeout pytest-rerunfailures requests
pip install pytest pytest-timeout requests
then cargo-build and install the deltachat bindings::
python install_python_bindings.py
The bindings will be installed in release mode but with debug symbols.
The release mode is necessary because some tests generate RSA keys
The release mode is neccessary because some tests generate RSA keys
which is prohibitively slow in debug mode.
After successful binding installation you can finally run the tests::
After succcessul binding installation you can finally run the tests::
pytest -v tests

View File

@@ -14,7 +14,7 @@ except ImportError:
import deltachat
from . import const
from .capi import ffi, lib
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array, DCLot
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array
from .chatting import Contact, Chat, Message
@@ -50,8 +50,9 @@ class Account(object):
self._configkeys = self.get_config("sys.config_keys").split()
self._imex_completed = threading.Event()
def __del__(self):
self.shutdown()
# XXX this can cause "illegal instructions" at test ends so we omit it for now
# def __del__(self):
# self.shutdown()
def _check_config_key(self, name):
if name not in self._configkeys:
@@ -328,56 +329,6 @@ class Account(object):
raise RuntimeError("could not send out autocrypt setup message")
return from_dc_charpointer(res)
def get_setup_contact_qr(self):
""" get/create Setup-Contact QR Code as ascii-string.
this string needs to be transferred to another DC account
in a second channel (typically used by mobiles with QRcode-show + scan UX)
where qr_setup_contact(qr) is called.
"""
res = lib.dc_get_securejoin_qr(self._dc_context, 0)
return from_dc_charpointer(res)
def check_qr(self, qr):
""" check qr code and return :class:`ScannedQRCode` instance representing the result"""
res = ffi.gc(
lib.dc_check_qr(self._dc_context, as_dc_charpointer(qr)),
lib.dc_lot_unref
)
lot = DCLot(res)
if lot.state() == const.DC_QR_ERROR:
raise ValueError("invalid or unknown QR code: {}".format(lot.text1()))
return ScannedQRCode(lot)
def qr_setup_contact(self, qr):
""" setup contact and return a Chat after contact is established.
Note that this function may block for a long time as messages are exchanged
with the emitter of the QR code. On success a :class:`deltachat.chatting.Chat` instance
is returned.
:param qr: valid "setup contact" QR code (all other QR codes will result in an exception)
"""
assert self.check_qr(qr).is_ask_verifycontact()
chat_id = lib.dc_join_securejoin(self._dc_context, as_dc_charpointer(qr))
if chat_id == 0:
raise ValueError("could not setup secure contact")
return Chat(self, chat_id)
def qr_join_chat(self, qr):
""" join a chat group through a QR code.
Note that this function may block for a long time as messages are exchanged
with the emitter of the QR code. On success a :class:`deltachat.chatting.Chat` instance
is returned which is the chat that we just joined.
:param qr: valid "join-group" QR code (all other QR codes will result in an exception)
"""
assert self.check_qr(qr).is_ask_verifygroup()
chat_id = lib.dc_join_securejoin(self._dc_context, as_dc_charpointer(qr))
if chat_id == 0:
raise ValueError("could not join group")
return Chat(self, chat_id)
def start_threads(self):
""" start IMAP/SMTP threads (and configure account if it hasn't happened).
@@ -396,8 +347,7 @@ class Account(object):
def shutdown(self, wait=True):
""" stop threads and close and remove underlying dc_context and callbacks. """
if hasattr(self, "_dc_context") and hasattr(self, "_threads"):
# print("SHUTDOWN", self)
self.stop_threads(wait=False)
self.stop_threads(wait=False) # to interrupt idle and tell python threads to stop
lib.dc_close(self._dc_context)
self.stop_threads(wait=wait) # to wait for threads
deltachat.clear_context_callback(self._dc_context)
@@ -427,12 +377,15 @@ class IOThreads:
def is_started(self):
return len(self._name2thread) > 0
def start(self, imap=True, smtp=True):
def start(self, imap=True, smtp=True, mvbox=True):
assert not self.is_started()
if imap:
self._start_one_thread("imap", self.imap_thread_run)
self._start_one_thread("inbox", self.inbox_thread_run)
if smtp:
self._start_one_thread("smtp", self.smtp_thread_run)
if mvbox:
self._start_one_thread("mvbox", self.mvbox_thread_run)
def _start_one_thread(self, name, func):
self._name2thread[name] = t = threading.Thread(target=func, name=name)
@@ -447,13 +400,20 @@ class IOThreads:
for name, thread in self._name2thread.items():
thread.join()
def imap_thread_run(self):
self._log_event("py-bindings-info", 0, "IMAP THREAD START")
def inbox_thread_run(self):
self._log_event("py-bindings-info", 0, "INBOX IMAP THREAD START")
while not self._thread_quitflag:
lib.dc_perform_imap_jobs(self._dc_context)
lib.dc_perform_imap_fetch(self._dc_context)
lib.dc_perform_imap_idle(self._dc_context)
self._log_event("py-bindings-info", 0, "IMAP THREAD FINISHED")
self._log_event("py-bindings-info", 0, "INBOX IMAP THREAD FINISHED")
def mvbox_thread_run(self):
self._log_event("py-bindings-info", 0, "MVBOX IMAP THREAD START")
while not self._thread_quitflag:
lib.dc_perform_mvbox_fetch(self._dc_context)
lib.dc_perform_mvbox_idle(self._dc_context)
self._log_event("py-bindings-info", 0, "MVBOX IMAP THREAD FINISHED")
def smtp_thread_run(self):
self._log_event("py-bindings-info", 0, "SMTP THREAD START")
@@ -483,10 +443,6 @@ class EventLogger:
def set_timeout(self, timeout):
self._timeout = timeout
def consume_events(self, check_error=True):
while not self._event_queue.empty():
self.get()
def get(self, timeout=None, check_error=True):
timeout = timeout or self._timeout
ev = self._event_queue.get(timeout=timeout)
@@ -546,18 +502,3 @@ def _destroy_dc_context(dc_context, dc_context_unref=lib.dc_context_unref):
# we are deep into Python Interpreter shutdown,
# so no need to clear the callback context mapping.
pass
class ScannedQRCode:
def __init__(self, dc_lot):
self._dc_lot = dc_lot
def is_ask_verifycontact(self):
return self._dc_lot.state() == const.DC_QR_ASK_VERIFYCONTACT
def is_ask_verifygroup(self):
return self._dc_lot.state() == const.DC_QR_ASK_VERIFYGROUP
@property
def contact_id(self):
return self._dc_lot.id()

View File

@@ -1,7 +1,6 @@
""" chatting related objects: Contact, Chat, Message. """
import mimetypes
import os
from . import props
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array
from .capi import lib, ffi
@@ -132,16 +131,6 @@ class Chat(object):
"""
return lib.dc_chat_get_type(self._dc_chat)
def get_join_qr(self):
""" get/create Join-Group QR Code as ascii-string.
this string needs to be transferred to another DC account
in a second channel (typically used by mobiles with QRcode-show + scan UX)
where account.join_with_qrcode(qr) needs to be called.
"""
res = lib.dc_get_securejoin_qr(self._dc_context, self.id)
return from_dc_charpointer(res)
# ------ chat messaging API ------------------------------
def send_text(self, text):
@@ -305,6 +294,7 @@ class Chat(object):
def get_contacts(self):
""" get all contacts for this chat.
:params: contact object.
:raises ValueError: if contact could not be added
:returns: list of :class:`deltachat.chatting.Contact` objects for this chat
"""
@@ -315,46 +305,3 @@ class Chat(object):
return list(iter_array(
dc_array, lambda id: Contact(self._dc_context, id))
)
def set_profile_image(self, img_path):
"""Set group profile image.
If the group is already promoted (any message was sent to the group),
all group members are informed by a special status message that is sent
automatically by this function.
:params img_path: path to image object
:raises ValueError: if profile image could not be set
:returns: None
"""
assert os.path.exists(img_path), img_path
p = as_dc_charpointer(img_path)
res = lib.dc_set_chat_profile_image(self._dc_context, self.id, p)
if res != 1:
raise ValueError("Setting Profile Image {!r} failed".format(p))
def remove_profile_image(self):
"""remove group profile image.
If the group is already promoted (any message was sent to the group),
all group members are informed by a special status message that is sent
automatically by this function.
:raises ValueError: if profile image could not be reset
:returns: None
"""
res = lib.dc_set_chat_profile_image(self._dc_context, self.id, ffi.NULL)
if res != 1:
raise ValueError("Removing Profile Image failed")
def get_profile_image(self):
"""Get group profile image.
For groups, this is the image set by any group member using
set_chat_profile_image(). For normal chats, this is the image
set by each remote user on their own using dc_set_config(context,
"selfavatar", image).
:returns: path to profile image, None if no profile image exists.
"""
dc_res = lib.dc_chat_get_profile_image(self._dc_chat)
if dc_res == ffi.NULL:
return None
return from_dc_charpointer(dc_res)

View File

@@ -8,23 +8,11 @@ from os.path import join as joinpath
# this works well when you in a git-checkout
# run "python deltachat/const.py" to regenerate events
# begin const generated
DC_PROVIDER_STATUS_OK = 1
DC_PROVIDER_STATUS_PREPARATION = 2
DC_PROVIDER_STATUS_BROKEN = 3
DC_GCL_ARCHIVED_ONLY = 0x01
DC_GCL_NO_SPECIALS = 0x02
DC_GCL_ADD_ALLDONE_HINT = 0x04
DC_GCL_VERIFIED_ONLY = 0x01
DC_GCL_ADD_SELF = 0x02
DC_QR_ASK_VERIFYCONTACT = 200
DC_QR_ASK_VERIFYGROUP = 202
DC_QR_FPR_OK = 210
DC_QR_FPR_MISMATCH = 220
DC_QR_FPR_WITHOUT_ADDR = 230
DC_QR_ADDR = 320
DC_QR_TEXT = 330
DC_QR_URL = 332
DC_QR_ERROR = 400
DC_CHAT_ID_DEADDROP = 1
DC_CHAT_ID_TRASH = 3
DC_CHAT_ID_MSGS_IN_CREATION = 4
@@ -81,14 +69,15 @@ DC_EVENT_IMEX_FILE_WRITTEN = 2052
DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060
DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061
DC_EVENT_GET_STRING = 2091
DC_EVENT_HTTP_GET = 2100
DC_EVENT_HTTP_POST = 2110
DC_EVENT_FILE_COPIED = 2055
DC_EVENT_IS_OFFLINE = 2081
# end const generated
def read_event_defines(f):
rex = re.compile(r'#define\s+((?:DC_EVENT_|DC_QR|DC_MSG|DC_STATE_|'
r'DC_CONTACT_ID_|DC_GCL|DC_CHAT|DC_PROVIDER)\S+)\s+([x\d]+).*')
rex = re.compile(r'#define\s+((?:DC_EVENT_|DC_MSG|DC_STATE_|DC_CONTACT_ID_|DC_GCL|DC_CHAT)\S+)\s+([x\d]+).*')
for line in f:
m = rex.match(line)
if m:
@@ -101,7 +90,7 @@ if __name__ == "__main__":
if len(sys.argv) >= 2:
deltah = sys.argv[1]
else:
deltah = joinpath(dirname(dirname(dirname(here_dir))), "deltachat-ffi", "deltachat.h")
deltah = joinpath(dirname(dirname(dirname(here_dir))), "src", "deltachat.h")
assert os.path.exists(deltah)
lines = []

View File

@@ -1,6 +1,5 @@
from .capi import lib
from .capi import ffi
from datetime import datetime
def as_dc_charpointer(obj):
@@ -18,29 +17,3 @@ def iter_array(dc_array_t, constructor):
def from_dc_charpointer(obj):
return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8")
class DCLot:
def __init__(self, dc_lot):
self._dc_lot = dc_lot
def id(self):
return lib.dc_lot_get_id(self._dc_lot)
def state(self):
return lib.dc_lot_get_state(self._dc_lot)
def text1(self):
return from_dc_charpointer(lib.dc_lot_get_text1(self._dc_lot))
def text1_meaning(self):
return lib.dc_lot_get_text1_meaning(self._dc_lot)
def text2(self):
return from_dc_charpointer(lib.dc_lot_get_text2(self._dc_lot))
def timestamp(self):
ts = lib.dc_lot_get_timestamp(self._dc_lot)
if ts == 0:
return None
return datetime.utcfromtimestamp(ts)

View File

@@ -110,13 +110,7 @@ class Message(object):
def continue_key_transfer(self, setup_code):
""" extract key and use it as primary key for this account. """
res = lib.dc_continue_key_transfer(
self._dc_context,
self.id,
as_dc_charpointer(setup_code)
)
if res == 0:
raise ValueError("could not decrypt")
lib.dc_continue_key_transfer(self._dc_context, self.id, as_dc_charpointer(setup_code))
@props.with_doc
def time_sent(self):

View File

@@ -1,67 +0,0 @@
"""Provider info class."""
from .capi import ffi, lib
from .cutil import as_dc_charpointer, from_dc_charpointer
class ProviderNotFoundError(Exception):
"""The provider information was not found."""
class Provider(object):
"""Provider information.
:param domain: The domain to get the provider info for, this is
normally the part following the `@` of the domain.
"""
def __init__(self, domain):
provider = ffi.gc(
lib.dc_provider_new_from_domain(as_dc_charpointer(domain)),
lib.dc_provider_unref,
)
if provider == ffi.NULL:
raise ProviderNotFoundError("Provider not found")
self._provider = provider
@classmethod
def from_email(cls, email):
"""Create provider info from an email address.
:param email: Email address to get provider info for.
"""
return cls(email.split('@')[-1])
@property
def overview_page(self):
"""URL to the overview page of the provider on providers.delta.chat."""
return from_dc_charpointer(
lib.dc_provider_get_overview_page(self._provider))
@property
def name(self):
"""The name of the provider."""
return from_dc_charpointer(lib.dc_provider_get_name(self._provider))
@property
def markdown(self):
"""Content of the information page, formatted as markdown."""
return from_dc_charpointer(
lib.dc_provider_get_markdown(self._provider))
@property
def status_date(self):
"""The date the provider info was last updated, as a string."""
return from_dc_charpointer(
lib.dc_provider_get_status_date(self._provider))
@property
def status(self):
"""The status of the provider information.
This is one of the
:attr:`deltachat.const.DC_PROVIDER_STATUS_OK`,
:attr:`deltachat.const.DC_PROVIDER_STATUS_PREPARATION` or
:attr:`deltachat.const.DC_PROVIDER_STATUS_BROKEN` constants.
"""
return lib.dc_provider_get_status(self._provider)

View File

@@ -24,6 +24,17 @@ def pytest_configure(config):
config.option.liveconfig = cfg
@pytest.hookimpl(trylast=True)
def pytest_runtest_call(item):
# perform early finalization because we otherwise get cloberred
# output from concurrent threads printing between execution
# of the test function and the teardown phase of that test function
if "acfactory" in item.funcargs:
print("*"*30, "finalizing", "*"*30)
acfactory = item.funcargs["acfactory"]
acfactory.finalize()
def pytest_report_header(config, startdir):
summary = []
@@ -125,17 +136,13 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig):
fin = self._finalizers.pop()
fin()
def make_account(self, path, logid):
ac = Account(path, logid=logid)
self._finalizers.append(ac.shutdown)
return ac
def get_unconfigured_account(self):
self.offline_count += 1
tmpdb = tmpdir.join("offlinedb%d" % self.offline_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
ac = Account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
ac._evlogger.init_time = self.init_time
ac._evlogger.set_timeout(2)
self._finalizers.append(ac.shutdown)
return ac
def get_configured_offline_account(self):
@@ -158,35 +165,26 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig):
if "e2ee_enabled" not in configdict:
configdict["e2ee_enabled"] = "1"
tmpdb = tmpdir.join("livedb%d" % self.live_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
ac = Account(tmpdb.strpath, logid="ac{}".format(self.live_count))
ac._evlogger.init_time = self.init_time
ac._evlogger.set_timeout(30)
ac.configure(**configdict)
ac.start_threads()
self._finalizers.append(ac.shutdown)
return ac
def get_two_online_accounts(self):
ac1 = self.get_online_configuring_account()
ac2 = self.get_online_configuring_account()
wait_successful_IMAP_SMTP_connection(ac1)
wait_configuration_progress(ac1, 1000)
wait_successful_IMAP_SMTP_connection(ac2)
wait_configuration_progress(ac2, 1000)
return ac1, ac2
def clone_online_account(self, account):
self.live_count += 1
tmpdb = tmpdir.join("livedb%d" % self.live_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
ac = Account(tmpdb.strpath, logid="ac{}".format(self.live_count))
ac._evlogger.init_time = self.init_time
ac._evlogger.set_timeout(30)
ac.configure(addr=account.get_config("addr"), mail_pw=account.get_config("mail_pw"))
ac.start_threads()
self._finalizers.append(ac.shutdown)
return ac
am = AccountMaker()
request.addfinalizer(am.finalize)
return am
return AccountMaker()
@pytest.fixture
@@ -215,15 +213,6 @@ def wait_configuration_progress(account, target):
break
def wait_securejoin_inviter_progress(account, target):
while 1:
evt_name, data1, data2 = \
account._evlogger.get_matching("DC_EVENT_SECUREJOIN_INVITER_PROGRESS")
if data2 >= target:
print("** SECUREJOINT-INVITER PROGRESS {}".format(target), account)
break
def wait_successful_IMAP_SMTP_connection(account):
imap_ok = smtp_ok = False
while not imap_ok or not smtp_ok:

View File

@@ -4,7 +4,7 @@ import os
from deltachat import const, Account
from deltachat.message import Message
from datetime import datetime, timedelta
from conftest import wait_configuration_progress, wait_successful_IMAP_SMTP_connection, wait_securejoin_inviter_progress
from conftest import wait_configuration_progress, wait_successful_IMAP_SMTP_connection
class TestOfflineAccountBasic:
@@ -140,22 +140,6 @@ class TestOfflineChat:
chat.set_name("title2")
assert chat.get_name() == "title2"
@pytest.mark.parametrize("verified", [True, False])
def test_group_chat_qr(self, acfactory, ac1, verified):
ac2 = acfactory.get_configured_offline_account()
chat = ac1.create_group_chat(name="title1", verified=verified)
qr = chat.get_join_qr()
assert ac2.check_qr(qr).is_ask_verifygroup
def test_get_set_profile_image_simple(self, ac1, data):
chat = ac1.create_group_chat(name="title1")
p = data.get_path("d.png")
chat.set_profile_image(p)
p2 = chat.get_profile_image()
assert open(p, "rb").read() == open(p2, "rb").read()
chat.remove_profile_image()
assert chat.get_profile_image() is None
def test_delete_and_send_fails(self, ac1, chat1):
chat1.delete()
ac1._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
@@ -322,23 +306,12 @@ class TestOfflineChat:
chat1.set_draft(None)
assert chat1.get_draft() is None
def test_qr_setup_contact(self, acfactory, lp):
ac1 = acfactory.get_configured_offline_account()
ac2 = acfactory.get_configured_offline_account()
qr = ac1.get_setup_contact_qr()
assert qr.startswith("OPENPGP4FPR:")
res = ac2.check_qr(qr)
assert res.is_ask_verifycontact()
assert not res.is_ask_verifygroup()
assert res.contact_id == 10
class TestOnlineAccount:
def get_chat(self, ac1, ac2):
c2 = ac1.create_contact(email=ac2.get_config("addr"))
chat = ac1.create_chat_by_contact(c2)
assert chat.id >= const.DC_CHAT_ID_LAST_SPECIAL
return chat
def test_one_account_init(self, acfactory):
ac1 = acfactory.get_online_configuring_account()
wait_successful_IMAP_SMTP_connection(ac1)
wait_configuration_progress(ac1, 1000)
def test_one_account_send(self, acfactory):
ac1 = acfactory.get_online_configuring_account()
@@ -354,8 +327,15 @@ class TestOnlineAccount:
assert ev[1] == msg_out.id
def test_two_accounts_send_receive(self, acfactory):
ac1, ac2 = acfactory.get_two_online_accounts()
chat = self.get_chat(ac1, ac2)
ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.get_online_configuring_account()
c2 = ac1.create_contact(email=ac2.get_config("addr"))
chat = ac1.create_chat_by_contact(c2)
assert chat.id >= const.DC_CHAT_ID_LAST_SPECIAL
wait_successful_IMAP_SMTP_connection(ac1)
wait_configuration_progress(ac1, 1000)
wait_successful_IMAP_SMTP_connection(ac2)
wait_configuration_progress(ac2, 1000)
msg_out = chat.send_text("message1")
@@ -366,8 +346,15 @@ class TestOnlineAccount:
assert msg_in.text == "message1"
def test_forward_messages(self, acfactory):
ac1, ac2 = acfactory.get_two_online_accounts()
chat = self.get_chat(ac1, ac2)
ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.get_online_configuring_account()
c2 = ac1.create_contact(email=ac2.get_config("addr"))
chat = ac1.create_chat_by_contact(c2)
assert chat.id >= const.DC_CHAT_ID_LAST_SPECIAL
wait_successful_IMAP_SMTP_connection(ac1)
wait_configuration_progress(ac1, 1000)
wait_successful_IMAP_SMTP_connection(ac2)
wait_configuration_progress(ac2, 1000)
msg_out = chat.send_text("message2")
@@ -391,10 +378,15 @@ class TestOnlineAccount:
assert not chat3.get_messages()
def test_send_and_receive_message(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("starting accounts, waiting for configuration")
ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.get_online_configuring_account()
c2 = ac1.create_contact(email=ac2.get_config("addr"))
chat = ac1.create_chat_by_contact(c2)
assert chat.id >= const.DC_CHAT_ID_LAST_SPECIAL
lp.sec("ac1: create chat with ac2")
chat = self.get_chat(ac1, ac2)
wait_configuration_progress(ac1, 1000)
wait_configuration_progress(ac2, 1000)
lp.sec("sending text message from ac1 to ac2")
msg_out = chat.send_text("message1")
@@ -436,10 +428,15 @@ class TestOnlineAccount:
assert msg_out.is_out_mdn_received()
def test_send_and_receive_will_encrypt_decrypt(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("starting accounts, waiting for configuration")
ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.get_online_configuring_account()
c2 = ac1.create_contact(email=ac2.get_config("addr"))
chat = ac1.create_chat_by_contact(c2)
assert chat.id >= const.DC_CHAT_ID_LAST_SPECIAL
lp.sec("ac1: create chat with ac2")
chat = self.get_chat(ac1, ac2)
wait_configuration_progress(ac1, 1000)
wait_configuration_progress(ac2, 1000)
lp.sec("sending text message from ac1 to ac2")
msg_out = chat.send_text("message1")
@@ -462,12 +459,14 @@ class TestOnlineAccount:
assert msg_back.text == "message-back"
def test_saved_mime_on_received_message(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("configure ac2 to save mime headers, create ac1/ac2 chat")
lp.sec("starting accounts, waiting for configuration")
ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.get_online_configuring_account()
ac2.set_config("save_mime_headers", "1")
chat = self.get_chat(ac1, ac2)
c2 = ac1.create_contact(email=ac2.get_config("addr"))
chat = ac1.create_chat_by_contact(c2)
wait_configuration_progress(ac1, 1000)
wait_configuration_progress(ac2, 1000)
lp.sec("sending text message from ac1 to ac2")
msg_out = chat.send_text("message1")
ac1._evlogger.get_matching("DC_EVENT_MSG_DELIVERED")
@@ -481,8 +480,14 @@ class TestOnlineAccount:
assert mime.get_all("Received")
def test_send_and_receive_image(self, acfactory, lp, data):
ac1, ac2 = acfactory.get_two_online_accounts()
chat = self.get_chat(ac1, ac2)
lp.sec("starting accounts, waiting for configuration")
ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.get_online_configuring_account()
c2 = ac1.create_contact(email=ac2.get_config("addr"))
chat = ac1.create_chat_by_contact(c2)
wait_configuration_progress(ac1, 1000)
wait_configuration_progress(ac2, 1000)
lp.sec("sending image message from ac1 to ac2")
path = data.get_path("d.png")
@@ -502,13 +507,13 @@ class TestOnlineAccount:
assert os.stat(msg_in.filename).st_size == os.stat(path).st_size
def test_import_export_online(self, acfactory, tmpdir):
backupdir = tmpdir.mkdir("backup")
ac1 = acfactory.get_online_configuring_account()
wait_configuration_progress(ac1, 1000)
contact1 = ac1.create_contact("some1@hello.com", name="some1")
chat = ac1.create_chat_by_contact(contact1)
chat.send_text("msg1")
backupdir = tmpdir.mkdir("backup")
path = ac1.export_to_dir(backupdir.strpath)
assert os.path.exists(path)
@@ -537,82 +542,7 @@ class TestOnlineAccount:
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg = ac2.get_message_by_id(ev[2])
assert msg.is_setup_message()
# first try a bad setup code
with pytest.raises(ValueError):
msg.continue_key_transfer(str(reversed(setup_code)))
print("*************** Incoming ASM File at: ", msg.filename)
print("*************** Setup Code: ", setup_code)
msg.continue_key_transfer(setup_code)
assert ac1.get_info()["fingerprint"] == ac2.get_info()["fingerprint"]
def test_qr_setup_contact(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("ac1: create QR code and let ac2 scan it, starting the securejoin")
qr = ac1.get_setup_contact_qr()
lp.sec("ac2: start QR-code based setup contact protocol")
ch = ac2.qr_setup_contact(qr)
assert ch.id >= 10
wait_securejoin_inviter_progress(ac1, 1000)
def test_qr_join_chat(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("ac1: create QR code and let ac2 scan it, starting the securejoin")
chat = ac1.create_group_chat("hello")
qr = chat.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
ch = ac2.qr_join_chat(qr)
assert ch.id >= 10
wait_securejoin_inviter_progress(ac1, 1000)
def test_set_get_profile_image(self, acfactory, data, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("create unpromoted group chat")
chat = ac1.create_group_chat("hello")
p = data.get_path("d.png")
lp.sec("ac1: set profile image on unpromoted chat")
chat.set_profile_image(p)
ac1._evlogger.get_matching("DC_EVENT_CHAT_MODIFIED")
assert not chat.is_promoted()
lp.sec("ac1: send text to promote chat (XXX without contact added)")
# XXX first promote the chat before adding contact
# because DC does not send out profile images for unpromoted chats
# otherwise
chat.send_text("ac1: initial message to promote chat (workaround)")
assert chat.is_promoted()
lp.sec("ac2: add ac1 to a chat so the message does not land in DEADDROP")
c1 = ac2.create_contact(email=ac1.get_config("addr"))
ac2.create_chat_by_contact(c1)
ev = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
lp.sec("ac1: add ac2 to promoted group chat")
c2 = ac1.create_contact(email=ac2.get_config("addr"))
chat.add_contact(c2)
lp.sec("ac1: send a first message to ac2")
chat.send_text("hi")
assert chat.is_promoted()
lp.sec("ac2: wait for receiving message from ac1")
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
msg_in = ac2.get_message_by_id(ev[2])
assert not msg_in.chat.is_deaddrop()
lp.sec("ac2: create chat and read profile image")
chat2 = ac2.create_chat_by_message(msg_in)
p2 = chat2.get_profile_image()
assert p2 is not None
assert open(p2, "rb").read() == open(p, "rb").read()
ac2._evlogger.consume_events()
ac1._evlogger.consume_events()
lp.sec("ac2: delete profile image from chat")
chat2.remove_profile_image()
ev = ac1._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
assert ev[1] == chat.id
chat1b = ac1.create_chat_by_message(ev[2])
assert chat1b.get_profile_image() is None
assert chat.get_profile_image() is None

View File

@@ -4,7 +4,7 @@ from deltachat import const
from conftest import wait_configuration_progress, wait_msgs_changed
class TestOnlineInCreation:
class TestInCreation:
def test_forward_increation(self, acfactory, data, lp):
ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.get_online_configuring_account()

View File

@@ -1,5 +1,5 @@
from __future__ import print_function
from deltachat import capi, cutil, const, set_context_callback, clear_context_callback
from deltachat import capi, const, set_context_callback, clear_context_callback
from deltachat.capi import ffi
from deltachat.capi import lib
from deltachat.account import EventLogger
@@ -17,19 +17,11 @@ def test_callback_None2int():
clear_context_callback(ctx)
def test_dc_close_events(tmpdir):
ctx = ffi.gc(
capi.lib.dc_context_new(capi.lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
def test_dc_close_events():
ctx = capi.lib.dc_context_new(capi.lib.py_dc_callback, ffi.NULL, ffi.NULL)
evlog = EventLogger(ctx)
evlog.set_timeout(5)
set_context_callback(
ctx,
lambda ctx, evt_name, data1, data2: evlog(evt_name, data1, data2)
)
p = tmpdir.join("hello.db")
lib.dc_open(ctx, p.strpath.encode("ascii"), ffi.NULL)
set_context_callback(ctx, lambda ctx, evt_name, data1, data2: evlog(evt_name, data1, data2))
capi.lib.dc_close(ctx)
def find(info_string):
@@ -83,18 +75,3 @@ def test_markseen_invalid_message_ids(acfactory):
msg_ids = [9]
lib.dc_markseen_msgs(ac1._dc_context, msg_ids, len(msg_ids))
ac1._evlogger.ensure_event_not_queued("DC_EVENT_WARNING|DC_EVENT_ERROR")
def test_provider_info():
provider = lib.dc_provider_new_from_email(cutil.as_dc_charpointer("ex@example.com"))
assert cutil.from_dc_charpointer(
lib.dc_provider_get_overview_page(provider)
) == "https://providers.delta.chat/example.com"
assert cutil.from_dc_charpointer(lib.dc_provider_get_name(provider)) == "Example"
assert cutil.from_dc_charpointer(lib.dc_provider_get_markdown(provider)) == "\n..."
assert cutil.from_dc_charpointer(lib.dc_provider_get_status_date(provider)) == "2018-09"
assert lib.dc_provider_get_status(provider) == const.DC_PROVIDER_STATUS_PREPARATION
def test_provider_info_none():
assert lib.dc_provider_new_from_email(cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL

View File

@@ -1,27 +0,0 @@
import pytest
from deltachat import const
from deltachat import provider
def test_provider_info_from_email():
example = provider.Provider.from_email("email@example.com")
assert example.overview_page == "https://providers.delta.chat/example.com"
assert example.name == "Example"
assert example.markdown == "\n..."
assert example.status_date == "2018-09"
assert example.status == const.DC_PROVIDER_STATUS_PREPARATION
def test_provider_info_from_domain():
example = provider.Provider("example.com")
assert example.overview_page == "https://providers.delta.chat/example.com"
assert example.name == "Example"
assert example.markdown == "\n..."
assert example.status_date == "2018-09"
assert example.status == const.DC_PROVIDER_STATUS_PREPARATION
def test_provider_info_none():
with pytest.raises(provider.ProviderNotFoundError):
provider.Provider.from_email("email@unexistent.no")

View File

@@ -1,6 +1,7 @@
[tox]
# make sure to update environment list in travis.yml and appveyor.yml
envlist =
py27
py35
lint
auditwheels
@@ -16,15 +17,12 @@ passenv =
DCC_PY_LIVECONFIG
deps =
pytest
pytest-rerunfailures
pytest-timeout
pytest-xdist
pytest-faulthandler
pdbpp
requests
[testenv:auditwheels]
skipsdist = True
deps = auditwheel
commands =
python tests/auditwheels.py {toxworkdir}/wheelhouse
@@ -45,7 +43,7 @@ commands =
[testenv:doc]
basepython = python3.5
deps =
sphinx==2.2.0
sphinx==2.0.1
breathe
changedir = doc
@@ -54,12 +52,11 @@ commands =
[pytest]
addopts = -v -rs --reruns 3 --reruns-delay 2
addopts = -v -rs
python_files = tests/test_*.py
norecursedirs = .tox
xfail_strict=true
timeout = 60
timeout_method = thread
timeout = 60
[flake8]
max-line-length = 120

372
spec.md
View File

@@ -1,372 +0,0 @@
# Chat-over-Email specification
Version 0.19.0
This document describes how emails can be used
to implement typical messenger functions
while staying compatible to existing MUAs.
- [Encryption](#encryption)
- [Outgoing messages](#outgoing-messages)
- [Incoming messages](#incoming-messages)
- [Forwarded messages](#forwarded-messages)
- [Groups](#groups)
- [Outgoing group messages](#outgoing-group-messages)
- [Incoming group messages](#incoming-group-messages)
- [Add and remove members](#add-and-remove-members)
- [Change group name](#change-group-name)
- [Set group image](#set-group-image)
- [Set profile image](#set-profile-image)
- [Miscellaneous](#miscellaneous)
# Encryption
Messages SHOULD be encrypted by the
[Autocrypt](https://autocrypt.org/level1.html) standard;
`prefer-encrypt=mutual` MAY be set by default.
Meta data (at least the subject and all chat-headers) SHOULD be encrypted
by the [Memoryhole](https://github.com/autocrypt/memoryhole) standard.
If Memoryhole is not used,
the subject of encrypted messages SHOULD be replaced by the string
`Chat: Encrypted message` where the part after the colon MAY be localized.
# Outgoing messages
Messengers MUST add a `Chat-Version: 1.0` header to outgoing messages.
For filtering and smart appearance of the messages in normal MUAs,
the `Subject` header SHOULD start with the characters `Chat:`
and SHOULD be an excerpt of the message.
Replies to messages MAY follow the typical `Re:`-format.
The body MAY contain text which MUST have the content type `text/plain`
or `mulipart/alternative` containing `text/plain`.
The text MAY be divided into a user-text-part and a footer-part using the
line `-- ` (minus, minus, space, lineend).
The user-text-part MUST contain only user generated content.
User generated content are eg. texts a user has actually typed
or pasted or forwarded from another user.
Full quotes, footers or sth. like that MUST NOT go to the user-text-part.
From: sender@domain
To: rcpt@domain
Chat-Version: 1.0
Content-Type: text/plain
Subject: Chat: Hello ...
Hello world!
# Incoming messages
The `Chat-Version` header MAY be used
to detect if a messages comes from a compatible messenger.
The `Subject` header MUST NOT be used
to detect compatible messengers, groups or whatever.
Messenger SHOULD show the `Subject`
if the message comes from a normal MUA together with the email-body.
The email-body SHOULD be converted
to plain text, full-quotes and similar regions SHOULD be cut.
Attachments SHOULD be shown where possible.
If an attachment cannot be shown, a non-distracting warning SHOULD be printed.
# Forwarded messages
Forwarded messages are outgoing messages that contain a forwarded-header
before the user generated content.
The forwarded header MUST contain two lines:
The first line contains the text
`---------- Forwarded message ----------`
(10 minus, space, text `Forwarded message`, space, 10 minus).
The second line starts with `From: ` followed by the original sender
which SHOULD be anonymized or just a placeholder.
From: sender@domain
To: rcpt@domain
Chat-Version: 1.0
Content-Type: text/plain
Subject: Chat: Forwarded message
---------- Forwarded message ----------
From: Messenger
Hello world!
Incoming forwarded messages are detected by the header.
The messenger SHOULD mark these messages in a way that
it becomes obvious that the message is not created by the sender.
Note that most messengers do not show the original sender of forwarded messages
but MUAs typically expose the sender in the UI.
# Groups
Groups are chats with usually more than one recipient,
each defined by an email-address.
The sender plus the recipients are the group members.
To allow different groups with the same members,
groups are identified by a group-id.
The group-id MUST be created only from the characters
`0`-`9`, `A`-`Z`, `a`-`z` `_` and `-`.
Groups MUST have a group-name.
The group-name is any non-zero-length UTF-8 string.
Groups MAY have a group-image.
## Outgoing groups messages
All group members MUST be added to the `From`/`To` headers.
The group-id MUST be written to the `Chat-Group-ID` header.
The group-name MUST be written to `Chat-Group-Name` header
(the forced presence of this header makes it easier
to join a group chat on a second device any time).
The `Subject` header of outgoing group messages
SHOULD start with the characters `Chat:`
followed by the group-name and a colon followed by an excerpt of the message.
To identify the group-id on replies from normal MUAs,
the group-id MUST also be added to the message-id of outgoing messages.
The message-id MUST have the format `Gr.<group-id>.<unique data>`.
From: member1@domain
To: member2@domain, member3@domain
Chat-Version: 1.0
Chat-Group-ID: 1234xyZ
Chat-Group-Name: My Group
Message-ID: Gr.1234xyZ.0001@domain
Subject: Chat: My Group: Hello group ...
Hello group - this group contains three members
Messengers adding the member list in the form `Name <email-address>`
MUST take care only to spread the names authorized by the contacts themselves.
Otherwise, names as _Daddy_ or _Honey_ may be spread
(this issue is also true for normal MUAs, however,
for more contact- and chat-centralized apps
such situations happen more frequently).
## Incoming group messages
The messenger MUST search incoming messages for the group-id
in the following headers: `Chat-Group-ID`,
`Message-ID`, `In-Reply-To` and `References` (in this order).
If the messenger finds a valid and existent group-id,
the message SHOULD be assigned to the given group.
If the messenger finds a valid but not existent group-id,
the messenger MAY create a new group.
If no group-id is found,
the message MAY be assigned
to a normal single-user chat with the email-address given in `From`.
## Add and remove members
Messenger clients MUST construct the member list
from the `From`/`To` headers only on the first group message
or if they see a `Chat-Group-Member-Added`
or `Chat-Group-Member-Removed` action header.
Both headers MUST have the email-address
of the added or removed member as the value.
Messenger clients MUST NOT construct the member list
on other group messages
(this is to avoid accidentally altered To-lists in normal MUAs;
the user does not expect adding a user to a _message_
will also add him to the _group_ "forever").
The messenger SHOULD send an explicit mail for each added or removed member.
The body of the message SHOULD contain
a localized description about what happened
and the message SHOULD appear as a message or action from the sender.
From: member1@domain
To: member2@domain, member3@domain, member4@domain
Chat-Version: 1.0
Chat-Group-ID: 1234xyZ
Chat-Group-Name: My Group
Chat-Group-Member-Added: member4@domain
Message-ID: Gr.1234xyZ.0002@domain
Subject: Chat: My Group: Hello, ...
Hello, I've added member4@domain to our group. Now we have 4 members.
To remove a member:
From: member1@domain
To: member2@domain, member3@domain
Chat-Version: 1.0
Chat-Group-ID: 1234xyZ
Chat-Group-Name: My Group
Chat-Group-Member-Removed: member4@domain
Message-ID: Gr.1234xyZ.0003@domain
Subject: Chat: My Group: Hello, ...
Hello, I've removed member4@domain from our group. Now we have 3 members.
## Change group name
To change the group-name,
the messenger MUST send the action header `Chat-Group-Name-Changed`
with the value set to the old group name to all group members.
The new group name goes to the header `Chat-Group-Name`.
The messenger SHOULD send an explicit mail for each name change.
The body of the message SHOULD contain
a localized description about what happened
and the message SHOULD appear as a message or action from the sender.
From: member1@domain
To: member2@domain, member3@domain
Chat-Version: 1.0
Chat-Group-ID: 1234xyZ
Chat-Group-Name: Our Group
Chat-Group-Name-Changed: My Group
Message-ID: Gr.1234xyZ.0004@domain
Subject: Chat: Our Group: Hello, ...
Hello, I've changed the group name from "My Group" to "Our Group".
## Set group image
A group MAY have a group-image.
To change or set the group-image,
the messenger MUST attach an image file to a message
and MUST add the header `Chat-Group-Image`
with the value set to the image name.
To remove the group-image,
the messenger MUST add the header `Chat-Group-Image: 0`.
The messenger SHOULD send an explicit mail for each group image change.
The body of the message SHOULD contain
a localized description about what happened
and the message SHOULD appear as a message or action from the sender.
From: member1@domain
To: member2@domain, member3@domain
Chat-Version: 1.0
Chat-Group-ID: 1234xyZ
Chat-Group-Name: Our Group
Chat-Group-Image: image.jpg
Message-ID: Gr.1234xyZ.0005@domain
Subject: Chat: Our Group: Hello, ...
Content-Type: multipart/mixed; boundary="==break=="
--==break==
Content-Type: text/plain
Hello, I've changed the group image.
--==break==
Content-Type: image/jpeg
Content-Disposition: attachment; filename="image.jpg"
/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBw ...
--==break==--
The image format SHOULD be image/jpeg or image/png.
To save data, it is RECOMMENDED
to add a `Chat-Group-Image` only on image changes.
# Set profile image
A user MAY have a profile-image that MAY be spread to his contacts.
To change or set the profile-image,
the messenger MUST attach an image file to a message
and MUST add the header `Chat-Profile-Image`
with the value set to the image name.
To remove the profile-image,
the messenger MUST add the header `Chat-Profile-Image: 0`.
To spread the image,
the messenger MAY send the profile image
together with the next mail to a given contact
(to do this only once,
the messenger has to keep a `profile_image_update_state` somewhere).
Alternatively, the messenger MAY send an explicit mail
for each profile-image change to all contacts using a compatible messenger.
The messenger SHOULD NOT send an explicit mail to normal MUAs.
From: sender@domain
To: rcpt@domain
Chat-Version: 1.0
Chat-Profile-Image: photo.jpg
Subject: Chat: Hello, ...
Content-Type: multipart/mixed; boundary="==break=="
--==break==
Content-Type: text/plain
Hello, I've changed my profile image.
--==break==
Content-Type: image/jpeg
Content-Disposition: attachment; filename="photo.jpg"
AKCgkJi3j4l5kjoldfUAKCgkJi3j4lldfHjgWICwgIEBQYFBA ...
--==break==--
The image format SHOULD be image/jpeg or image/png.
Note that `Chat-Profile-Image` may appear together with all other headers,
eg. there may be a `Chat-Profile-Image` and a `Chat-Group-Image` header
in the same message.
To save data, it is RECOMMENDED to add a `Chat-Profile-Image` header
only on image changes.
# Miscellaneous
Messengers SHOULD use the header `Chat-Predecessor`
instead of `In-Reply-To` as the latter one results
in infinite threads on typical MUAs.
Messengers SHOULD add a `Chat-Voice-message: 1` header
if an attached audio file is a voice message.
Messengers MAY add a `Chat-Duration` header
to specify the duration of attached audio or video files.
The value MUST be the duration in milliseconds.
This allows the receiver to show the time without knowing the file format.
Chat-Predecessor: foo123@domain
Chat-Voice-Message: 1
Chat-Duration: 10000
Messengers MAY send and receive Message Disposition Notifications
(MDNs, [RFC 8098](https://tools.ietf.org/html/rfc8098),
[RFC 3503](https://tools.ietf.org/html/rfc3503))
using the `Chat-Disposition-Notification-To` header
instead of the `Disposition-Notification-To`
(which unfortunately forces many other MUAs
to send weird mails not following any standard).
## Sync messages
If some action is required by a message header,
the action should only be performed if the _effective date_ is newer
than the date the last action was performed.
We define the effective date of a message
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.
Copyright © 2017-2019 Delta Chat contributors.

View File

@@ -7,6 +7,7 @@ use mmime::mailimf_types::*;
use crate::constants::*;
use crate::contact::*;
use crate::dc_tools::as_str;
use crate::key::*;
/// Possible values for encryption preference
@@ -63,8 +64,11 @@ impl Aheader {
}
}
pub fn from_imffields(wanted_from: &str, header: *const mailimf_fields) -> Option<Self> {
if header.is_null() {
pub fn from_imffields(
wanted_from: *const libc::c_char,
header: *const mailimf_fields,
) -> Option<Self> {
if wanted_from.is_null() || header.is_null() {
return None;
}
@@ -90,7 +94,7 @@ impl Aheader {
match Self::from_str(value) {
Ok(test) => {
if addr_cmp(&test.addr, wanted_from) {
if addr_cmp(&test.addr, as_str(wanted_from)) {
if fine_header.is_none() {
fine_header = Some(test);
} else {

File diff suppressed because it is too large Load Diff

View File

@@ -31,13 +31,17 @@ use crate::stock::StockMessage;
/// first entry and only present on new messages, there is the rough idea that it can be optionally always
/// present and sorted into the list by date. Rendering the deaddrop in the described way
/// would not add extra work in the UI then.
#[derive(Debug)]
pub struct Chatlist {
pub struct Chatlist<'a> {
context: &'a Context,
/// Stores pairs of `chat_id, message_id`
ids: Vec<(u32, u32)>,
}
impl Chatlist {
impl<'a> Chatlist<'a> {
pub fn get_context(&self) -> &Context {
self.context
}
/// Get a list of chats.
/// The list can be filtered by query parameters.
///
@@ -81,7 +85,7 @@ impl Chatlist {
/// `query_contact_id`: An optional contact ID for filtering the list. Only chats including this contact ID
/// are returned.
pub fn try_load(
context: &Context,
context: &'a Context,
listflags: usize,
query: Option<&str>,
query_contact_id: Option<u32>,
@@ -191,12 +195,12 @@ impl Chatlist {
if 0 != add_archived_link_item && dc_get_archived_cnt(context) > 0 {
if ids.is_empty() && 0 != listflags & DC_GCL_ADD_ALLDONE_HINT {
ids.push((DC_CHAT_ID_ALLDONE_HINT, 0));
ids.push((DC_CHAT_ID_ALLDONE_HINT as u32, 0));
}
ids.push((DC_CHAT_ID_ARCHIVED_LINK, 0));
ids.push((DC_CHAT_ID_ARCHIVED_LINK as u32, 0));
}
Ok(Chatlist { ids })
Ok(Chatlist { context, ids })
}
/// Find out the number of chats.
@@ -243,7 +247,7 @@ impl Chatlist {
/// - dc_lot_t::timestamp: the timestamp of the message. 0 if not applicable.
/// - dc_lot_t::state: The state of the message as one of the DC_STATE_* constants (see #dc_msg_get_state()).
// 0 if not applicable.
pub fn get_summary(&self, context: &Context, index: usize, chat: Option<&Chat>) -> Lot {
pub unsafe fn get_summary(&self, index: usize, chat: Option<&Chat<'a>>) -> Lot {
// The summary is created by the chat, not by the last message.
// This is because we may want to display drafts here or stuff as
// "is typing".
@@ -259,7 +263,7 @@ impl Chatlist {
let chat = if let Some(chat) = chat {
chat
} else {
if let Ok(chat) = Chat::load_from_db(context, self.ids[index].0) {
if let Ok(chat) = Chat::load_from_db(self.context, self.ids[index].0) {
chat_loaded = chat;
&chat_loaded
} else {
@@ -271,11 +275,11 @@ impl Chatlist {
let mut lastcontact = None;
let lastmsg = if 0 != lastmsg_id {
if let Ok(lastmsg) = dc_msg_load_from_db(context, lastmsg_id) {
if let Ok(lastmsg) = dc_msg_load_from_db(self.context, lastmsg_id) {
if lastmsg.from_id != 1 as libc::c_uint
&& (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup)
{
lastcontact = Contact::load_from_db(context, lastmsg.from_id).ok();
lastcontact = Contact::load_from_db(self.context, lastmsg.from_id).ok();
}
Some(lastmsg)
@@ -286,13 +290,19 @@ impl Chatlist {
None
};
if chat.id == DC_CHAT_ID_ARCHIVED_LINK {
if chat.id == DC_CHAT_ID_ARCHIVED_LINK as u32 {
ret.text2 = None;
} else if lastmsg.is_none() || lastmsg.as_ref().unwrap().from_id == DC_CONTACT_ID_UNDEFINED
} else if lastmsg.is_none()
|| lastmsg.as_ref().unwrap().from_id == DC_CONTACT_ID_UNDEFINED as u32
{
ret.text2 = Some(context.stock_str(StockMessage::NoMessages).to_string());
ret.text2 = Some(self.context.stock_str(StockMessage::NoMessages).to_string());
} else {
ret.fill(&mut lastmsg.unwrap(), chat, lastcontact.as_ref(), context);
ret.fill(
&mut lastmsg.unwrap(),
chat,
lastcontact.as_ref(),
self.context,
);
}
ret
@@ -302,10 +312,11 @@ impl Chatlist {
pub fn dc_get_archived_cnt(context: &Context) -> u32 {
context
.sql
.query_get_value(
.query_row_col(
context,
"SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;",
params![],
0,
)
.unwrap_or_default()
}
@@ -315,7 +326,7 @@ fn get_last_deaddrop_fresh_msg(context: &Context) -> u32 {
// only few fresh messages.
context
.sql
.query_get_value(
.query_row_col(
context,
"SELECT m.id FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id \
WHERE m.state=10 \
@@ -323,6 +334,7 @@ fn get_last_deaddrop_fresh_msg(context: &Context) -> u32 {
AND c.blocked=2 \
ORDER BY m.timestamp DESC, m.id DESC;",
params![],
0,
)
.unwrap_or_default()
}

View File

@@ -33,7 +33,6 @@ pub enum Config {
E2eeEnabled,
#[strum(props(default = "1"))]
MdnsEnabled,
#[strum(props(default = "1"))]
InboxWatch,
#[strum(props(default = "1"))]
SentboxWatch,
@@ -73,7 +72,7 @@ impl Context {
let value = match key {
Config::Selfavatar => {
let rel_path = self.sql.get_config(self, key);
rel_path.map(|p| dc_get_abs_path(self, &p).to_str().unwrap().to_string())
rel_path.map(|p| dc_get_abs_path_safe(self, &p).to_str().unwrap().to_string())
}
Config::SysVersion => Some((&*DC_VERSION_STR).clone()),
Config::SysMsgsizeMaxRecommended => Some(format!("{}", 24 * 1024 * 1024 / 4 * 3)),

View File

@@ -1,10 +1,9 @@
use quick_xml;
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
use crate::constants::*;
use crate::context::Context;
use crate::dc_loginparam::*;
use crate::dc_tools::*;
use crate::login_param::LoginParam;
use crate::x::*;
use super::read_autoconf_file;
@@ -14,10 +13,10 @@ use super::read_autoconf_file;
/* documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration */
#[repr(C)]
struct moz_autoconfigure_t<'a> {
pub in_0: &'a LoginParam,
pub in_emaildomain: &'a str,
pub in_emaillocalpart: &'a str,
pub out: LoginParam,
pub in_0: &'a dc_loginparam_t,
pub in_emaildomain: *mut libc::c_char,
pub in_emaillocalpart: *mut libc::c_char,
pub out: dc_loginparam_t,
pub out_imap_set: libc::c_int,
pub out_smtp_set: libc::c_int,
pub tag_server: libc::c_int,
@@ -27,37 +26,44 @@ struct moz_autoconfigure_t<'a> {
pub unsafe fn moz_autoconfigure(
context: &Context,
url: &str,
param_in: &LoginParam,
) -> Option<LoginParam> {
let xml_raw = read_autoconf_file(context, url);
param_in: &dc_loginparam_t,
) -> Option<dc_loginparam_t> {
let mut moz_ac = moz_autoconfigure_t {
in_0: param_in,
in_emaildomain: std::ptr::null_mut(),
in_emaillocalpart: std::ptr::null_mut(),
out: dc_loginparam_new(),
out_imap_set: 0,
out_smtp_set: 0,
tag_server: 0,
tag_config: 0,
};
let url_c = url.strdup();
let xml_raw = read_autoconf_file(context, url_c);
free(url_c as *mut libc::c_void);
if xml_raw.is_null() {
return None;
}
// Split address into local part and domain part.
let p = param_in.addr.find("@");
if p.is_none() {
moz_ac.in_emaillocalpart = param_in.addr.strdup();
let p = strchr(moz_ac.in_emaillocalpart, '@' as i32);
if p.is_null() {
free(xml_raw as *mut libc::c_void);
free(moz_ac.in_emaildomain as *mut libc::c_void);
free(moz_ac.in_emaillocalpart as *mut libc::c_void);
return None;
}
let (in_emaillocalpart, in_emaildomain) = param_in.addr.split_at(p.unwrap());
let in_emaildomain = &in_emaildomain[1..];
*p = 0 as libc::c_char;
moz_ac.in_emaildomain = dc_strdup(p.offset(1isize));
let mut reader = quick_xml::Reader::from_str(as_str(xml_raw));
reader.trim_text(true);
let mut buf = Vec::new();
let mut moz_ac = moz_autoconfigure_t {
in_0: param_in,
in_emaildomain,
in_emaillocalpart,
out: LoginParam::new(),
out_imap_set: 0,
out_smtp_set: 0,
tag_server: 0,
tag_config: 0,
};
loop {
match reader.read_event(&mut buf) {
Ok(quick_xml::events::Event::Start(ref e)) => {
@@ -70,6 +76,7 @@ pub unsafe fn moz_autoconfigure(
Err(e) => {
error!(
context,
0,
"Configure xml: Error at position {}: {:?}",
reader.buffer_position(),
e
@@ -86,13 +93,17 @@ pub unsafe fn moz_autoconfigure(
|| moz_ac.out.send_server.is_empty()
|| moz_ac.out.send_port == 0
{
let r = moz_ac.out.to_string();
warn!(context, "Bad or incomplete autoconfig: {}", r,);
let r = dc_loginparam_get_readable(&moz_ac.out);
warn!(context, 0, "Bad or incomplete autoconfig: {}", r,);
free(xml_raw as *mut libc::c_void);
free(moz_ac.in_emaildomain as *mut libc::c_void);
free(moz_ac.in_emaillocalpart as *mut libc::c_void);
return None;
}
free(xml_raw as *mut libc::c_void);
free(moz_ac.in_emaildomain as *mut libc::c_void);
free(moz_ac.in_emaillocalpart as *mut libc::c_void);
Some(moz_ac.out)
}
@@ -104,8 +115,8 @@ fn moz_autoconfigure_text_cb<B: std::io::BufRead>(
let val = event.unescape_and_decode(reader).unwrap_or_default();
let addr = &moz_ac.in_0.addr;
let email_local = moz_ac.in_emaillocalpart;
let email_domain = moz_ac.in_emaildomain;
let email_local = as_str(moz_ac.in_emaillocalpart);
let email_domain = as_str(moz_ac.in_emaildomain);
let val = val
.trim()
@@ -121,13 +132,13 @@ fn moz_autoconfigure_text_cb<B: std::io::BufRead>(
13 => {
let val_lower = val.to_lowercase();
if val_lower == "ssl" {
moz_ac.out.server_flags |= DC_LP_IMAP_SOCKET_SSL as i32
moz_ac.out.server_flags |= 0x200
}
if val_lower == "starttls" {
moz_ac.out.server_flags |= DC_LP_IMAP_SOCKET_STARTTLS as i32
moz_ac.out.server_flags |= 0x100
}
if val_lower == "plain" {
moz_ac.out.server_flags |= DC_LP_IMAP_SOCKET_PLAIN as i32
moz_ac.out.server_flags |= 0x400
}
}
_ => {}
@@ -140,13 +151,13 @@ fn moz_autoconfigure_text_cb<B: std::io::BufRead>(
13 => {
let val_lower = val.to_lowercase();
if val_lower == "ssl" {
moz_ac.out.server_flags |= DC_LP_SMTP_SOCKET_SSL as i32
moz_ac.out.server_flags |= 0x20000
}
if val_lower == "starttls" {
moz_ac.out.server_flags |= DC_LP_SMTP_SOCKET_STARTTLS as i32
moz_ac.out.server_flags |= 0x10000
}
if val_lower == "plain" {
moz_ac.out.server_flags |= DC_LP_SMTP_SOCKET_PLAIN as i32
moz_ac.out.server_flags |= 0x40000
}
}
_ => {}

View File

@@ -1,10 +1,9 @@
use quick_xml;
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
use crate::constants::*;
use crate::context::Context;
use crate::dc_loginparam::*;
use crate::dc_tools::*;
use crate::login_param::LoginParam;
use crate::x::*;
use std::ptr;
@@ -14,8 +13,8 @@ use super::read_autoconf_file;
******************************************************************************/
#[repr(C)]
struct outlk_autodiscover_t<'a> {
pub in_0: &'a LoginParam,
pub out: LoginParam,
pub in_0: &'a dc_loginparam_t,
pub out: dc_loginparam_t,
pub out_imap_set: libc::c_int,
pub out_smtp_set: libc::c_int,
pub tag_config: libc::c_int,
@@ -26,13 +25,13 @@ struct outlk_autodiscover_t<'a> {
pub unsafe fn outlk_autodiscover(
context: &Context,
url__: &str,
param_in: &LoginParam,
) -> Option<LoginParam> {
param_in: &dc_loginparam_t,
) -> Option<dc_loginparam_t> {
let mut xml_raw: *mut libc::c_char = ptr::null_mut();
let mut url = url__.strdup();
let mut outlk_ad = outlk_autodiscover_t {
in_0: param_in,
out: LoginParam::new(),
out: dc_loginparam_new(),
out_imap_set: 0,
out_smtp_set: 0,
tag_config: 0,
@@ -51,7 +50,7 @@ pub unsafe fn outlk_autodiscover(
0,
::std::mem::size_of::<outlk_autodiscover_t>(),
);
xml_raw = read_autoconf_file(context, as_str(url));
xml_raw = read_autoconf_file(context, url);
if xml_raw.is_null() {
ok_to_continue = false;
break;
@@ -76,6 +75,7 @@ pub unsafe fn outlk_autodiscover(
Err(e) => {
error!(
context,
0,
"Configure xml: Error at position {}: {:?}",
reader.buffer_position(),
e
@@ -108,8 +108,8 @@ pub unsafe fn outlk_autodiscover(
|| outlk_ad.out.send_server.is_empty()
|| outlk_ad.out.send_port == 0
{
let r = outlk_ad.out.to_string();
warn!(context, "Bad or incomplete autoconfig: {}", r,);
let r = dc_loginparam_get_readable(&outlk_ad.out);
warn!(context, 0, "Bad or incomplete autoconfig: {}", r,);
free(url as *mut libc::c_void);
free(xml_raw as *mut libc::c_void);
outlk_clean_config(&mut outlk_ad);
@@ -126,7 +126,7 @@ pub unsafe fn outlk_autodiscover(
unsafe fn outlk_clean_config(mut outlk_ad: *mut outlk_autodiscover_t) {
for i in 0..6 {
free((*outlk_ad).config[i] as *mut libc::c_void);
(*outlk_ad).config[i] = ptr::null_mut();
(*outlk_ad).config[i] = 0 as *mut libc::c_char;
}
}
@@ -168,9 +168,9 @@ unsafe fn outlk_autodiscover_endtag_cb(event: &BytesEnd, outlk_ad: &mut outlk_au
outlk_ad.out.mail_server = to_string(outlk_ad.config[2]);
outlk_ad.out.mail_port = port;
if 0 != ssl_on {
outlk_ad.out.server_flags |= DC_LP_IMAP_SOCKET_SSL as i32
outlk_ad.out.server_flags |= 0x200
} else if 0 != ssl_off {
outlk_ad.out.server_flags |= DC_LP_IMAP_SOCKET_PLAIN as i32
outlk_ad.out.server_flags |= 0x400
}
outlk_ad.out_imap_set = 1
} else if strcasecmp(
@@ -182,9 +182,9 @@ unsafe fn outlk_autodiscover_endtag_cb(event: &BytesEnd, outlk_ad: &mut outlk_au
outlk_ad.out.send_server = to_string(outlk_ad.config[2]);
outlk_ad.out.send_port = port;
if 0 != ssl_on {
outlk_ad.out.server_flags |= DC_LP_SMTP_SOCKET_SSL as i32
outlk_ad.out.server_flags |= 0x20000
} else if 0 != ssl_off {
outlk_ad.out.server_flags |= DC_LP_SMTP_SOCKET_PLAIN as i32
outlk_ad.out.server_flags |= 0x40000
}
outlk_ad.out_smtp_set = 1
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,10 @@
//! Constants
#![allow(non_camel_case_types, dead_code)]
use deltachat_derive::*;
use lazy_static::lazy_static;
use deltachat_derive::*;
lazy_static! {
pub static ref DC_VERSION_STR: String = env!("CARGO_PKG_VERSION").to_string();
}
@@ -17,12 +18,6 @@ pub enum MoveState {
Moving = 3,
}
impl Default for MoveState {
fn default() -> Self {
MoveState::Undefined
}
}
// some defaults
const DC_E2EE_DEFAULT_ENABLED: i32 = 1;
pub const DC_MDNS_DEFAULT_ENABLED: i32 = 1;
@@ -47,7 +42,7 @@ impl Default for Blocked {
pub const DC_IMAP_SEEN: u32 = 0x1;
pub const DC_HANDSHAKE_CONTINUE_NORMAL_PROCESSING: i32 = 0x01;
const DC_HANDSHAKE_CONTINUE_NORMAL_PROCESSING: i32 = 0x01;
pub const DC_HANDSHAKE_STOP_NORMAL_PROCESSING: i32 = 0x02;
pub const DC_HANDSHAKE_ADD_DELETE_JOB: i32 = 0x04;
@@ -57,7 +52,7 @@ pub const DC_GCL_ADD_ALLDONE_HINT: usize = 0x04;
const DC_GCM_ADDDAYMARKER: usize = 0x01;
pub const DC_GCL_VERIFIED_ONLY: usize = 0x01;
const DC_GCL_VERIFIED_ONLY: usize = 0x01;
pub const DC_GCL_ADD_SELF: usize = 0x02;
/// param1 is a directory where the keys are written to
@@ -70,19 +65,19 @@ const DC_IMEX_EXPORT_BACKUP: usize = 11;
const DC_IMEX_IMPORT_BACKUP: usize = 12;
/// virtual chat showing all messages belonging to chats flagged with chats.blocked=2
pub(crate) const DC_CHAT_ID_DEADDROP: u32 = 1;
pub(crate) const DC_CHAT_ID_DEADDROP: usize = 1;
/// messages that should be deleted get this chat_id; the messages are deleted from the working thread later then. This is also needed as rfc724_mid should be preset as long as the message is not deleted on the server (otherwise it is downloaded again)
pub const DC_CHAT_ID_TRASH: u32 = 3;
pub const DC_CHAT_ID_TRASH: usize = 3;
/// a message is just in creation but not yet assigned to a chat (eg. we may need the message ID to set up blobs; this avoids unready message to be sent and shown)
const DC_CHAT_ID_MSGS_IN_CREATION: u32 = 4;
const DC_CHAT_ID_MSGS_IN_CREATION: usize = 4;
/// virtual chat showing all messages flagged with msgs.starred=2
pub const DC_CHAT_ID_STARRED: u32 = 5;
const DC_CHAT_ID_STARRED: usize = 5;
/// only an indicator in a chatlist
pub const DC_CHAT_ID_ARCHIVED_LINK: u32 = 6;
pub const DC_CHAT_ID_ARCHIVED_LINK: usize = 6;
/// only an indicator in a chatlist
pub const DC_CHAT_ID_ALLDONE_HINT: u32 = 7;
pub const DC_CHAT_ID_ALLDONE_HINT: usize = 7;
/// larger chat IDs are "real" chats, their messages are "real" messages.
pub const DC_CHAT_ID_LAST_SPECIAL: u32 = 9;
pub const DC_CHAT_ID_LAST_SPECIAL: usize = 9;
#[derive(
Debug,
@@ -111,8 +106,8 @@ impl Default for Chattype {
}
}
pub const DC_MSG_ID_MARKER1: u32 = 1;
const DC_MSG_ID_DAYMARKER: u32 = 9;
pub const DC_MSG_ID_MARKER1: usize = 1;
const DC_MSG_ID_DAYMARKER: usize = 9;
pub const DC_MSG_ID_LAST_SPECIAL: u32 = 9;
/// approx. max. length returned by dc_msg_get_text()
@@ -120,13 +115,22 @@ const DC_MAX_GET_TEXT_LEN: usize = 30000;
/// approx. max. length returned by dc_get_msg_info()
const DC_MAX_GET_INFO_LEN: usize = 100000;
pub const DC_CONTACT_ID_UNDEFINED: u32 = 0;
pub const DC_CONTACT_ID_SELF: u32 = 1;
const DC_CONTACT_ID_DEVICE: u32 = 2;
pub const DC_CONTACT_ID_LAST_SPECIAL: u32 = 9;
pub const DC_CONTACT_ID_UNDEFINED: usize = 0;
pub const DC_CONTACT_ID_SELF: usize = 1;
const DC_CONTACT_ID_DEVICE: usize = 2;
pub const DC_CONTACT_ID_LAST_SPECIAL: usize = 9;
pub const DC_CREATE_MVBOX: usize = 1;
#[repr(i32)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
pub enum Delay {
Default = 0,
AtOnce = -1,
Standard = 3,
IncreationPoll = 2,
}
// Flags for configuring IMAP and SMTP servers.
// These flags are optional
// and may be set together with the username, password etc.
@@ -139,7 +143,7 @@ pub const DC_LP_AUTH_OAUTH2: usize = 0x2;
/// Force NORMAL authorization, this is the default.
/// If this flag is set, automatic configuration is skipped.
pub const DC_LP_AUTH_NORMAL: usize = 0x4;
const DC_LP_AUTH_NORMAL: usize = 0x4;
/// Connect to IMAP via STARTTLS.
/// If this flag is set, automatic configuration is skipped.
@@ -147,7 +151,7 @@ pub const DC_LP_IMAP_SOCKET_STARTTLS: usize = 0x100;
/// Connect to IMAP via SSL.
/// If this flag is set, automatic configuration is skipped.
pub const DC_LP_IMAP_SOCKET_SSL: usize = 0x200;
const DC_LP_IMAP_SOCKET_SSL: usize = 0x200;
/// Connect to IMAP unencrypted, this should not be used.
/// If this flag is set, automatic configuration is skipped.
@@ -159,27 +163,21 @@ pub const DC_LP_SMTP_SOCKET_STARTTLS: usize = 0x10000;
/// Connect to SMTP via SSL.
/// If this flag is set, automatic configuration is skipped.
pub const DC_LP_SMTP_SOCKET_SSL: usize = 0x20000;
const DC_LP_SMTP_SOCKET_SSL: usize = 0x20000;
/// Connect to SMTP unencrypted, this should not be used.
/// If this flag is set, automatic configuration is skipped.
pub const DC_LP_SMTP_SOCKET_PLAIN: usize = 0x40000;
/// if none of these flags are set, the default is chosen
pub const DC_LP_AUTH_FLAGS: usize = (DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL);
const DC_LP_AUTH_FLAGS: usize = (DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL);
/// if none of these flags are set, the default is chosen
pub const DC_LP_IMAP_SOCKET_FLAGS: usize =
const DC_LP_IMAP_SOCKET_FLAGS: usize =
(DC_LP_IMAP_SOCKET_STARTTLS | DC_LP_IMAP_SOCKET_SSL | DC_LP_IMAP_SOCKET_PLAIN);
/// if none of these flags are set, the default is chosen
pub const DC_LP_SMTP_SOCKET_FLAGS: usize =
const DC_LP_SMTP_SOCKET_FLAGS: usize =
(DC_LP_SMTP_SOCKET_STARTTLS | DC_LP_SMTP_SOCKET_SSL | DC_LP_SMTP_SOCKET_PLAIN);
// QR code scanning (view from Bob, the joiner)
pub const DC_VC_AUTH_REQUIRED: i32 = 2;
pub const DC_VC_CONTACT_CONFIRM: i32 = 6;
pub const DC_BOB_ERROR: i32 = 0;
pub const DC_BOB_SUCCESS: i32 = 1;
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[repr(i32)]
pub enum Viewtype {
@@ -225,12 +223,6 @@ pub enum Viewtype {
File = 60,
}
impl Default for Viewtype {
fn default() -> Self {
Viewtype::Unknown
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -246,6 +238,242 @@ mod tests {
// If you do not want to handle an event, it is always safe to return 0,
// so there is no need to add a "case" for every event.
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(u32)]
pub enum Event {
/// The library-user may write an informational string to the log.
/// Passed to the callback given to dc_context_new().
/// This event should not be reported to the end-user using a popup or something like that.
/// @param data1 0
/// @param data2 (const char*) Info string in english language.
/// Must not be free()'d or modified and is valid only until the callback returns.
/// @return 0
INFO = 100,
/// Emitted when SMTP connection is established and login was successful.
///
/// @param data1 0
/// @param data2 (const char*) Info string in english language.
/// Must not be free()'d or modified and is valid only until the callback returns.
/// @return 0
SMTP_CONNECTED = 101,
/// Emitted when IMAP connection is established and login was successful.
///
/// @param data1 0
/// @param data2 (const char*) Info string in english language.
/// Must not be free()'d or modified and is valid only until the callback returns.
/// @return 0
IMAP_CONNECTED = 102,
/// Emitted when a message was successfully sent to the SMTP server.
///
/// @param data1 0
/// @param data2 (const char*) Info string in english language.
/// Must not be free()'d or modified and is valid only until the callback returns.
/// @return 0
SMTP_MESSAGE_SENT = 103,
/// The library-user should write a warning string to the log.
/// Passed to the callback given to dc_context_new().
///
/// This event should not be reported to the end-user using a popup or something like that.
///
/// @param data1 0
/// @param data2 (const char*) Warning string in english language.
/// Must not be free()'d or modified and is valid only until the callback returns.
/// @return 0
WARNING = 300,
/// The library-user should report an error to the end-user.
/// Passed to the callback given to dc_context_new().
///
/// As most things are asynchronous, things may go wrong at any time and the user
/// should not be disturbed by a dialog or so. Instead, use a bubble or so.
///
/// However, for ongoing processes (eg. configure())
/// or for functions that are expected to fail (eg. dc_continue_key_transfer())
/// it might be better to delay showing these events until the function has really
/// failed (returned false). It should be sufficient to report only the _last_ error
/// in a messasge box then.
///
/// @param data1 0
/// @param data2 (const char*) Error string, always set, never NULL. Frequent error strings are
/// localized using #DC_EVENT_GET_STRING, however, most error strings will be in english language.
/// Must not be free()'d or modified and is valid only until the callback returns.
/// @return 0
ERROR = 400,
/// An action cannot be performed because there is no network available.
///
/// The library will typically try over after a some time
/// and when dc_maybe_network() is called.
///
/// Network errors should be reported to users in a non-disturbing way,
/// however, as network errors may come in a sequence,
/// it is not useful to raise each an every error to the user.
/// For this purpose, data1 is set to 1 if the error is probably worth reporting.
///
/// Moreover, if the UI detects that the device is offline,
/// it is probably more useful to report this to the user
/// instead of the string from data2.
///
/// @param data1 (int) 1=first/new network error, should be reported the user;
/// 0=subsequent network error, should be logged only
/// @param data2 (const char*) Error string, always set, never NULL.
/// Must not be free()'d or modified and is valid only until the callback returns.
/// @return 0
ERROR_NETWORK = 401,
/// An action cannot be performed because the user is not in the group.
/// Reported eg. after a call to
/// dc_set_chat_name(), dc_set_chat_profile_image(),
/// dc_add_contact_to_chat(), dc_remove_contact_from_chat(),
/// dc_send_text_msg() or another sending function.
///
/// @param data1 0
/// @param data2 (const char*) Info string in english language.
/// Must not be free()'d or modified
/// and is valid only until the callback returns.
/// @return 0
ERROR_SELF_NOT_IN_GROUP = 410,
/// Messages or chats changed. One or more messages or chats changed for various
/// reasons in the database:
/// - Messages sent, received or removed
/// - Chats created, deleted or archived
/// - A draft has been set
///
/// @param data1 (int) chat_id for single added messages
/// @param data2 (int) msg_id for single added messages
/// @return 0
MSGS_CHANGED = 2000,
/// There is a fresh message. Typically, the user will show an notification
/// when receiving this message.
///
/// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
///
/// @param data1 (int) chat_id
/// @param data2 (int) msg_id
/// @return 0
INCOMING_MSG = 2005,
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
/// DC_STATE_OUT_DELIVERED, see dc_msg_get_state().
///
/// @param data1 (int) chat_id
/// @param data2 (int) msg_id
/// @return 0
MSG_DELIVERED = 2010,
/// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
/// DC_STATE_OUT_FAILED, see dc_msg_get_state().
///
/// @param data1 (int) chat_id
/// @param data2 (int) msg_id
/// @return 0
MSG_FAILED = 2012,
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
/// DC_STATE_OUT_MDN_RCVD, see dc_msg_get_state().
///
/// @param data1 (int) chat_id
/// @param data2 (int) msg_id
/// @return 0
MSG_READ = 2015,
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
/// Or the verify state of a chat has changed.
/// See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
/// and dc_remove_contact_from_chat().
///
/// @param data1 (int) chat_id
/// @param data2 0
/// @return 0
CHAT_MODIFIED = 2020,
/// Contact(s) created, renamed, blocked or deleted.
///
/// @param data1 (int) If not 0, this is the contact_id of an added contact that should be selected.
/// @param data2 0
/// @return 0
CONTACTS_CHANGED = 2030,
/// Location of one or more contact has changed.
///
/// @param data1 (int) contact_id of the contact for which the location has changed.
/// If the locations of several contacts have been changed,
/// eg. after calling dc_delete_all_locations(), this parameter is set to 0.
/// @param data2 0
/// @return 0
LOCATION_CHANGED = 2035,
/// Inform about the configuration progress started by configure().
///
/// @param data1 (int) 0=error, 1-999=progress in permille, 1000=success and done
/// @param data2 0
/// @return 0
CONFIGURE_PROGRESS = 2041,
/// Inform about the import/export progress started by dc_imex().
///
/// @param data1 (int) 0=error, 1-999=progress in permille, 1000=success and done
/// @param data2 0
/// @return 0
IMEX_PROGRESS = 2051,
/// A file has been exported. A file has been written by dc_imex().
/// This event may be sent multiple times by a single call to dc_imex().
///
/// A typical purpose for a handler of this event may be to make the file public to some system
/// services.
///
/// @param data1 (const char*) Path and file name.
/// Must not be free()'d or modified and is valid only until the callback returns.
/// @param data2 0
/// @return 0
IMEX_FILE_WRITTEN = 2052,
/// Progress information of a secure-join handshake from the view of the inviter
/// (Alice, the person who shows the QR code).
///
/// These events are typically sent after a joiner has scanned the QR code
/// generated by dc_get_securejoin_qr().
///
/// @param data1 (int) ID of the contact that wants to join.
/// @param data2 (int) Progress as:
/// 300=vg-/vc-request received, typically shown as "bob@addr joins".
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
/// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
/// 1000=Protocol finished for this contact.
/// @return 0
SECUREJOIN_INVITER_PROGRESS = 2060,
/// Progress information of a secure-join handshake from the view of the joiner
/// (Bob, the person who scans the QR code).
/// The events are typically sent while dc_join_securejoin(), which
/// may take some time, is executed.
/// @param data1 (int) ID of the inviting contact.
/// @param data2 (int) Progress as:
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
/// (Bob has verified alice and waits until Alice does the same for him)
/// @return 0
SECUREJOIN_JOINER_PROGRESS = 2061,
// the following events are functions that should be provided by the frontends
/// Requeste a localized string from the frontend.
/// @param data1 (int) ID of the string to request, one of the DC_STR_/// constants.
/// @param data2 (int) The count. If the requested string contains a placeholder for a numeric value,
/// the ui may use this value to return different strings on different plural forms.
/// @return (const char*) Null-terminated UTF-8 string.
/// The string will be free()'d by the core,
/// so it must be allocated using malloc() or a compatible function.
/// Return 0 if the ui cannot provide the requested string
/// the core will use a default string in english language then.
GET_STRING = 2091,
}
const DC_EVENT_FILE_COPIED: usize = 2055; // deprecated;
const DC_EVENT_IS_OFFLINE: usize = 2081; // deprecated;
const DC_ERROR_SEE_STRING: usize = 0; // deprecated;
@@ -314,3 +542,12 @@ pub enum KeyType {
Public = 0,
Private = 1,
}
pub const DC_CMD_GROUPNAME_CHANGED: libc::c_int = 2;
pub const DC_CMD_GROUPIMAGE_CHANGED: libc::c_int = 3;
pub const DC_CMD_MEMBER_ADDED_TO_GROUP: libc::c_int = 4;
pub const DC_CMD_MEMBER_REMOVED_FROM_GROUP: libc::c_int = 5;
pub const DC_CMD_AUTOCRYPT_SETUP_MESSAGE: libc::c_int = 6;
const DC_CMD_SECUREJOIN_MESSAGE: libc::c_int = 7;
pub const DC_CMD_LOCATION_STREAMING_ENABLED: libc::c_int = 8;
const DC_CMD_LOCATION_ONLY: libc::c_int = 9;

View File

@@ -1,23 +1,24 @@
use std::path::PathBuf;
use deltachat_derive::*;
use itertools::Itertools;
use num_traits::{FromPrimitive, ToPrimitive};
use rusqlite;
use rusqlite::types::*;
use crate::aheader::EncryptPreference;
use crate::config::Config;
use crate::constants::*;
use crate::context::Context;
use crate::dc_e2ee::*;
use crate::dc_loginparam::*;
use crate::dc_tools::*;
use crate::e2ee;
use crate::error::Result;
use crate::events::Event;
use crate::key::*;
use crate::login_param::LoginParam;
use crate::message::MessageState;
use crate::peerstate::*;
use crate::sql;
use crate::stock::StockMessage;
use crate::types::*;
const DC_GCL_VERIFIED_ONLY: u32 = 0x01;
/// Contacts with at least this origin value are shown in the contact list.
const DC_ORIGIN_MIN_CONTACT_LIST: i32 = 0x100;
@@ -32,8 +33,8 @@ const DC_ORIGIN_MIN_CONTACT_LIST: i32 = 0x100;
/// For this purpose, internally, two names are tracked -
/// authorized-name and given-name.
/// By default, these names are equal, but functions working with contact names
#[derive(Debug)]
pub struct Contact {
pub struct Contact<'a> {
context: &'a Context,
/// The contact ID.
///
/// Special message IDs:
@@ -59,9 +60,7 @@ pub struct Contact {
}
/// Possible origins of a contact.
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromPrimitive, ToPrimitive, FromSql, ToSql,
)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromPrimitive, ToPrimitive)]
#[repr(i32)]
pub enum Origin {
Unknown = 0,
@@ -99,9 +98,20 @@ pub enum Origin {
ManuallyCreated = 0x4000000,
}
impl Default for Origin {
fn default() -> Self {
Origin::Unknown
impl ToSql for Origin {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
let num: i64 = self
.to_i64()
.expect("impossible: Origin -> i64 conversion failed");
Ok(ToSqlOutput::Owned(Value::Integer(num)))
}
}
impl FromSql for Origin {
fn column_result(col: ValueRef) -> FromSqlResult<Self> {
let inner = FromSql::column_result(col)?;
FromPrimitive::from_i64(inner).ok_or(FromSqlError::InvalidType)
}
}
@@ -140,10 +150,11 @@ pub enum VerifiedStatus {
BidirectVerified = 2,
}
impl Contact {
pub fn load_from_db(context: &Context, contact_id: u32) -> Result<Self> {
if contact_id == DC_CONTACT_ID_SELF {
impl<'a> Contact<'a> {
pub fn load_from_db(context: &'a Context, contact_id: u32) -> Result<Self> {
if contact_id == DC_CONTACT_ID_SELF as u32 {
let contact = Contact {
context,
id: contact_id,
name: context.stock_str(StockMessage::SelfMsg).into(),
authname: "".into(),
@@ -162,6 +173,7 @@ impl Contact {
params![contact_id as i32],
|row| {
let contact = Self {
context,
id: contact_id,
name: row.get::<_, String>(0)?,
authname: row.get::<_, String>(4)?,
@@ -180,7 +192,7 @@ impl Contact {
}
/// Check if a contact is blocked.
pub fn is_blocked_load(context: &Context, id: u32) -> bool {
pub fn is_blocked_load(context: &'a Context, id: u32) -> bool {
Self::load_from_db(context, id)
.map(|contact| contact.blocked)
.unwrap_or_default()
@@ -214,13 +226,15 @@ impl Contact {
let (contact_id, sth_modified) =
Contact::add_or_lookup(context, name, addr, Origin::ManuallyCreated)?;
let blocked = Contact::is_blocked_load(context, contact_id);
context.call_cb(Event::ContactsChanged(
if sth_modified == Modifier::Created {
Some(contact_id)
context.call_cb(
Event::CONTACTS_CHANGED,
(if sth_modified == Modifier::Created {
contact_id
} else {
None
},
));
0
}) as uintptr_t,
0 as uintptr_t,
);
if blocked {
Contact::unblock(context, contact_id);
}
@@ -241,10 +255,7 @@ impl Contact {
)
.is_ok()
{
context.call_cb(Event::MsgsChanged {
chat_id: 0,
msg_id: 0,
});
context.call_cb(Event::MSGS_CHANGED, 0, 0);
}
}
@@ -267,7 +278,7 @@ impl Contact {
return 1;
}
context.sql.query_get_value(
context.sql.query_row_col(
context,
"SELECT id FROM contacts WHERE addr=?1 COLLATE NOCASE AND id>?2 AND origin>=?3 AND blocked=0;",
params![
@@ -275,6 +286,7 @@ impl Contact {
DC_CONTACT_ID_LAST_SPECIAL as i32,
DC_ORIGIN_MIN_CONTACT_LIST,
],
0
).unwrap_or_default()
}
@@ -307,6 +319,7 @@ impl Contact {
if !may_be_valid_addr(&addr) {
warn!(
context,
0,
"Bad address \"{}\" for contact \"{}\".",
addr,
if !name.as_ref().is_empty() {
@@ -330,22 +343,19 @@ impl Contact {
let row_id = row.get(0)?;
let row_name: String = row.get(1)?;
let row_addr: String = row.get(2)?;
let row_origin: Origin = row.get(3)?;
let row_origin = row.get(3)?;
let row_authname: String = row.get(4)?;
if !name.as_ref().is_empty() {
if !row_name.is_empty() {
if origin >= row_origin && name.as_ref() != row_name {
update_name = true;
}
} else {
if !name.as_ref().is_empty() && !row_name.is_empty() {
if origin >= row_origin && name.as_ref() != row_name {
update_name = true;
}
if origin == Origin::IncomingUnknownFrom && name.as_ref() != row_authname {
update_authname = true;
}
} else {
update_name = true;
}
if origin == Origin::IncomingUnknownFrom && name.as_ref() != row_authname {
update_authname = true;
}
Ok((row_id, row_name, row_addr, row_origin, row_authname))
},
) {
@@ -385,7 +395,7 @@ impl Contact {
context,
&context.sql,
"UPDATE chats SET name=? WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?);",
params![name.as_ref(), Chattype::Single, row_id]
params![name.as_ref(), 100, row_id]
).ok();
}
sth_modified = Modifier::Modified;
@@ -402,7 +412,7 @@ impl Contact {
row_id = sql::get_rowid(context, &context.sql, "contacts", "addr", addr);
sth_modified = Modifier::Created;
} else {
error!(context, "Cannot add contact.");
error!(context, 0, "Cannot add contact.");
}
}
@@ -423,13 +433,19 @@ impl Contact {
/// To add a single contact entered by the user, you should prefer `Contact::create`,
/// however, for adding a bunch of addresses, this function is _much_ faster.
///
/// The `addr_book` is a multiline string in the format `Name one\nAddress one\nName two\nAddress two`.
/// The `adr_book` is a multiline string in the format `Name one\nAddress one\nName two\nAddress two`.
///
/// Returns the number of modified contacts.
pub fn add_address_book(context: &Context, addr_book: impl AsRef<str>) -> Result<usize> {
pub fn add_address_book(context: &Context, adr_book: impl AsRef<str>) -> Result<usize> {
let mut modify_cnt = 0;
for (name, addr) in split_address_book(addr_book.as_ref()).into_iter() {
for chunk in &adr_book.as_ref().lines().chunks(2) {
let chunk = chunk.collect::<Vec<_>>();
if chunk.len() < 2 {
break;
}
let name = chunk[0];
let addr = chunk[1];
let name = normalize_name(name);
let (_, modified) = Contact::add_or_lookup(context, name, addr, Origin::AdressBook)?;
if modified != Modifier::None {
@@ -437,7 +453,7 @@ impl Contact {
}
}
if modify_cnt > 0 {
context.call_cb(Event::ContactsChanged(None));
context.call_cb(Event::CONTACTS_CHANGED, 0 as uintptr_t, 0 as uintptr_t);
}
Ok(modify_cnt)
@@ -463,10 +479,8 @@ impl Contact {
let mut add_self = false;
let mut ret = Vec::new();
let flag_verified_only = listflags_has(listflags, DC_GCL_VERIFIED_ONLY);
let flag_add_self = listflags_has(listflags, DC_GCL_ADD_SELF);
if flag_verified_only || query.is_some() {
if (listflags & DC_GCL_VERIFIED_ONLY) > 0 || query.is_some() {
let s3str_like_cmd = format!(
"%{}%",
query
@@ -490,7 +504,7 @@ impl Contact {
0x100,
&s3str_like_cmd,
&s3str_like_cmd,
if flag_verified_only { 0 } else { 1 },
if 0 != listflags & 0x1 { 0 } else { 1 },
],
|row| row.get::<_, i32>(0),
|ids| {
@@ -530,8 +544,8 @@ impl Contact {
)?;
}
if flag_add_self && add_self {
ret.push(DC_CONTACT_ID_SELF);
if 0 != listflags & DC_GCL_ADD_SELF as u32 && add_self {
ret.push(DC_CONTACT_ID_SELF as u32);
}
Ok(ret)
@@ -540,10 +554,11 @@ impl Contact {
pub fn get_blocked_cnt(context: &Context) -> usize {
context
.sql
.query_get_value::<_, isize>(
.query_row_col::<_, isize>(
context,
"SELECT COUNT(*) FROM contacts WHERE id>? AND blocked!=0",
params![DC_CONTACT_ID_LAST_SPECIAL as i32],
0,
)
.unwrap_or_default() as usize
}
@@ -574,7 +589,7 @@ impl Contact {
if let Ok(contact) = Contact::load_from_db(context, contact_id) {
let peerstate = Peerstate::from_addr(context, &context.sql, &contact.addr);
let loginparam = LoginParam::from_database(context, "configured_");
let loginparam = dc_loginparam_read(context, &context.sql, "configured_");
let mut self_key = Key::from_self_public(context, &loginparam.addr, &context.sql);
@@ -588,7 +603,7 @@ impl Contact {
});
ret += &p;
if self_key.is_none() {
e2ee::ensure_secret_key_exists(context)?;
dc_ensure_secret_key_exists(context)?;
self_key = Key::from_self_public(context, &loginparam.addr, &context.sql);
}
let p = context.stock_str(StockMessage::FingerPrints);
@@ -640,26 +655,28 @@ impl Contact {
/// May result in a `#DC_EVENT_CONTACTS_CHANGED` event.
pub fn delete(context: &Context, contact_id: u32) -> Result<()> {
ensure!(
contact_id > DC_CONTACT_ID_LAST_SPECIAL,
contact_id > DC_CONTACT_ID_LAST_SPECIAL as u32,
"Can not delete special contact"
);
let count_contacts: i32 = context
.sql
.query_get_value(
.query_row_col(
context,
"SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;",
params![contact_id as i32],
0,
)
.unwrap_or_default();
let count_msgs: i32 = if count_contacts > 0 {
context
.sql
.query_get_value(
.query_row_col(
context,
"SELECT COUNT(*) FROM msgs WHERE from_id=? OR to_id=?;",
params![contact_id as i32, contact_id as i32],
0,
)
.unwrap_or_default()
} else {
@@ -674,11 +691,11 @@ impl Contact {
params![contact_id as i32],
) {
Ok(_) => {
context.call_cb(Event::ContactsChanged(None));
context.call_cb(Event::CONTACTS_CHANGED, 0, 0);
return Ok(());
}
Err(err) => {
error!(context, "delete_contact {} failed ({})", contact_id, err);
error!(context, 0, "delete_contact {} failed ({})", contact_id, err);
return Err(err);
}
}
@@ -686,7 +703,7 @@ impl Contact {
info!(
context,
"could not delete contact {}, there are {} messages with it", contact_id, count_msgs
0, "could not delete contact {}, there are {} messages with it", contact_id, count_msgs
);
bail!("Could not delete contact with messages in it");
}
@@ -762,11 +779,9 @@ impl Contact {
/// Get the contact's profile image.
/// This is the image set by each remote user on their own
/// using dc_set_config(context, "selfavatar", image).
pub fn get_profile_image(&self, context: &Context) -> Option<PathBuf> {
if self.id == DC_CONTACT_ID_SELF {
if let Some(p) = context.get_config(Config::Selfavatar) {
return Some(PathBuf::from(p));
}
pub fn get_profile_image(&self) -> Option<String> {
if self.id == DC_CONTACT_ID_SELF as u32 {
return self.context.get_config(Config::Selfavatar);
}
// TODO: else get image_abs from contact param
None
@@ -785,21 +800,17 @@ impl Contact {
///
/// The UI may draw a checkbox or something like that beside verified contacts.
///
pub fn is_verified(&self, context: &Context) -> VerifiedStatus {
self.is_verified_ex(context, None)
pub fn is_verified(&self) -> VerifiedStatus {
self.is_verified_ex(None)
}
/// Same as `Contact::is_verified` but allows speeding up things
/// by adding the peerstate belonging to the contact.
/// If you do not have the peerstate available, it is loaded automatically.
pub fn is_verified_ex(
&self,
context: &Context,
peerstate: Option<&Peerstate>,
) -> VerifiedStatus {
pub fn is_verified_ex(&self, peerstate: Option<&Peerstate<'a>>) -> VerifiedStatus {
// We're always sort of secured-verified as we could verify the key on this device any time with the key
// on this device
if self.id == DC_CONTACT_ID_SELF {
if self.id == DC_CONTACT_ID_SELF as u32 {
return VerifiedStatus::BidirectVerified;
}
@@ -809,7 +820,7 @@ impl Contact {
}
}
let peerstate = Peerstate::from_addr(context, &context.sql, &self.addr);
let peerstate = Peerstate::from_addr(self.context, &self.context.sql, &self.addr);
if let Some(ps) = peerstate {
if ps.verified_key().is_some() {
return VerifiedStatus::BidirectVerified;
@@ -843,10 +854,11 @@ impl Contact {
context
.sql
.query_get_value::<_, isize>(
.query_row_col::<_, isize>(
context,
"SELECT COUNT(*) FROM contacts WHERE id>?;",
params![DC_CONTACT_ID_LAST_SPECIAL as i32],
0,
)
.unwrap_or_default() as usize
}
@@ -939,7 +951,11 @@ fn set_block_contact(context: &Context, contact_id: u32, new_blocking: bool) {
params![new_blocking, 100, contact_id as i32],
).is_ok() {
Contact::mark_noticed(context, contact_id);
context.call_cb(Event::ContactsChanged(None));
context.call_cb(
Event::CONTACTS_CHANGED,
0,
0,
);
}
}
}
@@ -1027,21 +1043,6 @@ pub fn addr_equals_self(context: &Context, addr: impl AsRef<str>) -> bool {
false
}
fn split_address_book(book: &str) -> Vec<(&str, &str)> {
book.lines()
.chunks(2)
.into_iter()
.filter_map(|mut chunk| {
let name = chunk.next().unwrap();
let addr = match chunk.next() {
Some(a) => a,
None => return None,
};
Some((name, addr))
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
@@ -1077,14 +1078,4 @@ mod tests {
fn test_get_first_name() {
assert_eq!(get_first_name("John Doe"), "John");
}
#[test]
fn test_split_address_book() {
let book = "Name one\nAddress one\nName two\nAddress two\nrest name";
let list = split_address_book(&book);
assert_eq!(
list,
vec![("Name one", "Address one"), ("Name two", "Address two")]
)
}
}

View File

@@ -1,45 +1,31 @@
use std::collections::HashMap;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Condvar, Mutex, RwLock};
use libc::uintptr_t;
use crate::chat::*;
use crate::constants::*;
use crate::contact::*;
use crate::error::*;
use crate::events::Event;
use crate::dc_loginparam::*;
use crate::dc_move::*;
use crate::dc_receive_imf::*;
use crate::dc_tools::*;
use crate::imap::*;
use crate::job::*;
use crate::job_thread::JobThread;
use crate::key::*;
use crate::login_param::LoginParam;
use crate::lot::Lot;
use crate::message::*;
use crate::param::Params;
use crate::smtp::*;
use crate::sql::Sql;
use crate::types::*;
use crate::x::*;
use std::ptr;
/// Callback function type for [Context]
///
/// # Parameters
///
/// * `context` - The context object as returned by [Context::new].
/// * `event` - One of the [Event] items.
/// * `data1` - Depends on the event parameter, see [Event].
/// * `data2` - Depends on the event parameter, see [Event].
///
/// # Returns
///
/// This callback must return 0 unless stated otherwise in the event
/// description at [Event].
pub type ContextCallback = dyn Fn(&Context, Event) -> uintptr_t + Send + Sync;
use std::path::PathBuf;
#[derive(DebugStub)]
pub struct Context {
dbfile: PathBuf,
blobdir: PathBuf,
pub userdata: *mut libc::c_void,
pub dbfile: Arc<RwLock<Option<PathBuf>>>,
pub blobdir: Arc<RwLock<*mut libc::c_char>>,
pub sql: Sql,
pub inbox: Arc<RwLock<Imap>>,
pub perform_inbox_jobs_needed: Arc<RwLock<bool>>,
@@ -49,8 +35,7 @@ pub struct Context {
pub smtp: Arc<Mutex<Smtp>>,
pub smtp_state: Arc<(Mutex<SmtpState>, Condvar)>,
pub oauth2_critical: Arc<Mutex<()>>,
#[debug_stub = "Callback"]
cb: Box<ContextCallback>,
pub cb: Option<dc_callback_t>,
pub os_name: Option<String>,
pub cmdline_sel_chat_id: Arc<RwLock<u32>>,
pub bob: Arc<RwLock<BobStatus>>,
@@ -60,6 +45,9 @@ pub struct Context {
pub generating_key_mutex: Mutex<()>,
}
unsafe impl std::marker::Send for Context {}
unsafe impl std::marker::Sync for Context {}
#[derive(Debug, PartialEq, Eq)]
pub struct RunningState {
pub ongoing_running: bool,
@@ -67,330 +55,38 @@ pub struct RunningState {
}
impl Context {
pub fn new(cb: Box<ContextCallback>, os_name: String, dbfile: PathBuf) -> Result<Context> {
let mut blob_fname = OsString::new();
blob_fname.push(dbfile.file_name().unwrap_or_default());
blob_fname.push("-blobs");
let blobdir = dbfile.with_file_name(blob_fname);
if !blobdir.exists() {
std::fs::create_dir_all(&blobdir)?;
}
Context::with_blobdir(cb, os_name, dbfile, blobdir)
pub fn has_dbfile(&self) -> bool {
self.get_dbfile().is_some()
}
pub fn with_blobdir(
cb: Box<ContextCallback>,
os_name: String,
dbfile: PathBuf,
blobdir: PathBuf,
) -> Result<Context> {
ensure!(
blobdir.is_dir(),
"Blobdir does not exist: {}",
blobdir.display()
);
let ctx = Context {
blobdir,
dbfile,
inbox: Arc::new(RwLock::new(Imap::new())),
cb,
os_name: Some(os_name),
running_state: Arc::new(RwLock::new(Default::default())),
sql: Sql::new(),
smtp: Arc::new(Mutex::new(Smtp::new())),
smtp_state: Arc::new((Mutex::new(Default::default()), Condvar::new())),
oauth2_critical: Arc::new(Mutex::new(())),
bob: Arc::new(RwLock::new(Default::default())),
last_smeared_timestamp: Arc::new(RwLock::new(0)),
cmdline_sel_chat_id: Arc::new(RwLock::new(0)),
sentbox_thread: Arc::new(RwLock::new(JobThread::new(
"SENTBOX",
"configured_sentbox_folder",
Imap::new(),
))),
mvbox_thread: Arc::new(RwLock::new(JobThread::new(
"MVBOX",
"configured_mvbox_folder",
Imap::new(),
))),
probe_imap_network: Arc::new(RwLock::new(false)),
perform_inbox_jobs_needed: Arc::new(RwLock::new(false)),
generating_key_mutex: Mutex::new(()),
};
ensure!(
ctx.sql.open(&ctx, &ctx.dbfile, 0),
"Failed opening sqlite database"
);
Ok(ctx)
pub fn has_blobdir(&self) -> bool {
!self.get_blobdir().is_null()
}
pub fn get_dbfile(&self) -> &Path {
self.dbfile.as_path()
pub fn get_dbfile(&self) -> Option<PathBuf> {
(*self.dbfile.clone().read().unwrap())
.as_ref()
.map(|x| x.clone())
}
pub fn get_blobdir(&self) -> &Path {
self.blobdir.as_path()
pub fn get_blobdir(&self) -> *const libc::c_char {
*self.blobdir.clone().read().unwrap()
}
pub fn call_cb(&self, event: Event) -> uintptr_t {
(*self.cb)(self, event)
}
pub fn get_info(&self) -> HashMap<&'static str, String> {
let unset = "0";
let l = LoginParam::from_database(self, "");
let l2 = LoginParam::from_database(self, "configured_");
let displayname = self.sql.get_config(self, "displayname");
let chats = get_chat_cnt(self) as usize;
let real_msgs = dc_get_real_msg_cnt(self) as usize;
let deaddrop_msgs = dc_get_deaddrop_msg_cnt(self) as usize;
let contacts = Contact::get_real_cnt(self) as usize;
let is_configured = self
.sql
.get_config_int(self, "configured")
.unwrap_or_default();
let dbversion = self
.sql
.get_config_int(self, "dbversion")
.unwrap_or_default();
let e2ee_enabled = self
.sql
.get_config_int(self, "e2ee_enabled")
.unwrap_or_else(|| 1);
let mdns_enabled = self
.sql
.get_config_int(self, "mdns_enabled")
.unwrap_or_else(|| 1);
let prv_key_cnt: Option<isize> =
self.sql
.query_get_value(self, "SELECT COUNT(*) FROM keypairs;", rusqlite::NO_PARAMS);
let pub_key_cnt: Option<isize> = self.sql.query_get_value(
self,
"SELECT COUNT(*) FROM acpeerstates;",
rusqlite::NO_PARAMS,
);
let fingerprint_str = if let Some(key) = Key::from_self_public(self, &l2.addr, &self.sql) {
key.fingerprint()
pub fn call_cb(&self, event: Event, data1: uintptr_t, data2: uintptr_t) -> uintptr_t {
if let Some(cb) = self.cb {
unsafe { cb(self, event, data1, data2) }
} else {
"<Not yet calculated>".into()
};
let inbox_watch = self
.sql
.get_config_int(self, "inbox_watch")
.unwrap_or_else(|| 1);
let sentbox_watch = self
.sql
.get_config_int(self, "sentbox_watch")
.unwrap_or_else(|| 1);
let mvbox_watch = self
.sql
.get_config_int(self, "mvbox_watch")
.unwrap_or_else(|| 1);
let mvbox_move = self
.sql
.get_config_int(self, "mvbox_move")
.unwrap_or_else(|| 1);
let folders_configured = self
.sql
.get_config_int(self, "folders_configured")
.unwrap_or_default();
let configured_sentbox_folder = self
.sql
.get_config(self, "configured_sentbox_folder")
.unwrap_or_else(|| "<unset>".to_string());
let configured_mvbox_folder = self
.sql
.get_config(self, "configured_mvbox_folder")
.unwrap_or_else(|| "<unset>".to_string());
let mut res = HashMap::new();
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
res.insert("sqlite_version", rusqlite::version().to_string());
res.insert(
"sqlite_thread_safe",
unsafe { rusqlite::ffi::sqlite3_threadsafe() }.to_string(),
);
res.insert(
"arch",
(::std::mem::size_of::<*mut libc::c_void>())
.wrapping_mul(8)
.to_string(),
);
res.insert("number_of_chats", chats.to_string());
res.insert("number_of_chat_messages", real_msgs.to_string());
res.insert("messages_in_contact_requests", deaddrop_msgs.to_string());
res.insert("number_of_contacts", contacts.to_string());
res.insert("database_dir", self.get_dbfile().display().to_string());
res.insert("database_version", dbversion.to_string());
res.insert("blobdir", self.get_blobdir().display().to_string());
res.insert("display_name", displayname.unwrap_or_else(|| unset.into()));
res.insert("is_configured", is_configured.to_string());
res.insert("entered_account_settings", l.to_string());
res.insert("used_account_settings", l2.to_string());
res.insert("inbox_watch", inbox_watch.to_string());
res.insert("sentbox_watch", sentbox_watch.to_string());
res.insert("mvbox_watch", mvbox_watch.to_string());
res.insert("mvbox_move", mvbox_move.to_string());
res.insert("folders_configured", folders_configured.to_string());
res.insert("configured_sentbox_folder", configured_sentbox_folder);
res.insert("configured_mvbox_folder", configured_mvbox_folder);
res.insert("mdns_enabled", mdns_enabled.to_string());
res.insert("e2ee_enabled", e2ee_enabled.to_string());
res.insert(
"private_key_count",
prv_key_cnt.unwrap_or_default().to_string(),
);
res.insert(
"public_key_count",
pub_key_cnt.unwrap_or_default().to_string(),
);
res.insert("fingerprint", fingerprint_str);
res.insert("level", "awesome".into());
res
}
pub fn get_fresh_msgs(&self) -> Vec<u32> {
let show_deaddrop = 0;
self.sql
.query_map(
"SELECT m.id FROM msgs m LEFT JOIN contacts ct \
ON m.from_id=ct.id LEFT JOIN chats c ON m.chat_id=c.id WHERE m.state=? \
AND m.hidden=0 \
AND m.chat_id>? \
AND ct.blocked=0 \
AND (c.blocked=0 OR c.blocked=?) ORDER BY m.timestamp DESC,m.id DESC;",
&[10, 9, if 0 != show_deaddrop { 2 } else { 0 }],
|row| row.get(0),
|rows| {
let mut ret = Vec::new();
for row in rows {
let id: u32 = row?;
ret.push(id);
}
Ok(ret)
},
)
.unwrap()
}
#[allow(non_snake_case)]
pub fn search_msgs(&self, chat_id: u32, query: impl AsRef<str>) -> Vec<u32> {
let real_query = query.as_ref().trim();
if real_query.is_empty() {
return Vec::new();
}
let strLikeInText = format!("%{}%", real_query);
let strLikeBeg = format!("{}%", real_query);
let query = if 0 != chat_id {
"SELECT m.id, m.timestamp FROM msgs m LEFT JOIN contacts ct ON m.from_id=ct.id WHERE m.chat_id=? \
AND m.hidden=0 \
AND ct.blocked=0 AND (txt LIKE ? OR ct.name LIKE ?) ORDER BY m.timestamp,m.id;"
} else {
"SELECT m.id, m.timestamp FROM msgs m LEFT JOIN contacts ct ON m.from_id=ct.id \
LEFT JOIN chats c ON m.chat_id=c.id WHERE m.chat_id>9 AND m.hidden=0 \
AND (c.blocked=0 OR c.blocked=?) \
AND ct.blocked=0 AND (m.txt LIKE ? OR ct.name LIKE ?) ORDER BY m.timestamp DESC,m.id DESC;"
};
self.sql
.query_map(
query,
params![chat_id as i32, &strLikeInText, &strLikeBeg],
|row| row.get::<_, i32>(0),
|rows| {
let mut ret = Vec::new();
for id in rows {
ret.push(id? as u32);
}
Ok(ret)
},
)
.unwrap_or_default()
}
pub fn is_inbox(&self, folder_name: impl AsRef<str>) -> bool {
folder_name.as_ref() == "INBOX"
}
pub fn is_sentbox(&self, folder_name: impl AsRef<str>) -> bool {
let sentbox_name = self.sql.get_config(self, "configured_sentbox_folder");
if let Some(name) = sentbox_name {
name == folder_name.as_ref()
} else {
false
}
}
pub fn is_mvbox(&self, folder_name: impl AsRef<str>) -> bool {
let mvbox_name = self.sql.get_config(self, "configured_mvbox_folder");
if let Some(name) = mvbox_name {
name == folder_name.as_ref()
} else {
false
}
}
pub fn do_heuristics_moves(&self, folder: &str, msg_id: u32) {
if self
.sql
.get_config_int(self, "mvbox_move")
.unwrap_or_else(|| 1)
== 0
{
return;
}
if !self.is_inbox(folder) && !self.is_sentbox(folder) {
return;
}
if let Ok(msg) = dc_msg_new_load(self, msg_id) {
if dc_msg_is_setupmessage(&msg) {
// do not move setup messages;
// there may be a non-delta device that wants to handle it
return;
}
if self.is_mvbox(folder) {
dc_update_msg_move_state(self, msg.rfc724_mid, MoveState::Stay);
}
// 1 = dc message, 2 = reply to dc message
if 0 != msg.is_dc_message {
job_add(
self,
Action::MoveMsg,
msg.id as libc::c_int,
Params::new(),
0,
);
dc_update_msg_move_state(self, msg.rfc724_mid, MoveState::Moving);
}
0
}
}
}
impl Drop for Context {
fn drop(&mut self) {
info!(self, "disconnecting INBOX-watch",);
self.inbox.read().unwrap().disconnect(self);
info!(self, "disconnecting sentbox-thread",);
self.sentbox_thread.read().unwrap().imap.disconnect(self);
info!(self, "disconnecting mvbox-thread",);
self.mvbox_thread.read().unwrap().imap.disconnect(self);
info!(self, "disconnecting SMTP");
self.smtp.clone().lock().unwrap().disconnect();
self.sql.close(self);
unsafe {
dc_close(&self);
}
}
}
@@ -403,7 +99,7 @@ impl Default for RunningState {
}
}
#[derive(Debug, Default)]
#[derive(Default)]
pub struct BobStatus {
pub expects: i32,
pub status: i32,
@@ -419,91 +115,479 @@ pub struct SmtpState {
pub probe_network: bool,
}
pub fn get_version_str() -> &'static str {
&DC_VERSION_STR
// create/open/config/information
pub fn dc_context_new(
cb: Option<dc_callback_t>,
userdata: *mut libc::c_void,
os_name: Option<String>,
) -> Context {
Context {
blobdir: Arc::new(RwLock::new(std::ptr::null_mut())),
dbfile: Arc::new(RwLock::new(None)),
inbox: Arc::new(RwLock::new({
Imap::new(
cb_get_config,
cb_set_config,
cb_precheck_imf,
cb_receive_imf,
)
})),
userdata,
cb,
os_name,
running_state: Arc::new(RwLock::new(Default::default())),
sql: Sql::new(),
smtp: Arc::new(Mutex::new(Smtp::new())),
smtp_state: Arc::new((Mutex::new(Default::default()), Condvar::new())),
oauth2_critical: Arc::new(Mutex::new(())),
bob: Arc::new(RwLock::new(Default::default())),
last_smeared_timestamp: Arc::new(RwLock::new(0)),
cmdline_sel_chat_id: Arc::new(RwLock::new(0)),
sentbox_thread: Arc::new(RwLock::new(JobThread::new(
"SENTBOX",
"configured_sentbox_folder",
Imap::new(
cb_get_config,
cb_set_config,
cb_precheck_imf,
cb_receive_imf,
),
))),
mvbox_thread: Arc::new(RwLock::new(JobThread::new(
"MVBOX",
"configured_mvbox_folder",
Imap::new(
cb_get_config,
cb_set_config,
cb_precheck_imf,
cb_receive_imf,
),
))),
probe_imap_network: Arc::new(RwLock::new(false)),
perform_inbox_jobs_needed: Arc::new(RwLock::new(false)),
generating_key_mutex: Mutex::new(()),
}
}
unsafe fn cb_receive_imf(
context: &Context,
imf_raw_not_terminated: *const libc::c_char,
imf_raw_bytes: size_t,
server_folder: &str,
server_uid: uint32_t,
flags: uint32_t,
) {
dc_receive_imf(
context,
imf_raw_not_terminated,
imf_raw_bytes,
server_folder,
server_uid,
flags,
);
}
unsafe fn cb_precheck_imf(
context: &Context,
rfc724_mid: *const libc::c_char,
server_folder: &str,
server_uid: uint32_t,
) -> libc::c_int {
let rfc724_mid = as_str(rfc724_mid)
.trim_start_matches("<")
.trim_end_matches(">");
let mut mark_seen = false;
info!(
context,
0,
"cb_precheck_imf rfc724_mid={:?}, server_folder={}, server_uid={}",
rfc724_mid,
server_folder,
server_uid
);
if let Some(res) = dc_rfc724_mid_exists_with_msg_state(context, rfc724_mid) {
let (msg_id, _, old_server_folder, old_server_uid) = res;
info!(context, 0, "precheck msg_id={}", msg_id);
if msg_id != 0 {
if old_server_folder.is_empty() {
info!(context, 0, "[move] detected bbc-self {}", rfc724_mid,);
mark_seen = true;
} else if old_server_folder != server_folder {
info!(context, 0, "[move] detected moved message {}", rfc724_mid,);
}
dc_update_msg_move_state(context, rfc724_mid, MoveState::Stay);
}
if old_server_folder != server_folder || old_server_uid != server_uid {
info!(
context,
0, "precheck updated server_uid for msg_id={}", msg_id
);
dc_update_server_uid(context, rfc724_mid, server_folder, server_uid);
job_retry_msg_actions_at_once(context, msg_id);
}
dc_do_heuristics_moves(context, server_folder, msg_id);
if mark_seen {
job_add(
context,
Action::MarkseenMsgOnImap,
msg_id as libc::c_int,
Params::new(),
0,
);
}
return msg_id as i32;
}
0
}
fn cb_set_config(context: &Context, key: &str, value: Option<&str>) {
context.sql.set_config(context, key, value).ok();
}
/* *
* The following three callback are given to dc_imap_new() to read/write configuration
* and to handle received messages. As the imap-functions are typically used in
* a separate user-thread, also these functions may be called from a different thread.
*
* @private @memberof Context
*/
fn cb_get_config(context: &Context, key: &str) -> Option<String> {
context.sql.get_config(context, key)
}
pub unsafe fn dc_close(context: &Context) {
info!(context, 0, "disconnecting INBOX-watch",);
context.inbox.read().unwrap().disconnect(context);
info!(context, 0, "disconnecting sentbox-thread",);
context
.sentbox_thread
.read()
.unwrap()
.imap
.disconnect(context);
info!(context, 0, "disconnecting mvbox-thread",);
context
.mvbox_thread
.read()
.unwrap()
.imap
.disconnect(context);
info!(context, 0, "disconnecting SMTP");
context.smtp.clone().lock().unwrap().disconnect();
context.sql.close(context);
let mut dbfile = context.dbfile.write().unwrap();
*dbfile = None;
let mut blobdir = context.blobdir.write().unwrap();
free(*blobdir as *mut libc::c_void);
*blobdir = ptr::null_mut();
}
pub unsafe fn dc_is_open(context: &Context) -> libc::c_int {
context.sql.is_open() as libc::c_int
}
pub unsafe fn dc_get_userdata(context: &mut Context) -> *mut libc::c_void {
context.userdata as *mut _
}
pub unsafe fn dc_open(context: &Context, dbfile: &str, blobdir: Option<&str>) -> bool {
let mut success = false;
if 0 != dc_is_open(context) {
return false;
}
*context.dbfile.write().unwrap() = Some(PathBuf::from(dbfile));
if blobdir.is_some() && !blobdir.unwrap().is_empty() {
let dir = dc_ensure_no_slash_safe(blobdir.unwrap()).strdup();
*context.blobdir.write().unwrap() = dir;
} else {
let dir = dbfile.to_string() + "-blobs";
dc_create_folder(context, &dir);
*context.blobdir.write().unwrap() = dir.strdup();
}
// Create/open sqlite database, this may already use the blobdir
let dbfile_path = std::path::Path::new(dbfile);
if context.sql.open(context, dbfile_path, 0) {
success = true
}
if !success {
dc_close(context);
}
success
}
pub unsafe fn dc_get_blobdir(context: &Context) -> *mut libc::c_char {
dc_strdup(*context.blobdir.clone().read().unwrap())
}
/* ******************************************************************************
* INI-handling, Information
******************************************************************************/
pub unsafe fn dc_get_info(context: &Context) -> *mut libc::c_char {
let unset = "0";
let l = dc_loginparam_read(context, &context.sql, "");
let l2 = dc_loginparam_read(context, &context.sql, "configured_");
let displayname = context.sql.get_config(context, "displayname");
let chats = get_chat_cnt(context) as usize;
let real_msgs = dc_get_real_msg_cnt(context) as usize;
let deaddrop_msgs = dc_get_deaddrop_msg_cnt(context) as usize;
let contacts = Contact::get_real_cnt(context) as usize;
let is_configured = context
.sql
.get_config_int(context, "configured")
.unwrap_or_default();
let dbversion = context
.sql
.get_config_int(context, "dbversion")
.unwrap_or_default();
let e2ee_enabled = context
.sql
.get_config_int(context, "e2ee_enabled")
.unwrap_or_else(|| 1);
let mdns_enabled = context
.sql
.get_config_int(context, "mdns_enabled")
.unwrap_or_else(|| 1);
let prv_key_cnt: Option<isize> = context.sql.query_row_col(
context,
"SELECT COUNT(*) FROM keypairs;",
rusqlite::NO_PARAMS,
0,
);
let pub_key_cnt: Option<isize> = context.sql.query_row_col(
context,
"SELECT COUNT(*) FROM acpeerstates;",
rusqlite::NO_PARAMS,
0,
);
let fingerprint_str = if let Some(key) = Key::from_self_public(context, &l2.addr, &context.sql)
{
key.fingerprint()
} else {
"<Not yet calculated>".into()
};
let l_readable_str = dc_loginparam_get_readable(&l);
let l2_readable_str = dc_loginparam_get_readable(&l2);
let inbox_watch = context
.sql
.get_config_int(context, "inbox_watch")
.unwrap_or_else(|| 1);
let sentbox_watch = context
.sql
.get_config_int(context, "sentbox_watch")
.unwrap_or_else(|| 1);
let mvbox_watch = context
.sql
.get_config_int(context, "mvbox_watch")
.unwrap_or_else(|| 1);
let mvbox_move = context
.sql
.get_config_int(context, "mvbox_move")
.unwrap_or_else(|| 1);
let folders_configured = context
.sql
.get_config_int(context, "folders_configured")
.unwrap_or_default();
let configured_sentbox_folder = context
.sql
.get_config(context, "configured_sentbox_folder")
.unwrap_or_else(|| "<unset>".to_string());
let configured_mvbox_folder = context
.sql
.get_config(context, "configured_mvbox_folder")
.unwrap_or_else(|| "<unset>".to_string());
let res = format!(
"deltachat_core_version=v{}\n\
sqlite_version={}\n\
sqlite_thread_safe={}\n\
arch={}\n\
number_of_chats={}\n\
number_of_chat_messages={}\n\
messages_in_contact_requests={}\n\
number_of_contacts={}\n\
database_dir={}\n\
database_version={}\n\
blobdir={}\n\
display_name={}\n\
is_configured={}\n\
entered_account_settings={}\n\
used_account_settings={}\n\
inbox_watch={}\n\
sentbox_watch={}\n\
mvbox_watch={}\n\
mvbox_move={}\n\
folders_configured={}\n\
configured_sentbox_folder={}\n\
configured_mvbox_folder={}\n\
mdns_enabled={}\n\
e2ee_enabled={}\n\
private_key_count={}\n\
public_key_count={}\n\
fingerprint={}\n\
level=awesome\n",
&*DC_VERSION_STR,
rusqlite::version(),
sqlite3_threadsafe(),
// arch
(::std::mem::size_of::<*mut libc::c_void>()).wrapping_mul(8),
chats,
real_msgs,
deaddrop_msgs,
contacts,
context
.get_dbfile()
.as_ref()
.map_or(unset, |p| p.to_str().unwrap()),
dbversion,
if context.has_blobdir() {
as_str(context.get_blobdir())
} else {
unset
},
displayname.unwrap_or_else(|| unset.into()),
is_configured,
l_readable_str,
l2_readable_str,
inbox_watch,
sentbox_watch,
mvbox_watch,
mvbox_move,
folders_configured,
configured_sentbox_folder,
configured_mvbox_folder,
mdns_enabled,
e2ee_enabled,
prv_key_cnt.unwrap_or_default(),
pub_key_cnt.unwrap_or_default(),
fingerprint_str,
);
res.strdup()
}
pub unsafe fn dc_get_version_str() -> *mut libc::c_char {
(&*DC_VERSION_STR).strdup()
}
pub fn dc_get_fresh_msgs(context: &Context) -> Vec<u32> {
let show_deaddrop = 0;
context
.sql
.query_map(
"SELECT m.id FROM msgs m LEFT JOIN contacts ct \
ON m.from_id=ct.id LEFT JOIN chats c ON m.chat_id=c.id WHERE m.state=? \
AND m.hidden=0 \
AND m.chat_id>? \
AND ct.blocked=0 \
AND (c.blocked=0 OR c.blocked=?) ORDER BY m.timestamp DESC,m.id DESC;",
&[10, 9, if 0 != show_deaddrop { 2 } else { 0 }],
|row| row.get(0),
|rows| {
let mut ret = Vec::new();
for row in rows {
let id: u32 = row?;
ret.push(id);
}
Ok(ret)
},
)
.unwrap()
}
#[allow(non_snake_case)]
pub fn dc_search_msgs(
context: &Context,
chat_id: uint32_t,
query: *const libc::c_char,
) -> Vec<u32> {
if query.is_null() {
return Vec::new();
}
let real_query = to_string(query).trim().to_string();
if real_query.is_empty() {
return Vec::new();
}
let strLikeInText = format!("%{}%", &real_query);
let strLikeBeg = format!("{}%", &real_query);
let query = if 0 != chat_id {
"SELECT m.id, m.timestamp FROM msgs m LEFT JOIN contacts ct ON m.from_id=ct.id WHERE m.chat_id=? \
AND m.hidden=0 \
AND ct.blocked=0 AND (txt LIKE ? OR ct.name LIKE ?) ORDER BY m.timestamp,m.id;"
} else {
"SELECT m.id, m.timestamp FROM msgs m LEFT JOIN contacts ct ON m.from_id=ct.id \
LEFT JOIN chats c ON m.chat_id=c.id WHERE m.chat_id>9 AND m.hidden=0 \
AND (c.blocked=0 OR c.blocked=?) \
AND ct.blocked=0 AND (m.txt LIKE ? OR ct.name LIKE ?) ORDER BY m.timestamp DESC,m.id DESC;"
};
context
.sql
.query_map(
query,
params![chat_id as libc::c_int, &strLikeInText, &strLikeBeg],
|row| row.get::<_, i32>(0),
|rows| {
let mut ret = Vec::new();
for id in rows {
ret.push(id? as u32);
}
Ok(ret)
},
)
.unwrap_or_default()
}
pub fn dc_is_inbox(_context: &Context, folder_name: impl AsRef<str>) -> bool {
folder_name.as_ref() == "INBOX"
}
pub fn dc_is_sentbox(context: &Context, folder_name: impl AsRef<str>) -> bool {
let sentbox_name = context.sql.get_config(context, "configured_sentbox_folder");
if let Some(name) = sentbox_name {
name == folder_name.as_ref()
} else {
false
}
}
pub fn dc_is_mvbox(context: &Context, folder_name: impl AsRef<str>) -> bool {
let mvbox_name = context.sql.get_config(context, "configured_mvbox_folder");
if let Some(name) = mvbox_name {
name == folder_name.as_ref()
} else {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::*;
#[test]
fn test_wrong_db() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
std::fs::write(&dbfile, b"123").unwrap();
let res = Context::new(Box::new(|_, _| 0), "FakeOs".into(), dbfile);
assert!(res.is_err());
}
#[test]
fn test_get_fresh_msgs() {
let t = dummy_context();
let fresh = t.ctx.get_fresh_msgs();
assert!(fresh.is_empty())
}
#[test]
fn test_blobdir_exists() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
Context::new(Box::new(|_, _| 0), "FakeOS".into(), dbfile).unwrap();
let blobdir = tmp.path().join("db.sqlite-blobs");
assert!(blobdir.is_dir());
}
#[test]
fn test_wrong_blogdir() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("db.sqlite-blobs");
std::fs::write(&blobdir, b"123").unwrap();
let res = Context::new(Box::new(|_, _| 0), "FakeOS".into(), dbfile);
assert!(res.is_err());
}
#[test]
fn test_sqlite_parent_not_exists() {
let tmp = tempfile::tempdir().unwrap();
let subdir = tmp.path().join("subdir");
let dbfile = subdir.join("db.sqlite");
let dbfile2 = dbfile.clone();
Context::new(Box::new(|_, _| 0), "FakeOS".into(), dbfile).unwrap();
assert!(subdir.is_dir());
assert!(dbfile2.is_file());
}
#[test]
fn test_with_empty_blobdir() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = PathBuf::new();
let res = Context::with_blobdir(Box::new(|_, _| 0), "FakeOS".into(), dbfile, blobdir);
assert!(res.is_err());
}
#[test]
fn test_with_blobdir_not_exists() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("blobs");
let res = Context::with_blobdir(Box::new(|_, _| 0), "FakeOS".into(), dbfile, blobdir);
assert!(res.is_err());
}
#[test]
fn no_crashes_on_context_deref() {
let t = dummy_context();
std::mem::drop(t.ctx);
let ctx = dc_context_new(None, std::ptr::null_mut(), Some("Test OS".into()));
std::mem::drop(ctx);
}
#[test]
fn test_get_info() {
let t = dummy_context();
let info = t.ctx.get_info();
assert_eq!(info.get("level").unwrap(), "awesome");
fn test_context_double_close() {
let ctx = dc_context_new(None, std::ptr::null_mut(), None);
unsafe {
dc_close(&ctx);
dc_close(&ctx);
}
std::mem::drop(ctx);
}
}

View File

@@ -1,10 +1,11 @@
use crate::location::Location;
use crate::dc_location::dc_location;
use crate::types::*;
/* * the structure behind dc_array_t */
#[derive(Debug, Clone)]
#[derive(Clone)]
#[allow(non_camel_case_types)]
pub enum dc_array_t {
Locations(Vec<Location>),
Locations(Vec<dc_location>),
Uint(Vec<u32>),
}
@@ -18,7 +19,7 @@ impl dc_array_t {
dc_array_t::Locations(Vec::with_capacity(capacity))
}
pub fn add_id(&mut self, item: u32) {
pub fn add_id(&mut self, item: uint32_t) {
if let Self::Uint(array) = self {
array.push(item);
} else {
@@ -26,7 +27,7 @@ impl dc_array_t {
}
}
pub fn add_location(&mut self, location: Location) {
pub fn add_location(&mut self, location: dc_location) {
if let Self::Locations(array) = self {
array.push(location)
} else {
@@ -34,14 +35,14 @@ impl dc_array_t {
}
}
pub fn get_id(&self, index: usize) -> u32 {
pub fn get_id(&self, index: usize) -> uint32_t {
match self {
Self::Locations(array) => array[index].location_id,
Self::Uint(array) => array[index] as u32,
Self::Uint(array) => array[index] as uint32_t,
}
}
pub fn get_location(&self, index: usize) -> &Location {
pub fn get_location(&self, index: usize) -> &dc_location {
if let Self::Locations(array) = self {
&array[index]
} else {
@@ -107,8 +108,8 @@ impl From<Vec<u32>> for dc_array_t {
}
}
impl From<Vec<Location>> for dc_array_t {
fn from(array: Vec<Location>) -> Self {
impl From<Vec<dc_location>> for dc_array_t {
fn from(array: Vec<dc_location>) -> Self {
dc_array_t::Locations(array)
}
}

View File

@@ -2,6 +2,9 @@ use lazy_static::lazy_static;
use quick_xml;
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
use crate::dc_tools::*;
use crate::x::*;
lazy_static! {
static ref LINE_RE: regex::Regex = regex::Regex::new(r"(\r?\n)+").unwrap();
}
@@ -21,20 +24,19 @@ enum AddText {
// dc_dehtml() returns way too many lineends; however, an optimisation on this issue is not needed as
// the lineends are typically remove in further processing by the caller
pub fn dc_dehtml(buf_terminated: &str) -> String {
let buf_terminated = buf_terminated.trim();
if buf_terminated.is_empty() {
return "".into();
pub unsafe fn dc_dehtml(buf_terminated: *mut libc::c_char) -> *mut libc::c_char {
dc_trim(buf_terminated);
if *buf_terminated.offset(0isize) as libc::c_int == 0i32 {
return dc_strdup(b"\x00" as *const u8 as *const libc::c_char);
}
let mut dehtml = Dehtml {
strbuilder: String::with_capacity(buf_terminated.len()),
strbuilder: String::with_capacity(strlen(buf_terminated)),
add_text: AddText::YesRemoveLineEnds,
last_href: None,
};
let mut reader = quick_xml::Reader::from_str(buf_terminated);
let mut reader = quick_xml::Reader::from_str(as_str(buf_terminated));
let mut buf = Vec::new();
@@ -59,7 +61,7 @@ pub fn dc_dehtml(buf_terminated: &str) -> String {
buf.clear();
}
dehtml.strbuilder
dehtml.strbuilder.strdup()
}
fn dehtml_text_cb(event: &BytesText, dehtml: &mut Dehtml) {
@@ -165,28 +167,3 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dc_dehtml() {
let cases = vec![
(
"<a href='https://example.com'> Foo </a>",
"[ Foo ](https://example.com)",
),
("<img href='/foo.png'>", ""),
("<b> bar </b>", "* bar *"),
("<b> bar <i> foo", "* bar _ foo"),
("&amp; bar", "& bar"),
// Note missing '
("<a href='/foo.png>Hi</a> ", ""),
("", ""),
];
for (input, output) in cases {
assert_eq!(dc_dehtml(input), output);
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

776
src/dc_location.rs Normal file
View File

@@ -0,0 +1,776 @@
use quick_xml;
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
use crate::chat;
use crate::constants::Event;
use crate::constants::*;
use crate::context::*;
use crate::dc_tools::*;
use crate::job::*;
use crate::message::*;
use crate::param::*;
use crate::sql;
use crate::stock::StockMessage;
use crate::types::*;
use crate::x::*;
// location handling
#[derive(Clone, Default)]
#[allow(non_camel_case_types)]
pub struct dc_location {
pub location_id: uint32_t,
pub latitude: libc::c_double,
pub longitude: libc::c_double,
pub accuracy: libc::c_double,
pub timestamp: i64,
pub contact_id: uint32_t,
pub msg_id: uint32_t,
pub chat_id: uint32_t,
pub marker: Option<String>,
pub independent: uint32_t,
}
impl dc_location {
pub fn new() -> Self {
dc_location {
location_id: 0,
latitude: 0.0,
longitude: 0.0,
accuracy: 0.0,
timestamp: 0,
contact_id: 0,
msg_id: 0,
chat_id: 0,
marker: None,
independent: 0,
}
}
}
#[derive(Clone)]
#[allow(non_camel_case_types)]
pub struct dc_kml_t {
pub addr: *mut libc::c_char,
pub locations: Option<Vec<dc_location>>,
pub tag: libc::c_int,
pub curr: dc_location,
}
impl dc_kml_t {
pub fn new() -> Self {
dc_kml_t {
addr: std::ptr::null_mut(),
locations: None,
tag: 0,
curr: dc_location::new(),
}
}
}
// location streaming
pub unsafe fn dc_send_locations_to_chat(context: &Context, chat_id: uint32_t, seconds: i64) {
let now = time();
let mut msg: Message;
let is_sending_locations_before: bool;
if !(seconds < 0 || chat_id <= 9i32 as libc::c_uint) {
is_sending_locations_before = dc_is_sending_locations_to_chat(context, chat_id);
if sql::execute(
context,
&context.sql,
"UPDATE chats \
SET locations_send_begin=?, \
locations_send_until=? \
WHERE id=?",
params![
if 0 != seconds { now } else { 0 },
if 0 != seconds { now + seconds } else { 0 },
chat_id as i32,
],
)
.is_ok()
{
if 0 != seconds && !is_sending_locations_before {
msg = dc_msg_new(context, Viewtype::Text);
msg.text =
Some(context.stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0));
msg.param.set_int(Param::Cmd, 8);
chat::send_msg(context, chat_id, &mut msg).unwrap();
} else if 0 == seconds && is_sending_locations_before {
let stock_str =
context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0);
chat::add_device_msg(context, chat_id, stock_str);
}
context.call_cb(
Event::CHAT_MODIFIED,
chat_id as uintptr_t,
0i32 as uintptr_t,
);
if 0 != seconds {
schedule_MAYBE_SEND_LOCATIONS(context, 0i32);
job_add(
context,
Action::MaybeSendLocationsEnded,
chat_id as libc::c_int,
Params::new(),
seconds + 1,
);
}
}
}
}
/*******************************************************************************
* job to send locations out to all chats that want them
******************************************************************************/
#[allow(non_snake_case)]
unsafe fn schedule_MAYBE_SEND_LOCATIONS(context: &Context, flags: libc::c_int) {
if 0 != flags & 0x1 || !job_action_exists(context, Action::MaybeSendLocations) {
job_add(context, Action::MaybeSendLocations, 0, Params::new(), 60);
};
}
pub fn dc_is_sending_locations_to_chat(context: &Context, chat_id: u32) -> bool {
context
.sql
.exists(
"SELECT id FROM chats WHERE (? OR id=?) AND locations_send_until>?;",
params![if chat_id == 0 { 1 } else { 0 }, chat_id as i32, time()],
)
.unwrap_or_default()
}
pub fn dc_set_location(
context: &Context,
latitude: libc::c_double,
longitude: libc::c_double,
accuracy: libc::c_double,
) -> libc::c_int {
if latitude == 0.0 && longitude == 0.0 {
return 1;
}
context.sql.query_map(
"SELECT id FROM chats WHERE locations_send_until>?;",
params![time()], |row| row.get::<_, i32>(0),
|chats| {
let mut continue_streaming = false;
for chat in chats {
let chat_id = chat?;
context.sql.execute(
"INSERT INTO locations \
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
params![
latitude,
longitude,
accuracy,
time(),
chat_id,
1,
]
)?;
continue_streaming = true;
}
if continue_streaming {
context.call_cb(Event::LOCATION_CHANGED, 1, 0);
};
unsafe { schedule_MAYBE_SEND_LOCATIONS(context, 0) };
Ok(continue_streaming as libc::c_int)
}
).unwrap_or_default()
}
pub fn dc_get_locations(
context: &Context,
chat_id: uint32_t,
contact_id: uint32_t,
timestamp_from: i64,
mut timestamp_to: i64,
) -> Vec<dc_location> {
if timestamp_to == 0 {
timestamp_to = time() + 10;
}
context
.sql
.query_map(
"SELECT l.id, l.latitude, l.longitude, l.accuracy, l.timestamp, l.independent, \
m.id, l.from_id, l.chat_id, m.txt \
FROM locations l LEFT JOIN msgs m ON l.id=m.location_id WHERE (? OR l.chat_id=?) \
AND (? OR l.from_id=?) \
AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \
ORDER BY l.timestamp DESC, l.id DESC, m.id DESC;",
params![
if chat_id == 0 { 1 } else { 0 },
chat_id as i32,
if contact_id == 0 { 1 } else { 0 },
contact_id as i32,
timestamp_from,
timestamp_to,
],
|row| {
let msg_id = row.get(6)?;
let txt: String = row.get(9)?;
let marker = if msg_id != 0 && is_marker(&txt) {
Some(txt)
} else {
None
};
let loc = dc_location {
location_id: row.get(0)?,
latitude: row.get(1)?,
longitude: row.get(2)?,
accuracy: row.get(3)?,
timestamp: row.get(4)?,
independent: row.get(5)?,
msg_id,
contact_id: row.get(7)?,
chat_id: row.get(8)?,
marker,
};
Ok(loc)
},
|locations| {
let mut ret = Vec::new();
for location in locations {
ret.push(location?);
}
Ok(ret)
},
)
.unwrap_or_default()
}
fn is_marker(txt: &str) -> bool {
txt.len() == 1 && txt.chars().next().unwrap() != ' '
}
pub fn dc_delete_all_locations(context: &Context) -> bool {
if sql::execute(context, &context.sql, "DELETE FROM locations;", params![]).is_err() {
return false;
}
context.call_cb(Event::LOCATION_CHANGED, 0, 0);
true
}
pub fn dc_get_location_kml(
context: &Context,
chat_id: uint32_t,
last_added_location_id: *mut uint32_t,
) -> *mut libc::c_char {
let mut success: libc::c_int = 0;
let now = time();
let mut location_count: libc::c_int = 0;
let mut ret = String::new();
let self_addr = context
.sql
.get_config(context, "configured_addr")
.unwrap_or_default();
if let Ok((locations_send_begin, locations_send_until, locations_last_sent)) = context.sql.query_row(
"SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;",
params![chat_id as i32], |row| {
let send_begin: i64 = row.get(0)?;
let send_until: i64 = row.get(1)?;
let last_sent: i64 = row.get(2)?;
Ok((send_begin, send_until, last_sent))
}
) {
if !(locations_send_begin == 0 || now > locations_send_until) {
ret += &format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n<Document addr=\"{}\">\n",
self_addr,
);
context.sql.query_map(
"SELECT id, latitude, longitude, accuracy, timestamp\
FROM locations WHERE from_id=? \
AND timestamp>=? \
AND (timestamp>=? OR timestamp=(SELECT MAX(timestamp) FROM locations WHERE from_id=?)) \
AND independent=0 \
GROUP BY timestamp \
ORDER BY timestamp;",
params![1, locations_send_begin, locations_last_sent, 1],
|row| {
let location_id: i32 = row.get(0)?;
let latitude: f64 = row.get(1)?;
let longitude: f64 = row.get(2)?;
let accuracy: f64 = row.get(3)?;
let timestamp = unsafe { get_kml_timestamp(row.get(4)?) };
Ok((location_id, latitude, longitude, accuracy, timestamp))
},
|rows| {
for row in rows {
let (location_id, latitude, longitude, accuracy, timestamp) = row?;
ret += &format!(
"<Placemark><Timestamp><when>{}</when></Timestamp><Point><coordinates accuracy=\"{}\">{},{}</coordinates></Point></Placemark>\n\x00",
as_str(timestamp),
accuracy,
longitude,
latitude
);
location_count += 1;
if !last_added_location_id.is_null() {
unsafe { *last_added_location_id = location_id as u32 };
}
unsafe { free(timestamp as *mut libc::c_void) };
}
Ok(())
}
).unwrap(); // TODO: better error handling
}
}
if location_count > 0 {
ret += "</Document>\n</kml>";
success = 1;
}
if 0 != success {
unsafe { ret.strdup() }
} else {
std::ptr::null_mut()
}
}
/*******************************************************************************
* create kml-files
******************************************************************************/
unsafe fn get_kml_timestamp(utc: i64) -> *mut libc::c_char {
// Returns a string formatted as YYYY-MM-DDTHH:MM:SSZ. The trailing `Z` indicates UTC.
let res = chrono::NaiveDateTime::from_timestamp(utc, 0)
.format("%Y-%m-%dT%H:%M:%SZ")
.to_string();
res.strdup()
}
pub unsafe fn dc_get_message_kml(
timestamp: i64,
latitude: libc::c_double,
longitude: libc::c_double,
) -> *mut libc::c_char {
let timestamp_str = get_kml_timestamp(timestamp);
let latitude_str = dc_ftoa(latitude);
let longitude_str = dc_ftoa(longitude);
let ret = dc_mprintf(
b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n\
<Document>\n\
<Placemark>\
<Timestamp><when>%s</when></Timestamp>\
<Point><coordinates>%s,%s</coordinates></Point>\
</Placemark>\n\
</Document>\n\
</kml>\x00" as *const u8 as *const libc::c_char,
timestamp_str,
longitude_str, // reverse order!
latitude_str,
);
free(latitude_str as *mut libc::c_void);
free(longitude_str as *mut libc::c_void);
free(timestamp_str as *mut libc::c_void);
ret
}
pub fn dc_set_kml_sent_timestamp(context: &Context, chat_id: u32, timestamp: i64) -> bool {
sql::execute(
context,
&context.sql,
"UPDATE chats SET locations_last_sent=? WHERE id=?;",
params![timestamp, chat_id as i32],
)
.is_ok()
}
pub fn dc_set_msg_location_id(context: &Context, msg_id: u32, location_id: u32) -> bool {
sql::execute(
context,
&context.sql,
"UPDATE msgs SET location_id=? WHERE id=?;",
params![location_id, msg_id as i32],
)
.is_ok()
}
pub unsafe fn dc_save_locations(
context: &Context,
chat_id: u32,
contact_id: u32,
locations_opt: &Option<Vec<dc_location>>,
independent: libc::c_int,
) -> u32 {
if chat_id <= 9 || locations_opt.is_none() {
return 0;
}
let locations = locations_opt.as_ref().unwrap();
context
.sql
.prepare2(
"SELECT id FROM locations WHERE timestamp=? AND from_id=?",
"INSERT INTO locations\
(timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \
VALUES (?,?,?,?,?,?,?);",
|mut stmt_test, mut stmt_insert, conn| {
let mut newest_timestamp = 0;
let mut newest_location_id = 0;
for location in locations {
let exists =
stmt_test.exists(params![location.timestamp, contact_id as i32])?;
if 0 != independent || !exists {
stmt_insert.execute(params![
location.timestamp,
contact_id as i32,
chat_id as i32,
location.latitude,
location.longitude,
location.accuracy,
independent,
])?;
if location.timestamp > newest_timestamp {
newest_timestamp = location.timestamp;
newest_location_id = sql::get_rowid2_with_conn(
context,
conn,
"locations",
"timestamp",
location.timestamp,
"from_id",
contact_id as i32,
);
}
}
}
Ok(newest_location_id)
},
)
.unwrap_or_default()
}
pub unsafe fn dc_kml_parse(
context: &Context,
content: *const libc::c_char,
content_bytes: size_t,
) -> dc_kml_t {
let mut kml = dc_kml_t::new();
if content_bytes > (1 * 1024 * 1024) {
warn!(
context,
0, "A kml-files with {} bytes is larger than reasonably expected.", content_bytes,
);
return kml;
}
let content_null = dc_null_terminate(content, content_bytes as libc::c_int);
if !content_null.is_null() {
let mut reader = quick_xml::Reader::from_str(as_str(content_null));
reader.trim_text(true);
kml.locations = Some(Vec::with_capacity(100));
let mut buf = Vec::new();
loop {
match reader.read_event(&mut buf) {
Ok(quick_xml::events::Event::Start(ref e)) => kml_starttag_cb(e, &mut kml, &reader),
Ok(quick_xml::events::Event::End(ref e)) => kml_endtag_cb(e, &mut kml),
Ok(quick_xml::events::Event::Text(ref e)) => kml_text_cb(e, &mut kml, &reader),
Err(e) => {
error!(
context,
0,
"Location parsing: Error at position {}: {:?}",
reader.buffer_position(),
e
);
}
Ok(quick_xml::events::Event::Eof) => break,
_ => (),
}
buf.clear();
}
}
free(content_null.cast());
kml
}
fn kml_text_cb<B: std::io::BufRead>(
event: &BytesText,
kml: &mut dc_kml_t,
reader: &quick_xml::Reader<B>,
) {
if 0 != kml.tag & (0x4 | 0x10) {
let val = event.unescape_and_decode(reader).unwrap_or_default();
let val = val
.replace("\n", "")
.replace("\r", "")
.replace("\t", "")
.replace(" ", "");
if 0 != kml.tag & 0x4 && val.len() >= 19 {
// YYYY-MM-DDTHH:MM:SSZ
// 0 4 7 10 13 16 19
match chrono::NaiveDateTime::parse_from_str(&val, "%Y-%m-%dT%H:%M:%SZ") {
Ok(res) => {
kml.curr.timestamp = res.timestamp();
if kml.curr.timestamp > time() {
kml.curr.timestamp = time();
}
}
Err(_err) => {
kml.curr.timestamp = time();
}
}
} else if 0 != kml.tag & 0x10 {
let parts = val.splitn(2, ',').collect::<Vec<_>>();
if parts.len() == 2 {
kml.curr.longitude = parts[0].parse().unwrap_or_default();
kml.curr.latitude = parts[1].parse().unwrap_or_default();
}
}
}
}
fn kml_endtag_cb(event: &BytesEnd, kml: &mut dc_kml_t) {
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
if tag == "placemark" {
if 0 != kml.tag & 0x1
&& 0 != kml.curr.timestamp
&& 0. != kml.curr.latitude
&& 0. != kml.curr.longitude
{
if let Some(ref mut locations) = kml.locations {
locations.push(std::mem::replace(&mut kml.curr, dc_location::new()));
}
}
kml.tag = 0
};
}
fn kml_starttag_cb<B: std::io::BufRead>(
event: &BytesStart,
kml: &mut dc_kml_t,
reader: &quick_xml::Reader<B>,
) {
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
if tag == "document" {
if let Some(addr) = event.attributes().find(|attr| {
attr.as_ref()
.map(|a| String::from_utf8_lossy(a.key).trim().to_lowercase() == "addr")
.unwrap_or_default()
}) {
kml.addr = unsafe {
addr.unwrap()
.unescape_and_decode_value(reader)
.unwrap_or_default()
.strdup()
};
}
} else if tag == "placemark" {
kml.tag = 0x1;
kml.curr.timestamp = 0;
kml.curr.latitude = 0 as libc::c_double;
kml.curr.longitude = 0.0f64;
kml.curr.accuracy = 0.0f64
} else if tag == "timestamp" && 0 != kml.tag & 0x1 {
kml.tag = 0x1 | 0x2
} else if tag == "when" && 0 != kml.tag & 0x2 {
kml.tag = 0x1 | 0x2 | 0x4
} else if tag == "point" && 0 != kml.tag & 0x1 {
kml.tag = 0x1 | 0x8
} else if tag == "coordinates" && 0 != kml.tag & 0x8 {
kml.tag = 0x1 | 0x8 | 0x10;
if let Some(acc) = event.attributes().find(|attr| {
attr.as_ref()
.map(|a| String::from_utf8_lossy(a.key).trim().to_lowercase() == "accuracy")
.unwrap_or_default()
}) {
let v = acc
.unwrap()
.unescape_and_decode_value(reader)
.unwrap_or_default();
kml.curr.accuracy = v.trim().parse().unwrap_or_default();
}
}
}
pub unsafe fn dc_kml_unref(kml: &mut dc_kml_t) {
free(kml.addr as *mut libc::c_void);
}
#[allow(non_snake_case)]
pub unsafe fn dc_job_do_DC_JOB_MAYBE_SEND_LOCATIONS(context: &Context, _job: &Job) {
let now = time();
let mut continue_streaming: libc::c_int = 1;
info!(
context,
0, " ----------------- MAYBE_SEND_LOCATIONS -------------- ",
);
context
.sql
.query_map(
"SELECT id, locations_send_begin, locations_last_sent \
FROM chats \
WHERE locations_send_until>?;",
params![now],
|row| {
let chat_id: i32 = row.get(0)?;
let locations_send_begin: i64 = row.get(1)?;
let locations_last_sent: i64 = row.get(2)?;
continue_streaming = 1;
// be a bit tolerant as the timer may not align exactly with time(NULL)
if now - locations_last_sent < (60 - 3) {
Ok(None)
} else {
Ok(Some((chat_id, locations_send_begin, locations_last_sent)))
}
},
|rows| {
context.sql.prepare(
"SELECT id \
FROM locations \
WHERE from_id=? \
AND timestamp>=? \
AND timestamp>? \
AND independent=0 \
ORDER BY timestamp;",
|mut stmt_locations, _| {
for (chat_id, locations_send_begin, locations_last_sent) in
rows.filter_map(|r| match r {
Ok(Some(v)) => Some(v),
_ => None,
})
{
// TODO: do I need to reset?
if !stmt_locations
.exists(params![1, locations_send_begin, locations_last_sent,])
.unwrap_or_default()
{
// if there is no new location, there's nothing to send.
// however, maybe we want to bypass this test eg. 15 minutes
continue;
}
// pending locations are attached automatically to every message,
// so also to this empty text message.
// DC_CMD_LOCATION is only needed to create a nicer subject.
//
// for optimisation and to avoid flooding the sending queue,
// we could sending these messages only if we're really online.
// the easiest way to determine this, is to check for an empty message queue.
// (might not be 100%, however, as positions are sent combined later
// and dc_set_location() is typically called periodically, this is ok)
let mut msg = dc_msg_new(context, Viewtype::Text);
msg.hidden = true;
msg.param.set_int(Param::Cmd, 9);
// TODO: handle cleanup on error
chat::send_msg(context, chat_id as u32, &mut msg).unwrap();
}
Ok(())
},
)
},
)
.unwrap(); // TODO: Better error handling
if 0 != continue_streaming {
schedule_MAYBE_SEND_LOCATIONS(context, 0x1);
}
}
#[allow(non_snake_case)]
pub unsafe fn dc_job_do_DC_JOB_MAYBE_SEND_LOC_ENDED(context: &Context, job: &mut Job) {
// this function is called when location-streaming _might_ have ended for a chat.
// the function checks, if location-streaming is really ended;
// if so, a device-message is added if not yet done.
let chat_id = (*job).foreign_id;
if let Ok((send_begin, send_until)) = context.sql.query_row(
"SELECT locations_send_begin, locations_send_until FROM chats WHERE id=?",
params![chat_id as i32],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)),
) {
if !(send_begin != 0 && time() <= send_until) {
// still streaming -
// may happen as several calls to dc_send_locations_to_chat()
// do not un-schedule pending DC_MAYBE_SEND_LOC_ENDED jobs
if !(send_begin == 0 && send_until == 0) {
// not streaming, device-message already sent
if context.sql.execute(
"UPDATE chats SET locations_send_begin=0, locations_send_until=0 WHERE id=?",
params![chat_id as i32],
).is_ok() {
let stock_str = context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0);
chat::add_device_msg(context, chat_id, stock_str);
context.call_cb(
Event::CHAT_MODIFIED,
chat_id as usize,
0,
);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::dummy_context;
#[test]
fn test_dc_kml_parse() {
unsafe {
let context = dummy_context();
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>\x00"
as *const u8 as *const libc::c_char;
let mut kml = dc_kml_parse(&context.ctx, xml, strlen(xml));
assert!(!kml.addr.is_null());
assert_eq!(as_str(kml.addr as *const libc::c_char), "user@example.org",);
let locations_ref = &kml.locations.as_ref().unwrap();
assert_eq!(locations_ref.len(), 2);
assert!(locations_ref[0].latitude > 53.6f64);
assert!(locations_ref[0].latitude < 53.8f64);
assert!(locations_ref[0].longitude > 9.3f64);
assert!(locations_ref[0].longitude < 9.5f64);
assert!(locations_ref[0].accuracy > 31.9f64);
assert!(locations_ref[0].accuracy < 32.1f64);
assert_eq!(locations_ref[0].timestamp, 1551906597);
assert!(locations_ref[1].latitude > 63.6f64);
assert!(locations_ref[1].latitude < 63.8f64);
assert!(locations_ref[1].longitude > 19.3f64);
assert!(locations_ref[1].longitude < 19.5f64);
assert!(locations_ref[1].accuracy > 2.4f64);
assert!(locations_ref[1].accuracy < 2.6f64);
assert_eq!(locations_ref[1].timestamp, 1544739072);
dc_kml_unref(&mut kml);
}
}
}

214
src/dc_loginparam.rs Normal file
View File

@@ -0,0 +1,214 @@
use std::borrow::Cow;
use crate::context::Context;
use crate::sql::Sql;
#[derive(Default, Debug)]
#[allow(non_camel_case_types)]
pub struct dc_loginparam_t {
pub addr: String,
pub mail_server: String,
pub mail_user: String,
pub mail_pw: String,
pub mail_port: i32,
pub send_server: String,
pub send_user: String,
pub send_pw: String,
pub send_port: i32,
pub server_flags: i32,
}
impl dc_loginparam_t {
pub fn addr_str(&self) -> &str {
self.addr.as_str()
}
}
pub fn dc_loginparam_new() -> dc_loginparam_t {
Default::default()
}
pub fn dc_loginparam_read(
context: &Context,
sql: &Sql,
prefix: impl AsRef<str>,
) -> dc_loginparam_t {
let prefix = prefix.as_ref();
let key = format!("{}addr", prefix);
let addr = sql
.get_config(context, key)
.unwrap_or_default()
.trim()
.to_string();
let key = format!("{}mail_server", prefix);
let mail_server = sql.get_config(context, key).unwrap_or_default();
let key = format!("{}mail_port", prefix);
let mail_port = sql.get_config_int(context, key).unwrap_or_default();
let key = format!("{}mail_user", prefix);
let mail_user = sql.get_config(context, key).unwrap_or_default();
let key = format!("{}mail_pw", prefix);
let mail_pw = sql.get_config(context, key).unwrap_or_default();
let key = format!("{}send_server", prefix);
let send_server = sql.get_config(context, key).unwrap_or_default();
let key = format!("{}send_port", prefix);
let send_port = sql.get_config_int(context, key).unwrap_or_default();
let key = format!("{}send_user", prefix);
let send_user = sql.get_config(context, key).unwrap_or_default();
let key = format!("{}send_pw", prefix);
let send_pw = sql.get_config(context, key).unwrap_or_default();
let key = format!("{}server_flags", prefix);
let server_flags = sql.get_config_int(context, key).unwrap_or_default();
dc_loginparam_t {
addr: addr.to_string(),
mail_server,
mail_user,
mail_pw,
mail_port,
send_server,
send_user,
send_pw,
send_port,
server_flags,
}
}
pub fn dc_loginparam_write(
context: &Context,
loginparam: &dc_loginparam_t,
sql: &Sql,
prefix: impl AsRef<str>,
) {
let prefix = prefix.as_ref();
let key = format!("{}addr", prefix);
sql.set_config(context, key, Some(&loginparam.addr)).ok();
let key = format!("{}mail_server", prefix);
sql.set_config(context, key, Some(&loginparam.mail_server))
.ok();
let key = format!("{}mail_port", prefix);
sql.set_config_int(context, key, loginparam.mail_port).ok();
let key = format!("{}mail_user", prefix);
sql.set_config(context, key, Some(&loginparam.mail_user))
.ok();
let key = format!("{}mail_pw", prefix);
sql.set_config(context, key, Some(&loginparam.mail_pw)).ok();
let key = format!("{}send_server", prefix);
sql.set_config(context, key, Some(&loginparam.send_server))
.ok();
let key = format!("{}send_port", prefix);
sql.set_config_int(context, key, loginparam.send_port).ok();
let key = format!("{}send_user", prefix);
sql.set_config(context, key, Some(&loginparam.send_user))
.ok();
let key = format!("{}send_pw", prefix);
sql.set_config(context, key, Some(&loginparam.send_pw)).ok();
let key = format!("{}server_flags", prefix);
sql.set_config_int(context, key, loginparam.server_flags)
.ok();
}
fn unset_empty(s: &String) -> Cow<String> {
if s.is_empty() {
Cow::Owned("unset".to_string())
} else {
Cow::Borrowed(s)
}
}
pub fn dc_loginparam_get_readable(loginparam: &dc_loginparam_t) -> String {
let unset = "0";
let pw = "***";
let flags_readable = get_readable_flags(loginparam.server_flags);
format!(
"{} {}:{}:{}:{} {}:{}:{}:{} {}",
unset_empty(&loginparam.addr),
unset_empty(&loginparam.mail_user),
if !loginparam.mail_pw.is_empty() {
pw
} else {
unset
},
unset_empty(&loginparam.mail_server),
loginparam.mail_port,
unset_empty(&loginparam.send_user),
if !loginparam.send_pw.is_empty() {
pw
} else {
unset
},
unset_empty(&loginparam.send_server),
loginparam.send_port,
flags_readable,
)
}
fn get_readable_flags(flags: i32) -> String {
let mut res = String::new();
for bit in 0..31 {
if 0 != flags & 1 << bit {
let mut flag_added = 0;
if 1 << bit == 0x2 {
res += "OAUTH2 ";
flag_added = 1;
}
if 1 << bit == 0x4 {
res += "AUTH_NORMAL ";
flag_added = 1;
}
if 1 << bit == 0x100 {
res += "IMAP_STARTTLS ";
flag_added = 1;
}
if 1 << bit == 0x200 {
res += "IMAP_SSL ";
flag_added = 1;
}
if 1 << bit == 0x400 {
res += "IMAP_PLAIN ";
flag_added = 1;
}
if 1 << bit == 0x10000 {
res += "SMTP_STARTTLS ";
flag_added = 1
}
if 1 << bit == 0x20000 {
res += "SMTP_SSL ";
flag_added = 1
}
if 1 << bit == 0x40000 {
res += "SMTP_PLAIN ";
flag_added = 1
}
if 0 == flag_added {
res += &format!("{:#0x}", 1 << bit);
}
}
}
if res.is_empty() {
res += "0";
}
res
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

45
src/dc_move.rs Normal file
View File

@@ -0,0 +1,45 @@
use crate::constants::*;
use crate::context::*;
use crate::dc_tools::as_str;
use crate::job::*;
use crate::message::*;
use crate::param::Params;
pub unsafe fn dc_do_heuristics_moves(context: &Context, folder: &str, msg_id: u32) {
if context
.sql
.get_config_int(context, "mvbox_move")
.unwrap_or_else(|| 1)
== 0
{
return;
}
if !dc_is_inbox(context, folder) && !dc_is_sentbox(context, folder) {
return;
}
if let Ok(msg) = dc_msg_new_load(context, msg_id) {
if dc_msg_is_setupmessage(&msg) {
// do not move setup messages;
// there may be a non-delta device that wants to handle it
return;
}
if dc_is_mvbox(context, folder) {
dc_update_msg_move_state(context, as_str(msg.rfc724_mid), MoveState::Stay);
}
// 1 = dc message, 2 = reply to dc message
if 0 != msg.is_dc_message {
job_add(
context,
Action::MoveMsg,
msg.id as libc::c_int,
Params::new(),
0,
);
dc_update_msg_move_state(context, as_str(msg.rfc724_mid), MoveState::Moving);
}
}
}

File diff suppressed because it is too large Load Diff

1026
src/dc_securejoin.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,49 +1,64 @@
use crate::dc_dehtml::*;
use crate::dc_tools::*;
use crate::x::*;
#[derive(Copy, Clone)]
pub struct Simplify {
pub is_forwarded: bool,
}
/// Return index of footer line in vector of message lines, or vector length if
/// no footer is found.
///
/// Also return whether not-standard (rfc3676, §4.3) footer is found.
fn find_message_footer(lines: &[&str]) -> (usize, bool) {
for ix in 0..lines.len() {
let line = lines[ix];
// quoted-printable may encode `-- ` to `-- =20` which is converted
// back to `-- `
match line.as_ref() {
"-- " | "-- " => return (ix, false),
"--" | "---" | "----" => return (ix, true),
_ => (),
}
}
return (lines.len(), false);
pub is_cut_at_begin: bool,
pub is_cut_at_end: bool,
}
impl Simplify {
pub fn new() -> Self {
Simplify {
is_forwarded: false,
is_cut_at_begin: false,
is_cut_at_end: false,
}
}
/// Simplify and normalise text: Remove quotes, signatures, unnecessary
/// lineends etc.
/// The data returned from simplify() must be free()'d when no longer used.
pub fn simplify(&mut self, input: &str, is_html: bool, is_msgrmsg: bool) -> String {
let mut out = if is_html {
dc_dehtml(input)
} else {
input.to_string()
};
pub unsafe fn simplify(
&mut self,
in_unterminated: *const libc::c_char,
in_bytes: libc::c_int,
is_html: bool,
is_msgrmsg: libc::c_int,
) -> *mut libc::c_char {
if in_bytes <= 0 {
return "".strdup();
}
out.retain(|c| c != '\r');
out = self.simplify_plain_text(&out, is_msgrmsg);
out.retain(|c| c != '\r');
/* create a copy of the given buffer */
let mut out: *mut libc::c_char;
let mut temp: *mut libc::c_char;
self.is_forwarded = false;
self.is_cut_at_begin = false;
self.is_cut_at_end = false;
out = strndup(
in_unterminated as *mut libc::c_char,
in_bytes as libc::c_ulong,
);
if out.is_null() {
return dc_strdup(b"\x00" as *const u8 as *const libc::c_char);
}
if is_html {
temp = dc_dehtml(out);
if !temp.is_null() {
free(out as *mut libc::c_void);
out = temp
}
}
dc_remove_cr_chars(out);
temp = self.simplify_plain_text(out, is_msgrmsg);
if !temp.is_null() {
free(out as *mut libc::c_void);
out = temp
}
dc_remove_cr_chars(out);
out
}
@@ -52,75 +67,102 @@ impl Simplify {
* Simplify Plain Text
*/
#[allow(non_snake_case)]
fn simplify_plain_text(&mut self, buf_terminated: &str, is_msgrmsg: bool) -> String {
unsafe fn simplify_plain_text(
&mut self,
buf_terminated: *const libc::c_char,
is_msgrmsg: libc::c_int,
) -> *mut libc::c_char {
/* This function ...
... removes all text after the line `-- ` (footer mark)
... removes full quotes at the beginning and at the end of the text -
these are all lines starting with the character `>`
... remove a non-empty line before the removed quote (contains sth. like "On 2.9.2016, Bjoern wrote:" in different formats and lanugages) */
/* split the given buffer into lines */
let lines: Vec<_> = buf_terminated.split('\n').collect();
let lines = dc_split_into_lines(buf_terminated);
let mut l_first: usize = 0;
let mut is_cut_at_begin = false;
let (mut l_last, mut is_cut_at_end) = find_message_footer(&lines);
let mut l_last = lines.len();
let mut line: *mut libc::c_char;
let mut footer_mark: libc::c_int = 0i32;
for l in l_first..l_last {
line = lines[l];
if strcmp(line, b"-- \x00" as *const u8 as *const libc::c_char) == 0i32
|| strcmp(line, b"-- \x00" as *const u8 as *const libc::c_char) == 0i32
{
footer_mark = 1i32
}
if strcmp(line, b"--\x00" as *const u8 as *const libc::c_char) == 0i32
|| strcmp(line, b"---\x00" as *const u8 as *const libc::c_char) == 0i32
|| strcmp(line, b"----\x00" as *const u8 as *const libc::c_char) == 0i32
{
footer_mark = 1i32;
self.is_cut_at_end = true
}
if 0 != footer_mark {
l_last = l;
/* done */
break;
}
}
if l_last > l_first + 2 {
let line0 = lines[l_first];
let line1 = lines[l_first + 1];
let line2 = lines[l_first + 2];
if line0 == "---------- Forwarded message ----------"
&& line1.starts_with("From: ")
&& line2.is_empty()
let line0: *mut libc::c_char = lines[l_first];
let line1: *mut libc::c_char = lines[l_first + 1];
let line2: *mut libc::c_char = lines[l_first + 2];
if strcmp(
line0,
b"---------- Forwarded message ----------\x00" as *const u8 as *const libc::c_char,
) == 0i32
&& strncmp(line1, b"From: \x00" as *const u8 as *const libc::c_char, 6) == 0i32
&& *line2.offset(0isize) as libc::c_int == 0i32
{
self.is_forwarded = true;
l_first += 3
}
}
for l in l_first..l_last {
let line = lines[l];
if line == "-----"
|| line == "_____"
|| line == "====="
|| line == "*****"
|| line == "~~~~~"
line = lines[l];
if strncmp(line, b"-----\x00" as *const u8 as *const libc::c_char, 5) == 0i32
|| strncmp(line, b"_____\x00" as *const u8 as *const libc::c_char, 5) == 0i32
|| strncmp(line, b"=====\x00" as *const u8 as *const libc::c_char, 5) == 0i32
|| strncmp(line, b"*****\x00" as *const u8 as *const libc::c_char, 5) == 0i32
|| strncmp(line, b"~~~~~\x00" as *const u8 as *const libc::c_char, 5) == 0i32
{
l_last = l;
is_cut_at_end = true;
self.is_cut_at_end = true;
/* done */
break;
}
}
if !is_msgrmsg {
if 0 == is_msgrmsg {
let mut l_lastQuotedLine = None;
for l in (l_first..l_last).rev() {
let line = lines[l];
line = lines[l];
if is_plain_quote(line) {
l_lastQuotedLine = Some(l)
} else if !is_empty_line(line) {
break;
}
}
if let Some(last_quoted_line) = l_lastQuotedLine {
l_last = last_quoted_line;
is_cut_at_end = true;
if l_lastQuotedLine.is_some() {
l_last = l_lastQuotedLine.unwrap();
self.is_cut_at_end = true;
if l_last > 1 {
if is_empty_line(lines[l_last - 1]) {
l_last -= 1
}
}
if l_last > 1 {
let line = lines[l_last - 1];
line = lines[l_last - 1];
if is_quoted_headline(line) {
l_last -= 1
}
}
}
}
if !is_msgrmsg {
if 0 == is_msgrmsg {
let mut l_lastQuotedLine_0 = None;
let mut hasQuotedHeadline = 0;
for l in l_first..l_last {
let line = lines[l];
line = lines[l];
if is_plain_quote(line) {
l_lastQuotedLine_0 = Some(l)
} else if !is_empty_line(line) {
@@ -135,21 +177,21 @@ impl Simplify {
}
}
}
if let Some(last_quoted_line) = l_lastQuotedLine_0 {
l_first = last_quoted_line + 1;
is_cut_at_begin = true
if l_lastQuotedLine_0.is_some() {
l_first = l_lastQuotedLine_0.unwrap() + 1;
self.is_cut_at_begin = true
}
}
/* re-create buffer from the remaining lines */
let mut ret = String::new();
if is_cut_at_begin {
if self.is_cut_at_begin {
ret += "[...]";
}
/* we write empty lines only in case and non-empty line follows */
let mut pending_linebreaks: libc::c_int = 0i32;
let mut content_lines_added: libc::c_int = 0i32;
for l in l_first..l_last {
let line = lines[l];
line = lines[l];
if is_empty_line(line) {
pending_linebreaks += 1
} else {
@@ -163,115 +205,142 @@ impl Simplify {
}
}
// the incoming message might contain invalid UTF8
ret += line;
ret += &to_string_lossy(line);
content_lines_added += 1;
pending_linebreaks = 1i32
}
}
if is_cut_at_end && (!is_cut_at_begin || 0 != content_lines_added) {
if self.is_cut_at_end && (!self.is_cut_at_begin || 0 != content_lines_added) {
ret += " [...]";
}
dc_free_splitted_lines(lines);
ret
ret.strdup()
}
}
/**
* Tools
*/
fn is_empty_line(buf: &str) -> bool {
// XXX: can it be simplified to buf.chars().all(|c| c.is_whitespace())?
//
// Strictly speaking, it is not equivalent (^A is not whitespace, but less than ' '),
// but having control sequences in email body?!
//
// See discussion at: https://github.com/deltachat/deltachat-core-rust/pull/402#discussion_r317062392
for c in buf.chars() {
if c > ' ' {
unsafe fn is_empty_line(buf: *const libc::c_char) -> bool {
/* force unsigned - otherwise the `> ' '` comparison will fail */
let mut p1: *const libc::c_uchar = buf as *const libc::c_uchar;
while 0 != *p1 {
if *p1 as libc::c_int > ' ' as i32 {
return false;
}
p1 = p1.offset(1isize)
}
true
}
fn is_quoted_headline(buf: &str) -> bool {
unsafe fn is_quoted_headline(buf: *const libc::c_char) -> bool {
/* This function may be called for the line _directly_ before a quote.
The function checks if the line contains sth. like "On 01.02.2016, xy@z wrote:" in various languages.
- Currently, we simply check if the last character is a ':'.
- Checking for the existence of an email address may fail (headlines may show the user's name instead of the address) */
let buf_len: libc::c_int = strlen(buf) as libc::c_int;
if buf_len > 80i32 {
return false;
}
if buf_len > 0i32 && *buf.offset((buf_len - 1i32) as isize) as libc::c_int == ':' as i32 {
return true;
}
buf.len() <= 80 && buf.ends_with(':')
false
}
fn is_plain_quote(buf: &str) -> bool {
buf.starts_with(">")
unsafe fn is_plain_quote(buf: *const libc::c_char) -> bool {
if *buf.offset(0isize) as libc::c_int == '>' as i32 {
return true;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use std::ffi::CStr;
proptest! {
#[test]
// proptest does not support [[:graphical:][:space:]] regex.
fn test_simplify_plain_text_fuzzy(input in "[!-~\t \n]+") {
let output = Simplify::new().simplify_plain_text(&input, true);
assert!(output.split('\n').all(|s| s != "-- "));
#[test]
fn test_simplify_trim() {
unsafe {
let mut simplify = Simplify::new();
let html: *const libc::c_char =
b"\r\r\nline1<br>\r\n\r\n\r\rline2\n\r\x00" as *const u8 as *const libc::c_char;
let plain: *mut libc::c_char =
simplify.simplify(html, strlen(html) as libc::c_int, true, 0);
assert_eq!(
CStr::from_ptr(plain as *const libc::c_char)
.to_str()
.unwrap(),
"line1\nline2",
);
free(plain as *mut libc::c_void);
}
}
#[test]
fn test_simplify_trim() {
let mut simplify = Simplify::new();
let html = "\r\r\nline1<br>\r\n\r\n\r\rline2\n\r";
let plain = simplify.simplify(html, true, false);
assert_eq!(plain, "line1\nline2");
}
#[test]
fn test_simplify_parse_href() {
let mut simplify = Simplify::new();
let html = "<a href=url>text</a";
let plain = simplify.simplify(html, true, false);
unsafe {
let mut simplify = Simplify::new();
let html: *const libc::c_char =
b"<a href=url>text</a\x00" as *const u8 as *const libc::c_char;
let plain: *mut libc::c_char =
simplify.simplify(html, strlen(html) as libc::c_int, true, 0);
assert_eq!(plain, "[text](url)");
assert_eq!(
CStr::from_ptr(plain as *const libc::c_char)
.to_str()
.unwrap(),
"[text](url)",
);
free(plain as *mut libc::c_void);
}
}
#[test]
fn test_simplify_bold_text() {
let mut simplify = Simplify::new();
let html = "<!DOCTYPE name [<!DOCTYPE ...>]><!-- comment -->text <b><?php echo ... ?>bold</b><![CDATA[<>]]>";
let plain = simplify.simplify(html, true, false);
unsafe {
let mut simplify = Simplify::new();
let html: *const libc::c_char =
b"<!DOCTYPE name [<!DOCTYPE ...>]><!-- comment -->text <b><?php echo ... ?>bold</b><![CDATA[<>]]>\x00"
as *const u8 as *const libc::c_char;
let plain = simplify.simplify(html, strlen(html) as libc::c_int, true, 0);
assert_eq!(plain, "text *bold*<>");
assert_eq!(
CStr::from_ptr(plain as *const libc::c_char)
.to_str()
.unwrap(),
"text *bold*<>",
);
free(plain as *mut libc::c_void);
}
}
#[test]
fn test_simplify_html_encoded() {
let mut simplify = Simplify::new();
let html =
"&lt;&gt;&quot;&apos;&amp; &auml;&Auml;&ouml;&Ouml;&uuml;&Uuml;&szlig; foo&AElig;&ccedil;&Ccedil; &diams;&lrm;&rlm;&zwnj;&noent;&zwj;";
unsafe {
let mut simplify = Simplify::new();
let html =
b"&lt;&gt;&quot;&apos;&amp; &auml;&Auml;&ouml;&Ouml;&uuml;&Uuml;&szlig; foo&AElig;&ccedil;&Ccedil; &diams;&lrm;&rlm;&zwnj;&noent;&zwj;\x00"
as *const u8 as *const libc::c_char;
let plain = simplify.simplify(html, strlen(html) as libc::c_int, true, 0);
let plain = simplify.simplify(html, true, false);
assert_eq!(
CStr::from_ptr(plain as *const libc::c_char)
.to_str()
.unwrap(),
"<>\"\'& äÄöÖüÜß fooÆçÇ \u{2666}\u{200e}\u{200f}\u{200c}&noent;\u{200d}"
);
assert_eq!(
plain,
"<>\"\'& äÄöÖüÜß fooÆçÇ \u{2666}\u{200e}\u{200f}\u{200c}&noent;\u{200d}"
);
}
#[test]
fn test_simplify_utilities() {
assert!(is_empty_line(" \t"));
assert!(is_empty_line(""));
assert!(is_empty_line(" \r"));
assert!(!is_empty_line(" x"));
assert!(is_plain_quote("> hello world"));
assert!(is_plain_quote(">>"));
assert!(!is_plain_quote("Life is pain"));
assert!(!is_plain_quote(""));
free(plain as *mut libc::c_void);
}
}
}

View File

@@ -1,39 +1,88 @@
use std::borrow::Cow;
use std::ffi::CString;
use std::ptr;
use std::ffi::{CStr, CString};
use charset::Charset;
use mmime::mailmime_decode::*;
use mmime::mmapstring::*;
use mmime::other::*;
use percent_encoding::{percent_decode, utf8_percent_encode, AsciiSet, CONTROLS};
use crate::dc_tools::*;
use crate::types::*;
use crate::x::*;
/**
* Encode non-ascii-strings as `=?UTF-8?Q?Bj=c3=b6rn_Petersen?=`.
* Belongs to RFC 2047: https://tools.ietf.org/html/rfc2047
*
* We do not fold at position 72; this would result in empty words as `=?utf-8?Q??=` which are correct,
* but cannot be displayed by some mail programs (eg. Android Stock Mail).
* however, this is not needed, as long as _one_ word is not longer than 72 characters.
* _if_ it is, the display may get weird. This affects the subject only.
* the best solution wor all this would be if libetpan encodes the line as only libetpan knowns when a header line is full.
*
* @param to_encode Null-terminated UTF-8-string to encode.
* @return Returns the encoded string which must be free()'d when no longed needed.
* On errors, NULL is returned.
*/
pub unsafe fn dc_encode_header_words(to_encode_r: impl AsRef<str>) -> *mut libc::c_char {
let to_encode =
CString::new(to_encode_r.as_ref().as_bytes()).expect("invalid cstring to_encode");
#[inline]
fn isalnum(c: libc::c_int) -> libc::c_int {
if c < std::u8::MAX as libc::c_int {
(c as u8 as char).is_ascii_alphanumeric() as libc::c_int
} else {
0
}
}
/* ******************************************************************************
* URL encoding and decoding, RFC 3986
******************************************************************************/
unsafe fn int_2_uppercase_hex(code: libc::c_char) -> libc::c_char {
static mut HEX: [libc::c_char; 17] = [
48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70, 0,
];
HEX[(code as libc::c_int & 15i32) as usize]
}
pub unsafe fn dc_urldecode(to_decode: *const libc::c_char) -> *mut libc::c_char {
let mut pstr: *const libc::c_char = to_decode;
if to_decode.is_null() {
return dc_strdup(b"\x00" as *const u8 as *const libc::c_char);
}
let buf: *mut libc::c_char = malloc(strlen(to_decode).wrapping_add(1)) as *mut libc::c_char;
let mut pbuf: *mut libc::c_char = buf;
assert!(!buf.is_null());
while 0 != *pstr {
if *pstr as libc::c_int == '%' as i32 {
if 0 != *pstr.offset(1isize) as libc::c_int && 0 != *pstr.offset(2isize) as libc::c_int
{
let fresh5 = pbuf;
pbuf = pbuf.offset(1);
*fresh5 = ((hex_2_int(*pstr.offset(1isize)) as libc::c_int) << 4i32
| hex_2_int(*pstr.offset(2isize)) as libc::c_int)
as libc::c_char;
pstr = pstr.offset(2isize)
}
} else if *pstr as libc::c_int == '+' as i32 {
let fresh6 = pbuf;
pbuf = pbuf.offset(1);
*fresh6 = ' ' as i32 as libc::c_char
} else {
let fresh7 = pbuf;
pbuf = pbuf.offset(1);
*fresh7 = *pstr
}
pstr = pstr.offset(1isize)
}
*pbuf = '\u{0}' as i32 as libc::c_char;
buf
}
fn hex_2_int(ch: libc::c_char) -> libc::c_char {
let ch = ch as u8 as char;
if !ch.is_ascii_hexdigit() {
return (ch.to_ascii_lowercase() as i32 - 'a' as i32 + 10) as libc::c_char;
}
match ch.to_digit(16) {
Some(res) => res as libc::c_char,
None => 0,
}
}
pub unsafe fn dc_encode_header_words(to_encode: *const libc::c_char) -> *mut libc::c_char {
let mut ok_to_continue = true;
let mut ret_str: *mut libc::c_char = ptr::null_mut();
let mut cur: *const libc::c_char = to_encode.as_ptr();
let mut ret_str: *mut libc::c_char = 0 as *mut libc::c_char;
let mut cur: *const libc::c_char = to_encode;
let mmapstr: *mut MMAPString = mmap_string_new(b"\x00" as *const u8 as *const libc::c_char);
if mmapstr.is_null() {
if to_encode.is_null() || mmapstr.is_null() {
ok_to_continue = false;
}
loop {
@@ -68,7 +117,7 @@ pub unsafe fn dc_encode_header_words(to_encode_r: impl AsRef<str>) -> *mut libc:
b"utf-8\x00" as *const u8 as *const libc::c_char,
mmapstr,
begin,
end.wrapping_offset_from(begin) as libc::size_t,
end.wrapping_offset_from(begin) as size_t,
) {
ok_to_continue = false;
continue;
@@ -84,7 +133,7 @@ pub unsafe fn dc_encode_header_words(to_encode_r: impl AsRef<str>) -> *mut libc:
if mmap_string_append_len(
mmapstr,
end,
cur.wrapping_offset_from(end) as libc::size_t,
cur.wrapping_offset_from(end) as size_t,
)
.is_null()
{
@@ -95,7 +144,7 @@ pub unsafe fn dc_encode_header_words(to_encode_r: impl AsRef<str>) -> *mut libc:
} else if mmap_string_append_len(
mmapstr,
begin,
cur.wrapping_offset_from(begin) as libc::size_t,
cur.wrapping_offset_from(begin) as size_t,
)
.is_null()
{
@@ -124,10 +173,10 @@ unsafe fn quote_word(
display_charset: *const libc::c_char,
mmapstr: *mut MMAPString,
word: *const libc::c_char,
size: libc::size_t,
size: size_t,
) -> bool {
let mut cur: *const libc::c_char;
let mut i = 0;
let mut i: size_t = 0i32 as size_t;
let mut hex: [libc::c_char; 4] = [0; 4];
// let mut col: libc::c_int = 0i32;
if mmap_string_append(mmapstr, b"=?\x00" as *const u8 as *const libc::c_char).is_null() {
@@ -190,7 +239,7 @@ unsafe fn get_word(
{
cur = cur.offset(1isize)
}
*pto_be_quoted = to_be_quoted(begin, cur.wrapping_offset_from(begin) as libc::size_t);
*pto_be_quoted = to_be_quoted(begin, cur.wrapping_offset_from(begin) as size_t);
*pend = cur;
}
@@ -199,9 +248,9 @@ unsafe fn get_word(
******************************************************************************/
/* see comment below */
unsafe fn to_be_quoted(word: *const libc::c_char, size: libc::size_t) -> bool {
unsafe fn to_be_quoted(word: *const libc::c_char, size: size_t) -> bool {
let mut cur: *const libc::c_char = word;
let mut i = 0;
let mut i: size_t = 0i32 as size_t;
while i < size {
match *cur as libc::c_int {
44 | 58 | 33 | 34 | 35 | 36 | 64 | 91 | 92 | 93 | 94 | 96 | 123 | 124 | 125 | 126
@@ -221,10 +270,10 @@ unsafe fn to_be_quoted(word: *const libc::c_char, size: libc::size_t) -> bool {
pub unsafe fn dc_decode_header_words(in_0: *const libc::c_char) -> *mut libc::c_char {
if in_0.is_null() {
return ptr::null_mut();
return 0 as *mut libc::c_char;
}
let mut out: *mut libc::c_char = ptr::null_mut();
let mut cur_token = 0;
let mut out: *mut libc::c_char = 0 as *mut libc::c_char;
let mut cur_token: size_t = 0i32 as size_t;
let r: libc::c_int = mailmime_encoded_phrase_parse(
b"iso-8859-1\x00" as *const u8 as *const libc::c_char,
in_0,
@@ -240,88 +289,374 @@ pub unsafe fn dc_decode_header_words(in_0: *const libc::c_char) -> *mut libc::c_
out
}
pub fn dc_decode_header_words_safe(input: &str) -> String {
static FROM_ENCODING: &[u8] = b"iso-8859-1\x00";
static TO_ENCODING: &[u8] = b"utf-8\x00";
let mut out = ptr::null_mut();
let mut cur_token = 0;
let input_c = CString::yolo(input);
unsafe {
let r = mailmime_encoded_phrase_parse(
FROM_ENCODING.as_ptr().cast(),
input_c.as_ptr(),
input.len(),
&mut cur_token,
TO_ENCODING.as_ptr().cast(),
&mut out,
);
if r as u32 != MAILIMF_NO_ERROR || out.is_null() {
input.to_string()
} else {
let res = to_string(out);
free(out.cast());
res
#[cfg(test)]
unsafe fn dc_encode_modified_utf7(
mut to_encode: *const libc::c_char,
change_spaces: libc::c_int,
) -> *mut libc::c_char {
let mut utf8pos: libc::c_uint;
let mut utf8total: libc::c_uint;
let mut c: libc::c_uint;
let mut utf7mode: libc::c_uint;
let mut bitstogo: libc::c_uint;
let mut utf16flag: libc::c_uint;
let mut ucs4: libc::c_ulong = 0;
let mut bitbuf: libc::c_ulong = 0;
let mut dst: *mut libc::c_char;
let res: *mut libc::c_char;
if to_encode.is_null() {
return dc_strdup(b"\x00" as *const u8 as *const libc::c_char);
}
res = malloc(2usize.wrapping_mul(strlen(to_encode)).wrapping_add(1)) as *mut libc::c_char;
dst = res;
assert!(!dst.is_null());
utf7mode = 0i32 as libc::c_uint;
utf8total = 0i32 as libc::c_uint;
bitstogo = 0i32 as libc::c_uint;
utf8pos = 0i32 as libc::c_uint;
loop {
c = *to_encode as libc::c_uchar as libc::c_uint;
if !(c != '\u{0}' as i32 as libc::c_uint) {
break;
}
}
}
pub fn dc_needs_ext_header(to_check: impl AsRef<str>) -> bool {
let to_check = to_check.as_ref();
if to_check.is_empty() {
return false;
}
to_check.chars().any(|c| {
!(c.is_ascii_alphanumeric()
|| c == '-'
|| c == '_'
|| c == '_'
|| c == '.'
|| c == '~'
|| c == '%')
})
}
const EXT_ASCII_ST: &AsciiSet = &CONTROLS
.add(b' ')
.add(b'-')
.add(b'_')
.add(b'.')
.add(b'~')
.add(b'%');
/// Encode an UTF-8 string to the extended header format.
pub fn dc_encode_ext_header(to_encode: impl AsRef<str>) -> String {
let encoded = utf8_percent_encode(to_encode.as_ref(), &EXT_ASCII_ST);
format!("utf-8''{}", encoded)
}
/// Decode an extended-header-format strings to UTF-8.
pub fn dc_decode_ext_header(to_decode: &[u8]) -> Cow<str> {
if let Some(index) = bytes!(b'\'').find(to_decode) {
let (charset, rest) = to_decode.split_at(index);
if !charset.is_empty() {
// skip language
if let Some(index2) = bytes!(b'\'').find(&rest[1..]) {
let decoded = percent_decode(&rest[index2 + 2..]);
if charset != b"utf-8" && charset != b"UTF-8" {
if let Some(encoding) = Charset::for_label(charset) {
let bytes = decoded.collect::<Vec<u8>>();
let (res, _, _) = encoding.decode(&bytes);
return Cow::Owned(res.into_owned());
} else {
return decoded.decode_utf8_lossy();
}
to_encode = to_encode.offset(1isize);
// normal character?
if c >= ' ' as i32 as libc::c_uint
&& c <= '~' as i32 as libc::c_uint
&& (c != '_' as i32 as libc::c_uint || 0 == change_spaces)
{
if 0 != utf7mode {
if 0 != bitstogo {
let fresh8 = dst;
dst = dst.offset(1);
*fresh8 = BASE64CHARS
[(bitbuf << (6i32 as libc::c_uint).wrapping_sub(bitstogo) & 0x3f) as usize]
}
let fresh9 = dst;
dst = dst.offset(1);
*fresh9 = '-' as i32 as libc::c_char;
utf7mode = 0i32 as libc::c_uint;
utf8pos = 0i32 as libc::c_uint;
bitstogo = 0i32 as libc::c_uint;
utf8total = 0i32 as libc::c_uint
}
if 0 != change_spaces && c == ' ' as i32 as libc::c_uint {
let fresh10 = dst;
dst = dst.offset(1);
*fresh10 = '_' as i32 as libc::c_char
} else {
let fresh11 = dst;
dst = dst.offset(1);
*fresh11 = c as libc::c_char
}
if c == '&' as i32 as libc::c_uint {
let fresh12 = dst;
dst = dst.offset(1);
*fresh12 = '-' as i32 as libc::c_char
}
} else {
if 0 == utf7mode {
let fresh13 = dst;
dst = dst.offset(1);
*fresh13 = '&' as i32 as libc::c_char;
utf7mode = 1i32 as libc::c_uint
}
// encode ascii characters as themselves
if c < 0x80i32 as libc::c_uint {
ucs4 = c as libc::c_ulong
} else if 0 != utf8total {
ucs4 = ucs4 << 6i32 | c as libc::c_ulong & 0x3f;
utf8pos = utf8pos.wrapping_add(1);
if utf8pos < utf8total {
continue;
}
} else {
utf8pos = 1i32 as libc::c_uint;
if c < 0xe0i32 as libc::c_uint {
utf8total = 2i32 as libc::c_uint;
ucs4 = (c & 0x1fi32 as libc::c_uint) as libc::c_ulong
} else if c < 0xf0i32 as libc::c_uint {
utf8total = 3i32 as libc::c_uint;
ucs4 = (c & 0xfi32 as libc::c_uint) as libc::c_ulong
} else {
return decoded.decode_utf8_lossy();
utf8total = 4i32 as libc::c_uint;
ucs4 = (c & 0x3i32 as libc::c_uint) as libc::c_ulong
}
continue;
}
utf8total = 0i32 as libc::c_uint;
loop {
if ucs4 >= 0x10000 {
ucs4 = ucs4.wrapping_sub(0x10000);
bitbuf = bitbuf << 16 | (ucs4 >> 10).wrapping_add(0xd800);
ucs4 = (ucs4 & 0x3ff).wrapping_add(0xdc00);
utf16flag = 1i32 as libc::c_uint
} else {
bitbuf = bitbuf << 16 | ucs4;
utf16flag = 0i32 as libc::c_uint
}
bitstogo = bitstogo.wrapping_add(16i32 as libc::c_uint);
while bitstogo >= 6i32 as libc::c_uint {
bitstogo = bitstogo.wrapping_sub(6i32 as libc::c_uint);
let fresh14 = dst;
dst = dst.offset(1);
*fresh14 = BASE64CHARS[(if 0 != bitstogo {
bitbuf >> bitstogo
} else {
bitbuf
} & 0x3f) as usize]
}
if !(0 != utf16flag) {
break;
}
}
}
}
if 0 != utf7mode {
if 0 != bitstogo {
let fresh15 = dst;
dst = dst.offset(1);
*fresh15 = BASE64CHARS
[(bitbuf << (6i32 as libc::c_uint).wrapping_sub(bitstogo) & 0x3f) as usize]
}
let fresh16 = dst;
dst = dst.offset(1);
*fresh16 = '-' as i32 as libc::c_char
}
*dst = '\u{0}' as i32 as libc::c_char;
String::from_utf8_lossy(to_decode)
res
}
/* ******************************************************************************
* Encode/decode modified UTF-7 as needed for IMAP, see RFC 2192
******************************************************************************/
// UTF7 modified base64 alphabet
#[cfg(test)]
static mut BASE64CHARS: [libc::c_char; 65] = [
65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88,
89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114,
115, 116, 117, 118, 119, 120, 121, 122, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 43, 44, 0,
];
#[cfg(test)]
unsafe fn dc_decode_modified_utf7(
to_decode: *const libc::c_char,
change_spaces: libc::c_int,
) -> *mut libc::c_char {
let mut c: libc::c_uint;
let mut i: libc::c_uint;
let mut bitcount: libc::c_uint;
let mut ucs4: libc::c_ulong;
let mut utf16: libc::c_ulong;
let mut bitbuf: libc::c_ulong;
let mut base64: [libc::c_uchar; 256] = [0; 256];
let mut src: *const libc::c_char;
let mut dst: *mut libc::c_char;
let res: *mut libc::c_char;
if to_decode.is_null() {
return dc_strdup(b"\x00" as *const u8 as *const libc::c_char);
}
res = malloc(4usize.wrapping_mul(strlen(to_decode)).wrapping_add(1)) as *mut libc::c_char;
dst = res;
src = to_decode;
assert!(!dst.is_null());
libc::memset(
base64.as_mut_ptr() as *mut libc::c_void,
64,
::std::mem::size_of::<[libc::c_uchar; 256]>(),
);
i = 0i32 as libc::c_uint;
while (i as libc::c_ulong) < ::std::mem::size_of::<[libc::c_char; 65]>() as libc::c_ulong {
base64[BASE64CHARS[i as usize] as libc::c_uint as usize] = i as libc::c_uchar;
i = i.wrapping_add(1)
}
while *src as libc::c_int != '\u{0}' as i32 {
let fresh17 = src;
src = src.offset(1);
c = *fresh17 as libc::c_uint;
if c != '&' as i32 as libc::c_uint || *src as libc::c_int == '-' as i32 {
if 0 != change_spaces && c == '_' as i32 as libc::c_uint {
let fresh18 = dst;
dst = dst.offset(1);
*fresh18 = ' ' as i32 as libc::c_char
} else {
let fresh19 = dst;
dst = dst.offset(1);
*fresh19 = c as libc::c_char
}
if c == '&' as i32 as libc::c_uint {
src = src.offset(1isize)
}
} else {
bitbuf = 0;
bitcount = 0i32 as libc::c_uint;
ucs4 = 0;
loop {
c = base64[*src as libc::c_uchar as usize] as libc::c_uint;
if !(c != 64i32 as libc::c_uint) {
break;
}
src = src.offset(1isize);
bitbuf = bitbuf << 6i32 | c as libc::c_ulong;
bitcount = bitcount.wrapping_add(6i32 as libc::c_uint);
// enough bits for a UTF-16 character?
if !(bitcount >= 16i32 as libc::c_uint) {
continue;
}
bitcount = bitcount.wrapping_sub(16i32 as libc::c_uint);
utf16 = if 0 != bitcount {
bitbuf >> bitcount
} else {
bitbuf
} & 0xffff;
// convert UTF16 to UCS4
if utf16 >= 0xd800 && utf16 <= 0xdbff {
ucs4 = utf16.wrapping_sub(0xd800) << 10i32
} else {
if utf16 >= 0xdc00 && utf16 <= 0xdfff {
ucs4 = ucs4.wrapping_add(utf16.wrapping_sub(0xdc00).wrapping_add(0x10000))
} else {
ucs4 = utf16
}
if ucs4 <= 0x7f {
*dst.offset(0isize) = ucs4 as libc::c_char;
dst = dst.offset(1isize)
} else if ucs4 <= 0x7ff {
*dst.offset(0isize) = (0xc0 | ucs4 >> 6i32) as libc::c_char;
*dst.offset(1isize) = (0x80 | ucs4 & 0x3f) as libc::c_char;
dst = dst.offset(2isize)
} else if ucs4 <= 0xffff {
*dst.offset(0isize) = (0xe0 | ucs4 >> 12i32) as libc::c_char;
*dst.offset(1isize) = (0x80 | ucs4 >> 6i32 & 0x3f) as libc::c_char;
*dst.offset(2isize) = (0x80 | ucs4 & 0x3f) as libc::c_char;
dst = dst.offset(3isize)
} else {
*dst.offset(0isize) = (0xf0 | ucs4 >> 18i32) as libc::c_char;
*dst.offset(1isize) = (0x80 | ucs4 >> 12i32 & 0x3f) as libc::c_char;
*dst.offset(2isize) = (0x80 | ucs4 >> 6i32 & 0x3f) as libc::c_char;
*dst.offset(3isize) = (0x80 | ucs4 & 0x3f) as libc::c_char;
dst = dst.offset(4isize)
}
}
}
if *src as libc::c_int == '-' as i32 {
src = src.offset(1isize)
}
}
}
*dst = '\u{0}' as i32 as libc::c_char;
res
}
pub unsafe fn dc_needs_ext_header(mut to_check: *const libc::c_char) -> bool {
if !to_check.is_null() {
while 0 != *to_check {
if 0 == isalnum(*to_check as libc::c_int)
&& *to_check as libc::c_int != '-' as i32
&& *to_check as libc::c_int != '_' as i32
&& *to_check as libc::c_int != '.' as i32
&& *to_check as libc::c_int != '~' as i32
{
return true;
}
to_check = to_check.offset(1isize)
}
}
false
}
pub unsafe fn dc_encode_ext_header(to_encode: *const libc::c_char) -> *mut libc::c_char {
let mut pstr: *const libc::c_char = to_encode;
if to_encode.is_null() {
return dc_strdup(b"utf-8\'\'\x00" as *const u8 as *const libc::c_char);
}
let buf: *mut libc::c_char = malloc(
strlen(b"utf-8\'\'\x00" as *const u8 as *const libc::c_char)
.wrapping_add(strlen(to_encode).wrapping_mul(3))
.wrapping_add(1),
) as *mut libc::c_char;
assert!(!buf.is_null());
let mut pbuf: *mut libc::c_char = buf;
strcpy(pbuf, b"utf-8\'\'\x00" as *const u8 as *const libc::c_char);
pbuf = pbuf.offset(strlen(pbuf) as isize);
while 0 != *pstr {
if 0 != isalnum(*pstr as libc::c_int)
|| *pstr as libc::c_int == '-' as i32
|| *pstr as libc::c_int == '_' as i32
|| *pstr as libc::c_int == '.' as i32
|| *pstr as libc::c_int == '~' as i32
{
let fresh20 = pbuf;
pbuf = pbuf.offset(1);
*fresh20 = *pstr
} else {
let fresh21 = pbuf;
pbuf = pbuf.offset(1);
*fresh21 = '%' as i32 as libc::c_char;
let fresh22 = pbuf;
pbuf = pbuf.offset(1);
*fresh22 = int_2_uppercase_hex((*pstr as libc::c_int >> 4i32) as libc::c_char);
let fresh23 = pbuf;
pbuf = pbuf.offset(1);
*fresh23 = int_2_uppercase_hex((*pstr as libc::c_int & 15i32) as libc::c_char)
}
pstr = pstr.offset(1isize)
}
*pbuf = '\u{0}' as i32 as libc::c_char;
buf
}
pub unsafe fn dc_decode_ext_header(to_decode: *const libc::c_char) -> *mut libc::c_char {
let mut decoded: *mut libc::c_char = 0 as *mut libc::c_char;
let mut charset: *mut libc::c_char = 0 as *mut libc::c_char;
let mut p2: *const libc::c_char;
if !to_decode.is_null() {
// get char set
p2 = strchr(to_decode, '\'' as i32);
if !(p2.is_null() || p2 == to_decode) {
/*no empty charset allowed*/
charset =
dc_null_terminate(to_decode, p2.wrapping_offset_from(to_decode) as libc::c_int);
p2 = p2.offset(1isize);
// skip language
p2 = strchr(p2, '\'' as i32);
if !p2.is_null() {
p2 = p2.offset(1isize);
decoded = dc_urldecode(p2);
if !charset.is_null()
&& strcmp(charset, b"utf-8\x00" as *const u8 as *const libc::c_char) != 0i32
&& strcmp(charset, b"UTF-8\x00" as *const u8 as *const libc::c_char) != 0i32
{
if let Some(encoding) =
Charset::for_label(CStr::from_ptr(charset).to_str().unwrap().as_bytes())
{
let data =
std::slice::from_raw_parts(decoded as *const u8, strlen(decoded));
let (res, _, _) = encoding.decode(data);
free(decoded as *mut _);
let r = std::ffi::CString::new(res.as_bytes()).unwrap();
decoded = dc_strdup(r.as_ptr());
}
}
}
}
}
free(charset as *mut libc::c_void);
if !decoded.is_null() {
decoded
} else {
dc_strdup(to_decode)
}
}
unsafe fn print_hex(target: *mut libc::c_char, cur: *const libc::c_char) {
@@ -336,8 +671,16 @@ unsafe fn print_hex(target: *mut libc::c_char, cur: *const libc::c_char) {
#[cfg(test)]
mod tests {
use super::*;
use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
use std::ffi::CStr;
#[test]
fn test_isalnum() {
assert_eq!(isalnum(0), 0);
assert_eq!(isalnum('5' as libc::c_int), 1);
assert_eq!(isalnum('Q' as libc::c_int), 1);
}
#[test]
fn test_dc_decode_header_words() {
unsafe {
@@ -358,13 +701,12 @@ mod tests {
assert_eq!(CStr::from_ptr(buf1).to_str().unwrap(), "just ascii test");
free(buf1 as *mut libc::c_void);
buf1 = dc_encode_header_words("abcdef");
buf1 = dc_encode_header_words(b"abcdef\x00" as *const u8 as *const libc::c_char);
assert_eq!(CStr::from_ptr(buf1).to_str().unwrap(), "abcdef");
free(buf1 as *mut libc::c_void);
buf1 = dc_encode_header_words(
std::string::String::from_utf8(b"test\xc3\xa4\xc3\xb6\xc3\xbc.txt".to_vec())
.unwrap(),
b"test\xc3\xa4\xc3\xb6\xc3\xbc.txt\x00" as *const u8 as *const libc::c_char,
);
assert_eq!(
strncmp(buf1, b"=?utf-8\x00" as *const u8 as *const libc::c_char, 7),
@@ -399,43 +741,99 @@ mod tests {
#[test]
fn test_dc_encode_ext_header() {
let buf1 = dc_encode_ext_header("Björn Petersen");
assert_eq!(&buf1, "utf-8\'\'Bj%C3%B6rn%20Petersen");
let buf2 = dc_decode_ext_header(buf1.as_bytes());
assert_eq!(&buf2, "Björn Petersen",);
unsafe {
let mut buf1 = dc_encode_ext_header(
b"Bj\xc3\xb6rn Petersen\x00" as *const u8 as *const libc::c_char,
);
assert_eq!(
CStr::from_ptr(buf1).to_str().unwrap(),
"utf-8\'\'Bj%C3%B6rn%20Petersen"
);
let buf2 = dc_decode_ext_header(buf1);
assert_eq!(
strcmp(
buf2,
b"Bj\xc3\xb6rn Petersen\x00" as *const u8 as *const libc::c_char,
),
0
);
free(buf1 as *mut libc::c_void);
free(buf2 as *mut libc::c_void);
let buf1 = dc_decode_ext_header(b"iso-8859-1\'en\'%A3%20rates");
assert_eq!(buf1, "£ rates",);
buf1 = dc_decode_ext_header(
b"iso-8859-1\'en\'%A3%20rates\x00" as *const u8 as *const libc::c_char,
);
assert_eq!(
strcmp(
buf1,
b"\xc2\xa3 rates\x00" as *const u8 as *const libc::c_char,
),
0
);
free(buf1 as *mut libc::c_void);
let buf1 = dc_decode_ext_header(b"wrong\'format");
assert_eq!(buf1, "wrong\'format",);
buf1 = dc_decode_ext_header(b"wrong\'format\x00" as *const u8 as *const libc::c_char);
assert_eq!(
strcmp(
buf1,
b"wrong\'format\x00" as *const u8 as *const libc::c_char,
),
0
);
free(buf1 as *mut libc::c_void);
let buf1 = dc_decode_ext_header(b"\'\'");
assert_eq!(buf1, "\'\'");
buf1 = dc_decode_ext_header(b"\'\'\x00" as *const u8 as *const libc::c_char);
assert_eq!(
strcmp(buf1, b"\'\'\x00" as *const u8 as *const libc::c_char),
0
);
free(buf1 as *mut libc::c_void);
let buf1 = dc_decode_ext_header(b"x\'\'");
assert_eq!(buf1, "");
buf1 = dc_decode_ext_header(b"x\'\'\x00" as *const u8 as *const libc::c_char);
assert_eq!(strcmp(buf1, b"\x00" as *const u8 as *const libc::c_char), 0);
free(buf1 as *mut libc::c_void);
let buf1 = dc_decode_ext_header(b"\'");
assert_eq!(buf1, "\'");
buf1 = dc_decode_ext_header(b"\'\x00" as *const u8 as *const libc::c_char);
assert_eq!(
strcmp(buf1, b"\'\x00" as *const u8 as *const libc::c_char),
0
);
free(buf1 as *mut libc::c_void);
let buf1 = dc_decode_ext_header(b"");
assert_eq!(buf1, "");
// regressions
assert_eq!(
dc_decode_ext_header(dc_encode_ext_header("%0A").as_bytes()),
"%0A"
);
buf1 = dc_decode_ext_header(b"\x00" as *const u8 as *const libc::c_char);
assert_eq!(strcmp(buf1, b"\x00" as *const u8 as *const libc::c_char), 0);
free(buf1 as *mut libc::c_void);
}
}
#[test]
fn test_dc_needs_ext_header() {
assert_eq!(dc_needs_ext_header("Björn"), true);
assert_eq!(dc_needs_ext_header("Bjoern"), false);
assert_eq!(dc_needs_ext_header(""), false);
assert_eq!(dc_needs_ext_header(" "), true);
assert_eq!(dc_needs_ext_header("a b"), true);
unsafe {
assert_eq!(
dc_needs_ext_header(b"Bj\xc3\xb6rn\x00" as *const u8 as *const libc::c_char),
true
);
assert_eq!(
dc_needs_ext_header(b"Bjoern\x00" as *const u8 as *const libc::c_char),
false
);
assert_eq!(
dc_needs_ext_header(b"\x00" as *const u8 as *const libc::c_char),
false
);
assert_eq!(
dc_needs_ext_header(b" \x00" as *const u8 as *const libc::c_char),
true
);
assert_eq!(
dc_needs_ext_header(b"a b\x00" as *const u8 as *const libc::c_char),
true
);
assert_eq!(
dc_needs_ext_header(0 as *const u8 as *const libc::c_char),
false
);
}
}
#[test]
@@ -450,20 +848,69 @@ mod tests {
assert_eq!(to_string(hex.as_ptr() as *const _), "=3A");
}
use proptest::prelude::*;
#[test]
fn test_dc_urlencode_urldecode() {
unsafe {
let buf1 = percent_encode(b"Bj\xc3\xb6rn Petersen", NON_ALPHANUMERIC)
.to_string()
.strdup();
proptest! {
#[test]
fn test_ext_header_roundtrip(buf: String) {
let encoded = dc_encode_ext_header(&buf);
let decoded = dc_decode_ext_header(encoded.as_bytes());
assert_eq!(buf, decoded);
}
assert_eq!(
CStr::from_ptr(buf1 as *const libc::c_char)
.to_str()
.unwrap(),
"Bj%C3%B6rn%20Petersen"
);
#[test]
fn test_ext_header_decode_anything(buf: Vec<u8>) {
// make sure this never panics
let _decoded = dc_decode_ext_header(&buf);
let buf2 = dc_urldecode(buf1);
assert_eq!(
strcmp(
buf2,
b"Bj\xc3\xb6rn Petersen\x00" as *const u8 as *const libc::c_char
),
0
);
free(buf1 as *mut libc::c_void);
free(buf2 as *mut libc::c_void);
}
}
#[test]
fn test_dc_encode_decode_modified_utf7() {
unsafe {
let buf1 = dc_encode_modified_utf7(
b"Bj\xc3\xb6rn Petersen\x00" as *const u8 as *const libc::c_char,
1,
);
assert_eq!(
CStr::from_ptr(buf1 as *const libc::c_char)
.to_str()
.unwrap(),
"Bj&APY-rn_Petersen"
);
let buf2 = dc_decode_modified_utf7(buf1, 1);
assert_eq!(
strcmp(
buf2,
b"Bj\xc3\xb6rn Petersen\x00" as *const u8 as *const libc::c_char
),
0
);
free(buf1 as *mut libc::c_void);
free(buf2 as *mut libc::c_void);
}
}
#[test]
fn test_hex_to_int() {
assert_eq!(hex_2_int(b'A' as libc::c_char), 10);
assert_eq!(hex_2_int(b'a' as libc::c_char), 10);
assert_eq!(hex_2_int(b'4' as libc::c_char), 4);
assert_eq!(hex_2_int(b'K' as libc::c_char), 20);
}
}

65
src/dc_token.rs Normal file
View File

@@ -0,0 +1,65 @@
use crate::context::Context;
use crate::dc_tools::*;
use crate::sql;
// Token namespaces
#[allow(non_camel_case_types)]
type dc_tokennamespc_t = usize;
pub const DC_TOKEN_AUTH: dc_tokennamespc_t = 110;
pub const DC_TOKEN_INVITENUMBER: dc_tokennamespc_t = 100;
// Functions to read/write token from/to the database. A token is any string associated with a key.
pub fn dc_token_save(
context: &Context,
namespc: dc_tokennamespc_t,
foreign_id: u32,
token: *const libc::c_char,
) -> bool {
if token.is_null() {
return false;
}
// foreign_id may be 0
sql::execute(
context,
&context.sql,
"INSERT INTO tokens (namespc, foreign_id, token, timestamp) VALUES (?, ?, ?, ?);",
params![namespc as i32, foreign_id as i32, as_str(token), time()],
)
.is_ok()
}
pub fn dc_token_lookup(
context: &Context,
namespc: dc_tokennamespc_t,
foreign_id: u32,
) -> *mut libc::c_char {
context
.sql
.query_row_col::<_, String>(
context,
"SELECT token FROM tokens WHERE namespc=? AND foreign_id=?;",
params![namespc as i32, foreign_id as i32],
0,
)
.map(|s| unsafe { s.strdup() })
.unwrap_or_else(|| std::ptr::null_mut())
}
pub fn dc_token_exists(
context: &Context,
namespc: dc_tokennamespc_t,
token: *const libc::c_char,
) -> bool {
if token.is_null() {
return false;
}
context
.sql
.exists(
"SELECT id FROM tokens WHERE namespc=? AND token=?;",
params![namespc as i32, as_str(token)],
)
.unwrap_or_default()
}

View File

@@ -1,23 +1,25 @@
//! Some tools and enhancements to the used libraries, there should be
//! no references to Context and other "larger" entities here.
use std::borrow::Cow;
use std::ffi::{CStr, CString, OsString};
use std::path::{Path, PathBuf};
use std::ffi::{CStr, CString};
use std::path::Path;
use std::str::FromStr;
use std::time::SystemTime;
use std::{fmt, fs, ptr};
use chrono::{Local, TimeZone};
use libc::uintptr_t;
use mmime::clist::*;
use mmime::mailimf_types::*;
use rand::{thread_rng, Rng};
use crate::context::Context;
use crate::error::Error;
use crate::types::*;
use crate::x::*;
use itertools::max;
/* Some tools and enhancements to the used libraries, there should be
no references to Context and other "larger" classes here. */
/* ** library-private **********************************************************/
/* math tools */
pub fn dc_exactly_one_bit_set(v: libc::c_int) -> bool {
0 != v && 0 == v & (v - 1)
}
@@ -81,6 +83,14 @@ pub unsafe fn dc_atoi_null_is_0(s: *const libc::c_char) -> libc::c_int {
}
}
pub fn dc_atof(s: *const libc::c_char) -> libc::c_double {
if s.is_null() {
return 0.;
}
as_str(s).parse().unwrap_or_default()
}
pub unsafe fn dc_str_replace(
haystack: *mut *mut libc::c_char,
needle: *const libc::c_char,
@@ -95,8 +105,23 @@ pub unsafe fn dc_str_replace(
*haystack = haystack_s.replace(&needle_s, &replacement_s).strdup();
}
pub unsafe fn dc_ftoa(f: libc::c_double) -> *mut libc::c_char {
// hack around printf(%f) that may return `,` as decimal point on mac
let test: *mut libc::c_char = dc_mprintf(b"%f\x00" as *const u8 as *const libc::c_char, 1.2f64);
*test.offset(2isize) = 0 as libc::c_char;
let mut str: *mut libc::c_char = dc_mprintf(b"%f\x00" as *const u8 as *const libc::c_char, f);
dc_str_replace(
&mut str,
test.offset(1isize),
b".\x00" as *const u8 as *const libc::c_char,
);
free(test as *mut libc::c_void);
str
}
unsafe fn dc_ltrim(buf: *mut libc::c_char) {
let mut len: libc::size_t;
let mut len: size_t;
let mut cur: *const libc::c_uchar;
if !buf.is_null() && 0 != *buf as libc::c_int {
len = strlen(buf);
@@ -116,7 +141,7 @@ unsafe fn dc_ltrim(buf: *mut libc::c_char) {
}
unsafe fn dc_rtrim(buf: *mut libc::c_char) {
let mut len: libc::size_t;
let mut len: size_t;
let mut cur: *mut libc::c_uchar;
if !buf.is_null() && 0 != *buf as libc::c_int {
len = strlen(buf);
@@ -259,18 +284,11 @@ pub unsafe fn dc_replace_bad_utf8_chars(buf: *mut libc::c_char) {
pub fn dc_truncate(buf: &str, approx_chars: usize, do_unwrap: bool) -> Cow<str> {
let ellipse = if do_unwrap { "..." } else { "[...]" };
let count = buf.chars().count();
if approx_chars > 0 && count > approx_chars + ellipse.len() {
let end_pos = buf
.char_indices()
.nth(approx_chars)
.map(|(n, _)| n)
.unwrap_or_default();
if let Some(index) = buf[..end_pos].rfind(|c| c == ' ' || c == '\n') {
if approx_chars > 0 && buf.len() > approx_chars + ellipse.len() {
if let Some(index) = buf[..approx_chars].rfind(|c| c == ' ' || c == '\n') {
Cow::Owned(format!("{}{}", &buf[..index + 1], ellipse))
} else {
Cow::Owned(format!("{}{}", &buf[..end_pos], ellipse))
Cow::Owned(format!("{}{}", &buf[..approx_chars], ellipse))
}
} else {
Cow::Borrowed(buf)
@@ -299,9 +317,9 @@ pub unsafe fn dc_truncate_n_unwrap_str(
if *p1 as libc::c_int > ' ' as i32 {
lastIsCharacter = 1
} else if 0 != lastIsCharacter {
let used_bytes = (p1 as uintptr_t).wrapping_sub(buf as uintptr_t) as libc::size_t;
let used_bytes: size_t = (p1 as uintptr_t).wrapping_sub(buf as uintptr_t) as size_t;
if dc_utf8_strnlen(buf, used_bytes) >= approx_characters as usize {
let buf_bytes = strlen(buf);
let buf_bytes: size_t = strlen(buf);
if buf_bytes.wrapping_sub(used_bytes) >= strlen(ellipse_utf8) {
strcpy(p1 as *mut libc::c_char, ellipse_utf8);
}
@@ -322,12 +340,12 @@ pub unsafe fn dc_truncate_n_unwrap_str(
};
}
unsafe fn dc_utf8_strnlen(s: *const libc::c_char, n: libc::size_t) -> libc::size_t {
unsafe fn dc_utf8_strnlen(s: *const libc::c_char, n: size_t) -> size_t {
if s.is_null() {
return 0;
}
let mut j: libc::size_t = 0;
let mut j: size_t = 0;
for i in 0..n {
if *s.add(i) as libc::c_int & 0xc0 != 0x80 {
j = j.wrapping_add(1)
@@ -337,6 +355,33 @@ unsafe fn dc_utf8_strnlen(s: *const libc::c_char, n: libc::size_t) -> libc::size
j
}
/* split string into lines*/
pub unsafe fn dc_split_into_lines(buf_terminated: *const libc::c_char) -> Vec<*mut libc::c_char> {
let mut lines = Vec::new();
let mut line_chars = 0;
let mut p1: *const libc::c_char = buf_terminated;
let mut line_start: *const libc::c_char = p1;
while 0 != *p1 {
if *p1 as libc::c_int == '\n' as i32 {
lines.push(strndup(line_start, line_chars));
p1 = p1.offset(1isize);
line_start = p1;
line_chars = 0;
} else {
p1 = p1.offset(1isize);
line_chars += 1;
}
}
lines.push(strndup(line_start, line_chars));
lines
}
pub unsafe fn dc_free_splitted_lines(lines: Vec<*mut libc::c_char>) {
for s in lines {
free(s as *mut libc::c_void);
}
}
pub unsafe fn dc_str_from_clist(
list: *const clist,
delimiter: *const libc::c_char,
@@ -440,10 +485,21 @@ pub unsafe fn clist_free_content(haystack: *const clist) {
pub unsafe fn clist_search_string_nocase(
haystack: *const clist,
needle: *const libc::c_char,
) -> bool {
(&*haystack)
.into_iter()
.any(|data| strcasecmp(data.cast(), needle) == 0)
) -> libc::c_int {
let mut iter = (*haystack).first;
while !iter.is_null() {
if strcasecmp((*iter).data as *const libc::c_char, needle) == 0 {
return 1;
}
iter = if !iter.is_null() {
(*iter).next
} else {
ptr::null_mut()
}
}
0
}
/* date/time tools */
@@ -488,7 +544,7 @@ pub fn dc_gm2local_offset() -> i64 {
}
/* timesmearing */
pub fn dc_smeared_time(context: &Context) -> i64 {
pub unsafe fn dc_smeared_time(context: &Context) -> i64 {
/* function returns a corrected time(NULL) */
let mut now = time();
let ts = *context.last_smeared_timestamp.clone().read().unwrap();
@@ -541,7 +597,7 @@ pub fn dc_create_id() -> String {
- the group-id should be a string with the characters [a-zA-Z0-9\-_] */
let mut rng = thread_rng();
let buf: [u32; 3] = [rng.gen(), rng.gen(), rng.gen()];
let buf: [uint32_t; 3] = [rng.gen(), rng.gen(), rng.gen()];
encode_66bits_as_base64(buf[0usize], buf[1usize], buf[2usize])
}
@@ -568,7 +624,7 @@ fn encode_66bits_as_base64(v1: u32, v2: u32, fill: u32) -> String {
String::from_utf8(wrapped_writer).unwrap()
}
pub fn dc_create_incoming_rfc724_mid(
pub unsafe fn dc_create_incoming_rfc724_mid(
message_timestamp: i64,
contact_id_from: u32,
contact_ids_to: &Vec<u32>,
@@ -577,51 +633,51 @@ pub fn dc_create_incoming_rfc724_mid(
return ptr::null_mut();
}
/* find out the largest receiver ID (we could also take the smallest, but it should be unique) */
let largest_id_to = contact_ids_to.iter().max().copied().unwrap_or_default();
let largest_id_to = max(contact_ids_to.iter());
let result = format!(
"{}-{}-{}@stub",
message_timestamp, contact_id_from, largest_id_to
);
unsafe { result.strdup() }
dc_mprintf(
b"%lu-%lu-%lu@stub\x00" as *const u8 as *const libc::c_char,
message_timestamp as libc::c_ulong,
contact_id_from as libc::c_ulong,
*largest_id_to.unwrap() as libc::c_ulong,
)
}
/// Function generates a Message-ID that can be used for a new outgoing message.
/// - 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 as this may give unneeded information to the receiver
pub unsafe fn dc_create_outgoing_rfc724_mid(
grpid: *const libc::c_char,
from_addr: *const libc::c_char,
) -> *mut libc::c_char {
let rand2 = dc_create_id();
let at_hostname = as_opt_str(strchr(from_addr, '@' as i32)).unwrap_or_else(|| "@nohost");
let ret = if !grpid.is_null() {
format!("Gr.{}.{}{}", as_str(grpid), rand2, at_hostname,)
} else {
let rand1 = dc_create_id();
format!("Mr.{}.{}{}", rand1, rand2, at_hostname)
};
ret.strdup()
}
/// Generate globally-unique message-id for a new outgoing message.
///
/// Note: Do not add a counter or any private data as as this may give
/// unneeded information to the receiver
pub fn dc_create_outgoing_rfc724_mid_safe(grpid: Option<&str>, from_addr: &str) -> String {
let hostname = from_addr
.find('@')
.map(|k| &from_addr[k..])
.unwrap_or("@nohost");
match grpid {
Some(grpid) => format!("Gr.{}.{}{}", grpid, dc_create_id(), hostname),
None => format!("Mr.{}.{}{}", dc_create_id(), dc_create_id(), hostname),
/* Function generates a Message-ID that can be used for a new outgoing message.
- 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 as this may give unneeded information to the receiver */
let mut rand1: *mut libc::c_char = ptr::null_mut();
let rand2: *mut libc::c_char = dc_create_id().strdup();
let ret: *mut libc::c_char;
let mut at_hostname: *const libc::c_char = strchr(from_addr, '@' as i32);
if at_hostname.is_null() {
at_hostname = b"@nohost\x00" as *const u8 as *const libc::c_char
}
if !grpid.is_null() {
ret = dc_mprintf(
b"Gr.%s.%s%s\x00" as *const u8 as *const libc::c_char,
grpid,
rand2,
at_hostname,
)
} else {
rand1 = dc_create_id().strdup();
ret = dc_mprintf(
b"Mr.%s.%s%s\x00" as *const u8 as *const libc::c_char,
rand1,
rand2,
at_hostname,
)
}
free(rand1 as *mut libc::c_void);
free(rand2 as *mut libc::c_void);
ret
}
/// Extract the group id (grpid) from a message id (mid)
@@ -679,6 +735,16 @@ pub unsafe fn dc_extract_grpid_from_rfc724_mid_list(list: *const clist) -> *mut
ptr::null_mut()
}
#[allow(non_snake_case)]
unsafe fn dc_ensure_no_slash(pathNfilename: *mut libc::c_char) {
let path_len = strlen(pathNfilename);
if path_len > 0 && *pathNfilename.add(path_len - 1) as libc::c_int == '/' as i32
|| *pathNfilename.add(path_len - 1) as libc::c_int == '\\' as i32
{
*pathNfilename.add(path_len - 1) = 0 as libc::c_char;
}
}
pub fn dc_ensure_no_slash_safe(path: &str) -> &str {
if path.ends_with('/') || path.ends_with('\\') {
return &path[..path.len() - 1];
@@ -686,12 +752,18 @@ pub fn dc_ensure_no_slash_safe(path: &str) -> &str {
path
}
/// Function modifies the given buffer and replaces all characters not valid in filenames by a "-".
fn validate_filename(filename: &str) -> String {
filename
.replace('/', "-")
.replace('\\', "-")
.replace(':', "-")
unsafe fn dc_validate_filename(filename: *mut libc::c_char) {
/* function modifies the given buffer and replaces all characters not valid in filenames by a "-" */
let mut p1: *mut libc::c_char = filename;
while 0 != *p1 {
if *p1 as libc::c_int == '/' as i32
|| *p1 as libc::c_int == '\\' as i32
|| *p1 as libc::c_int == ':' as i32
{
*p1 = '-' as i32 as libc::c_char
}
p1 = p1.offset(1isize)
}
}
pub unsafe fn dc_get_filename(path_filename: impl AsRef<str>) -> *mut libc::c_char {
@@ -702,6 +774,42 @@ pub unsafe fn dc_get_filename(path_filename: impl AsRef<str>) -> *mut libc::c_ch
}
}
// the case of the suffix is preserved
#[allow(non_snake_case)]
unsafe fn dc_split_filename(
pathNfilename: *const libc::c_char,
ret_basename: *mut *mut libc::c_char,
ret_all_suffixes_incl_dot: *mut *mut libc::c_char,
) {
if pathNfilename.is_null() {
return;
}
/* splits a filename into basename and all suffixes, eg. "/path/foo.tar.gz" is split into "foo.tar" and ".gz",
(we use the _last_ dot which allows the usage inside the filename which are very usual;
maybe the detection could be more intelligent, however, for the moment, it is just file)
- if there is no suffix, the returned suffix string is empty, eg. "/path/foobar" is split into "foobar" and ""
- the case of the returned suffix is preserved; this is to allow reconstruction of (similar) names */
let basename: *mut libc::c_char = dc_get_filename(as_str(pathNfilename));
let suffix: *mut libc::c_char;
let p1: *mut libc::c_char = strrchr(basename, '.' as i32);
if !p1.is_null() {
suffix = dc_strdup(p1);
*p1 = 0 as libc::c_char
} else {
suffix = dc_strdup(ptr::null())
}
if !ret_basename.is_null() {
*ret_basename = basename
} else {
free(basename as *mut libc::c_void);
}
if !ret_all_suffixes_incl_dot.is_null() {
*ret_all_suffixes_incl_dot = suffix
} else {
free(suffix as *mut libc::c_void);
};
}
// the returned suffix is lower-case
#[allow(non_snake_case)]
pub unsafe fn dc_get_filesuffix_lc(path_filename: impl AsRef<str>) -> *mut libc::c_char {
@@ -723,45 +831,67 @@ pub fn dc_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 fn dc_get_abs_path<P: AsRef<std::path::Path>>(
pub fn dc_get_abs_path_safe<P: AsRef<std::path::Path>>(
context: &Context,
path: P,
) -> std::path::PathBuf {
let p: &std::path::Path = path.as_ref();
if let Ok(p) = p.strip_prefix("$BLOBDIR") {
context.get_blobdir().join(p)
assert!(
context.has_blobdir(),
"Expected context to have blobdir to substitute $BLOBDIR",
);
std::path::PathBuf::from(as_str(context.get_blobdir())).join(p)
} else {
p.into()
}
}
pub fn dc_file_exist(context: &Context, path: impl AsRef<std::path::Path>) -> bool {
dc_get_abs_path(context, &path).exists()
pub unsafe fn dc_get_abs_path(
context: &Context,
path_filename: impl AsRef<str>,
) -> *mut libc::c_char {
let starts = path_filename.as_ref().starts_with("$BLOBDIR");
if starts && !context.has_blobdir() {
return ptr::null_mut();
}
let mut path_filename_abs = path_filename.as_ref().strdup();
if starts && context.has_blobdir() {
dc_str_replace(
&mut path_filename_abs,
b"$BLOBDIR\x00" as *const u8 as *const libc::c_char,
context.get_blobdir(),
);
}
path_filename_abs
}
pub fn dc_get_filebytes(context: &Context, path: impl AsRef<std::path::Path>) -> u64 {
let path_abs = dc_get_abs_path(context, &path);
pub fn dc_file_exist(context: &Context, path: impl AsRef<std::path::Path>) -> bool {
dc_get_abs_path_safe(context, &path).exists()
}
pub fn dc_get_filebytes(context: &Context, path: impl AsRef<std::path::Path>) -> uint64_t {
let path_abs = dc_get_abs_path_safe(context, &path);
match fs::metadata(&path_abs) {
Ok(meta) => meta.len() as u64,
Ok(meta) => meta.len() as uint64_t,
Err(_err) => 0,
}
}
pub fn dc_delete_file(context: &Context, path: impl AsRef<std::path::Path>) -> bool {
let path_abs = dc_get_abs_path(context, &path);
if !path_abs.is_file() {
warn!(
context,
"Will not delete directory \"{}\".",
path.as_ref().display()
);
return false;
}
let path_abs = dc_get_abs_path_safe(context, &path);
let res = if path_abs.is_file() {
fs::remove_file(path_abs)
} else {
fs::remove_dir_all(path_abs)
};
match fs::remove_file(path_abs) {
match res {
Ok(_) => true,
Err(_err) => {
warn!(context, "Cannot delete \"{}\".", path.as_ref().display());
warn!(context, 0, "Cannot delete \"{}\".", path.as_ref().display());
false
}
}
@@ -772,13 +902,14 @@ pub fn dc_copy_file(
src: impl AsRef<std::path::Path>,
dest: impl AsRef<std::path::Path>,
) -> bool {
let src_abs = dc_get_abs_path(context, &src);
let dest_abs = dc_get_abs_path(context, &dest);
let src_abs = dc_get_abs_path_safe(context, &src);
let dest_abs = dc_get_abs_path_safe(context, &dest);
match fs::copy(&src_abs, &dest_abs) {
Ok(_) => true,
Err(_) => {
error!(
context,
0,
"Cannot copy \"{}\" to \"{}\".",
src.as_ref().display(),
dest.as_ref().display(),
@@ -789,13 +920,14 @@ pub fn dc_copy_file(
}
pub fn dc_create_folder(context: &Context, path: impl AsRef<std::path::Path>) -> bool {
let path_abs = dc_get_abs_path(context, &path);
let path_abs = dc_get_abs_path_safe(context, &path);
if !path_abs.exists() {
match fs::create_dir_all(path_abs) {
Ok(_) => true,
Err(_err) => {
warn!(
context,
0,
"Cannot create directory \"{}\".",
path.as_ref().display(),
);
@@ -807,12 +939,28 @@ pub fn dc_create_folder(context: &Context, path: impl AsRef<std::path::Path>) ->
}
}
/// Write a the given content to provied file path.
pub fn dc_write_file(context: &Context, path: impl AsRef<Path>, buf: &[u8]) -> bool {
let path_abs = dc_get_abs_path(context, &path);
#[allow(non_snake_case)]
pub unsafe fn dc_write_file(
context: &Context,
pathNfilename: *const libc::c_char,
buf: *const libc::c_void,
buf_bytes: size_t,
) -> libc::c_int {
let bytes = std::slice::from_raw_parts(buf as *const u8, buf_bytes);
dc_write_file_safe(context, as_str(pathNfilename), bytes) as libc::c_int
}
pub fn dc_write_file_safe<P: AsRef<std::path::Path>>(
context: &Context,
path: P,
buf: &[u8],
) -> bool {
let path_abs = dc_get_abs_path_safe(context, &path);
if let Err(_err) = fs::write(&path_abs, buf) {
warn!(
context,
0,
"Cannot write {} bytes to \"{}\".",
buf.len(),
path.as_ref().display(),
@@ -828,7 +976,7 @@ pub unsafe fn dc_read_file(
context: &Context,
pathNfilename: *const libc::c_char,
buf: *mut *mut libc::c_void,
buf_bytes: *mut libc::size_t,
buf_bytes: *mut size_t,
) -> libc::c_int {
if pathNfilename.is_null() {
return 0;
@@ -844,12 +992,13 @@ pub unsafe fn dc_read_file(
}
pub fn dc_read_file_safe<P: AsRef<std::path::Path>>(context: &Context, path: P) -> Option<Vec<u8>> {
let path_abs = dc_get_abs_path(context, &path);
let path_abs = dc_get_abs_path_safe(context, &path);
match fs::read(&path_abs) {
Ok(bytes) => Some(bytes),
Err(_err) => {
warn!(
context,
0,
"Cannot read \"{}\" or file is empty.",
path.as_ref().display()
);
@@ -858,83 +1007,104 @@ pub fn dc_read_file_safe<P: AsRef<std::path::Path>>(context: &Context, path: P)
}
}
pub fn dc_get_fine_path_filename(
#[allow(non_snake_case)]
pub unsafe fn dc_get_fine_pathNfilename(
context: &Context,
folder: impl AsRef<Path>,
desired_filename_suffix: impl AsRef<str>,
) -> PathBuf {
pathNfolder: *const libc::c_char,
desired_filenameNsuffix__: *const libc::c_char,
) -> *mut libc::c_char {
let mut ret: *mut libc::c_char = ptr::null_mut();
let pathNfolder_wo_slash: *mut libc::c_char;
let filenameNsuffix: *mut libc::c_char;
let mut basename: *mut libc::c_char = ptr::null_mut();
let mut dotNSuffix: *mut libc::c_char = ptr::null_mut();
let now = time();
let folder = PathBuf::from(folder.as_ref());
let suffix = validate_filename(desired_filename_suffix.as_ref());
pathNfolder_wo_slash = dc_strdup(pathNfolder);
dc_ensure_no_slash(pathNfolder_wo_slash);
filenameNsuffix = dc_strdup(desired_filenameNsuffix__);
dc_validate_filename(filenameNsuffix);
dc_split_filename(filenameNsuffix, &mut basename, &mut dotNSuffix);
let file_name = PathBuf::from(suffix);
let extension = file_name.extension().map(|c| c.clone());
for i in 0..100_000 {
let ret = if i == 0 {
let mut folder = folder.clone();
folder.push(&file_name);
folder
} else {
for i in 0..1000i64 {
/*no deadlocks, please*/
if 0 != i {
let idx = if i < 100 { i } else { now + i };
let file_name = if let Some(stem) = file_name.file_stem() {
let mut stem = stem.to_os_string();
stem.push(format!("-{}", idx));
stem
} else {
OsString::from(idx.to_string())
};
let mut folder = folder.clone();
folder.push(file_name);
if let Some(ext) = extension {
folder.set_extension(&ext);
}
folder
};
if !dc_file_exist(context, &ret) {
// fine filename found
return ret;
ret = dc_mprintf(
b"%s/%s-%lu%s\x00" as *const u8 as *const libc::c_char,
pathNfolder_wo_slash,
basename,
idx as libc::c_ulong,
dotNSuffix,
)
} else {
ret = dc_mprintf(
b"%s/%s%s\x00" as *const u8 as *const libc::c_char,
pathNfolder_wo_slash,
basename,
dotNSuffix,
)
}
if !dc_file_exist(context, as_path(ret)) {
/* fine filename found */
break;
}
free(ret as *mut libc::c_void);
ret = ptr::null_mut();
}
panic!("Something is really wrong, you need to clean up your disk");
free(filenameNsuffix as *mut libc::c_void);
free(basename as *mut libc::c_void);
free(dotNSuffix as *mut libc::c_void);
free(pathNfolder_wo_slash as *mut libc::c_void);
ret
}
pub fn dc_is_blobdir_path(context: &Context, path: impl AsRef<str>) -> bool {
context
.get_blobdir()
.to_str()
.map(|s| path.as_ref().starts_with(s))
.unwrap_or_default()
path.as_ref().starts_with(as_str(context.get_blobdir()))
|| path.as_ref().starts_with("$BLOBDIR")
}
fn dc_make_rel_path(context: &Context, path: &mut String) {
if context
.get_blobdir()
.to_str()
.map(|s| path.starts_with(s))
.unwrap_or_default()
{
*path = path.replace("$BLOBDIR", context.get_blobdir().to_str().unwrap());
if path.starts_with(as_str(context.get_blobdir())) {
*path = path.replace("$BLOBDIR", as_str(context.get_blobdir()));
}
}
pub fn dc_make_rel_and_copy(context: &Context, path: &mut String) -> bool {
let mut success = false;
let mut filename = ptr::null_mut();
let mut blobdir_path = ptr::null_mut();
if dc_is_blobdir_path(context, &path) {
dc_make_rel_path(context, path);
return true;
success = true;
} else {
filename = unsafe { dc_get_filename(&path) };
if !(filename.is_null()
|| {
blobdir_path = unsafe {
dc_get_fine_pathNfilename(
context,
b"$BLOBDIR\x00" as *const u8 as *const libc::c_char,
filename,
)
};
blobdir_path.is_null()
}
|| !dc_copy_file(context, &path, as_path(blobdir_path)))
{
*path = to_string(blobdir_path);
blobdir_path = ptr::null_mut();
dc_make_rel_path(context, path);
success = true;
}
}
let blobdir_path = dc_get_fine_path_filename(context, "$BLOBDIR", &path);
if dc_copy_file(context, &path, &blobdir_path) {
*path = blobdir_path.to_string_lossy().to_string();
dc_make_rel_path(context, path);
return true;
unsafe {
free(blobdir_path.cast());
free(filename.cast());
}
false
success
}
/// Error type for the [OsStrExt] trait
@@ -1240,32 +1410,6 @@ impl FromStr for EmailAddress {
}
}
/// Utility to check if a in the binary represantion of listflags
/// the bit at position bitindex is 1.
///
///
/// # Example
///
/// ```
/// use std::convert::TryInto;
/// use deltachat::dc_tools::listflags_has;
/// use deltachat::constants::{DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY};
/// let listflags: u32 = 0x1101;
/// assert!(listflags_has(listflags, 0x1) == true);
/// assert!(listflags_has(listflags, 0x10) == false);
/// assert!(listflags_has(listflags, 0x100) == true);
/// assert!(listflags_has(listflags, 0x1000) == true);
/// let listflags: u32 = (DC_GCL_ADD_SELF | DC_GCL_VERIFIED_ONLY).try_into().unwrap();
/// assert!(listflags_has(listflags, DC_GCL_VERIFIED_ONLY) == true);
/// assert!(listflags_has(listflags, DC_GCL_ADD_SELF) == true);
/// let listflags: u32 = DC_GCL_VERIFIED_ONLY.try_into().unwrap();
/// assert!(listflags_has(listflags, DC_GCL_ADD_SELF) == false);
/// ```
pub fn listflags_has(listflags: u32, bitindex: usize) -> bool {
let listflags = listflags as usize;
(listflags & bitindex) == bitindex
}
#[cfg(test)]
mod tests {
use super::*;
@@ -1307,7 +1451,7 @@ mod tests {
);
assert_ne!(str_a, str_a_copy);
let str_a = ptr::null();
let str_a = 0 as *const u8 as *const libc::c_char;
let str_a_copy = dc_strdup_keep_null(str_a);
assert_eq!(str_a.is_null(), true);
assert_eq!(str_a_copy.is_null(), true);
@@ -1362,6 +1506,23 @@ mod tests {
}
}
#[test]
fn test_dc_atof() {
let f: libc::c_double = dc_atof(b"1.23\x00" as *const u8 as *const libc::c_char);
assert!(f > 1.22f64);
assert!(f < 1.24f64);
}
#[test]
fn test_dc_ftoa() {
unsafe {
let s: *mut libc::c_char = dc_ftoa(1.23f64);
assert!(dc_atof(s) > 1.22f64);
assert!(dc_atof(s) < 1.24f64);
free(s as *mut libc::c_void);
}
}
#[test]
fn test_rust_ftoa() {
assert_eq!("1.22", format!("{}", 1.22));
@@ -1409,36 +1570,6 @@ mod tests {
assert_eq!(dc_truncate("123456", 4, true), "123456");
}
#[test]
fn test_dc_truncate_edge() {
assert_eq!(dc_truncate("", 4, false), "");
assert_eq!(dc_truncate("", 4, true), "");
assert_eq!(dc_truncate("\n hello \n world", 4, false), "\n [...]");
assert_eq!(dc_truncate("\n hello \n world", 4, true), "\n ...");
assert_eq!(
dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 1, false),
"𐠈[...]"
);
assert_eq!(
dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 0, false),
"𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ"
);
// 9 characters, so no truncation
assert_eq!(
dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠", 6, false),
"𑒀ὐ¢🜀\u{1e01b}A a🟠",
);
// 12 characters, truncation
assert_eq!(
dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠bcd", 6, false),
"𑒀ὐ¢🜀\u{1e01b}A[...]",
);
}
#[test]
fn test_dc_null_terminate_1() {
unsafe {
@@ -1776,47 +1907,4 @@ mod tests {
assert_eq!(EmailAddress::new("u@.tt").is_ok(), false);
assert_eq!(EmailAddress::new("@d.tt").is_ok(), false);
}
use proptest::prelude::*;
proptest! {
#[test]
fn test_dc_truncate(
buf: String,
approx_chars in 0..10000usize,
do_unwrap: bool,
) {
let res = dc_truncate(&buf, approx_chars, do_unwrap);
let el_len = if do_unwrap { 3 } else { 5 };
let l = res.chars().count();
if approx_chars > 0 {
assert!(
l <= approx_chars + el_len,
"buf: '{}' - res: '{}' - len {}, approx {}",
&buf, &res, res.len(), approx_chars
);
} else {
assert_eq!(&res, &buf);
}
if approx_chars > 0 && buf.chars().count() > approx_chars + el_len {
let l = res.len();
if do_unwrap {
assert_eq!(&res[l-3..l], "...", "missing ellipsis in {}", &res);
} else {
assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {}", &res);
}
}
}
}
#[test]
fn test_dc_create_incoming_rfc724_mid() {
let res = dc_create_incoming_rfc724_mid(123, 45, &vec![6, 7]);
assert_eq!(as_str(res), "123-45-7@stub");
unsafe {
free(res.cast());
}
}
}

View File

@@ -22,10 +22,6 @@ pub enum Error {
Image(image_meta::ImageError),
#[fail(display = "{:?}", _0)]
Utf8(std::str::Utf8Error),
#[fail(display = "{:?}", _0)]
CStringError(crate::dc_tools::CStringError),
#[fail(display = "PGP: {:?}", _0)]
Pgp(pgp::errors::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
@@ -66,18 +62,6 @@ impl From<image_meta::ImageError> for Error {
}
}
impl From<crate::dc_tools::CStringError> for Error {
fn from(err: crate::dc_tools::CStringError) -> Error {
Error::CStringError(err)
}
}
impl From<pgp::errors::Error> for Error {
fn from(err: pgp::errors::Error) -> Error {
Error::Pgp(err)
}
}
#[macro_export]
macro_rules! bail {
($e:expr) => {

View File

@@ -1,229 +0,0 @@
use std::path::PathBuf;
use strum::EnumProperty;
use crate::stock::StockMessage;
impl Event {
/// Returns the corresponding Event id.
pub fn as_id(&self) -> i32 {
self.get_str("id")
.expect("missing id")
.parse()
.expect("invalid id")
}
}
#[derive(Debug, Clone, PartialEq, Eq, EnumProperty)]
pub enum Event {
/// The library-user may write an informational string to the log.
/// Passed to the callback given to dc_context_new().
/// This event should not be reported to the end-user using a popup or something like that.
///
/// @return 0
#[strum(props(id = "100"))]
Info(String),
/// Emitted when SMTP connection is established and login was successful.
///
/// @return 0
#[strum(props(id = "101"))]
SmtpConnected(String),
/// Emitted when IMAP connection is established and login was successful.
///
/// @return 0
#[strum(props(id = "102"))]
ImapConnected(String),
/// Emitted when a message was successfully sent to the SMTP server.
///
/// @return 0
#[strum(props(id = "103"))]
SmtpMessageSent(String),
/// The library-user should write a warning string to the log.
/// Passed to the callback given to dc_context_new().
///
/// This event should not be reported to the end-user using a popup or something like that.
///
/// @return 0
#[strum(props(id = "300"))]
Warning(String),
/// The library-user should report an error to the end-user.
/// Passed to the callback given to dc_context_new().
///
/// As most things are asynchronous, things may go wrong at any time and the user
/// should not be disturbed by a dialog or so. Instead, use a bubble or so.
///
/// However, for ongoing processes (eg. configure())
/// or for functions that are expected to fail (eg. dc_continue_key_transfer())
/// it might be better to delay showing these events until the function has really
/// failed (returned false). It should be sufficient to report only the _last_ error
/// in a messasge box then.
///
/// @return
#[strum(props(id = "400"))]
Error(String),
/// An action cannot be performed because there is no network available.
///
/// The library will typically try over after a some time
/// and when dc_maybe_network() is called.
///
/// Network errors should be reported to users in a non-disturbing way,
/// however, as network errors may come in a sequence,
/// it is not useful to raise each an every error to the user.
/// For this purpose, data1 is set to 1 if the error is probably worth reporting.
///
/// Moreover, if the UI detects that the device is offline,
/// it is probably more useful to report this to the user
/// instead of the string from data2.
///
/// @return 0
#[strum(props(id = "401"))]
ErrorNetwork(String),
/// An action cannot be performed because the user is not in the group.
/// Reported eg. after a call to
/// dc_set_chat_name(), dc_set_chat_profile_image(),
/// dc_add_contact_to_chat(), dc_remove_contact_from_chat(),
/// dc_send_text_msg() or another sending function.
///
/// @return 0
#[strum(props(id = "410"))]
ErrorSelfNotInGroup(String),
/// Messages or chats changed. One or more messages or chats changed for various
/// reasons in the database:
/// - Messages sent, received or removed
/// - Chats created, deleted or archived
/// - A draft has been set
///
/// @return 0
#[strum(props(id = "2000"))]
MsgsChanged { chat_id: u32, msg_id: u32 },
/// There is a fresh message. Typically, the user will show an notification
/// when receiving this message.
///
/// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
///
/// @return 0
#[strum(props(id = "2005"))]
IncomingMsg { chat_id: u32, msg_id: u32 },
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
/// DC_STATE_OUT_DELIVERED, see dc_msg_get_state().
///
/// @return 0
#[strum(props(id = "2010"))]
MsgDelivered { chat_id: u32, msg_id: u32 },
/// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
/// DC_STATE_OUT_FAILED, see dc_msg_get_state().
///
/// @return 0
#[strum(props(id = "2012"))]
MsgFailed { chat_id: u32, msg_id: u32 },
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
/// DC_STATE_OUT_MDN_RCVD, see dc_msg_get_state().
///
/// @return 0
#[strum(props(id = "2015"))]
MsgRead { chat_id: u32, msg_id: u32 },
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
/// Or the verify state of a chat has changed.
/// See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
/// and dc_remove_contact_from_chat().
///
/// @return 0
#[strum(props(id = "2020"))]
ChatModified(u32),
/// Contact(s) created, renamed, blocked or deleted.
///
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
/// @return 0
#[strum(props(id = "2030"))]
ContactsChanged(Option<u32>),
/// Location of one or more contact has changed.
///
/// @param data1 (u32) contact_id of the contact for which the location has changed.
/// If the locations of several contacts have been changed,
/// eg. after calling dc_delete_all_locations(), this parameter is set to `None`.
/// @return 0
#[strum(props(id = "2035"))]
LocationChanged(Option<u32>),
/// Inform about the configuration progress started by configure().
///
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
/// @return 0
#[strum(props(id = "2041"))]
ConfigureProgress(usize),
/// Inform about the import/export progress started by dc_imex().
///
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
/// @param data2 0
/// @return 0
#[strum(props(id = "2051"))]
ImexProgress(usize),
/// A file has been exported. A file has been written by dc_imex().
/// This event may be sent multiple times by a single call to dc_imex().
///
/// A typical purpose for a handler of this event may be to make the file public to some system
/// services.
///
/// @param data2 0
/// @return 0
#[strum(props(id = "2052"))]
ImexFileWritten(PathBuf),
/// Progress information of a secure-join handshake from the view of the inviter
/// (Alice, the person who shows the QR code).
///
/// These events are typically sent after a joiner has scanned the QR code
/// generated by dc_get_securejoin_qr().
///
/// @param data1 (int) ID of the contact that wants to join.
/// @param data2 (int) Progress as:
/// 300=vg-/vc-request received, typically shown as "bob@addr joins".
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
/// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
/// 1000=Protocol finished for this contact.
/// @return 0
#[strum(props(id = "2060"))]
SecurejoinInviterProgress { contact_id: u32, progress: usize },
/// Progress information of a secure-join handshake from the view of the joiner
/// (Bob, the person who scans the QR code).
/// The events are typically sent while dc_join_securejoin(), which
/// may take some time, is executed.
/// @param data1 (int) ID of the inviting contact.
/// @param data2 (int) Progress as:
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
/// (Bob has verified alice and waits until Alice does the same for him)
/// @return 0
#[strum(props(id = "2061"))]
SecurejoinJoinerProgress { contact_id: u32, progress: usize },
// the following events are functions that should be provided by the frontends
/// Requeste a localized string from the frontend.
/// @param data1 (int) ID of the string to request, one of the DC_STR_/// constants.
/// @param data2 (int) The count. If the requested string contains a placeholder for a numeric value,
/// the ui may use this value to return different strings on different plural forms.
/// @return (const char*) Null-terminated UTF-8 string.
/// The string will be free()'d by the core,
/// so it must be allocated using malloc() or a compatible function.
/// Return 0 if the ui cannot provide the requested string
/// the core will use a default string in english language then.
#[strum(props(id = "2091"))]
GetString { id: StockMessage, count: usize },
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,7 @@
use std::ffi::CStr;
use std::ptr;
use std::time::Duration;
use deltachat_derive::{FromSql, ToSql};
use mmime::clist::*;
use rand::{thread_rng, Rng};
use crate::chat;
@@ -11,37 +9,28 @@ use crate::configure::*;
use crate::constants::*;
use crate::context::Context;
use crate::dc_imex::*;
use crate::dc_location::*;
use crate::dc_loginparam::*;
use crate::dc_mimefactory::*;
use crate::dc_tools::*;
use crate::events::Event;
use crate::imap::*;
use crate::location;
use crate::login_param::LoginParam;
use crate::message::*;
use crate::param::*;
use crate::sql;
use crate::types::*;
use crate::x::*;
/// Thread IDs
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[repr(i32)]
enum Thread {
Unknown = 0,
Imap = 100,
Smtp = 5000,
}
impl Default for Thread {
fn default() -> Self {
Thread::Unknown
}
}
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[repr(i32)]
pub enum Action {
Unknown = 0,
// Jobs in the INBOX-thread, range from DC_IMAP_THREAD..DC_IMAP_THREAD+999
Housekeeping = 105, // low priority ...
DeleteMsgOnImap = 110,
@@ -60,19 +49,11 @@ pub enum Action {
SendMsgToSmtp = 5901, // ... high priority
}
impl Default for Action {
fn default() -> Self {
Action::Unknown
}
}
impl From<Action> for Thread {
fn from(action: Action) -> Thread {
use Action::*;
match action {
Unknown => Thread::Unknown,
Housekeeping => Thread::Imap,
DeleteMsgOnImap => Thread::Imap,
MarkseenMdnOnImap => Thread::Imap,
@@ -100,7 +81,7 @@ pub struct Job {
pub added_timestamp: i64,
pub tries: i32,
pub param: Params,
pub try_again: i32,
pub try_again: Delay,
pub pending_error: Option<String>,
}
@@ -131,132 +112,130 @@ impl Job {
fn do_DC_JOB_SEND(&mut self, context: &Context) {
/* connect to SMTP server, if not yet done */
if !context.smtp.lock().unwrap().is_connected() {
let loginparam = LoginParam::from_database(context, "configured_");
let loginparam = dc_loginparam_read(context, &context.sql, "configured_");
let connected = context.smtp.lock().unwrap().connect(context, &loginparam);
if !connected {
self.try_again_later(3i32, None);
self.try_again_later(Delay::Standard, None);
return;
}
}
if let Some(filename) = self.param.get(Param::File) {
if let Some(body) = dc_read_file_safe(context, filename) {
if let Some(recipients) = self.param.get(Param::Recipients) {
let recipients_list = recipients
.split("\x1e")
.filter_map(|addr| match lettre::EmailAddress::new(addr.to_string()) {
Ok(addr) => Some(addr),
Err(err) => {
eprintln!("WARNING: invalid recipient: {} {:?}", addr, err);
None
}
})
.collect::<Vec<_>>();
/* if there is a msg-id and it does not exist in the db, cancel sending.
this happends if dc_delete_msgs() was called
before the generated mime was sent out */
if 0 != self.foreign_id && !dc_msg_exists(context, self.foreign_id) {
warn!(
context,
"Message {} for job {} does not exist", self.foreign_id, self.job_id,
);
return;
};
// hold the smtp lock during sending of a job and
// its ok/error response processing. Note that if a message
// was sent we need to mark it in the database as we
// otherwise might send it twice.
let mut sock = context.smtp.lock().unwrap();
if 0 == sock.send(context, recipients_list, body) {
sock.disconnect();
self.try_again_later(-1i32, sock.error.clone());
} else {
dc_delete_file(context, filename);
if 0 != self.foreign_id {
dc_update_msg_state(
context,
self.foreign_id,
MessageState::OutDelivered,
);
let chat_id: i32 = context
.sql
.query_get_value(
context,
"SELECT chat_id FROM msgs WHERE id=?",
params![self.foreign_id as i32],
)
.unwrap_or_default();
context.call_cb(Event::MsgDelivered {
chat_id: chat_id as u32,
msg_id: self.foreign_id,
});
}
}
} else {
warn!(context, "Missing recipients for job {}", self.job_id,);
}
let filename = self.param.get(Param::File).unwrap_or_default();
let body = match dc_read_file_safe(context, filename) {
Some(bytes) => bytes,
None => {
warn!(context, 0, "job {} error", self.job_id);
return;
}
};
let recipients = self.param.get(Param::Recipients);
if recipients.is_none() {
error!(context, 0, "Missing recipients for job {}", self.job_id,);
return;
}
let recipients_list = recipients
.unwrap()
.split("\x1e")
.filter_map(|addr| match lettre::EmailAddress::new(addr.to_string()) {
Ok(addr) => Some(addr),
Err(err) => {
eprintln!("WARNING: invalid recipient: {} {:?}", addr, err);
None
}
})
.collect::<Vec<_>>();
/* if there is a msg-id and it does not exist in the db, cancel sending.
this happends if dc_delete_msgs() was called
before the generated mime was sent out */
if 0 != self.foreign_id {
if !dc_msg_exists(context, self.foreign_id) {
warn!(
context,
0, "Message {} for job {} does not exist", self.foreign_id, self.job_id,
);
return;
}
}
/* send message while holding the smtp lock long enough
to also mark success in the database, to reduce chances
of a message getting sent twice.
*/
let mut sock = context.smtp.lock().unwrap();
if 0 == sock.send(context, recipients_list, body) {
sock.disconnect();
self.try_again_later(Delay::AtOnce, Some(as_str(sock.error)));
return;
}
dc_delete_file(context, filename);
if 0 != self.foreign_id {
dc_update_msg_state(context, self.foreign_id, MessageState::OutDelivered);
let chat_id: i32 = context
.sql
.query_row_col(
context,
"SELECT chat_id FROM msgs WHERE id=?",
params![self.foreign_id as i32],
0,
)
.unwrap_or_default();
context.call_cb(
Event::MSG_DELIVERED,
chat_id as uintptr_t,
self.foreign_id as uintptr_t,
);
}
}
// this value does not increase the number of tries
fn try_again_later(&mut self, try_again: libc::c_int, pending_error: Option<String>) {
fn try_again_later(&mut self, try_again: Delay, pending_error: Option<&str>) {
self.try_again = try_again;
self.pending_error = pending_error;
self.pending_error = pending_error.map(|s| s.to_string());
}
#[allow(non_snake_case)]
fn do_DC_JOB_MOVE_MSG(&mut self, context: &Context) {
let ok_to_continue;
let mut dest_uid = 0;
let inbox = context.inbox.read().unwrap();
if !inbox.is_connected() {
connect_to_inbox(context, &inbox);
if !inbox.is_connected() {
self.try_again_later(3, None);
ok_to_continue = false;
} else {
ok_to_continue = true;
}
} else {
ok_to_continue = true;
if !connect_to_inbox(context, &inbox) {
self.try_again_later(Delay::Standard, None);
return;
}
if ok_to_continue {
if let Ok(msg) = dc_msg_load_from_db(context, self.foreign_id) {
if context
.sql
.get_config_int(context, "folders_configured")
.unwrap_or_default()
< 3
{
inbox.configure_folders(context, 0x1i32);
}
let dest_folder = context.sql.get_config(context, "configured_mvbox_folder");
if let Ok(msg) = dc_msg_load_from_db(context, self.foreign_id) {
if context
.sql
.get_config_int(context, "folders_configured")
.unwrap_or_default()
< 3
{
inbox.configure_folders(context, 0x1i32);
}
let dest_folder = context.sql.get_config(context, "configured_mvbox_folder");
if let Some(dest_folder) = dest_folder {
let server_folder = msg.server_folder.as_ref().unwrap();
if let Some(dest_folder) = dest_folder {
let server_folder = msg.server_folder.as_ref().unwrap();
let mut dest_uid = 0;
match inbox.mv(
context,
server_folder,
msg.server_uid,
&dest_folder,
&mut dest_uid,
) as libc::c_uint
{
1 => {
self.try_again_later(3i32, None);
}
3 => {
dc_update_server_uid(context, msg.rfc724_mid, &dest_folder, dest_uid);
}
0 | 2 | _ => {}
match inbox.mv(
context,
server_folder,
msg.server_uid,
&dest_folder,
&mut dest_uid,
) {
ImapResult::RetryLater => {
self.try_again_later(Delay::Standard, None);
}
ImapResult::Success => {
// TODO: dest_uid is not (yet) set by mv() so remains 0
dc_update_server_uid(
context,
as_str(msg.rfc724_mid),
&dest_folder,
dest_uid,
);
}
_ => {}
}
}
}
@@ -264,53 +243,34 @@ impl Job {
#[allow(non_snake_case)]
fn do_DC_JOB_DELETE_MSG_ON_IMAP(&mut self, context: &Context) {
let mut delete_from_server = 1;
let inbox = context.inbox.read().unwrap();
if let Ok(mut msg) = dc_msg_load_from_db(context, self.foreign_id) {
if !(msg.rfc724_mid.is_null()
|| unsafe { *msg.rfc724_mid.offset(0isize) as libc::c_int == 0 })
{
let ok_to_continue1;
/* eg. device messages have no Message-ID */
if dc_rfc724_mid_cnt(context, msg.rfc724_mid) != 1 {
info!(
context,
"The message is deleted from the server when all parts are deleted.",
0, "The message is deleted from the server when all parts are deleted.",
);
delete_from_server = 0i32
return;
}
/* if this is the last existing part of the message, we delete the message from the server */
if 0 != delete_from_server {
let ok_to_continue;
if !inbox.is_connected() {
connect_to_inbox(context, &inbox);
if !inbox.is_connected() {
self.try_again_later(3i32, None);
ok_to_continue = false;
} else {
ok_to_continue = true;
}
} else {
ok_to_continue = true;
}
if ok_to_continue {
let mid = unsafe { CStr::from_ptr(msg.rfc724_mid).to_str().unwrap() };
let server_folder = msg.server_folder.as_ref().unwrap();
if 0 == inbox.delete_msg(context, mid, server_folder, &mut msg.server_uid) {
self.try_again_later(-1i32, None);
ok_to_continue1 = false;
} else {
ok_to_continue1 = true;
}
} else {
ok_to_continue1 = false;
}
} else {
ok_to_continue1 = true;
if !connect_to_inbox(context, &inbox) {
self.try_again_later(Delay::Standard, None);
return;
}
if ok_to_continue1 {
dc_delete_msg_from_db(context, msg.id);
let mid = unsafe { CStr::from_ptr(msg.rfc724_mid).to_str().unwrap() };
let server_folder = msg.server_folder.as_ref().unwrap();
match inbox.delete_msg(context, mid, server_folder, &mut msg.server_uid) {
ImapResult::RetryLater => {
self.try_again_later(Delay::AtOnce, None);
}
_ => {
dc_delete_msg_from_db(context, msg.id);
}
}
}
}
@@ -318,57 +278,71 @@ impl Job {
#[allow(non_snake_case)]
fn do_DC_JOB_MARKSEEN_MSG_ON_IMAP(&mut self, context: &Context) {
let ok_to_continue;
let inbox = context.inbox.read().unwrap();
if !inbox.is_connected() {
connect_to_inbox(context, &inbox);
if !inbox.is_connected() {
self.try_again_later(3i32, None);
ok_to_continue = false;
} else {
ok_to_continue = true;
}
} else {
ok_to_continue = true;
if !connect_to_inbox(context, &inbox) {
self.try_again_later(Delay::Standard, None);
return;
}
if ok_to_continue {
if let Ok(msg) = dc_msg_load_from_db(context, self.foreign_id) {
let server_folder = msg.server_folder.as_ref().unwrap();
match inbox.set_seen(context, server_folder, msg.server_uid) as libc::c_uint {
0 => {}
1 => {
self.try_again_later(3i32, None);
}
_ => {
if 0 != msg.param.get_int(Param::WantsMdn).unwrap_or_default()
&& 0 != context
.sql
.get_config_int(context, "mdns_enabled")
.unwrap_or_else(|| 1)
{
let folder = msg.server_folder.as_ref().unwrap();
match inbox.set_mdnsent(context, folder, msg.server_uid) as libc::c_uint
{
1 => {
self.try_again_later(3i32, None);
}
3 => {
send_mdn(context, msg.id);
}
0 | 2 | _ => {}
}
}
}
if let Ok(msg) = dc_msg_load_from_db(context, self.foreign_id) {
if msg.server_uid == 0 {
// the message has likely been moved
// try again later
self.try_again_later(Delay::Standard, None);
return;
}
let server_folder = msg.server_folder.as_ref().unwrap();
info!(context, 0, "job_markseen_msg db id={}", self.foreign_id);
match inbox.set_seen(context, server_folder, msg.server_uid) {
ImapResult::Failed => {
info!(context, 0, "job_markseen_msg failed");
return;
}
ImapResult::RetryLater => {
info!(context, 0, "job_markseen_msg retry-later");
self.try_again_later(Delay::Standard, None);
return;
}
_ => {}
};
info!(
context,
0,
"set_mdnsent wantsmdn={} mdns_enabled={}",
msg.param.get_int(Param::WantsMdn).unwrap_or_default(),
context
.sql
.get_config_int(context, "mdns_enabled")
.unwrap_or_else(|| 1)
);
/* XXX
if 0 != msg.param.get_int(Param::WantsMdn).unwrap_or_default()
&& 0 != context
.sql
.get_config_int(context, "mdns_enabled")
.unwrap_or_else(|| 1)
*/
{
let folder = msg.server_folder.as_ref().unwrap();
match inbox.set_mdnsent(context, folder, msg.server_uid) {
ImapResult::RetryLater => {
self.try_again_later(Delay::Standard, None);
}
ImapResult::Success => {
send_mdn(context, msg.id);
}
ImapResult::AlreadyDone => {}
ImapResult::Failed => {}
};
}
}
}
#[allow(non_snake_case)]
fn do_DC_JOB_MARKSEEN_MDN_ON_IMAP(&mut self, context: &Context) {
let ok_to_continue;
let folder = self
.param
.get(Param::ServerFolder)
@@ -378,37 +352,37 @@ impl Job {
let mut dest_uid = 0;
let inbox = context.inbox.read().unwrap();
if !inbox.is_connected() {
connect_to_inbox(context, &inbox);
if !inbox.is_connected() {
self.try_again_later(3, None);
ok_to_continue = false;
} else {
ok_to_continue = true;
}
} else {
ok_to_continue = true;
if !connect_to_inbox(context, &inbox) {
self.try_again_later(Delay::Standard, None);
return;
}
if ok_to_continue {
if inbox.set_seen(context, &folder, uid) == 0 {
self.try_again_later(3i32, None);
match inbox.set_seen(context, &folder, uid) {
ImapResult::RetryLater => {
self.try_again_later(Delay::Standard, None);
return;
}
if 0 != self.param.get_int(Param::AlsoMove).unwrap_or_default() {
if context
.sql
.get_config_int(context, "folders_configured")
.unwrap_or_default()
< 3
{
inbox.configure_folders(context, 0x1i32);
}
let dest_folder = context.sql.get_config(context, "configured_mvbox_folder");
if let Some(dest_folder) = dest_folder {
if 1 == inbox.mv(context, folder, uid, dest_folder, &mut dest_uid)
as libc::c_uint
{
self.try_again_later(3, None);
ImapResult::Failed => {
return;
}
_ => {}
};
if 0 != self.param.get_int(Param::AlsoMove).unwrap_or_default() {
if context
.sql
.get_config_int(context, "folders_configured")
.unwrap_or_default()
< 3
{
inbox.configure_folders(context, 0x1i32);
}
let dest_folder = context.sql.get_config(context, "configured_mvbox_folder");
if let Some(dest_folder) = dest_folder {
match inbox.mv(context, folder, uid, dest_folder, &mut dest_uid) {
ImapResult::RetryLater => {
self.try_again_later(Delay::Standard, None);
}
_ => {}
}
}
}
@@ -430,7 +404,7 @@ pub fn perform_imap_fetch(context: &Context) {
let inbox = context.inbox.read().unwrap();
let start = std::time::Instant::now();
if 0 == connect_to_inbox(context, &inbox) {
if !connect_to_inbox(context, &inbox) {
return;
}
if context
@@ -439,17 +413,18 @@ pub fn perform_imap_fetch(context: &Context) {
.unwrap_or_else(|| 1)
== 0
{
info!(context, "INBOX-watch disabled.",);
info!(context, 0, "INBOX-watch disabled.",);
return;
}
info!(context, "INBOX-fetch started...",);
info!(context, 0, "INBOX-fetch started...",);
inbox.fetch(context);
if inbox.should_reconnect() {
info!(context, "INBOX-fetch aborted, starting over...",);
info!(context, 0, "INBOX-fetch aborted, starting over...",);
inbox.fetch(context);
}
info!(
context,
0,
"INBOX-fetch done in {:.4} ms.",
start.elapsed().as_nanos() as f64 / 1000.0,
);
@@ -463,13 +438,13 @@ pub fn perform_imap_idle(context: &Context) {
if *context.perform_inbox_jobs_needed.clone().read().unwrap() {
info!(
context,
"INBOX-IDLE will not be started because of waiting jobs."
0, "INBOX-IDLE will not be started because of waiting jobs."
);
return;
}
info!(context, "INBOX-IDLE started...");
info!(context, 0, "INBOX-IDLE started...");
inbox.idle(context);
info!(context, "INBOX-IDLE ended.");
info!(context, 0, "INBOX-IDLE ended.");
}
pub fn perform_mvbox_fetch(context: &Context) {
@@ -546,16 +521,16 @@ pub fn perform_smtp_jobs(context: &Context) {
state.perform_jobs_needed = 0;
if state.suspended {
info!(context, "SMTP-jobs suspended.",);
info!(context, 0, "SMTP-jobs suspended.",);
return;
}
state.doing_jobs = true;
probe_smtp_network
};
info!(context, "SMTP-jobs started...",);
info!(context, 0, "SMTP-jobs started...",);
job_perform(context, Thread::Smtp, probe_smtp_network);
info!(context, "SMTP-jobs ended.");
info!(context, 0, "SMTP-jobs ended.");
{
let &(ref lock, _) = &*context.smtp_state.clone();
@@ -566,7 +541,7 @@ pub fn perform_smtp_jobs(context: &Context) {
}
pub fn perform_smtp_idle(context: &Context) {
info!(context, "SMTP-idle started...",);
info!(context, 0, "SMTP-idle started...",);
{
let &(ref lock, ref cvar) = &*context.smtp_state.clone();
let mut state = lock.lock().unwrap();
@@ -574,7 +549,7 @@ pub fn perform_smtp_idle(context: &Context) {
if state.perform_jobs_needed == 1 {
info!(
context,
"SMTP-idle will not be started because of waiting jobs.",
0, "SMTP-idle will not be started because of waiting jobs.",
);
} else {
let dur = get_next_wakeup_time(context, Thread::Smtp);
@@ -592,16 +567,17 @@ pub fn perform_smtp_idle(context: &Context) {
}
}
info!(context, "SMTP-idle ended.",);
info!(context, 0, "SMTP-idle ended.",);
}
fn get_next_wakeup_time(context: &Context, thread: Thread) -> Duration {
let t: i64 = context
.sql
.query_get_value(
.query_row_col(
context,
"SELECT MIN(desired_timestamp) FROM jobs WHERE thread=?;",
params![thread],
0,
)
.unwrap_or_default();
@@ -633,6 +609,21 @@ pub fn maybe_network(context: &Context) {
interrupt_sentbox_idle(context);
}
pub fn job_retry_msg_actions_at_once(context: &Context, msg_id: uint32_t) -> bool {
match sql::execute(
context,
&context.sql,
"UPDATE jobs SET desired_timestamp=? WHERE foreign_id=?;",
params![time(), msg_id],
) {
Ok(_) => true,
Err(_err) => {
info!(context, 0, "job-retry-at-once failed {:?}", _err);
false
}
}
}
pub fn job_action_exists(context: &Context, action: Action) -> bool {
context
.sql
@@ -642,7 +633,7 @@ pub fn job_action_exists(context: &Context, action: Action) -> bool {
/* special case for DC_JOB_SEND_MSG_TO_SMTP */
#[allow(non_snake_case)]
pub unsafe fn job_send_msg(context: &Context, msg_id: u32) -> libc::c_int {
pub unsafe fn job_send_msg(context: &Context, msg_id: uint32_t) -> libc::c_int {
let mut success = 0;
/* load message data */
@@ -650,7 +641,7 @@ pub unsafe fn job_send_msg(context: &Context, msg_id: u32) -> libc::c_int {
if mimefactory.is_err() || mimefactory.as_ref().unwrap().from_addr.is_null() {
warn!(
context,
"Cannot load data to send, maybe the message is deleted in between.",
0, "Cannot load data to send, maybe the message is deleted in between.",
);
} else {
let mut mimefactory = mimefactory.unwrap();
@@ -675,12 +666,12 @@ pub unsafe fn job_send_msg(context: &Context, msg_id: u32) -> libc::c_int {
mimefactory.msg.param.set_int(Param::Height, height as i32);
}
}
dc_msg_save_param_to_disk(context, &mut mimefactory.msg);
dc_msg_save_param_to_disk(&mut mimefactory.msg);
}
}
}
/* create message */
if !dc_mimefactory_render(context, &mut mimefactory) {
if 0 == dc_mimefactory_render(&mut mimefactory) {
dc_set_msg_failed(context, msg_id, as_opt_str(mimefactory.error));
} else if 0
!= mimefactory
@@ -692,6 +683,7 @@ pub unsafe fn job_send_msg(context: &Context, msg_id: u32) -> libc::c_int {
{
warn!(
context,
0,
"e2e encryption unavailable {} - {:?}",
msg_id,
mimefactory.msg.param.get_int(Param::GuranteeE2ee),
@@ -703,11 +695,13 @@ pub unsafe fn job_send_msg(context: &Context, msg_id: u32) -> libc::c_int {
);
} else {
/* unrecoverable */
if !clist_search_string_nocase(mimefactory.recipients_addr, mimefactory.from_addr) {
if clist_search_string_nocase(mimefactory.recipients_addr, mimefactory.from_addr)
== 0i32
{
clist_insert_after(
mimefactory.recipients_names,
(*mimefactory.recipients_names).last,
ptr::null_mut(),
0 as *mut libc::c_void,
);
clist_insert_after(
mimefactory.recipients_addr,
@@ -719,19 +713,13 @@ pub unsafe fn job_send_msg(context: &Context, msg_id: u32) -> libc::c_int {
chat::set_gossiped_timestamp(context, mimefactory.msg.chat_id, time());
}
if 0 != mimefactory.out_last_added_location_id {
if let Err(err) =
location::set_kml_sent_timestamp(context, mimefactory.msg.chat_id, time())
{
error!(context, "Failed to set kml sent_timestamp: {:?}", err);
}
dc_set_kml_sent_timestamp(context, mimefactory.msg.chat_id, time());
if !mimefactory.msg.hidden {
if let Err(err) = location::set_msg_location_id(
dc_set_msg_location_id(
context,
mimefactory.msg.id,
mimefactory.out_last_added_location_id,
) {
error!(context, "Failed to set msg_location_id: {:?}", err);
}
);
}
}
if 0 != mimefactory.out_encrypted
@@ -743,7 +731,7 @@ pub unsafe fn job_send_msg(context: &Context, msg_id: u32) -> libc::c_int {
== 0
{
mimefactory.msg.param.set_int(Param::GuranteeE2ee, 1);
dc_msg_save_param_to_disk(context, &mut mimefactory.msg);
dc_msg_save_param_to_disk(&mut mimefactory.msg);
}
success = add_smtp_job(context, Action::SendMsgToSmtp, &mut mimefactory);
}
@@ -753,14 +741,14 @@ pub unsafe fn job_send_msg(context: &Context, msg_id: u32) -> libc::c_int {
}
pub fn perform_imap_jobs(context: &Context) {
info!(context, "dc_perform_imap_jobs starting.",);
info!(context, 0, "dc_perform_imap_jobs starting.",);
let probe_imap_network = *context.probe_imap_network.clone().read().unwrap();
*context.probe_imap_network.write().unwrap() = false;
*context.perform_inbox_jobs_needed.write().unwrap() = false;
job_perform(context, Thread::Imap, probe_imap_network);
info!(context, "dc_perform_imap_jobs ended.",);
info!(context, 0, "dc_perform_imap_jobs ended.",);
}
fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
@@ -797,7 +785,7 @@ fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
added_timestamp: row.get(4)?,
tries: row.get(6)?,
param: row.get::<_, String>(3)?.parse().unwrap_or_default(),
try_again: 0,
try_again: Delay::Default,
pending_error: None,
};
@@ -808,13 +796,14 @@ fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
match jobs {
Ok(ref _res) => {}
Err(ref err) => {
info!(context, "query failed: {:?}", err);
info!(context, 0, "query failed: {:?}", err);
}
}
for mut job in jobs.unwrap_or_default() {
info!(
context,
0,
"{}-job #{}, action {} started...",
if thread == Thread::Imap {
"INBOX"
@@ -828,7 +817,7 @@ fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
// some configuration jobs are "exclusive":
// - they are always executed in the imap-thread and the smtp-thread is suspended during execution
// - they may change the database handle change the database handle; we do not keep old pointers therefore
// - they can be re-executed one time AT_ONCE, but they are not save in the database for later execution
// - they can be re-executed one time AtOnce, but they are not save in the database for later execution
if Action::ConfigureImap == job.action || Action::ImexImap == job.action {
job_kill_action(context, job.action);
&context
@@ -848,13 +837,10 @@ fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
let mut tries = 0;
while tries <= 1 {
// this can be modified by a job using dc_job_try_again_later()
job.try_again = 0;
// this can be modified by a job using try_again_later()
job.try_again = Delay::Default;
match job.action {
Action::Unknown => {
warn!(context, "Unknown job id found");
}
Action::SendMsgToSmtp => job.do_DC_JOB_SEND(context),
Action::DeleteMsgOnImap => job.do_DC_JOB_DELETE_MSG_ON_IMAP(context),
Action::MarkseenMsgOnImap => job.do_DC_JOB_MARKSEEN_MSG_ON_IMAP(context),
@@ -863,17 +849,17 @@ fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
Action::SendMdn => job.do_DC_JOB_SEND(context),
Action::ConfigureImap => unsafe { dc_job_do_DC_JOB_CONFIGURE_IMAP(context, &job) },
Action::ImexImap => unsafe { dc_job_do_DC_JOB_IMEX_IMAP(context, &job) },
Action::MaybeSendLocations => {
location::job_do_DC_JOB_MAYBE_SEND_LOCATIONS(context, &job)
}
Action::MaybeSendLocationsEnded => {
location::job_do_DC_JOB_MAYBE_SEND_LOC_ENDED(context, &mut job)
}
Action::MaybeSendLocations => unsafe {
dc_job_do_DC_JOB_MAYBE_SEND_LOCATIONS(context, &job)
},
Action::MaybeSendLocationsEnded => unsafe {
dc_job_do_DC_JOB_MAYBE_SEND_LOC_ENDED(context, &mut job)
},
Action::Housekeeping => sql::housekeeping(context),
Action::SendMdnOld => {}
Action::SendMsgToSmtpOld => {}
}
if job.try_again != -1 {
if job.try_again != Delay::AtOnce {
break;
}
tries += 1
@@ -893,10 +879,11 @@ fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
.unsuspend(context);
suspend_smtp_thread(context, false);
break;
} else if job.try_again == 2 {
} else if job.try_again == Delay::IncreationPoll {
// just try over next loop unconditionally, the ui typically interrupts idle when the file (video) is ready
info!(
context,
0,
"{}-job #{} not yet ready and will be delayed.",
if thread == Thread::Imap {
"INBOX"
@@ -905,7 +892,7 @@ fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
},
job.job_id
);
} else if job.try_again == -1 || job.try_again == 3 {
} else if job.try_again == Delay::AtOnce || job.try_again == Delay::Standard {
let tries = job.tries + 1;
if tries < 17 {
job.tries = tries;
@@ -914,6 +901,7 @@ fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
job.update(context);
info!(
context,
0,
"{}-job #{} not succeeded on try #{}, retry in ADD_TIME+{} (in {} seconds).",
if thread == Thread::Imap {
"INBOX"
@@ -980,17 +968,23 @@ fn suspend_smtp_thread(context: &Context, suspend: bool) {
}
}
fn connect_to_inbox(context: &Context, inbox: &Imap) -> libc::c_int {
let ret_connected = dc_connect_to_configured_imap(context, inbox);
if 0 != ret_connected {
inbox.set_watch_folder("INBOX".into());
fn connect_to_inbox(context: &Context, inbox: &Imap) -> bool {
if inbox.is_connected() {
true
} else {
let ret_connected = dc_connect_to_configured_imap(context, inbox);
if 0 != ret_connected {
inbox.set_watch_folder("INBOX".into());
true
} else {
false
}
}
ret_connected
}
fn send_mdn(context: &Context, msg_id: u32) {
pub fn send_mdn(context: &Context, msg_id: uint32_t) {
if let Ok(mut mimefactory) = unsafe { dc_mimefactory_load_mdn(context, msg_id) } {
if unsafe { dc_mimefactory_render(context, &mut mimefactory) } {
if 0 != unsafe { dc_mimefactory_render(&mut mimefactory) } {
add_smtp_job(context, Action::SendMdn, &mut mimefactory);
}
}
@@ -998,23 +992,40 @@ fn send_mdn(context: &Context, msg_id: u32) {
#[allow(non_snake_case)]
fn add_smtp_job(context: &Context, action: Action, mimefactory: &dc_mimefactory_t) -> libc::c_int {
let pathNfilename: *mut libc::c_char;
let mut success: libc::c_int = 0i32;
let mut recipients: *mut libc::c_char = ptr::null_mut();
let mut recipients: *mut libc::c_char = 0 as *mut libc::c_char;
let mut param = Params::new();
let path_filename =
dc_get_fine_path_filename(context, "$BLOBDIR", as_str(mimefactory.rfc724_mid));
let bytes = unsafe {
std::slice::from_raw_parts(
(*mimefactory.out).str_0 as *const u8,
(*mimefactory.out).len,
pathNfilename = unsafe {
dc_get_fine_pathNfilename(
context,
b"$BLOBDIR\x00" as *const u8 as *const libc::c_char,
mimefactory.rfc724_mid,
)
};
if !dc_write_file(context, &path_filename, bytes) {
if pathNfilename.is_null() {
error!(
context,
0,
"Could not find free file name for message with ID <{}>.",
to_string(mimefactory.rfc724_mid),
);
} else if 0
== unsafe {
dc_write_file(
context,
pathNfilename,
(*mimefactory.out).str_0 as *const libc::c_void,
(*mimefactory.out).len,
)
}
{
error!(
context,
0,
"Could not write message <{}> to \"{}\".",
to_string(mimefactory.rfc724_mid),
path_filename.display(),
as_str(pathNfilename),
);
} else {
recipients = unsafe {
@@ -1023,7 +1034,7 @@ fn add_smtp_job(context: &Context, action: Action, mimefactory: &dc_mimefactory_
b"\x1e\x00" as *const u8 as *const libc::c_char,
)
};
param.set(Param::File, path_filename.to_string_lossy());
param.set(Param::File, as_str(pathNfilename));
param.set(Param::Recipients, as_str(recipients));
job_add(
context,
@@ -1042,6 +1053,7 @@ fn add_smtp_job(context: &Context, action: Action, mimefactory: &dc_mimefactory_
}
unsafe {
free(recipients.cast());
free(pathNfilename.cast());
}
success
}
@@ -1053,11 +1065,6 @@ pub fn job_add(
param: Params,
delay_seconds: i64,
) {
if action == Action::Unknown {
error!(context, "Invalid action passed to job_add");
return;
}
let timestamp = time();
let thread: Thread = action.into();
@@ -1078,12 +1085,11 @@ pub fn job_add(
match thread {
Thread::Imap => interrupt_imap_idle(context),
Thread::Smtp => interrupt_smtp_idle(context),
Thread::Unknown => {}
}
}
pub fn interrupt_smtp_idle(context: &Context) {
info!(context, "Interrupting SMTP-idle...",);
info!(context, 0, "Interrupting SMTP-idle...",);
let &(ref lock, ref cvar) = &*context.smtp_state.clone();
let mut state = lock.lock().unwrap();
@@ -1094,7 +1100,7 @@ pub fn interrupt_smtp_idle(context: &Context) {
}
pub fn interrupt_imap_idle(context: &Context) {
info!(context, "Interrupting IMAP-IDLE...",);
info!(context, 0, "Interrupting IMAP-IDLE...",);
*context.perform_inbox_jobs_needed.write().unwrap() = true;
context.inbox.read().unwrap().interrupt_idle();

View File

@@ -4,7 +4,6 @@ use crate::configure::*;
use crate::context::Context;
use crate::imap::Imap;
#[derive(Debug)]
pub struct JobThread {
pub name: &'static str,
pub folder_config_name: &'static str,
@@ -31,7 +30,7 @@ impl JobThread {
}
pub fn suspend(&self, context: &Context) {
info!(context, "Suspending {}-thread.", self.name,);
info!(context, 0, "Suspending {}-thread.", self.name,);
{
self.state.0.lock().unwrap().suspended = true;
}
@@ -46,7 +45,7 @@ impl JobThread {
}
pub fn unsuspend(&self, context: &Context) {
info!(context, "Unsuspending {}-thread.", self.name);
info!(context, 0, "Unsuspending {}-thread.", self.name);
let &(ref lock, ref cvar) = &*self.state.clone();
let mut state = lock.lock().unwrap();
@@ -61,7 +60,7 @@ impl JobThread {
self.state.0.lock().unwrap().jobs_needed = 1;
}
info!(context, "Interrupting {}-IDLE...", self.name);
info!(context, 0, "Interrupting {}-IDLE...", self.name);
self.imap.interrupt_idle();
@@ -87,15 +86,16 @@ impl JobThread {
if use_network {
let start = std::time::Instant::now();
if self.connect_to_imap(context) {
info!(context, "{}-fetch started...", self.name);
info!(context, 0, "{}-fetch started...", self.name);
self.imap.fetch(context);
if self.imap.should_reconnect() {
info!(context, "{}-fetch aborted, starting over...", self.name,);
info!(context, 0, "{}-fetch aborted, starting over...", self.name,);
self.imap.fetch(context);
}
info!(
context,
0,
"{}-fetch done in {:.3} ms.",
self.name,
start.elapsed().as_millis(),
@@ -142,6 +142,7 @@ impl JobThread {
if 0 != state.jobs_needed {
info!(
context,
0,
"{}-IDLE will not be started as it was interrupted while not ideling.",
self.name,
);
@@ -171,9 +172,9 @@ impl JobThread {
}
self.connect_to_imap(context);
info!(context, "{}-IDLE started...", self.name,);
info!(context, 0, "{}-IDLE started...", self.name,);
self.imap.idle(context);
info!(context, "{}-IDLE ended.", self.name);
info!(context, 0, "{}-IDLE ended.", self.name);
self.state.0.lock().unwrap().using_handle = false;
}

View File

@@ -1,6 +1,7 @@
use std::collections::BTreeMap;
use std::ffi::{CStr, CString};
use std::io::Cursor;
use std::path::Path;
use std::slice;
use libc;
use pgp::composed::{Deserializable, SignedPublicKey, SignedSecretKey};
@@ -11,6 +12,7 @@ use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::*;
use crate::sql::{self, Sql};
use crate::x::*;
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Key {
@@ -104,6 +106,15 @@ impl Key {
}
}
pub fn from_binary(data: *const u8, len: libc::c_int, key_type: KeyType) -> Option<Self> {
if data.is_null() || len == 0 {
return None;
}
let bytes = unsafe { slice::from_raw_parts(data, len as usize) };
Self::from_slice(bytes, key_type)
}
pub fn from_armored_string(
data: &str,
key_type: KeyType,
@@ -141,10 +152,11 @@ impl Key {
) -> Option<Self> {
let addr = self_addr.as_ref();
sql.query_get_value(
sql.query_row_col(
context,
"SELECT public_key FROM keypairs WHERE addr=? AND is_default=1;",
&[addr],
0,
)
.and_then(|blob: Vec<u8>| Self::from_slice(&blob, KeyType::Public))
}
@@ -154,10 +166,11 @@ impl Key {
self_addr: impl AsRef<str>,
sql: &Sql,
) -> Option<Self> {
sql.query_get_value(
sql.query_row_col(
context,
"SELECT private_key FROM keypairs WHERE addr=? AND is_default=1;",
&[self_addr.as_ref()],
0,
)
.and_then(|blob: Vec<u8>| Self::from_slice(&blob, KeyType::Private))
}
@@ -215,15 +228,30 @@ impl Key {
.expect("failed to serialize key")
}
pub fn write_asc_to_file(&self, file: impl AsRef<Path>, context: &Context) -> bool {
let file_content = self.to_asc(None).into_bytes();
if dc_write_file(context, &file, &file_content) {
return true;
} else {
error!(context, "Cannot write key to {}", file.as_ref().display());
pub fn write_asc_to_file(&self, file: *const libc::c_char, context: &Context) -> bool {
if file.is_null() {
return false;
}
let file_content = self.to_asc(None);
let file_content_c = CString::new(file_content).unwrap();
let success = if 0
== unsafe {
dc_write_file(
context,
file,
file_content_c.as_ptr() as *const libc::c_void,
file_content_c.as_bytes().len(),
)
} {
error!(context, 0, "Cannot write key to {}", to_string(file));
false
} else {
true
};
success
}
pub fn fingerprint(&self) -> String {
@@ -233,11 +261,23 @@ impl Key {
}
}
pub fn fingerprint_c(&self) -> *mut libc::c_char {
let res = CString::new(self.fingerprint()).unwrap();
unsafe { strdup(res.as_ptr()) }
}
pub fn formatted_fingerprint(&self) -> String {
let rawhex = self.fingerprint();
dc_format_fingerprint(&rawhex)
}
pub fn formatted_fingerprint_c(&self) -> *mut libc::c_char {
let res = CString::new(self.formatted_fingerprint()).unwrap();
unsafe { strdup(res.as_ptr()) }
}
pub fn split_key(&self) -> Option<Key> {
match self {
Key::Public(_) => None,
@@ -283,6 +323,14 @@ pub fn dc_format_fingerprint(fingerprint: &str) -> String {
res
}
pub fn dc_format_fingerprint_c(fp: *const libc::c_char) -> *mut libc::c_char {
let input = unsafe { CStr::from_ptr(fp).to_str().unwrap() };
let res = dc_format_fingerprint(input);
let res_c = CString::new(res).unwrap();
unsafe { strdup(res_c.as_ptr()) }
}
/// Bring a human-readable or otherwise formatted fingerprint back to the 40-characters-uppercase-hex format.
pub fn dc_normalize_fingerprint(fp: &str) -> String {
fp.to_uppercase()
@@ -291,6 +339,14 @@ pub fn dc_normalize_fingerprint(fp: &str) -> String {
.collect()
}
pub fn dc_normalize_fingerprint_c(fp: *const libc::c_char) -> *mut libc::c_char {
let input = unsafe { CStr::from_ptr(fp).to_str().unwrap() };
let res = dc_normalize_fingerprint(input);
let res_c = CString::new(res).unwrap();
unsafe { strdup(res_c.as_ptr()) }
}
#[cfg(test)]
mod tests {
use super::*;
@@ -393,27 +449,6 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
assert_eq!(private_key, private_key2);
}
#[test]
fn test_from_slice_bad_data() {
let mut bad_data: [u8; 4096] = [0; 4096];
for i in 0..4096 {
bad_data[i] = (i & 0xff) as u8;
}
for j in 0..(4096 / 40) {
let bad_key = Key::from_slice(
&bad_data[j..j + 4096 / 2 + j],
if 0 != j & 1 {
KeyType::Public
} else {
KeyType::Private
},
);
assert!(bad_key.is_none());
}
}
#[test]
#[ignore] // is too expensive
fn test_ascii_roundtrip() {

View File

@@ -33,10 +33,11 @@ impl<'a> Keyring<'a> {
self_addr: impl AsRef<str>,
sql: &Sql,
) -> bool {
sql.query_get_value(
sql.query_row_col(
context,
"SELECT private_key FROM keypairs ORDER BY addr=? DESC, is_default DESC;",
&[self_addr.as_ref()],
0,
)
.and_then(|blob: Vec<u8>| Key::from_slice(&blob, KeyType::Private))
.map(|key| self.add_owned(key))

View File

@@ -1,4 +1,4 @@
#![deny(clippy::correctness, missing_debug_implementations)]
#![deny(clippy::correctness)]
// TODO: make all of these errors, such that clippy actually passes.
#![warn(clippy::all, clippy::perf, clippy::not_unsafe_ptr_arg_deref)]
// This is nice, but for now just annoying.
@@ -16,19 +16,12 @@ extern crate rusqlite;
extern crate strum;
#[macro_use]
extern crate strum_macros;
#[macro_use]
extern crate jetscii;
#[macro_use]
extern crate debug_stub_derive;
#[macro_use]
mod log;
#[macro_use]
pub mod error;
pub(crate) mod events;
pub use events::*;
mod aheader;
pub mod chat;
pub mod chatlist;
@@ -37,13 +30,11 @@ pub mod configure;
pub mod constants;
pub mod contact;
pub mod context;
mod e2ee;
mod imap;
pub mod job;
mod job_thread;
pub mod key;
pub mod keyring;
pub mod location;
pub mod lot;
pub mod message;
pub mod oauth2;
@@ -54,20 +45,24 @@ pub mod qr;
mod smtp;
pub mod sql;
mod stock;
pub mod types;
pub mod x;
pub mod dc_array;
mod dc_dehtml;
mod dc_e2ee;
pub mod dc_imex;
pub mod dc_location;
mod dc_loginparam;
mod dc_mimefactory;
pub mod dc_mimeparser;
mod dc_move;
pub mod dc_receive_imf;
pub mod dc_securejoin;
mod dc_simplify;
mod dc_strencode;
mod dc_token;
pub mod dc_tools;
mod login_param;
pub mod securejoin;
mod token;
#[cfg(test)]
mod test_utils;

View File

@@ -1,694 +0,0 @@
use bitflags::bitflags;
use quick_xml;
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
use crate::chat;
use crate::constants::*;
use crate::context::*;
use crate::dc_tools::*;
use crate::error::Error;
use crate::events::Event;
use crate::job::*;
use crate::message::*;
use crate::param::*;
use crate::sql;
use crate::stock::StockMessage;
// location handling
#[derive(Debug, Clone, Default)]
pub struct Location {
pub location_id: u32,
pub latitude: f64,
pub longitude: f64,
pub accuracy: f64,
pub timestamp: i64,
pub contact_id: u32,
pub msg_id: u32,
pub chat_id: u32,
pub marker: Option<String>,
pub independent: u32,
}
impl Location {
pub fn new() -> Self {
Default::default()
}
}
#[derive(Debug, Clone, Default)]
pub struct Kml {
pub addr: Option<String>,
pub locations: Vec<Location>,
tag: KmlTag,
pub curr: Location,
}
bitflags! {
#[derive(Default)]
struct KmlTag: i32 {
const UNDEFINED = 0x00;
const PLACEMARK = 0x01;
const TIMESTAMP = 0x02;
const WHEN = 0x04;
const POINT = 0x08;
const COORDINATES = 0x10;
}
}
impl Kml {
pub fn new() -> Self {
Default::default()
}
pub fn parse(context: &Context, content: impl AsRef<str>) -> Result<Self, Error> {
ensure!(
content.as_ref().len() <= (1 * 1024 * 1024),
"A kml-files with {} bytes is larger than reasonably expected.",
content.as_ref().len()
);
let mut reader = quick_xml::Reader::from_str(content.as_ref());
reader.trim_text(true);
let mut kml = Kml::new();
kml.locations = Vec::with_capacity(100);
let mut buf = Vec::new();
loop {
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();
}
Ok(kml)
}
fn text_cb<B: std::io::BufRead>(&mut self, event: &BytesText, reader: &quick_xml::Reader<B>) {
if self.tag.contains(KmlTag::WHEN) || self.tag.contains(KmlTag::COORDINATES) {
let val = event.unescape_and_decode(reader).unwrap_or_default();
let val = val
.replace("\n", "")
.replace("\r", "")
.replace("\t", "")
.replace(" ", "");
if self.tag.contains(KmlTag::WHEN) && val.len() >= 19 {
// YYYY-MM-DDTHH:MM:SSZ
// 0 4 7 10 13 16 19
match chrono::NaiveDateTime::parse_from_str(&val, "%Y-%m-%dT%H:%M:%SZ") {
Ok(res) => {
self.curr.timestamp = res.timestamp();
if self.curr.timestamp > time() {
self.curr.timestamp = time();
}
}
Err(_err) => {
self.curr.timestamp = time();
}
}
} else if self.tag.contains(KmlTag::COORDINATES) {
let parts = val.splitn(2, ',').collect::<Vec<_>>();
if parts.len() == 2 {
self.curr.longitude = parts[0].parse().unwrap_or_default();
self.curr.latitude = parts[1].parse().unwrap_or_default();
}
}
}
}
fn endtag_cb(&mut self, event: &BytesEnd) {
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
if tag == "placemark" {
if self.tag.contains(KmlTag::PLACEMARK)
&& 0 != self.curr.timestamp
&& 0. != self.curr.latitude
&& 0. != self.curr.longitude
{
self.locations
.push(std::mem::replace(&mut self.curr, Location::new()));
}
self.tag = KmlTag::UNDEFINED;
};
}
fn starttag_cb<B: std::io::BufRead>(
&mut self,
event: &BytesStart,
reader: &quick_xml::Reader<B>,
) {
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
if tag == "document" {
if let Some(addr) = event.attributes().find(|attr| {
attr.as_ref()
.map(|a| String::from_utf8_lossy(a.key).trim().to_lowercase() == "addr")
.unwrap_or_default()
}) {
self.addr = addr.unwrap().unescape_and_decode_value(reader).ok();
}
} else if tag == "placemark" {
self.tag = KmlTag::PLACEMARK;
self.curr.timestamp = 0;
self.curr.latitude = 0.0;
self.curr.longitude = 0.0;
self.curr.accuracy = 0.0
} else if tag == "timestamp" && self.tag.contains(KmlTag::PLACEMARK) {
self.tag = KmlTag::PLACEMARK | KmlTag::TIMESTAMP
} else if tag == "when" && self.tag.contains(KmlTag::TIMESTAMP) {
self.tag = KmlTag::PLACEMARK | KmlTag::TIMESTAMP | KmlTag::WHEN
} else if tag == "point" && self.tag.contains(KmlTag::PLACEMARK) {
self.tag = KmlTag::PLACEMARK | KmlTag::POINT
} else if tag == "coordinates" && self.tag.contains(KmlTag::POINT) {
self.tag = KmlTag::PLACEMARK | KmlTag::POINT | KmlTag::COORDINATES;
if let Some(acc) = event.attributes().find(|attr| {
attr.as_ref()
.map(|a| String::from_utf8_lossy(a.key).trim().to_lowercase() == "accuracy")
.unwrap_or_default()
}) {
let v = acc
.unwrap()
.unescape_and_decode_value(reader)
.unwrap_or_default();
self.curr.accuracy = v.trim().parse().unwrap_or_default();
}
}
}
}
// location streaming
pub fn send_locations_to_chat(context: &Context, chat_id: u32, seconds: i64) {
let now = time();
let mut msg: Message;
let is_sending_locations_before: bool;
if !(seconds < 0 || chat_id <= 9i32 as libc::c_uint) {
is_sending_locations_before = is_sending_locations_to_chat(context, chat_id);
if sql::execute(
context,
&context.sql,
"UPDATE chats \
SET locations_send_begin=?, \
locations_send_until=? \
WHERE id=?",
params![
if 0 != seconds { now } else { 0 },
if 0 != seconds { now + seconds } else { 0 },
chat_id as i32,
],
)
.is_ok()
{
if 0 != seconds && !is_sending_locations_before {
msg = dc_msg_new(Viewtype::Text);
msg.text =
Some(context.stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0));
msg.param.set_int(Param::Cmd, 8);
chat::send_msg(context, chat_id, &mut msg).unwrap();
} else if 0 == seconds && is_sending_locations_before {
let stock_str =
context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0);
chat::add_device_msg(context, chat_id, stock_str);
}
context.call_cb(Event::ChatModified(chat_id));
if 0 != seconds {
schedule_MAYBE_SEND_LOCATIONS(context, 0i32);
job_add(
context,
Action::MaybeSendLocationsEnded,
chat_id as libc::c_int,
Params::new(),
seconds + 1,
);
}
}
}
}
#[allow(non_snake_case)]
fn schedule_MAYBE_SEND_LOCATIONS(context: &Context, flags: i32) {
if 0 != flags & 0x1 || !job_action_exists(context, Action::MaybeSendLocations) {
job_add(context, Action::MaybeSendLocations, 0, Params::new(), 60);
};
}
pub fn is_sending_locations_to_chat(context: &Context, chat_id: u32) -> bool {
context
.sql
.exists(
"SELECT id FROM chats WHERE (? OR id=?) AND locations_send_until>?;",
params![if chat_id == 0 { 1 } else { 0 }, chat_id as i32, time()],
)
.unwrap_or_default()
}
pub fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> libc::c_int {
if latitude == 0.0 && longitude == 0.0 {
return 1;
}
let mut continue_streaming = false;
if let Ok(chats) = context.sql.query_map(
"SELECT id FROM chats WHERE locations_send_until>?;",
params![time()],
|row| row.get::<_, i32>(0),
|chats| chats.collect::<Result<Vec<_>, _>>().map_err(Into::into),
) {
for chat_id in chats {
if let Err(err) = context.sql.execute(
"INSERT INTO locations \
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
params![
latitude,
longitude,
accuracy,
time(),
chat_id,
1,
]
) {
warn!(context, "failed to store location {:?}", err);
} else {
continue_streaming = true;
}
}
if continue_streaming {
context.call_cb(Event::LocationChanged(Some(1)));
};
schedule_MAYBE_SEND_LOCATIONS(context, 0);
}
continue_streaming as libc::c_int
}
pub fn get_range(
context: &Context,
chat_id: u32,
contact_id: u32,
timestamp_from: i64,
mut timestamp_to: i64,
) -> Vec<Location> {
if timestamp_to == 0 {
timestamp_to = time() + 10;
}
context
.sql
.query_map(
"SELECT l.id, l.latitude, l.longitude, l.accuracy, l.timestamp, l.independent, \
m.id, l.from_id, l.chat_id, m.txt \
FROM locations l LEFT JOIN msgs m ON l.id=m.location_id WHERE (? OR l.chat_id=?) \
AND (? OR l.from_id=?) \
AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \
ORDER BY l.timestamp DESC, l.id DESC, m.id DESC;",
params![
if chat_id == 0 { 1 } else { 0 },
chat_id as i32,
if contact_id == 0 { 1 } else { 0 },
contact_id as i32,
timestamp_from,
timestamp_to,
],
|row| {
let msg_id = row.get(6)?;
let txt: String = row.get(9)?;
let marker = if msg_id != 0 && is_marker(&txt) {
Some(txt)
} else {
None
};
let loc = Location {
location_id: row.get(0)?,
latitude: row.get(1)?,
longitude: row.get(2)?,
accuracy: row.get(3)?,
timestamp: row.get(4)?,
independent: row.get(5)?,
msg_id,
contact_id: row.get(7)?,
chat_id: row.get(8)?,
marker,
};
Ok(loc)
},
|locations| {
let mut ret = Vec::new();
for location in locations {
ret.push(location?);
}
Ok(ret)
},
)
.unwrap_or_default()
}
fn is_marker(txt: &str) -> bool {
txt.len() == 1 && txt.chars().next().unwrap() != ' '
}
pub fn delete_all(context: &Context) -> Result<(), Error> {
sql::execute(context, &context.sql, "DELETE FROM locations;", params![])?;
context.call_cb(Event::LocationChanged(None));
Ok(())
}
pub fn get_kml(context: &Context, chat_id: u32) -> Result<(String, u32), Error> {
let now = time();
let mut location_count = 0;
let mut ret = String::new();
let mut last_added_location_id = 0;
let self_addr = context
.sql
.get_config(context, "configured_addr")
.unwrap_or_default();
let (locations_send_begin, locations_send_until, locations_last_sent) = context.sql.query_row(
"SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;",
params![chat_id as i32], |row| {
let send_begin: i64 = row.get(0)?;
let send_until: i64 = row.get(1)?;
let last_sent: i64 = row.get(2)?;
Ok((send_begin, send_until, last_sent))
})?;
if !(locations_send_begin == 0 || now > locations_send_until) {
ret += &format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n<Document addr=\"{}\">\n",
self_addr,
);
context.sql.query_map(
"SELECT id, latitude, longitude, accuracy, timestamp\
FROM locations WHERE from_id=? \
AND timestamp>=? \
AND (timestamp>=? OR timestamp=(SELECT MAX(timestamp) FROM locations WHERE from_id=?)) \
AND independent=0 \
GROUP BY timestamp \
ORDER BY timestamp;",
params![1, locations_send_begin, locations_last_sent, 1],
|row| {
let location_id: i32 = row.get(0)?;
let latitude: f64 = row.get(1)?;
let longitude: f64 = row.get(2)?;
let accuracy: f64 = row.get(3)?;
let timestamp = get_kml_timestamp(row.get(4)?);
Ok((location_id, latitude, longitude, accuracy, timestamp))
},
|rows| {
for row in rows {
let (location_id, latitude, longitude, accuracy, timestamp) = row?;
ret += &format!(
"<Placemark><Timestamp><when>{}</when></Timestamp><Point><coordinates accuracy=\"{}\">{},{}</coordinates></Point></Placemark>\n\x00",
timestamp,
accuracy,
longitude,
latitude
);
location_count += 1;
last_added_location_id = location_id as u32;
}
Ok(())
}
)?;
}
ensure!(location_count > 0, "No locations processed");
ret += "</Document>\n</kml>";
Ok((ret, last_added_location_id))
}
fn get_kml_timestamp(utc: i64) -> String {
// Returns a string formatted as YYYY-MM-DDTHH:MM:SSZ. The trailing `Z` indicates UTC.
chrono::NaiveDateTime::from_timestamp(utc, 0)
.format("%Y-%m-%dT%H:%M:%SZ")
.to_string()
}
pub fn get_message_kml(timestamp: i64, latitude: f64, longitude: f64) -> String {
format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n\
<Document>\n\
<Placemark>\
<Timestamp><when>{}</when></Timestamp>\
<Point><coordinates>{:.2},{:.2}</coordinates></Point>\
</Placemark>\n\
</Document>\n\
</kml>",
get_kml_timestamp(timestamp),
longitude,
latitude,
)
}
pub fn set_kml_sent_timestamp(
context: &Context,
chat_id: u32,
timestamp: i64,
) -> Result<(), Error> {
sql::execute(
context,
&context.sql,
"UPDATE chats SET locations_last_sent=? WHERE id=?;",
params![timestamp, chat_id as i32],
)?;
Ok(())
}
pub fn set_msg_location_id(context: &Context, msg_id: u32, location_id: u32) -> Result<(), Error> {
sql::execute(
context,
&context.sql,
"UPDATE msgs SET location_id=? WHERE id=?;",
params![location_id, msg_id as i32],
)?;
Ok(())
}
pub fn save(
context: &Context,
chat_id: u32,
contact_id: u32,
locations: &[Location],
independent: i32,
) -> Result<u32, Error> {
ensure!(chat_id > 9, "Invalid chat id");
context.sql.prepare2(
"SELECT id FROM locations WHERE timestamp=? AND from_id=?",
"INSERT INTO locations\
(timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \
VALUES (?,?,?,?,?,?,?);",
|mut stmt_test, mut stmt_insert, conn| {
let mut newest_timestamp = 0;
let mut newest_location_id = 0;
for location in locations {
let exists = stmt_test.exists(params![location.timestamp, contact_id as i32])?;
if 0 != independent || !exists {
stmt_insert.execute(params![
location.timestamp,
contact_id as i32,
chat_id as i32,
location.latitude,
location.longitude,
location.accuracy,
independent,
])?;
if location.timestamp > newest_timestamp {
newest_timestamp = location.timestamp;
newest_location_id = sql::get_rowid2_with_conn(
context,
conn,
"locations",
"timestamp",
location.timestamp,
"from_id",
contact_id as i32,
);
}
}
}
Ok(newest_location_id)
},
)
}
#[allow(non_snake_case)]
pub fn job_do_DC_JOB_MAYBE_SEND_LOCATIONS(context: &Context, _job: &Job) {
let now = time();
let mut continue_streaming: libc::c_int = 1;
info!(
context,
" ----------------- MAYBE_SEND_LOCATIONS -------------- ",
);
if let Ok(rows) = context.sql.query_map(
"SELECT id, locations_send_begin, locations_last_sent \
FROM chats \
WHERE locations_send_until>?;",
params![now],
|row| {
let chat_id: i32 = row.get(0)?;
let locations_send_begin: i64 = row.get(1)?;
let locations_last_sent: i64 = row.get(2)?;
continue_streaming = 1;
// be a bit tolerant as the timer may not align exactly with time(NULL)
if now - locations_last_sent < (60 - 3) {
Ok(None)
} else {
Ok(Some((chat_id, locations_send_begin, locations_last_sent)))
}
},
|rows| {
rows.filter_map(|v| v.transpose())
.collect::<Result<Vec<_>, _>>()
.map_err(Into::into)
},
) {
let msgs = context
.sql
.prepare(
"SELECT id \
FROM locations \
WHERE from_id=? \
AND timestamp>=? \
AND timestamp>? \
AND independent=0 \
ORDER BY timestamp;",
|mut stmt_locations, _| {
let msgs = rows
.into_iter()
.filter_map(|(chat_id, locations_send_begin, locations_last_sent)| {
if !stmt_locations
.exists(params![1, locations_send_begin, locations_last_sent,])
.unwrap_or_default()
{
// if there is no new location, there's nothing to send.
// however, maybe we want to bypass this test eg. 15 minutes
None
} else {
// pending locations are attached automatically to every message,
// so also to this empty text message.
// DC_CMD_LOCATION is only needed to create a nicer subject.
//
// for optimisation and to avoid flooding the sending queue,
// we could sending these messages only if we're really online.
// the easiest way to determine this, is to check for an empty message queue.
// (might not be 100%, however, as positions are sent combined later
// and dc_set_location() is typically called periodically, this is ok)
let mut msg = dc_msg_new(Viewtype::Text);
msg.hidden = true;
msg.param.set_int(Param::Cmd, 9);
Some((chat_id, msg))
}
})
.collect::<Vec<_>>();
Ok(msgs)
},
)
.unwrap_or_default(); // TODO: Better error handling
for (chat_id, mut msg) in msgs.into_iter() {
// TODO: better error handling
chat::send_msg(context, chat_id as u32, &mut msg).unwrap();
}
}
if 0 != continue_streaming {
schedule_MAYBE_SEND_LOCATIONS(context, 0x1);
}
}
#[allow(non_snake_case)]
pub fn job_do_DC_JOB_MAYBE_SEND_LOC_ENDED(context: &Context, job: &mut Job) {
// this function is called when location-streaming _might_ have ended for a chat.
// the function checks, if location-streaming is really ended;
// if so, a device-message is added if not yet done.
let chat_id = job.foreign_id;
if let Ok((send_begin, send_until)) = context.sql.query_row(
"SELECT locations_send_begin, locations_send_until FROM chats WHERE id=?",
params![chat_id as i32],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)),
) {
if !(send_begin != 0 && time() <= send_until) {
// still streaming -
// may happen as several calls to dc_send_locations_to_chat()
// do not un-schedule pending DC_MAYBE_SEND_LOC_ENDED jobs
if !(send_begin == 0 && send_until == 0) {
// not streaming, device-message already sent
if context.sql.execute(
"UPDATE chats SET locations_send_begin=0, locations_send_until=0 WHERE id=?",
params![chat_id as i32],
).is_ok() {
let stock_str = context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0);
chat::add_device_msg(context, chat_id, stock_str);
context.call_cb(Event::ChatModified(chat_id));
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::dummy_context;
#[test]
fn test_kml_parse() {
let context = dummy_context();
let xml =
"<?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(&context.ctx, &xml).expect("parsing failed");
assert!(kml.addr.is_some());
assert_eq!(kml.addr.as_ref().unwrap(), "user@example.org",);
let locations_ref = &kml.locations;
assert_eq!(locations_ref.len(), 2);
assert!(locations_ref[0].latitude > 53.6f64);
assert!(locations_ref[0].latitude < 53.8f64);
assert!(locations_ref[0].longitude > 9.3f64);
assert!(locations_ref[0].longitude < 9.5f64);
assert!(locations_ref[0].accuracy > 31.9f64);
assert!(locations_ref[0].accuracy < 32.1f64);
assert_eq!(locations_ref[0].timestamp, 1551906597);
assert!(locations_ref[1].latitude > 63.6f64);
assert!(locations_ref[1].latitude < 63.8f64);
assert!(locations_ref[1].longitude > 19.3f64);
assert!(locations_ref[1].longitude < 19.5f64);
assert!(locations_ref[1].accuracy > 2.4f64);
assert!(locations_ref[1].accuracy < 2.6f64);
assert_eq!(locations_ref[1].timestamp, 1544739072);
}
}

View File

@@ -1,39 +1,59 @@
#[macro_export]
macro_rules! info {
($ctx:expr, $msg:expr) => {
info!($ctx, $msg,)
};
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {
let formatted = format!($msg, $($args),*);
emit_event!($ctx, $crate::Event::Info(formatted));
($ctx:expr, $data1:expr, $msg:expr) => {
info!($ctx, $data1, $msg,)
};
($ctx:expr, $data1:expr, $msg:expr, $($args:expr),* $(,)?) => {
#[allow(unused_unsafe)]
unsafe {
let formatted = format!($msg, $($args),*);
let formatted_c = std::ffi::CString::new(formatted).unwrap();
$ctx.call_cb($crate::constants::Event::INFO, $data1 as libc::uintptr_t,
formatted_c.as_ptr() as libc::uintptr_t);
}};
}
#[macro_export]
macro_rules! warn {
($ctx:expr, $msg:expr) => {
warn!($ctx, $msg,)
};
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {
let formatted = format!($msg, $($args),*);
emit_event!($ctx, $crate::Event::Warning(formatted));
($ctx:expr, $data1:expr, $msg:expr) => {
warn!($ctx, $data1, $msg,)
};
($ctx:expr, $data1:expr, $msg:expr, $($args:expr),* $(,)?) => {
#[allow(unused_unsafe)]
unsafe {
let formatted = format!($msg, $($args),*);
let formatted_c = std::ffi::CString::new(formatted).unwrap();
$ctx.call_cb($crate::constants::Event::WARNING, $data1 as libc::uintptr_t,
formatted_c.as_ptr() as libc::uintptr_t);
}};
}
#[macro_export]
macro_rules! error {
($ctx:expr, $msg:expr) => {
error!($ctx, $msg,)
($ctx:expr, $data1:expr, $msg:expr) => {
error!($ctx, $data1, $msg,)
};
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {
($ctx:expr, $data1:expr, $msg:expr, $($args:expr),* $(,)?) => {
#[allow(unused_unsafe)]
unsafe {
let formatted = format!($msg, $($args),*);
emit_event!($ctx, $crate::Event::Error(formatted));
};
let formatted_c = std::ffi::CString::new(formatted).unwrap();
$ctx.call_cb($crate::constants::Event::ERROR, $data1 as libc::uintptr_t,
formatted_c.as_ptr() as libc::uintptr_t);
}};
}
#[macro_export]
macro_rules! emit_event {
($ctx:expr, $event:expr) => {
$ctx.call_cb($event);
macro_rules! log_event {
($ctx:expr, $data1:expr, $msg:expr) => {
log_event!($ctx, $data1, $msg,)
};
($ctx:expr, $event:expr, $data1:expr, $msg:expr, $($args:expr),* $(,)?) => {
#[allow(unused_unsafe)]
unsafe {
let formatted = format!($msg, $($args),*);
let formatted_c = std::ffi::CString::new(formatted).unwrap();
$ctx.call_cb($event, $data1 as libc::uintptr_t,
formatted_c.as_ptr() as libc::uintptr_t);
}};
}

View File

@@ -1,206 +0,0 @@
use std::borrow::Cow;
use std::fmt;
use crate::context::Context;
use crate::error::Error;
#[derive(Default, Debug)]
pub struct LoginParam {
pub addr: String,
pub mail_server: String,
pub mail_user: String,
pub mail_pw: String,
pub mail_port: i32,
pub send_server: String,
pub send_user: String,
pub send_pw: String,
pub send_port: i32,
pub server_flags: i32,
}
impl LoginParam {
/// Create a new `LoginParam` with default values.
pub fn new() -> Self {
Default::default()
}
/// Read the login parameters from the database.
pub fn from_database(context: &Context, prefix: impl AsRef<str>) -> Self {
let prefix = prefix.as_ref();
let sql = &context.sql;
let key = format!("{}addr", prefix);
let addr = sql
.get_config(context, key)
.unwrap_or_default()
.trim()
.to_string();
let key = format!("{}mail_server", prefix);
let mail_server = sql.get_config(context, key).unwrap_or_default();
let key = format!("{}mail_port", prefix);
let mail_port = sql.get_config_int(context, key).unwrap_or_default();
let key = format!("{}mail_user", prefix);
let mail_user = sql.get_config(context, key).unwrap_or_default();
let key = format!("{}mail_pw", prefix);
let mail_pw = sql.get_config(context, key).unwrap_or_default();
let key = format!("{}send_server", prefix);
let send_server = sql.get_config(context, key).unwrap_or_default();
let key = format!("{}send_port", prefix);
let send_port = sql.get_config_int(context, key).unwrap_or_default();
let key = format!("{}send_user", prefix);
let send_user = sql.get_config(context, key).unwrap_or_default();
let key = format!("{}send_pw", prefix);
let send_pw = sql.get_config(context, key).unwrap_or_default();
let key = format!("{}server_flags", prefix);
let server_flags = sql.get_config_int(context, key).unwrap_or_default();
LoginParam {
addr: addr.to_string(),
mail_server,
mail_user,
mail_pw,
mail_port,
send_server,
send_user,
send_pw,
send_port,
server_flags,
}
}
pub fn addr_str(&self) -> &str {
self.addr.as_str()
}
/// Save this loginparam to the database.
pub fn save_to_database(
&self,
context: &Context,
prefix: impl AsRef<str>,
) -> Result<(), Error> {
let prefix = prefix.as_ref();
let sql = &context.sql;
let key = format!("{}addr", prefix);
sql.set_config(context, key, Some(&self.addr))?;
let key = format!("{}mail_server", prefix);
sql.set_config(context, key, Some(&self.mail_server))?;
let key = format!("{}mail_port", prefix);
sql.set_config_int(context, key, self.mail_port)?;
let key = format!("{}mail_user", prefix);
sql.set_config(context, key, Some(&self.mail_user))?;
let key = format!("{}mail_pw", prefix);
sql.set_config(context, key, Some(&self.mail_pw))?;
let key = format!("{}send_server", prefix);
sql.set_config(context, key, Some(&self.send_server))?;
let key = format!("{}send_port", prefix);
sql.set_config_int(context, key, self.send_port)?;
let key = format!("{}send_user", prefix);
sql.set_config(context, key, Some(&self.send_user))?;
let key = format!("{}send_pw", prefix);
sql.set_config(context, key, Some(&self.send_pw))?;
let key = format!("{}server_flags", prefix);
sql.set_config_int(context, key, self.server_flags)?;
Ok(())
}
}
impl fmt::Display for LoginParam {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let unset = "0";
let pw = "***";
let flags_readable = get_readable_flags(self.server_flags);
write!(
f,
"{} {}:{}:{}:{} {}:{}:{}:{} {}",
unset_empty(&self.addr),
unset_empty(&self.mail_user),
if !self.mail_pw.is_empty() { pw } else { unset },
unset_empty(&self.mail_server),
self.mail_port,
unset_empty(&self.send_user),
if !self.send_pw.is_empty() { pw } else { unset },
unset_empty(&self.send_server),
self.send_port,
flags_readable,
)
}
}
fn unset_empty(s: &String) -> Cow<String> {
if s.is_empty() {
Cow::Owned("unset".to_string())
} else {
Cow::Borrowed(s)
}
}
fn get_readable_flags(flags: i32) -> String {
let mut res = String::new();
for bit in 0..31 {
if 0 != flags & 1 << bit {
let mut flag_added = 0;
if 1 << bit == 0x2 {
res += "OAUTH2 ";
flag_added = 1;
}
if 1 << bit == 0x4 {
res += "AUTH_NORMAL ";
flag_added = 1;
}
if 1 << bit == 0x100 {
res += "IMAP_STARTTLS ";
flag_added = 1;
}
if 1 << bit == 0x200 {
res += "IMAP_SSL ";
flag_added = 1;
}
if 1 << bit == 0x400 {
res += "IMAP_PLAIN ";
flag_added = 1;
}
if 1 << bit == 0x10000 {
res += "SMTP_STARTTLS ";
flag_added = 1
}
if 1 << bit == 0x20000 {
res += "SMTP_SSL ";
flag_added = 1
}
if 1 << bit == 0x40000 {
res += "SMTP_PLAIN ";
flag_added = 1
}
if 0 == flag_added {
res += &format!("{:#0x}", 1 << bit);
}
}
}
if res.is_empty() {
res += "0";
}
res
}

View File

@@ -94,6 +94,7 @@ pub enum LotState {
MsgInFresh = 10,
MsgInNoticed = 13,
MsgInSeen = 16,
MsgInMDNSent = 17,
MsgOutPreparing = 18,
MsgOutDraft = 19,
MsgOutPending = 20,

View File

@@ -1,5 +1,5 @@
use std::ffi::CString;
use std::path::{Path, PathBuf};
use std::path::Path;
use std::ptr;
use deltachat_derive::{FromSql, ToSql};
@@ -9,16 +9,15 @@ use crate::chat::{self, Chat};
use crate::constants::*;
use crate::contact::*;
use crate::context::*;
use crate::dc_mimeparser::SystemMessage;
use crate::dc_tools::*;
use crate::error::Error;
use crate::events::Event;
use crate::job::*;
use crate::lot::{Lot, LotState, Meaning};
use crate::param::*;
use crate::pgp::*;
use crate::sql;
use crate::stock::StockMessage;
use crate::types::*;
use crate::x::*;
/// In practice, the user additionally cuts the string himself pixel-accurate.
@@ -31,6 +30,7 @@ pub enum MessageState {
InFresh = 10,
InNoticed = 13,
InSeen = 16,
InMDNSent = 17,
OutPreparing = 18,
OutDraft = 19,
OutPending = 20,
@@ -39,12 +39,6 @@ pub enum MessageState {
OutMdnRcvd = 28,
}
impl Default for MessageState {
fn default() -> Self {
MessageState::Undefined
}
}
impl From<MessageState> for LotState {
fn from(s: MessageState) -> Self {
use MessageState::*;
@@ -53,6 +47,7 @@ impl From<MessageState> for LotState {
InFresh => LotState::MsgInFresh,
InNoticed => LotState::MsgInNoticed,
InSeen => LotState::MsgInSeen,
InMDNSent => LotState::MsgInMDNSent,
OutPreparing => LotState::MsgOutPreparing,
OutDraft => LotState::MsgOutDraft,
OutPending => LotState::MsgOutPending,
@@ -87,8 +82,8 @@ impl Lot {
if msg.state == MessageState::OutDraft {
self.text1 = Some(context.stock_str(StockMessage::Draft).to_owned().into());
self.text1_meaning = Meaning::Text1Draft;
} else if msg.from_id == DC_CONTACT_ID_SELF {
if dc_msg_is_info(msg) || chat.is_self_talk() {
} else if msg.from_id == DC_CONTACT_ID_SELF as u32 {
if 0 != dc_msg_is_info(msg) || chat.is_self_talk() {
self.text1 = None;
self.text1_meaning = Meaning::None;
} else {
@@ -96,11 +91,11 @@ impl Lot {
self.text1_meaning = Meaning::Text1Self;
}
} else if chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup {
if dc_msg_is_info(msg) || contact.is_none() {
if 0 != dc_msg_is_info(msg) || contact.is_none() {
self.text1 = None;
self.text1_meaning = Meaning::None;
} else {
if chat.id == DC_CHAT_ID_DEADDROP {
if chat.id == DC_CHAT_ID_DEADDROP as u32 {
if let Some(contact) = contact {
self.text1 = Some(contact.get_display_name().into());
} else {
@@ -137,8 +132,8 @@ impl Lot {
/// 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)]
pub struct Message {
#[derive(Clone)]
pub struct Message<'a> {
pub id: u32,
pub from_id: u32,
pub to_id: u32,
@@ -151,8 +146,9 @@ pub struct Message {
pub timestamp_sent: i64,
pub timestamp_rcvd: i64,
pub text: Option<String>,
pub context: &'a Context,
pub rfc724_mid: *mut libc::c_char,
pub in_reply_to: Option<String>,
pub in_reply_to: *mut libc::c_char,
pub server_folder: Option<String>,
pub server_uid: u32,
// TODO: enum
@@ -165,6 +161,7 @@ pub struct Message {
// handle messages
pub unsafe fn dc_get_msg_info(context: &Context, msg_id: u32) -> *mut libc::c_char {
let mut p: *mut libc::c_char;
let mut ret = String::new();
let msg = dc_msg_load_from_db(context, msg_id);
@@ -174,10 +171,11 @@ pub unsafe fn dc_get_msg_info(context: &Context, msg_id: u32) -> *mut libc::c_ch
let msg = msg.unwrap();
let rawtxt: Option<String> = context.sql.query_get_value(
let rawtxt: Option<String> = context.sql.query_row_col(
context,
"SELECT txt_raw FROM msgs WHERE id=?;",
params![msg_id as i32],
0,
);
if rawtxt.is_none() {
@@ -212,28 +210,33 @@ pub unsafe fn dc_get_msg_info(context: &Context, msg_id: u32) -> *mut libc::c_ch
return ret.strdup();
}
if let Ok(rows) = context.sql.query_map(
"SELECT contact_id, timestamp_sent FROM msgs_mdns WHERE msg_id=?;",
params![msg_id as i32],
|row| {
let contact_id: i32 = row.get(0)?;
let ts: i64 = row.get(1)?;
Ok((contact_id, ts))
},
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
) {
for (contact_id, ts) in rows {
let fts = dc_timestamp_to_str(ts);
ret += &format!("Read: {}", fts);
context
.sql
.query_map(
"SELECT contact_id, timestamp_sent FROM msgs_mdns WHERE msg_id=?;",
params![msg_id as i32],
|row| {
let contact_id: i32 = row.get(0)?;
let ts: i64 = row.get(1)?;
Ok((contact_id, ts))
},
|rows| {
for row in rows {
let (contact_id, ts) = row?;
let fts = dc_timestamp_to_str(ts);
ret += &format!("Read: {}", fts);
let name = Contact::load_from_db(context, contact_id as u32)
.map(|contact| contact.get_name_n_addr())
.unwrap_or_default();
let name = Contact::load_from_db(context, contact_id as u32)
.map(|contact| contact.get_name_n_addr())
.unwrap_or_default();
ret += &format!(" by {}", name);
ret += "\n";
}
}
ret += &format!(" by {}", name);
ret += "\n";
}
Ok(())
},
)
.unwrap(); // TODO: better error handling
ret += "State: ";
use MessageState::*;
@@ -269,16 +272,23 @@ pub unsafe fn dc_get_msg_info(context: &Context, msg_id: u32) -> *mut libc::c_ch
_ => {}
}
if let Some(path) = dc_msg_get_file(context, &msg) {
let bytes = dc_get_filebytes(context, &path);
ret += &format!("\nFile: {}, {}, bytes\n", path.display(), bytes);
p = dc_msg_get_file(&msg);
if !p.is_null() && 0 != *p.offset(0isize) as libc::c_int {
ret += &format!(
"\nFile: {}, {}, bytes\n",
as_str(p),
dc_get_filebytes(context, as_path(p)) as libc::c_int,
);
}
free(p as *mut libc::c_void);
if msg.type_0 != Viewtype::Text {
ret += "Type: ";
ret += &format!("{}", msg.type_0);
ret += "\n";
ret += &format!("Mimetype: {}\n", &dc_msg_get_filemime(&msg));
p = dc_msg_get_filemime(&msg);
ret += &format!("Mimetype: {}\n", as_str(p));
free(p as *mut libc::c_void);
}
let w = msg.param.get_int(Param::Width).unwrap_or_default();
let h = msg.param.get_int(Param::Height).unwrap_or_default();
@@ -304,11 +314,11 @@ pub unsafe fn dc_get_msg_info(context: &Context, msg_id: u32) -> *mut libc::c_ch
ret.strdup()
}
pub fn dc_msg_new_untyped() -> Message {
dc_msg_new(Viewtype::Unknown)
pub unsafe fn dc_msg_new_untyped<'a>(context: &'a Context) -> Message<'a> {
dc_msg_new(context, Viewtype::Unknown)
}
pub fn dc_msg_new(viewtype: Viewtype) -> Message {
pub fn dc_msg_new<'a>(context: &'a Context, viewtype: Viewtype) -> Message<'a> {
Message {
id: 0,
from_id: 0,
@@ -322,8 +332,9 @@ pub fn dc_msg_new(viewtype: Viewtype) -> Message {
timestamp_sent: 0,
timestamp_rcvd: 0,
text: None,
context,
rfc724_mid: std::ptr::null_mut(),
in_reply_to: None,
in_reply_to: std::ptr::null_mut(),
server_folder: None,
server_uid: 0,
is_dc_message: 0,
@@ -334,24 +345,25 @@ pub fn dc_msg_new(viewtype: Viewtype) -> Message {
}
}
impl Drop for Message {
impl<'a> Drop for Message<'a> {
fn drop(&mut self) {
unsafe {
free(self.rfc724_mid.cast());
free(self.in_reply_to.cast());
}
}
}
pub fn dc_msg_get_filemime(msg: &Message) -> String {
pub unsafe fn dc_msg_get_filemime(msg: &Message) -> *mut libc::c_char {
if let Some(m) = msg.param.get(Param::MimeType) {
return m.to_string();
return m.strdup();
} else if let Some(file) = msg.param.get(Param::File) {
if let Some((_, mime)) = dc_msg_guess_msgtype_from_suffix(Path::new(file)) {
return mime.to_string();
return mime.strdup();
}
}
"application/octet-stream".to_string()
"application/octet-stream".strdup()
}
pub fn dc_msg_guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> {
@@ -372,10 +384,17 @@ pub fn dc_msg_guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)>
KNOWN.get(extension).map(|x| *x)
}
pub unsafe fn dc_msg_get_file(context: &Context, msg: &Message) -> Option<PathBuf> {
msg.param
.get(Param::File)
.map(|f| dc_get_abs_path(context, f))
pub unsafe fn dc_msg_get_file(msg: &Message) -> *mut libc::c_char {
let mut file_abs = 0 as *mut libc::c_char;
if let Some(file_rel) = msg.param.get(Param::File) {
file_abs = dc_get_abs_path(msg.context, file_rel);
}
if !file_abs.is_null() {
file_abs
} else {
dc_strdup(0 as *const libc::c_char)
}
}
/**
@@ -425,7 +444,7 @@ pub fn dc_msg_get_timestamp(msg: &Message) -> i64 {
}
}
pub fn dc_msg_load_from_db(context: &Context, id: u32) -> Result<Message, Error> {
pub fn dc_msg_load_from_db<'a>(context: &'a Context, id: u32) -> Result<Message<'a>, Error> {
context.sql.query_row(
"SELECT \
m.id,rfc724_mid,m.mime_in_reply_to,m.server_folder,m.server_uid,m.move_state,m.chat_id, \
@@ -436,10 +455,14 @@ pub fn dc_msg_load_from_db(context: &Context, id: u32) -> Result<Message, Error>
params![id as i32],
|row| {
unsafe {
let mut msg = dc_msg_new_untyped();
let mut msg = dc_msg_new_untyped(context);
msg.context = context;
msg.id = row.get::<_, i32>(0)? as u32;
msg.rfc724_mid = row.get::<_, String>(1)?.strdup();
msg.in_reply_to = row.get::<_, Option<String>>(2)?;
msg.in_reply_to = match row.get::<_, Option<String>>(2)? {
Some(s) => s.strdup(),
None => std::ptr::null_mut(),
};
msg.server_folder = row.get::<_, Option<String>>(3)?;
msg.server_uid = row.get(4)?;
msg.move_state = row.get(5)?;
@@ -458,11 +481,12 @@ pub fn dc_msg_load_from_db(context: &Context, id: u32) -> Result<Message, Error>
if let Ok(t) = String::from_utf8(buf.to_vec()) {
text = t;
} else {
warn!(context, "dc_msg_load_from_db: could not get text column as non-lossy utf8 id {}", id);
warn!(context, 0, "dc_msg_load_from_db: could not get text column as non-lossy utf8 id {}", id);
text = String::from_utf8_lossy(buf).into_owned();
}
} else {
text = "".to_string();
warn!(context, 0, "dc_msg_load_from_db: could not get text column for id {}", id);
text = "[ Could not read from db ]".to_string();
}
msg.text = Some(text);
@@ -487,10 +511,11 @@ pub fn dc_msg_load_from_db(context: &Context, id: u32) -> Result<Message, Error>
}
pub unsafe fn dc_get_mime_headers(context: &Context, msg_id: u32) -> *mut libc::c_char {
let headers: Option<String> = context.sql.query_get_value(
let headers: Option<String> = context.sql.query_row_col(
context,
"SELECT mime_headers FROM msgs WHERE id=?;",
params![msg_id as i32],
0,
);
if let Some(headers) = headers {
@@ -519,10 +544,7 @@ pub unsafe fn dc_delete_msgs(context: &Context, msg_ids: *const u32, msg_cnt: li
}
if 0 != msg_cnt {
context.call_cb(Event::MsgsChanged {
chat_id: 0,
msg_id: 0,
});
context.call_cb(Event::MSGS_CHANGED, 0 as uintptr_t, 0 as uintptr_t);
job_kill_action(context, Action::Housekeeping);
job_add(context, Action::Housekeeping, 0, Params::new(), 10);
};
@@ -562,7 +584,7 @@ pub fn dc_markseen_msgs(context: &Context, msg_ids: *const u32, msg_cnt: usize)
);
if msgs.is_err() {
warn!(context, "markseen_msgs failed: {:?}", msgs);
warn!(context, 0, "markseen_msgs failed: {:?}", msgs);
return false;
}
let mut send_event = false;
@@ -572,7 +594,7 @@ pub fn dc_markseen_msgs(context: &Context, msg_ids: *const u32, msg_cnt: usize)
if curr_blocked == Blocked::Not {
if curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed {
dc_update_msg_state(context, id, MessageState::InSeen);
info!(context, "Seen message #{}.", id);
info!(context, 0, "Seen message #{}.", id);
job_add(
context,
@@ -590,10 +612,7 @@ pub fn dc_markseen_msgs(context: &Context, msg_ids: *const u32, msg_cnt: usize)
}
if send_event {
context.call_cb(Event::MsgsChanged {
chat_id: 0,
msg_id: 0,
});
context.call_cb(Event::MSGS_CHANGED, 0, 0);
}
true
@@ -629,7 +648,7 @@ pub fn dc_star_msgs(
.is_ok()
}
pub fn dc_get_msg(context: &Context, msg_id: u32) -> Result<Message, Error> {
pub fn dc_get_msg<'a>(context: &'a Context, msg_id: u32) -> Result<Message<'a>, Error> {
dc_msg_load_from_db(context, msg_id)
}
@@ -675,7 +694,7 @@ pub unsafe fn dc_msg_get_text(msg: &Message) -> *mut libc::c_char {
#[allow(non_snake_case)]
pub unsafe fn dc_msg_get_filename(msg: &Message) -> *mut libc::c_char {
let mut ret = ptr::null_mut();
let mut ret = 0 as *mut libc::c_char;
if let Some(file) = msg.param.get(Param::File) {
ret = dc_get_filename(file);
@@ -687,10 +706,11 @@ pub unsafe fn dc_msg_get_filename(msg: &Message) -> *mut libc::c_char {
}
}
pub fn dc_msg_get_filebytes(context: &Context, msg: &Message) -> u64 {
pub unsafe fn dc_msg_get_filebytes(msg: &Message) -> uint64_t {
if let Some(file) = msg.param.get(Param::File) {
return dc_get_filebytes(context, &file);
return dc_get_filebytes(msg.context, &file);
}
0
}
@@ -706,18 +726,23 @@ pub fn dc_msg_get_duration(msg: &Message) -> libc::c_int {
msg.param.get_int(Param::Duration).unwrap_or_default()
}
pub fn dc_msg_get_showpadlock(msg: &Message) -> bool {
msg.param.get_int(Param::GuranteeE2ee).unwrap_or_default() != 0
// TODO should return bool /rtn
pub fn dc_msg_get_showpadlock(msg: &Message) -> libc::c_int {
if msg.param.get_int(Param::GuranteeE2ee).unwrap_or_default() != 0 {
return 1;
}
0
}
pub fn dc_msg_get_summary(context: &Context, msg: &mut Message, chat: Option<&Chat>) -> Lot {
pub unsafe fn dc_msg_get_summary<'a>(msg: &mut Message<'a>, chat: Option<&Chat<'a>>) -> Lot {
let mut ret = Lot::new();
let chat_loaded: Chat;
let chat = if let Some(chat) = chat {
chat
} else {
if let Ok(chat) = Chat::load_from_db(context, msg.chat_id) {
if let Ok(chat) = Chat::load_from_db(msg.context, msg.chat_id) {
chat_loaded = chat;
&chat_loaded
} else {
@@ -728,18 +753,17 @@ pub fn dc_msg_get_summary(context: &Context, msg: &mut Message, chat: Option<&Ch
let contact = if msg.from_id != DC_CONTACT_ID_SELF as libc::c_uint
&& ((*chat).typ == Chattype::Group || (*chat).typ == Chattype::VerifiedGroup)
{
Contact::get_by_id(context, msg.from_id).ok()
Contact::get_by_id((*chat).context, msg.from_id).ok()
} else {
None
};
ret.fill(msg, chat, contact.as_ref(), context);
ret.fill(msg, chat, contact.as_ref(), msg.context);
ret
}
pub unsafe fn dc_msg_get_summarytext(
context: &Context,
msg: &mut Message,
approx_characters: usize,
) -> *mut libc::c_char {
@@ -748,7 +772,7 @@ pub unsafe fn dc_msg_get_summarytext(
msg.text.as_ref(),
&mut msg.param,
approx_characters,
context,
msg.context,
)
.strdup()
}
@@ -768,7 +792,7 @@ pub fn dc_msg_get_summarytext_by_raw(
Viewtype::Video => context.stock_str(StockMessage::Video).into_owned(),
Viewtype::Voice => context.stock_str(StockMessage::VoiceMessage).into_owned(),
Viewtype::Audio | Viewtype::File => {
if param.get_cmd() == SystemMessage::AutocryptSetupMessage {
if param.get_int(Param::Cmd) == Some(6) {
append_text = false;
context
.stock_str(StockMessage::AcSetupMsgSubject)
@@ -794,7 +818,7 @@ pub fn dc_msg_get_summarytext_by_raw(
}
}
_ => {
if param.get_cmd() != SystemMessage::LocationOnly {
if param.get_int(Param::Cmd) != Some(9) {
"".to_string()
} else {
append_text = false;
@@ -802,21 +826,19 @@ pub fn dc_msg_get_summarytext_by_raw(
}
}
};
if !append_text {
return prefix;
}
if let Some(text) = text {
if prefix.is_empty() {
dc_truncate(text.as_ref(), approx_characters, true).to_string()
} else {
let ret = if append_text && text.is_some() {
let text = text.unwrap();
if !prefix.is_empty() {
let tmp = format!("{} {}", prefix, text.as_ref());
dc_truncate(&tmp, approx_characters, true).to_string()
} else {
dc_truncate(text.as_ref(), approx_characters, true).to_string()
}
} else {
prefix
}
};
ret
}
pub unsafe fn dc_msg_has_deviating_timestamp(msg: &Message) -> libc::c_int {
@@ -827,27 +849,48 @@ pub unsafe fn dc_msg_has_deviating_timestamp(msg: &Message) -> libc::c_int {
(sort_timestamp / 86400 != send_timestamp / 86400) as libc::c_int
}
pub fn dc_msg_is_sent(msg: &Message) -> bool {
msg.state as i32 >= MessageState::OutDelivered as i32
// TODO should return bool /rtn
pub fn dc_msg_is_sent(msg: &Message) -> libc::c_int {
if msg.state as i32 >= MessageState::OutDelivered as i32 {
1
} else {
0
}
}
pub fn dc_msg_is_starred(msg: &Message) -> bool {
msg.starred
}
pub fn dc_msg_is_forwarded(msg: &Message) -> bool {
0 != msg.param.get_int(Param::Forwarded).unwrap_or_default()
// TODO should return bool /rtn
pub fn dc_msg_is_forwarded(msg: &Message) -> libc::c_int {
if 0 != msg.param.get_int(Param::Forwarded).unwrap_or_default() {
1
} else {
0
}
}
pub fn dc_msg_is_info(msg: &Message) -> bool {
let cmd = msg.param.get_cmd();
msg.from_id == 2i32 as libc::c_uint
// TODO should return bool /rtn
pub fn dc_msg_is_info(msg: &Message) -> libc::c_int {
let cmd = msg.param.get_int(Param::Cmd).unwrap_or_default();
if msg.from_id == 2i32 as libc::c_uint
|| msg.to_id == 2i32 as libc::c_uint
|| cmd != SystemMessage::Unknown && cmd != SystemMessage::AutocryptSetupMessage
|| 0 != cmd && cmd != 6i32
{
return 1;
}
0
}
pub fn dc_msg_is_increation(msg: &Message) -> bool {
chat::msgtype_has_file(msg.type_0) && msg.state == MessageState::OutPreparing
// TODO should return bool /rtn
pub fn dc_msg_is_increation(msg: &Message) -> libc::c_int {
if chat::msgtype_has_file(msg.type_0) && msg.state == MessageState::OutPreparing {
1
} else {
0
}
}
pub fn dc_msg_is_setupmessage(msg: &Message) -> bool {
@@ -855,24 +898,37 @@ pub fn dc_msg_is_setupmessage(msg: &Message) -> bool {
return false;
}
msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage
msg.param.get_int(Param::Cmd) == Some(6)
}
pub unsafe fn dc_msg_get_setupcodebegin(context: &Context, msg: &Message) -> *mut libc::c_char {
pub unsafe fn dc_msg_get_setupcodebegin(msg: &Message) -> *mut libc::c_char {
let mut filename: *mut libc::c_char = 0 as *mut libc::c_char;
let mut buf: *mut libc::c_char = 0 as *mut libc::c_char;
let mut buf_bytes: size_t = 0i32 as size_t;
// just a pointer inside buf, MUST NOT be free()'d
let mut buf_headerline: *const libc::c_char = ptr::null();
let mut buf_headerline: *const libc::c_char = 0 as *const libc::c_char;
// just a pointer inside buf, MUST NOT be free()'d
let mut buf_setupcodebegin: *const libc::c_char = ptr::null();
let mut ret: *mut libc::c_char = ptr::null_mut();
let mut buf_setupcodebegin: *const libc::c_char = 0 as *const libc::c_char;
let mut ret: *mut libc::c_char = 0 as *mut libc::c_char;
if dc_msg_is_setupmessage(msg) {
if let Some(filename) = dc_msg_get_file(context, msg) {
if let Some(mut buf) = dc_read_file_safe(context, filename) {
filename = dc_msg_get_file(msg);
if !(filename.is_null() || *filename.offset(0isize) as libc::c_int == 0i32) {
if !(0
== dc_read_file(
msg.context,
filename,
&mut buf as *mut *mut libc::c_char as *mut *mut libc::c_void,
&mut buf_bytes,
)
|| buf.is_null()
|| buf_bytes <= 0)
{
if dc_split_armored_data(
buf.as_mut_ptr().cast(),
buf,
&mut buf_headerline,
&mut buf_setupcodebegin,
ptr::null_mut(),
ptr::null_mut(),
0 as *mut *const libc::c_char,
0 as *mut *const libc::c_char,
) && strcmp(
buf_headerline,
b"-----BEGIN PGP MESSAGE-----\x00" as *const u8 as *const libc::c_char,
@@ -884,6 +940,8 @@ pub unsafe fn dc_msg_get_setupcodebegin(context: &Context, msg: &Message) -> *mu
}
}
}
free(filename as *mut libc::c_void);
free(buf as *mut libc::c_void);
if !ret.is_null() {
ret
} else {
@@ -922,7 +980,6 @@ pub fn dc_msg_set_duration(msg: &mut Message, duration: libc::c_int) {
}
pub fn dc_msg_latefiling_mediasize(
context: &Context,
msg: &mut Message,
width: libc::c_int,
height: libc::c_int,
@@ -935,20 +992,20 @@ pub fn dc_msg_latefiling_mediasize(
if duration > 0 {
msg.param.set_int(Param::Duration, duration);
}
dc_msg_save_param_to_disk(context, msg);
dc_msg_save_param_to_disk(msg);
}
pub fn dc_msg_save_param_to_disk(context: &Context, msg: &mut Message) -> bool {
pub fn dc_msg_save_param_to_disk(msg: &mut Message) -> bool {
sql::execute(
context,
&context.sql,
msg.context,
&msg.context.sql,
"UPDATE msgs SET param=? WHERE id=?;",
params![msg.param.to_string(), msg.id as i32],
)
.is_ok()
}
pub fn dc_msg_new_load(context: &Context, msg_id: u32) -> Result<Message, Error> {
pub fn dc_msg_new_load<'a>(context: &'a Context, msg_id: u32) -> Result<Message<'a>, Error> {
dc_msg_load_from_db(context, msg_id)
}
@@ -979,35 +1036,31 @@ The value is also used for CC:-summaries */
// Context functions to work with messages
pub fn dc_msg_exists(context: &Context, msg_id: u32) -> bool {
if msg_id <= DC_CHAT_ID_LAST_SPECIAL {
return false;
}
if msg_id > DC_MSG_ID_LAST_SPECIAL {
let chat_id: Option<i32> = context.sql.query_row_col(
context,
"SELECT chat_id FROM msgs WHERE id=?;",
params![msg_id as i32],
0,
);
let chat_id: Option<u32> = context.sql.query_get_value(
context,
"SELECT chat_id FROM msgs WHERE id=?;",
params![msg_id],
);
if let Some(chat_id) = chat_id {
chat_id != DC_CHAT_ID_TRASH
} else {
false
if let Some(chat_id) = chat_id {
if chat_id != 3 {
return true;
}
}
}
false
}
pub fn dc_update_msg_move_state(
context: &Context,
rfc724_mid: *const libc::c_char,
state: MoveState,
) -> bool {
pub fn dc_update_msg_move_state(context: &Context, rfc724_mid: &str, state: MoveState) -> bool {
// we update the move_state for all messages belonging to a given Message-ID
// so that the state stay intact when parts are deleted
sql::execute(
context,
&context.sql,
"UPDATE msgs SET move_state=? WHERE rfc724_mid=?;",
params![state as i32, as_str(rfc724_mid)],
params![state as i32, rfc724_mid],
)
.is_ok()
}
@@ -1019,7 +1072,7 @@ pub fn dc_set_msg_failed(context: &Context, msg_id: u32, error: Option<impl AsRe
}
if let Some(error) = error {
msg.param.set(Param::Error, error.as_ref());
error!(context, "{}", error.as_ref());
error!(context, 0, "{}", error.as_ref());
}
if sql::execute(
@@ -1030,10 +1083,11 @@ pub fn dc_set_msg_failed(context: &Context, msg_id: u32, error: Option<impl AsRe
)
.is_ok()
{
context.call_cb(Event::MsgFailed {
chat_id: msg.chat_id,
msg_id,
});
context.call_cb(
Event::MSG_FAILED,
msg.chat_id as uintptr_t,
msg_id as uintptr_t,
);
}
}
}
@@ -1104,10 +1158,11 @@ pub unsafe fn dc_mdn_from_ext(
/* send event about new state */
let ist_cnt: i32 = context
.sql
.query_get_value(
.query_row_col(
context,
"SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=?;",
params![*ret_msg_id as i32],
0,
)
.unwrap_or_default();
/*
@@ -1146,13 +1201,13 @@ pub fn dc_get_real_msg_cnt(context: &Context) -> libc::c_int {
) {
Ok(res) => res,
Err(err) => {
error!(context, "dc_get_real_msg_cnt() failed. {}", err);
error!(context, 0, "dc_get_real_msg_cnt() failed. {}", err);
0
}
}
}
pub fn dc_get_deaddrop_msg_cnt(context: &Context) -> libc::size_t {
pub fn dc_get_deaddrop_msg_cnt(context: &Context) -> size_t {
match context.sql.query_row(
"SELECT COUNT(*) \
FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id \
@@ -1160,9 +1215,9 @@ pub fn dc_get_deaddrop_msg_cnt(context: &Context) -> libc::size_t {
rusqlite::NO_PARAMS,
|row| row.get::<_, isize>(0),
) {
Ok(res) => res as libc::size_t,
Ok(res) => res as size_t,
Err(err) => {
error!(context, "dc_get_deaddrop_msg_cnt() failed. {}", err);
error!(context, 0, "dc_get_deaddrop_msg_cnt() failed. {}", err);
0
}
}
@@ -1177,12 +1232,35 @@ pub fn dc_rfc724_mid_cnt(context: &Context, rfc724_mid: *const libc::c_char) ->
) {
Ok(res) => res,
Err(err) => {
error!(context, "dc_get_rfc724_mid_cnt() failed. {}", err);
error!(context, 0, "dc_get_rfc724_mid_cnt() failed. {}", err);
0
}
}
}
pub fn dc_rfc724_mid_exists_with_msg_state(
context: &Context,
rfc724_mid: &str,
) -> Option<(u32, MessageState, String, u32)> {
if rfc724_mid.is_empty() {
return None;
}
match context.sql.query_row(
"SELECT id, state, server_folder, server_uid FROM msgs WHERE rfc724_mid=?",
&[rfc724_mid],
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
) {
Ok(res) => Some(res),
Err(_err) => {
info!(
context,
0, "dc_rfc724_mid_exists_with_msg_state ERR={:?}", _err
);
None
}
}
}
pub fn dc_rfc724_mid_exists(
context: &Context,
rfc724_mid: *const libc::c_char,
@@ -1208,7 +1286,7 @@ pub fn dc_rfc724_mid_exists(
Ok(res) => res,
Err(_err) => {
if !ret_server_folder.is_null() {
unsafe { *ret_server_folder = ptr::null_mut() };
unsafe { *ret_server_folder = 0 as *mut libc::c_char };
}
if !ret_server_uid.is_null() {
unsafe { *ret_server_uid = 0 };
@@ -1221,17 +1299,17 @@ pub fn dc_rfc724_mid_exists(
pub fn dc_update_server_uid(
context: &Context,
rfc724_mid: *const libc::c_char,
rfc724_mid: &str,
server_folder: impl AsRef<str>,
server_uid: u32,
) {
match context.sql.execute(
"UPDATE msgs SET server_folder=?, server_uid=? WHERE rfc724_mid=?;",
params![server_folder.as_ref(), server_uid, as_str(rfc724_mid)],
params![server_folder.as_ref(), server_uid, rfc724_mid],
) {
Ok(_) => {}
Err(err) => {
warn!(context, "msg: failed to update server_uid: {}", err);
warn!(context, 0, "msg: failed to update server_uid: {}", err);
}
}
}
@@ -1264,7 +1342,7 @@ mod tests {
let chat = chat::create_by_contact_id(ctx, contact).unwrap();
let mut msg = dc_msg_new(Viewtype::Text);
let mut msg = dc_msg_new(ctx, Viewtype::Text);
let msg_id = chat::prepare_msg(ctx, chat, &mut msg).unwrap();

View File

@@ -74,14 +74,14 @@ pub fn dc_get_oauth2_access_token(
context: &Context,
addr: impl AsRef<str>,
code: impl AsRef<str>,
regenerate: bool,
flags: usize,
) -> Option<String> {
if let Some(oauth2) = Oauth2::from_address(addr) {
let lock = context.oauth2_critical.clone();
let _l = lock.lock().unwrap();
// read generated token
if !regenerate && !is_expired(context) {
if 0 == flags & 0x1 && !is_expired(context) {
let access_token = context.sql.get_config(context, "oauth2_access_token");
if access_token.is_some() {
// success
@@ -97,7 +97,10 @@ pub fn dc_get_oauth2_access_token(
let (redirect_uri, token_url, update_redirect_uri_on_success) =
if refresh_token.is_none() || refresh_token_for != code.as_ref() {
info!(context, "Generate OAuth2 refresh_token and access_token...",);
info!(
context,
0, "Generate OAuth2 refresh_token and access_token...",
);
(
context
.sql
@@ -109,7 +112,7 @@ pub fn dc_get_oauth2_access_token(
} else {
info!(
context,
"Regenerate OAuth2 access_token by refresh_token...",
0, "Regenerate OAuth2 access_token by refresh_token...",
);
(
context
@@ -131,7 +134,7 @@ pub fn dc_get_oauth2_access_token(
if response.is_err() {
warn!(
context,
"Error calling OAuth2 at {}: {:?}", token_url, response
0, "Error calling OAuth2 at {}: {:?}", token_url, response
);
return None;
}
@@ -139,6 +142,7 @@ pub fn dc_get_oauth2_access_token(
if !response.status().is_success() {
warn!(
context,
0,
"Error calling OAuth2 at {}: {:?}",
token_url,
response.status()
@@ -150,7 +154,7 @@ pub fn dc_get_oauth2_access_token(
if parsed.is_err() {
warn!(
context,
"Failed to parse OAuth2 JSON response from {}: error: {:?}", token_url, parsed
0, "Failed to parse OAuth2 JSON response from {}: error: {:?}", token_url, parsed
);
return None;
}
@@ -191,12 +195,12 @@ pub fn dc_get_oauth2_access_token(
.ok();
}
} else {
warn!(context, "Failed to find OAuth2 access token");
warn!(context, 0, "Failed to find OAuth2 access token");
}
response.access_token
} else {
warn!(context, "Internal OAuth2 error: 2");
warn!(context, 0, "Internal OAuth2 error: 2");
None
}
@@ -216,13 +220,12 @@ pub fn dc_get_oauth2_addr(
return None;
}
if let Some(access_token) =
dc_get_oauth2_access_token(context, addr.as_ref(), code.as_ref(), false)
if let Some(access_token) = dc_get_oauth2_access_token(context, addr.as_ref(), code.as_ref(), 0)
{
let addr_out = oauth2.get_addr(context, access_token);
if addr_out.is_none() {
// regenerate
if let Some(access_token) = dc_get_oauth2_access_token(context, addr, code, true) {
if let Some(access_token) = dc_get_oauth2_access_token(context, addr, code, 0x1) {
oauth2.get_addr(context, access_token)
} else {
None
@@ -265,12 +268,17 @@ impl Oauth2 {
// }
let response = reqwest::Client::new().get(&userinfo_url).send();
if response.is_err() {
warn!(context, "Error getting userinfo: {:?}", response);
warn!(context, 0, "Error getting userinfo: {:?}", response);
return None;
}
let mut response = response.unwrap();
if !response.status().is_success() {
warn!(context, "Error getting userinfo: {:?}", response.status());
warn!(
context,
0,
"Error getting userinfo: {:?}",
response.status()
);
return None;
}
@@ -278,19 +286,19 @@ impl Oauth2 {
if parsed.is_err() {
warn!(
context,
"Failed to parse userinfo JSON response: {:?}", parsed
0, "Failed to parse userinfo JSON response: {:?}", parsed
);
return None;
}
if let Ok(response) = parsed {
let addr = response.get("email");
if addr.is_none() {
warn!(context, "E-mail missing in userinfo.");
warn!(context, 0, "E-mail missing in userinfo.");
}
addr.map(|addr| addr.to_string())
} else {
warn!(context, "Failed to parse userinfo.");
warn!(context, 0, "Failed to parse userinfo.");
None
}
}

View File

@@ -4,7 +4,6 @@ use std::str;
use num_traits::FromPrimitive;
use crate::dc_mimeparser::SystemMessage;
use crate::error;
/// Available param keys.
@@ -95,7 +94,7 @@ impl fmt::Display for Params {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, (key, value)) in self.inner.iter().enumerate() {
if i > 0 {
writeln!(f)?;
write!(f, "\n")?;
}
write!(f, "{}={}", *key as u8 as char, value)?;
}
@@ -179,13 +178,6 @@ impl Params {
self.get(key).and_then(|s| s.parse().ok())
}
/// Get the parameter behind `Param::Cmd` interpreted as `SystemMessage`.
pub fn get_cmd(&self) -> SystemMessage {
self.get_int(Param::Cmd)
.and_then(SystemMessage::from_i32)
.unwrap_or_default()
}
/// Get the given parameter and parse as `f64`.
pub fn get_float(&self, key: Param) -> Option<f64> {
self.get(key).and_then(|s| s.parse().ok())

View File

@@ -178,11 +178,8 @@ impl<'a> Peerstate<'a> {
OR gossip_key_fingerprint=? COLLATE NOCASE \
ORDER BY public_key_fingerprint=? DESC;";
Self::from_stmt(
context,
query,
params![fingerprint, fingerprint, fingerprint],
)
let fp = fingerprint.as_bytes();
Self::from_stmt(context, query, params![fp, fp, fp])
}
fn from_stmt<P>(context: &'a Context, query: &str, params: P) -> Option<Self>
@@ -499,11 +496,7 @@ mod tests {
let ctx = crate::test_utils::dummy_context();
let addr = "hello@mail.com";
let pub_key = crate::key::Key::from_base64(
include_str!("../test-data/key/public.asc"),
KeyType::Public,
)
.unwrap();
let pub_key = crate::key::Key::from_base64("xsBNBFztUVkBCADYaQl/UOUpRPd32nLRzx8eU0eI+jQEnG+g5anjYA+3oct1rROGl5SygjMULDKdaUy27O3o9Srsti0YjA7uxZnavIqhSopJhFidqY1M1wA9JZa/duucZdNwUGbjGIRsS/4Cjr5+3svscK24hVYub1dvDWXpwUTnj3K6xOEnJdoM+MhCqtSD5+zcJhFc9vyZm9ZTGWUxAhKh0iJTcCD8V6CQ3XZ2z9GruwzZT/FTFovWrz7m3TUI2OdSSHh0eZLRGEoxMCT/vzflAFGAr8ijCaRsEIfqP6FW8uQWnFTqkjxEUCZG6XkeFHB84aj5jqYG/1KCLjL5vEKwfl1tz/WnPhY7ABEBAAHNEDxoZWxsb0BtYWlsLmNvbT7CwIkEEAEIADMCGQEFAlztUVoCGwMECwkIBwYVCAkKCwIDFgIBFiEEgMjHGVbvLXe6ioRROg8oKCvye7gACgkQOg8oKCvye7ijAwf+PTsuawUax9cNPn1bN90H+g9qyHZJMEwKXtUnNaXJxPW3iB7ThhpCiCzsZwP7+l7ArS8tmLeNDw2bENtcf1XCv4wovP2fdXOP3QOUUFX/GdakcTwv7DzC7CO0grB1HtaPhGw/6UX2o2cx2i9xiUf4Givq2MfCbgAW5zloH6WXGPb6yLQYJXxqDIphr4+uZDb+bMAyWHN/DUkAjHrV8nnVki7PMHqzzZpwglalxMX8RGeiGZE39ALJKL/Og87DMFah87/yoxQWGoS7Wqv0XDcCPKoTCPrpk8pOe2KEsq/lz215nefHd4aRpfUX5YCYa8HPvvfPQbGF73uvyQw5w7qjis7ATQRc7VFZAQgAt8ONdnX6KEEQ5Jw6ilJ+LBtY44SP5t0I3eK+goKepgIiKhjGDa+Mntyi4jdhH+HO6kvK5SHMh2sPp4rRO/WKHJwWFySyM1OdyiywhyH0J9R5rBY4vPHsJjf6vSKJdWLWT+ho1fNet2IIC+jVCYli91MAMbRvk6EKVj1nCc+67giOahXEkHt6xxkeCGlOvbw8hxGj1A8+AC1BLms/OR3oc4JMi9O3kq6uG0z9tlUEerac9HVwcjoO1XLe+hJhoT5H+TbnGjPuhuURP3pFiIKHpbRYgUfdSAY0dTObO7t4I5y/drPOrCTnWrBUg2wXAECUhpRKow9/ai2YemLv9KqhhwARAQABwsB2BBgBCAAgBQJc7VFaAhsMFiEEgMjHGVbvLXe6ioRROg8oKCvye7gACgkQOg8oKCvye7jmyggAhs4QzCzIbT2OsAReBxkxtm0AI+g1HZ1KFKof5NDHfgv9C/Qu1I8mKEjlZzA4qFyPmLqntgwJ0RuFy6gLbljZBNCFO7vB478AhYtnWjuKZmA40HUPwcB1hEJ31c42akzfUbioY1TLLepngdsJg7Cm8O+rhI9+1WRA66haJDgFs793SVUDyJh8f9NX50l5zR87/bsV30CFSw0q4OSSy9VI/z+2g5khn1LnuuOrCfFnYIPYtJED1BfkXkosxGlgbzy79VvGmI9d23x4atDK7oBPCzIj+lP8sytJ0u3HOguXi9OgDitKy+Pt1r8gH8frdktMJr5Ts6DW+tIn2vR23KR8aA==", KeyType::Public).unwrap();
let mut peerstate = Peerstate {
context: &ctx.ctx,
@@ -530,10 +523,6 @@ 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, &ctx.ctx.sql, &pub_key.fingerprint())
.expect("failed to load peerstate from db");
assert_eq!(peerstate, peerstate_new2);
}
#[test]
@@ -541,11 +530,7 @@ mod tests {
let ctx = crate::test_utils::dummy_context();
let addr = "hello@mail.com";
let pub_key = crate::key::Key::from_base64(
include_str!("../test-data/key/public.asc"),
KeyType::Public,
)
.unwrap();
let pub_key = crate::key::Key::from_base64("xsBNBFztUVkBCADYaQl/UOUpRPd32nLRzx8eU0eI+jQEnG+g5anjYA+3oct1rROGl5SygjMULDKdaUy27O3o9Srsti0YjA7uxZnavIqhSopJhFidqY1M1wA9JZa/duucZdNwUGbjGIRsS/4Cjr5+3svscK24hVYub1dvDWXpwUTnj3K6xOEnJdoM+MhCqtSD5+zcJhFc9vyZm9ZTGWUxAhKh0iJTcCD8V6CQ3XZ2z9GruwzZT/FTFovWrz7m3TUI2OdSSHh0eZLRGEoxMCT/vzflAFGAr8ijCaRsEIfqP6FW8uQWnFTqkjxEUCZG6XkeFHB84aj5jqYG/1KCLjL5vEKwfl1tz/WnPhY7ABEBAAHNEDxoZWxsb0BtYWlsLmNvbT7CwIkEEAEIADMCGQEFAlztUVoCGwMECwkIBwYVCAkKCwIDFgIBFiEEgMjHGVbvLXe6ioRROg8oKCvye7gACgkQOg8oKCvye7ijAwf+PTsuawUax9cNPn1bN90H+g9qyHZJMEwKXtUnNaXJxPW3iB7ThhpCiCzsZwP7+l7ArS8tmLeNDw2bENtcf1XCv4wovP2fdXOP3QOUUFX/GdakcTwv7DzC7CO0grB1HtaPhGw/6UX2o2cx2i9xiUf4Givq2MfCbgAW5zloH6WXGPb6yLQYJXxqDIphr4+uZDb+bMAyWHN/DUkAjHrV8nnVki7PMHqzzZpwglalxMX8RGeiGZE39ALJKL/Og87DMFah87/yoxQWGoS7Wqv0XDcCPKoTCPrpk8pOe2KEsq/lz215nefHd4aRpfUX5YCYa8HPvvfPQbGF73uvyQw5w7qjis7ATQRc7VFZAQgAt8ONdnX6KEEQ5Jw6ilJ+LBtY44SP5t0I3eK+goKepgIiKhjGDa+Mntyi4jdhH+HO6kvK5SHMh2sPp4rRO/WKHJwWFySyM1OdyiywhyH0J9R5rBY4vPHsJjf6vSKJdWLWT+ho1fNet2IIC+jVCYli91MAMbRvk6EKVj1nCc+67giOahXEkHt6xxkeCGlOvbw8hxGj1A8+AC1BLms/OR3oc4JMi9O3kq6uG0z9tlUEerac9HVwcjoO1XLe+hJhoT5H+TbnGjPuhuURP3pFiIKHpbRYgUfdSAY0dTObO7t4I5y/drPOrCTnWrBUg2wXAECUhpRKow9/ai2YemLv9KqhhwARAQABwsB2BBgBCAAgBQJc7VFaAhsMFiEEgMjHGVbvLXe6ioRROg8oKCvye7gACgkQOg8oKCvye7jmyggAhs4QzCzIbT2OsAReBxkxtm0AI+g1HZ1KFKof5NDHfgv9C/Qu1I8mKEjlZzA4qFyPmLqntgwJ0RuFy6gLbljZBNCFO7vB478AhYtnWjuKZmA40HUPwcB1hEJ31c42akzfUbioY1TLLepngdsJg7Cm8O+rhI9+1WRA66haJDgFs793SVUDyJh8f9NX50l5zR87/bsV30CFSw0q4OSSy9VI/z+2g5khn1LnuuOrCfFnYIPYtJED1BfkXkosxGlgbzy79VvGmI9d23x4atDK7oBPCzIj+lP8sytJ0u3HOguXi9OgDitKy+Pt1r8gH8frdktMJr5Ts6DW+tIn2vR23KR8aA==", KeyType::Public).unwrap();
let mut peerstate = Peerstate {
context: &ctx.ctx,

View File

@@ -1,5 +1,6 @@
use std::collections::HashSet;
use std::convert::TryInto;
use std::ffi::CStr;
use std::io::Cursor;
use std::ptr;
@@ -12,9 +13,9 @@ use pgp::types::{CompressionAlgorithm, KeyTrait, SecretKeyTrait, StringToKey};
use rand::thread_rng;
use crate::dc_tools::*;
use crate::error::Error;
use crate::key::*;
use crate::keyring::*;
use crate::types::*;
use crate::x::*;
pub unsafe fn dc_split_armored_data(
@@ -25,14 +26,14 @@ pub unsafe fn dc_split_armored_data(
ret_base64: *mut *const libc::c_char,
) -> bool {
let mut success = false;
let mut line_chars: libc::size_t = 0;
let mut line_chars: size_t = 0i32 as size_t;
let mut line: *mut libc::c_char = buf;
let mut p1: *mut libc::c_char = buf;
let mut p2: *mut libc::c_char;
let mut headerline: *mut libc::c_char = ptr::null_mut();
let mut base64: *mut libc::c_char = ptr::null_mut();
if !ret_headerline.is_null() {
*ret_headerline = ptr::null()
*ret_headerline = 0 as *const libc::c_char
}
if !ret_setupcodebegin.is_null() {
*ret_setupcodebegin = ptr::null_mut();
@@ -105,7 +106,7 @@ pub unsafe fn dc_split_armored_data(
}
p1 = p1.offset(1isize);
line = p1;
line_chars = 0;
line_chars = 0i32 as size_t
} else {
p1 = p1.offset(1isize);
line_chars = line_chars.wrapping_add(1)
@@ -187,11 +188,15 @@ pub fn dc_pgp_create_keypair(addr: impl AsRef<str>) -> Option<(Key, Key)> {
}
pub fn dc_pgp_pk_encrypt(
plain: &[u8],
plain_text: *const libc::c_void,
plain_bytes: size_t,
public_keys_for_encryption: &Keyring,
private_key_for_signing: Option<&Key>,
) -> Result<String, Error> {
let lit_msg = Message::new_literal_bytes("", plain);
) -> Option<String> {
assert!(!plain_text.is_null() && !plain_bytes > 0, "invalid input");
let bytes = unsafe { std::slice::from_raw_parts(plain_text as *const u8, plain_bytes) };
let lit_msg = Message::new_literal_bytes("", bytes);
let pkeys: Vec<&SignedPublicKey> = public_keys_for_encryption
.keys()
.into_iter()
@@ -204,10 +209,9 @@ pub fn dc_pgp_pk_encrypt(
let mut rng = thread_rng();
// TODO: measure time
// TODO: better error handling
let encrypted_msg = if let Some(private_key) = private_key_for_signing {
let skey: &SignedSecretKey = private_key
.try_into()
.map_err(|_| format_err!("Invalid private key"))?;
let skey: &SignedSecretKey = private_key.try_into().unwrap();
lit_msg
.sign(skey, || "".into(), Default::default())
@@ -217,84 +221,110 @@ pub fn dc_pgp_pk_encrypt(
lit_msg.encrypt_to_keys(&mut rng, Default::default(), &pkeys)
};
let msg = encrypted_msg?;
let encoded_msg = msg.to_armored_string(None)?;
Ok(encoded_msg)
encrypted_msg
.and_then(|msg| msg.to_armored_string(None))
.ok()
}
pub fn dc_pgp_pk_decrypt(
ctext: &[u8],
ctext: *const libc::c_void,
ctext_bytes: size_t,
private_keys_for_decryption: &Keyring,
public_keys_for_validation: &Keyring,
ret_signature_fingerprints: Option<&mut HashSet<String>>,
) -> Result<Vec<u8>, Error> {
let (msg, _) = Message::from_armor_single(Cursor::new(ctext))?;
let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption
.keys()
.iter()
.filter_map(|key| {
let k: &Key = &key;
k.try_into().ok()
})
.collect();
) -> Option<Vec<u8>> {
assert!(!ctext.is_null() && ctext_bytes > 0, "invalid input");
let (decryptor, _) = msg.decrypt(|| "".into(), || "".into(), &skeys[..])?;
let msgs = decryptor.collect::<Result<Vec<_>, _>>()?;
ensure!(!msgs.is_empty(), "No valid messages found");
let ctext = unsafe { std::slice::from_raw_parts(ctext as *const u8, ctext_bytes) };
let dec_msg = &msgs[0];
// TODO: proper error handling
if let Ok((msg, _)) = Message::from_armor_single(Cursor::new(ctext)) {
let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption
.keys()
.iter()
.filter_map(|key| {
let k: &Key = &key;
k.try_into().ok()
})
.collect();
if let Some(ret_signature_fingerprints) = ret_signature_fingerprints {
if !public_keys_for_validation.keys().is_empty() {
let pkeys: Vec<&SignedPublicKey> = public_keys_for_validation
.keys()
.iter()
.filter_map(|key| {
let k: &Key = &key;
k.try_into().ok()
})
.collect();
msg.decrypt(|| "".into(), || "".into(), &skeys[..])
.and_then(|(mut decryptor, _)| {
// TODO: how to handle the case when we detect multiple messages?
decryptor.next().expect("no message")
})
.and_then(|dec_msg| {
if let Some(ret_signature_fingerprints) = ret_signature_fingerprints {
if !public_keys_for_validation.keys().is_empty() {
let pkeys: Vec<&SignedPublicKey> = public_keys_for_validation
.keys()
.iter()
.filter_map(|key| {
let k: &Key = &key;
k.try_into().ok()
})
.collect();
for pkey in &pkeys {
if dec_msg.verify(&pkey.primary_key).is_ok() {
let fp = hex::encode_upper(pkey.fingerprint());
ret_signature_fingerprints.insert(fp);
for pkey in &pkeys {
if dec_msg.verify(&pkey.primary_key).is_ok() {
let fp = hex::encode_upper(pkey.fingerprint());
ret_signature_fingerprints.insert(fp);
}
}
}
}
}
}
}
match dec_msg.get_content()? {
Some(content) => Ok(content),
None => bail!("Decrypted message is empty"),
dec_msg.get_content()
})
.ok()
.and_then(|content| content)
} else {
None
}
}
/// Symmetric encryption.
pub fn dc_pgp_symm_encrypt(passphrase: &str, plain: &[u8]) -> Result<String, Error> {
pub fn dc_pgp_symm_encrypt(
passphrase: *const libc::c_char,
plain: *const libc::c_void,
plain_bytes: size_t,
) -> Option<String> {
assert!(!passphrase.is_null(), "invalid passphrase");
assert!(!plain.is_null() && !plain_bytes > 0, "invalid input");
let pw = unsafe { CStr::from_ptr(passphrase).to_str().unwrap() };
let bytes = unsafe { std::slice::from_raw_parts(plain as *const u8, plain_bytes) };
let mut rng = thread_rng();
let lit_msg = Message::new_literal_bytes("", plain);
let lit_msg = Message::new_literal_bytes("", bytes);
let s2k = StringToKey::new_default(&mut rng);
let msg =
lit_msg.encrypt_with_password(&mut rng, s2k, Default::default(), || passphrase.into())?;
let msg = lit_msg.encrypt_with_password(&mut rng, s2k, Default::default(), || pw.into());
let encoded_msg = msg.to_armored_string(None)?;
Ok(encoded_msg)
msg.and_then(|msg| msg.to_armored_string(None)).ok()
}
/// Symmetric decryption.
pub fn dc_pgp_symm_decrypt(passphrase: &str, ctext: &[u8]) -> Result<Vec<u8>, Error> {
let enc_msg = Message::from_bytes(Cursor::new(ctext))?;
let decryptor = enc_msg.decrypt_with_password(|| passphrase.into())?;
pub fn dc_pgp_symm_decrypt(
passphrase: *const libc::c_char,
ctext: *const libc::c_void,
ctext_bytes: size_t,
) -> Option<Vec<u8>> {
assert!(!passphrase.is_null(), "invalid passphrase");
assert!(!ctext.is_null() && !ctext_bytes > 0, "invalid input");
let msgs = decryptor.collect::<Result<Vec<_>, _>>()?;
ensure!(!msgs.is_empty(), "No valid messages found");
let pw = unsafe { CStr::from_ptr(passphrase).to_str().unwrap() };
let bytes = unsafe { std::slice::from_raw_parts(ctext as *const u8, ctext_bytes) };
match msgs[0].get_content()? {
Some(content) => Ok(content),
None => bail!("Decrypted message is empty"),
}
let enc_msg = Message::from_bytes(Cursor::new(bytes));
enc_msg
.and_then(|msg| {
let mut decryptor = msg
.decrypt_with_password(|| pw.into())
.expect("failed decryption");
decryptor.next().expect("no message")
})
.and_then(|msg| msg.get_content())
.ok()
.and_then(|content| content)
}

View File

@@ -37,7 +37,7 @@ impl Into<Lot> for Error {
pub fn check_qr(context: &Context, qr: impl AsRef<str>) -> Lot {
let qr = qr.as_ref();
info!(context, "Scanned QR code: {}", qr);
info!(context, 0, "Scanned QR code: {}", qr);
if qr.starts_with(OPENPGP4FPR_SCHEME) {
decode_openpgp(context, qr)
@@ -70,14 +70,19 @@ fn decode_openpgp(context: &Context, qr: &str) -> Lot {
None => return format_err!("Invalid OPENPGP4FPR found").into(),
};
dbg!(fingerprint);
dbg!(fragment);
// replace & with \n to match expected param format
let fragment = fragment.replace('&', "\n");
dbg!(&fragment);
// Then parse the parameters
let param: Params = match fragment.parse() {
Ok(params) => params,
Err(err) => return err.into(),
};
dbg!(&param);
let addr = if let Some(addr) = param.get(Param::Forwarded) {
match normalize_address(addr) {

View File

@@ -1,747 +0,0 @@
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
use crate::aheader::EncryptPreference;
use crate::chat::{self, Chat};
use crate::config::*;
use crate::configure::*;
use crate::constants::*;
use crate::contact::*;
use crate::context::Context;
use crate::dc_mimeparser::*;
use crate::e2ee::*;
use crate::error::Error;
use crate::events::Event;
use crate::key::*;
use crate::lot::LotState;
use crate::message::*;
use crate::param::*;
use crate::peerstate::*;
use crate::qr::check_qr;
use crate::stock::StockMessage;
use crate::token;
pub const NON_ALPHANUMERIC_WITHOUT_DOT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'.');
macro_rules! joiner_progress {
($context:tt, $contact_id:expr, $progress:expr) => {
assert!(
$progress >= 0 && $progress <= 1000,
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
);
$context.call_cb($crate::events::Event::SecurejoinJoinerProgress {
contact_id: $contact_id,
progress: $progress,
});
};
}
macro_rules! inviter_progress {
($context:tt, $contact_id:expr, $progress:expr) => {
assert!(
$progress >= 0 && $progress <= 1000,
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
);
$context.call_cb($crate::events::Event::SecurejoinInviterProgress {
contact_id: $contact_id,
progress: $progress,
});
};
}
macro_rules! get_qr_attr {
($context:tt, $attr:ident) => {
$context
.bob
.read()
.unwrap()
.qr_scan
.as_ref()
.unwrap()
.$attr
.as_ref()
.unwrap()
};
}
pub fn dc_get_securejoin_qr(context: &Context, group_chat_id: u32) -> Option<String> {
/* =========================================================
==== Alice - the inviter side ====
==== Step 1 in "Setup verified contact" protocol ====
========================================================= */
let fingerprint: String;
ensure_secret_key_exists(context).ok();
let invitenumber = token::lookup_or_new(context, token::Namespace::InviteNumber, group_chat_id);
let auth = token::lookup_or_new(context, token::Namespace::Auth, group_chat_id);
let self_addr = match context.get_config(Config::ConfiguredAddr) {
Some(addr) => addr,
None => {
error!(context, "Not configured, cannot generate QR code.",);
return None;
}
};
let self_name = context
.sql
.get_config(context, "displayname")
.unwrap_or_default();
fingerprint = match get_self_fingerprint(context) {
Some(fp) => fp,
None => {
return None;
}
};
let self_addr_urlencoded =
utf8_percent_encode(&self_addr, NON_ALPHANUMERIC_WITHOUT_DOT).to_string();
let self_name_urlencoded =
utf8_percent_encode(&self_name, NON_ALPHANUMERIC_WITHOUT_DOT).to_string();
let qr = if 0 != group_chat_id {
if let Ok(chat) = Chat::load_from_db(context, group_chat_id) {
let group_name = chat.get_name();
let group_name_urlencoded =
utf8_percent_encode(&group_name, NON_ALPHANUMERIC).to_string();
Some(format!(
"OPENPGP4FPR:{}#a={}&g={}&x={}&i={}&s={}",
fingerprint,
self_addr_urlencoded,
&group_name_urlencoded,
&chat.grpid,
&invitenumber,
&auth,
))
} else {
error!(context, "Cannot get QR-code for chat-id {}", group_chat_id,);
return None;
}
} else {
Some(format!(
"OPENPGP4FPR:{}#a={}&n={}&i={}&s={}",
fingerprint, self_addr_urlencoded, self_name_urlencoded, &invitenumber, &auth,
))
};
info!(context, "Generated QR code: {}", qr.as_ref().unwrap());
qr
}
fn get_self_fingerprint(context: &Context) -> Option<String> {
if let Some(self_addr) = context.get_config(Config::ConfiguredAddr) {
if let Some(key) = Key::from_self_public(context, self_addr, &context.sql) {
return Some(key.fingerprint());
}
}
None
}
pub fn dc_join_securejoin(context: &Context, qr: &str) -> u32 {
let cleanup =
|context: &Context, contact_chat_id: u32, ongoing_allocated: bool, join_vg: bool| {
let mut bob = context.bob.write().unwrap();
bob.expects = 0;
let ret_chat_id = if bob.status == DC_BOB_SUCCESS {
if join_vg {
chat::get_chat_id_by_grpid(
context,
bob.qr_scan.as_ref().unwrap().text2.as_ref().unwrap(),
)
.0
} else {
contact_chat_id
}
} else {
0
};
bob.qr_scan = None;
if ongoing_allocated {
dc_free_ongoing(context);
}
ret_chat_id as u32
};
/* ==========================================================
==== Bob - the joiner's side =====
==== Step 2 in "Setup verified contact" protocol =====
========================================================== */
let mut contact_chat_id: u32 = 0;
let mut join_vg: bool = false;
info!(context, "Requesting secure-join ...",);
ensure_secret_key_exists(context).ok();
if !dc_alloc_ongoing(context) {
return cleanup(&context, contact_chat_id, false, join_vg);
}
let qr_scan = check_qr(context, &qr);
if qr_scan.state != LotState::QrAskVerifyContact && qr_scan.state != LotState::QrAskVerifyGroup
{
error!(context, "Unknown QR code.",);
return cleanup(&context, contact_chat_id, true, join_vg);
}
contact_chat_id = chat::create_by_contact_id(context, qr_scan.id).unwrap_or_default();
if contact_chat_id == 0 {
error!(context, "Unknown contact.",);
return cleanup(&context, contact_chat_id, true, join_vg);
}
if check_exit(context) {
return cleanup(&context, contact_chat_id, true, join_vg);
}
join_vg = qr_scan.get_state() == LotState::QrAskVerifyGroup;
{
let mut bob = context.bob.write().unwrap();
bob.status = 0;
bob.qr_scan = Some(qr_scan);
}
if fingerprint_equals_sender(
context,
context
.bob
.read()
.unwrap()
.qr_scan
.as_ref()
.unwrap()
.fingerprint
.as_ref()
.unwrap(),
contact_chat_id,
) {
info!(context, "Taking protocol shortcut.");
context.bob.write().unwrap().expects = DC_VC_CONTACT_CONFIRM;
joiner_progress!(context, chat_id_2_contact_id(context, contact_chat_id), 400);
let own_fingerprint = get_self_fingerprint(context).unwrap();
send_handshake_msg(
context,
contact_chat_id,
if join_vg {
"vg-request-with-auth"
} else {
"vc-request-with-auth"
},
get_qr_attr!(context, auth).to_string(),
Some(own_fingerprint),
if join_vg {
get_qr_attr!(context, text2).to_string()
} else {
"".to_string()
},
);
} else {
context.bob.write().unwrap().expects = DC_VC_AUTH_REQUIRED;
send_handshake_msg(
context,
contact_chat_id,
if join_vg { "vg-request" } else { "vc-request" },
get_qr_attr!(context, invitenumber),
None,
"",
);
}
// Bob -> Alice
while !check_exit(&context) {
std::thread::sleep(std::time::Duration::new(0, 3_000_000));
}
cleanup(&context, contact_chat_id, true, join_vg)
}
fn check_exit(context: &Context) -> bool {
context
.running_state
.clone()
.read()
.unwrap()
.shall_stop_ongoing
}
fn send_handshake_msg(
context: &Context,
contact_chat_id: u32,
step: &str,
param2: impl AsRef<str>,
fingerprint: Option<String>,
grpid: impl AsRef<str>,
) {
let mut msg = dc_msg_new_untyped();
msg.type_0 = Viewtype::Text;
msg.text = Some(format!("Secure-Join: {}", step));
msg.hidden = true;
msg.param.set_int(Param::Cmd, 7);
if step.is_empty() {
msg.param.remove(Param::Arg);
} else {
msg.param.set(Param::Arg, step);
}
if !param2.as_ref().is_empty() {
msg.param.set(Param::Arg2, param2);
}
if let Some(fp) = fingerprint {
msg.param.set(Param::Arg3, fp);
}
if !grpid.as_ref().is_empty() {
msg.param.set(Param::Arg4, grpid.as_ref());
}
if step == "vg-request" || step == "vc-request" {
msg.param.set_int(
Param::ForcePlaintext,
ForcePlaintext::AddAutocryptHeader as i32,
);
} else {
msg.param.set_int(Param::GuranteeE2ee, 1);
}
// TODO. handle cleanup on error
chat::send_msg(context, contact_chat_id, &mut msg).unwrap();
}
fn chat_id_2_contact_id(context: &Context, contact_chat_id: u32) -> u32 {
let contacts = chat::get_chat_contacts(context, contact_chat_id);
if contacts.len() == 1 {
contacts[0]
} else {
0
}
}
fn fingerprint_equals_sender(
context: &Context,
fingerprint: impl AsRef<str>,
contact_chat_id: u32,
) -> bool {
let contacts = chat::get_chat_contacts(context, contact_chat_id);
if contacts.len() == 1 {
if let Ok(contact) = Contact::load_from_db(context, contacts[0]) {
if let Some(peerstate) = Peerstate::from_addr(context, &context.sql, contact.get_addr())
{
let fingerprint_normalized = dc_normalize_fingerprint(fingerprint.as_ref());
if peerstate.public_key_fingerprint.is_some()
&& &fingerprint_normalized == peerstate.public_key_fingerprint.as_ref().unwrap()
{
return true;
}
}
}
}
false
}
/* library private: secure-join */
pub fn handle_securejoin_handshake(
context: &Context,
mimeparser: &MimeParser,
contact_id: u32,
) -> libc::c_int {
let own_fingerprint: String;
if contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
return 0;
}
let step = match mimeparser.lookup_optional_field("Secure-Join") {
Some(s) => s,
None => {
return 0;
}
};
info!(
context,
">>>>>>>>>>>>>>>>>>>>>>>>> secure-join message \'{}\' received", step,
);
let (contact_chat_id, contact_chat_id_blocked) =
chat::create_or_lookup_by_contact_id(context, contact_id, Blocked::Not).unwrap_or_default();
if contact_chat_id_blocked != Blocked::Not {
chat::unblock(context, contact_chat_id);
}
let mut ret: libc::c_int = DC_HANDSHAKE_STOP_NORMAL_PROCESSING;
let join_vg = step.starts_with("vg-");
match step.as_str() {
"vg-request" | "vc-request" => {
/* =========================================================
==== Alice - the inviter side ====
==== Step 3 in "Setup verified contact" protocol ====
========================================================= */
// this message may be unencrypted (Bob, the joinder and the sender, might not have Alice's key yet)
// it just ensures, we have Bobs key now. If we do _not_ have the key because eg. MitM has removed it,
// send_message() will fail with the error "End-to-end-encryption unavailable unexpectedly.", so, there is no additional check needed here.
// verify that the `Secure-Join-Invitenumber:`-header matches invitenumber written to the QR code
let invitenumber = match mimeparser.lookup_optional_field("Secure-Join-Invitenumber") {
Some(n) => n,
None => {
warn!(context, "Secure-join denied (invitenumber missing).",);
return ret;
}
};
if !token::exists(context, token::Namespace::InviteNumber, &invitenumber) {
warn!(context, "Secure-join denied (bad invitenumber).",);
return ret;
}
info!(context, "Secure-join requested.",);
inviter_progress!(context, contact_id, 300);
send_handshake_msg(
context,
contact_chat_id,
&format!("{}-auth-required", &step[..2]),
"",
None,
"",
);
}
"vg-auth-required" | "vc-auth-required" => {
let cond = {
let bob = context.bob.read().unwrap();
let scan = bob.qr_scan.as_ref();
scan.is_none()
|| bob.expects != DC_VC_AUTH_REQUIRED
|| join_vg && scan.unwrap().state != LotState::QrAskVerifyGroup
};
if cond {
warn!(context, "auth-required message out of sync.",);
// no error, just aborted somehow or a mail from another handshake
return ret;
}
let scanned_fingerprint_of_alice = get_qr_attr!(context, fingerprint).to_string();
let auth = get_qr_attr!(context, auth).to_string();
if !encrypted_and_signed(mimeparser, &scanned_fingerprint_of_alice) {
could_not_establish_secure_connection(
context,
contact_chat_id,
if mimeparser.e2ee_helper.encrypted {
"No valid signature."
} else {
"Not encrypted."
},
);
end_bobs_joining(context, DC_BOB_ERROR);
return ret;
}
if !fingerprint_equals_sender(context, &scanned_fingerprint_of_alice, contact_chat_id) {
could_not_establish_secure_connection(
context,
contact_chat_id,
"Fingerprint mismatch on joiner-side.",
);
end_bobs_joining(context, DC_BOB_ERROR);
return ret;
}
info!(context, "Fingerprint verified.",);
own_fingerprint = get_self_fingerprint(context).unwrap();
joiner_progress!(context, contact_id, 400);
context.bob.write().unwrap().expects = DC_VC_CONTACT_CONFIRM;
send_handshake_msg(
context,
contact_chat_id,
&format!("{}-request-with-auth", &step[..2]),
auth,
Some(own_fingerprint),
if join_vg {
get_qr_attr!(context, text2).to_string()
} else {
"".to_string()
},
);
}
"vg-request-with-auth" | "vc-request-with-auth" => {
/* ============================================================
==== Alice - the inviter side ====
==== Steps 5+6 in "Setup verified contact" protocol ====
==== Step 6 in "Out-of-band verified groups" protocol ====
============================================================ */
// verify that Secure-Join-Fingerprint:-header matches the fingerprint of Bob
let fingerprint = match mimeparser.lookup_optional_field("Secure-Join-Fingerprint") {
Some(fp) => fp,
None => {
could_not_establish_secure_connection(
context,
contact_chat_id,
"Fingerprint not provided.",
);
return ret;
}
};
if !encrypted_and_signed(mimeparser, &fingerprint) {
could_not_establish_secure_connection(
context,
contact_chat_id,
"Auth not encrypted.",
);
return ret;
}
if !fingerprint_equals_sender(context, &fingerprint, contact_chat_id) {
could_not_establish_secure_connection(
context,
contact_chat_id,
"Fingerprint mismatch on inviter-side.",
);
return ret;
}
info!(context, "Fingerprint verified.",);
// verify that the `Secure-Join-Auth:`-header matches the secret written to the QR code
let auth_0 = match mimeparser.lookup_optional_field("Secure-Join-Auth") {
Some(auth) => auth,
None => {
could_not_establish_secure_connection(
context,
contact_chat_id,
"Auth not provided.",
);
return ret;
}
};
if !token::exists(context, token::Namespace::Auth, &auth_0) {
could_not_establish_secure_connection(context, contact_chat_id, "Auth invalid.");
return ret;
}
if mark_peer_as_verified(context, fingerprint).is_err() {
could_not_establish_secure_connection(
context,
contact_chat_id,
"Fingerprint mismatch on inviter-side.",
);
return ret;
}
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited);
info!(context, "Auth verified.",);
secure_connection_established(context, contact_chat_id);
emit_event!(context, Event::ContactsChanged(Some(contact_id)));
inviter_progress!(context, contact_id, 600);
if join_vg {
let field_grpid = mimeparser
.lookup_optional_field("Secure-Join-Group")
.unwrap_or_default();
let (group_chat_id, _, _) = chat::get_chat_id_by_grpid(context, &field_grpid);
if group_chat_id == 0 {
error!(context, "Chat {} not found.", &field_grpid);
return ret;
} else {
chat::add_contact_to_chat_ex(context, group_chat_id, contact_id, 0x1i32);
}
} else {
send_handshake_msg(context, contact_chat_id, "vc-contact-confirm", "", None, "");
inviter_progress!(context, contact_id, 1000);
}
}
"vg-member-added" | "vc-contact-confirm" => {
if join_vg {
ret = DC_HANDSHAKE_CONTINUE_NORMAL_PROCESSING;
}
if context.bob.read().unwrap().expects != DC_VC_CONTACT_CONFIRM {
info!(context, "Message belongs to a different handshake.",);
return ret;
}
let cond = {
let bob = context.bob.read().unwrap();
let scan = bob.qr_scan.as_ref();
scan.is_none() || join_vg && scan.unwrap().state != LotState::QrAskVerifyGroup
};
if cond {
warn!(
context,
"Message out of sync or belongs to a different handshake.",
);
return ret;
}
let scanned_fingerprint_of_alice = get_qr_attr!(context, fingerprint).to_string();
let vg_expect_encrypted = if join_vg {
let group_id = get_qr_attr!(context, text2).to_string();
let (_, is_verified_group, _) = chat::get_chat_id_by_grpid(context, group_id);
// when joining a non-verified group
// the vg-member-added message may be unencrypted
// when not all group members have keys or prefer encryption.
// So only expect encryption if this is a verified group
is_verified_group
} else {
// setup contact is always encrypted
true
};
if vg_expect_encrypted
&& !encrypted_and_signed(mimeparser, &scanned_fingerprint_of_alice)
{
could_not_establish_secure_connection(
context,
contact_chat_id,
"Contact confirm message not encrypted.",
);
end_bobs_joining(context, DC_BOB_ERROR);
return ret;
}
if mark_peer_as_verified(context, &scanned_fingerprint_of_alice).is_err() {
could_not_establish_secure_connection(
context,
contact_chat_id,
"Fingerprint mismatch on joiner-side.",
);
return ret;
}
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinJoined);
emit_event!(context, Event::ContactsChanged(None));
let cg_member_added = mimeparser
.lookup_optional_field("Chat-Group-Member-Added")
.unwrap_or_default();
if join_vg && !addr_equals_self(context, cg_member_added) {
info!(context, "Message belongs to a different handshake (scaled up contact anyway to allow creation of group).");
return ret;
}
secure_connection_established(context, contact_chat_id);
context.bob.write().unwrap().expects = 0;
if join_vg {
send_handshake_msg(
context,
contact_chat_id,
"vg-member-added-received",
"",
None,
"",
);
}
end_bobs_joining(context, DC_BOB_SUCCESS);
}
"vg-member-added-received" => {
/* ============================================================
==== Alice - the inviter side ====
==== Step 8 in "Out-of-band verified groups" protocol ====
============================================================ */
if let Ok(contact) = Contact::get_by_id(context, contact_id) {
if contact.is_verified(context) == VerifiedStatus::Unverified {
warn!(context, "vg-member-added-received invalid.",);
return ret;
}
inviter_progress!(context, contact_id, 800);
inviter_progress!(context, contact_id, 1000);
} else {
warn!(context, "vg-member-added-received invalid.",);
return ret;
}
}
_ => {
warn!(context, "invalid step: {}", step);
}
}
if ret == DC_HANDSHAKE_STOP_NORMAL_PROCESSING {
ret |= DC_HANDSHAKE_ADD_DELETE_JOB;
}
ret
}
fn end_bobs_joining(context: &Context, status: libc::c_int) {
context.bob.write().unwrap().status = status;
dc_stop_ongoing_process(context);
}
fn secure_connection_established(context: &Context, contact_chat_id: u32) {
let contact_id: u32 = chat_id_2_contact_id(context, contact_chat_id);
let contact = Contact::get_by_id(context, contact_id);
let addr = if let Ok(ref contact) = contact {
contact.get_addr()
} else {
"?"
};
let msg = context.stock_string_repl_str(StockMessage::ContactVerified, addr);
chat::add_device_msg(context, contact_chat_id, msg);
emit_event!(context, Event::ChatModified(contact_chat_id));
}
fn could_not_establish_secure_connection(context: &Context, contact_chat_id: u32, details: &str) {
let contact_id = chat_id_2_contact_id(context, contact_chat_id);
let contact = Contact::get_by_id(context, contact_id);
let msg = context.stock_string_repl_str(
StockMessage::ContactNotVerified,
if let Ok(ref contact) = contact {
contact.get_addr()
} else {
"?"
},
);
chat::add_device_msg(context, contact_chat_id, &msg);
error!(context, "{} ({})", &msg, details);
}
fn mark_peer_as_verified(context: &Context, fingerprint: impl AsRef<str>) -> Result<(), Error> {
if let Some(ref mut peerstate) =
Peerstate::from_fingerprint(context, &context.sql, fingerprint.as_ref())
{
if peerstate.set_verified(1, fingerprint.as_ref(), 2) {
peerstate.prefer_encrypt = EncryptPreference::Mutual;
peerstate.to_save = Some(ToSave::All);
peerstate.save_to_db(&context.sql, false);
return Ok(());
}
}
bail!(
"could not mark peer as verified for fingerprint {}",
fingerprint.as_ref()
);
}
/* ******************************************************************************
* Tools: Misc.
******************************************************************************/
fn encrypted_and_signed(mimeparser: &MimeParser, expected_fingerprint: impl AsRef<str>) -> bool {
if !mimeparser.e2ee_helper.encrypted {
warn!(mimeparser.context, "Message not encrypted.",);
false
} else if mimeparser.e2ee_helper.signatures.len() <= 0 {
warn!(mimeparser.context, "Message not signed.",);
false
} else if expected_fingerprint.as_ref().is_empty() {
warn!(mimeparser.context, "Fingerprint for comparison missing.",);
false
} else if !mimeparser
.e2ee_helper
.signatures
.contains(expected_fingerprint.as_ref())
{
warn!(
mimeparser.context,
"Message does not match expected fingerprint {}.",
expected_fingerprint.as_ref(),
);
false
} else {
true
}
}
pub fn handle_degrade_event(context: &Context, peerstate: &Peerstate) {
// - we do not issue an warning for DC_DE_ENCRYPTION_PAUSED as this is quite normal
// - currently, we do not issue an extra warning for DC_DE_VERIFICATION_LOST - this always comes
// together with DC_DE_FINGERPRINT_CHANGED which is logged, the idea is not to bother
// with things they cannot fix, so the user is just kicked from the verified group
// (and he will know this and can fix this)
if Some(DegradeEvent::FingerprintChanged) == peerstate.degrade_event {
let contact_id: i32 = context
.sql
.query_get_value(
context,
"SELECT id FROM contacts WHERE addr=?;",
params![&peerstate.addr],
)
.unwrap_or_default();
if contact_id > 0 {
let (contact_chat_id, _) =
chat::create_or_lookup_by_contact_id(context, contact_id as u32, Blocked::Deaddrop)
.unwrap_or_default();
let peeraddr: &str = match peerstate.addr {
Some(ref addr) => &addr,
None => "",
};
let msg = context.stock_string_repl_str(StockMessage::ContactSetupChanged, peeraddr);
chat::add_device_msg(context, contact_chat_id, msg);
emit_event!(context, Event::ChatModified(contact_chat_id));
}
}
}

View File

@@ -1,20 +1,18 @@
use lettre::smtp::client::net::*;
use lettre::*;
use crate::constants::Event;
use crate::constants::*;
use crate::context::Context;
use crate::events::Event;
use crate::login_param::LoginParam;
use crate::dc_loginparam::*;
use crate::oauth2::*;
#[derive(DebugStub)]
pub struct Smtp {
#[debug_stub(some = "SmtpTransport")]
transport: Option<lettre::smtp::SmtpTransport>,
transport_connected: bool,
/// Email address we are sending from.
from: Option<EmailAddress>,
pub error: Option<String>,
pub error: *mut libc::c_char,
}
impl Smtp {
@@ -24,7 +22,7 @@ impl Smtp {
transport: None,
transport_connected: false,
from: None,
error: None,
error: std::ptr::null_mut(),
}
}
@@ -45,14 +43,14 @@ impl Smtp {
}
/// Connect using the provided login params
pub fn connect(&mut self, context: &Context, lp: &LoginParam) -> bool {
pub fn connect(&mut self, context: &Context, lp: &dc_loginparam_t) -> bool {
if self.is_connected() {
warn!(context, "SMTP already connected.");
warn!(context, 0, "SMTP already connected.");
return true;
}
if lp.send_server.is_empty() || lp.send_port == 0 {
context.call_cb(Event::ErrorNetwork("SMTP bad parameters.".into()));
log_event!(context, Event::ERROR_NETWORK, 0, "SMTP bad parameters.",);
}
self.from = if let Ok(addr) = EmailAddress::new(lp.addr.clone()) {
@@ -82,7 +80,7 @@ impl Smtp {
// oauth2
let addr = &lp.addr;
let send_pw = &lp.send_pw;
let access_token = dc_get_oauth2_access_token(context, addr, send_pw, false);
let access_token = dc_get_oauth2_access_token(context, addr, send_pw, 0);
if access_token.is_none() {
return false;
}
@@ -111,14 +109,17 @@ impl Smtp {
.credentials(creds)
.connection_reuse(lettre::smtp::ConnectionReuseParameters::ReuseUnlimited);
self.transport = Some(client.transport());
context.call_cb(Event::SmtpConnected(format!(
log_event!(
context,
Event::SMTP_CONNECTED,
0,
"SMTP-LOGIN as {} ok",
lp.send_user,
)));
);
true
}
Err(err) => {
warn!(context, "SMTP: failed to establish connection {:?}", err);
warn!(context, 0, "SMTP: failed to establish connection {:?}", err);
false
}
}
@@ -140,15 +141,17 @@ impl Smtp {
match transport.send(mail) {
Ok(_) => {
context.call_cb(Event::SmtpMessageSent(
"Message was sent to SMTP server".into(),
));
log_event!(
context,
Event::SMTP_MESSAGE_SENT,
0,
"Message was sent to SMTP server",
);
self.transport_connected = true;
1
}
Err(err) => {
warn!(context, "SMTP failed to send message: {}", err);
self.error = Some(format!("{}", err));
warn!(context, 0, "SMTP failed to send message: {}", err);
0
}
}

View File

@@ -13,10 +13,8 @@ use crate::peerstate::*;
const DC_OPEN_READONLY: usize = 0x01;
/// A wrapper around the underlying Sqlite3 object.
#[derive(DebugStub)]
pub struct Sql {
pool: RwLock<Option<r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>>>,
#[debug_stub = "ThreadLocal<String>"]
in_use: Arc<ThreadLocal<String>>,
}
@@ -37,7 +35,7 @@ impl Sql {
self.in_use.remove();
// drop closes the connection
info!(context, "Database closed.");
info!(context, 0, "Database closed.");
}
// return true on success, false on failure
@@ -157,13 +155,19 @@ impl Sql {
.unwrap_or_default()
}
pub fn query_get_value<P, T>(&self, context: &Context, query: &str, params: P) -> Option<T>
pub fn query_row_col<P, T>(
&self,
context: &Context,
query: &str,
params: P,
column: usize,
) -> Option<T>
where
P: IntoIterator,
P::Item: rusqlite::ToSql,
T: rusqlite::types::FromSql,
{
match self.query_row(query, params, |row| row.get::<_, T>(0)) {
match self.query_row(query, params, |row| row.get::<_, T>(column)) {
Ok(res) => Some(res),
Err(Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => None,
Err(Error::Sql(rusqlite::Error::InvalidColumnType(
@@ -172,7 +176,7 @@ impl Sql {
rusqlite::types::Type::Null,
))) => None,
Err(err) => {
error!(context, "sql: Failed query_row: {}", err);
error!(context, 0, "sql: Failed query_row: {}", err);
None
}
}
@@ -189,7 +193,7 @@ impl Sql {
value: Option<&str>,
) -> Result<()> {
if !self.is_open() {
error!(context, "set_config(): Database not ready.");
error!(context, 0, "set_config(): Database not ready.");
return Err(Error::SqlNoConnection);
}
@@ -223,7 +227,7 @@ impl Sql {
match res {
Ok(_) => Ok(()),
Err(err) => {
error!(context, "set_config(): Cannot change value. {:?}", &err);
error!(context, 0, "set_config(): Cannot change value. {:?}", &err);
Err(err.into())
}
}
@@ -234,10 +238,11 @@ impl Sql {
if !self.is_open() || key.as_ref().is_empty() {
return None;
}
self.query_get_value(
self.query_row_col(
context,
"SELECT value FROM config WHERE keyname=?;",
params![key.as_ref()],
0,
)
}
@@ -254,20 +259,6 @@ impl Sql {
self.get_config(context, key).and_then(|s| s.parse().ok())
}
pub fn get_config_bool(&self, context: &Context, key: impl AsRef<str>) -> bool {
// Not the most obvious way to encode bool as string, but it is matter
// of backward compatibility.
self.get_config_int(context, key).unwrap_or_default() > 0
}
pub fn set_config_bool<T>(&self, context: &Context, key: T, value: bool) -> Result<()>
where
T: AsRef<str>,
{
let value = if value { Some("1") } else { None };
self.set_config(context, key, value)
}
pub fn set_config_int64(
&self,
context: &Context,
@@ -312,6 +303,7 @@ fn open(
if sql.is_open() {
error!(
context,
0,
"Cannot open, database \"{:?}\" already opened.",
dbfile.as_ref(),
);
@@ -345,6 +337,7 @@ fn open(
if !sql.table_exists("config") {
info!(
context,
0,
"First time init: creating tables in {:?}.",
dbfile.as_ref(),
);
@@ -460,6 +453,7 @@ fn open(
{
error!(
context,
0,
"Cannot create tables in new database \"{:?}\".",
dbfile.as_ref(),
);
@@ -680,7 +674,7 @@ fn open(
sql.set_config_int(context, "dbversion", 46)?;
}
if dbversion < 47 {
info!(context, "[migration] v47");
info!(context, 0, "[migration] v47");
sql.execute(
"ALTER TABLE jobs ADD COLUMN tries INTEGER DEFAULT 0;",
params![],
@@ -689,7 +683,7 @@ fn open(
sql.set_config_int(context, "dbversion", 47)?;
}
if dbversion < 48 {
info!(context, "[migration] v48");
info!(context, 0, "[migration] v48");
sql.execute(
"ALTER TABLE msgs ADD COLUMN move_state INTEGER DEFAULT 1;",
params![],
@@ -699,7 +693,7 @@ fn open(
sql.set_config_int(context, "dbversion", 48)?;
}
if dbversion < 49 {
info!(context, "[migration] v49");
info!(context, 0, "[migration] v49");
sql.execute(
"ALTER TABLE chats ADD COLUMN gossiped_timestamp INTEGER DEFAULT 0;",
params![],
@@ -708,7 +702,7 @@ fn open(
sql.set_config_int(context, "dbversion", 49)?;
}
if dbversion < 50 {
info!(context, "[migration] v50");
info!(context, 0, "[migration] v50");
if 0 != exists_before_update {
sql.set_config_int(context, "show_emails", 2)?;
}
@@ -716,7 +710,7 @@ fn open(
sql.set_config_int(context, "dbversion", 50)?;
}
if dbversion < 53 {
info!(context, "[migration] v53");
info!(context, 0, "[migration] v53");
sql.execute(
"CREATE TABLE locations ( id INTEGER PRIMARY KEY AUTOINCREMENT, latitude REAL DEFAULT 0.0, longitude REAL DEFAULT 0.0, accuracy REAL DEFAULT 0.0, timestamp INTEGER DEFAULT 0, chat_id INTEGER DEFAULT 0, from_id INTEGER DEFAULT 0);",
params![]
@@ -749,7 +743,7 @@ fn open(
sql.set_config_int(context, "dbversion", 53)?;
}
if dbversion < 54 {
info!(context, "[migration] v54");
info!(context, 0, "[migration] v54");
sql.execute(
"ALTER TABLE msgs ADD COLUMN location_id INTEGER DEFAULT 0;",
params![],
@@ -789,11 +783,11 @@ fn open(
// for newer versions, we copy files always to the blob directory and store relative paths.
// this snippet converts older databases and can be removed after some time.
info!(context, "[open] update file paths");
info!(context, 0, "[open] update file paths");
let repl_from = sql
.get_config(context, "backup_for")
.unwrap_or_else(|| context.get_blobdir().to_string_lossy().into());
.unwrap_or_else(|| to_string(context.get_blobdir()));
let repl_from = dc_ensure_no_slash_safe(&repl_from);
sql.execute(
@@ -816,7 +810,7 @@ fn open(
}
}
info!(context, "Opened {:?}.", dbfile.as_ref(),);
info!(context, 0, "Opened {:?}.", dbfile.as_ref(),);
Ok(())
}
@@ -831,6 +825,7 @@ where
Err(err) => {
error!(
context,
0,
"execute failed: {:?} for {}",
&err,
querystr.as_ref()
@@ -847,6 +842,7 @@ pub fn try_execute(context: &Context, sql: &Sql, querystr: impl AsRef<str>) -> R
Err(err) => {
warn!(
context,
0,
"Try-execute for \"{}\" failed: {}",
querystr.as_ref(),
&err,
@@ -890,7 +886,7 @@ pub fn get_rowid_with_conn(
Err(err) => {
error!(
context,
"sql: Failed to retrieve rowid: {} in {}", err, query
0, "sql: Failed to retrieve rowid: {} in {}", err, query
);
0
}
@@ -937,7 +933,7 @@ pub fn get_rowid2_with_conn(
) {
Ok(id) => id,
Err(err) => {
error!(context, "sql: Failed to retrieve rowid2: {}", err);
error!(context, 0, "sql: Failed to retrieve rowid2: {}", err);
0
}
}
@@ -947,7 +943,7 @@ pub fn housekeeping(context: &Context) {
let mut files_in_use = HashSet::new();
let mut unreferenced_count = 0;
info!(context, "Start housekeeping...");
info!(context, 0, "Start housekeeping...");
maybe_add_from_param(
context,
&mut files_in_use,
@@ -987,12 +983,12 @@ pub fn housekeeping(context: &Context) {
},
)
.unwrap_or_else(|err| {
warn!(context, "sql: failed query: {}", err);
warn!(context, 0, "sql: failed query: {}", err);
});
info!(context, "{} files in use.", files_in_use.len(),);
info!(context, 0, "{} files in use.", files_in_use.len(),);
/* go through directory and delete unused files */
let p = context.get_blobdir();
let p = std::path::Path::new(as_str(context.get_blobdir()));
match std::fs::read_dir(p) {
Ok(dir_handle) => {
/* avoid deletion of files that are just created to build a message object */
@@ -1019,16 +1015,17 @@ pub fn housekeeping(context: &Context) {
match std::fs::metadata(entry.path()) {
Ok(stats) => {
let recently_created = stats.created().is_ok()
let created = stats.created().is_ok()
&& stats.created().unwrap() > keep_files_newer_than;
let recently_modified = stats.modified().is_ok()
let modified = stats.modified().is_ok()
&& stats.modified().unwrap() > keep_files_newer_than;
let recently_accessed = stats.accessed().is_ok()
let accessed = stats.accessed().is_ok()
&& stats.accessed().unwrap() > keep_files_newer_than;
if recently_created || recently_modified || recently_accessed {
if created || modified || accessed {
info!(
context,
0,
"Housekeeping: Keeping new unreferenced file #{}: {:?}",
unreferenced_count,
entry.file_name(),
@@ -1040,6 +1037,7 @@ pub fn housekeeping(context: &Context) {
}
info!(
context,
0,
"Housekeeping: Deleting unreferenced file #{}: {:?}",
unreferenced_count,
entry.file_name()
@@ -1051,14 +1049,15 @@ pub fn housekeeping(context: &Context) {
Err(err) => {
warn!(
context,
0,
"Housekeeping: Cannot open {}. ({})",
context.get_blobdir().display(),
as_str(context.get_blobdir()),
err
);
}
}
info!(context, "Housekeeping done.",);
info!(context, 0, "Housekeeping done.",);
}
fn is_file_in_use(files_in_use: &HashSet<String>, namespc_opt: Option<&str>, name: &str) -> bool {
@@ -1091,22 +1090,15 @@ fn maybe_add_from_param(
) {
context
.sql
.query_map(
query,
NO_PARAMS,
|row| row.get::<_, String>(0),
|rows| {
for row in rows {
let param: Params = row?.parse().unwrap_or_default();
if let Some(file) = param.get(param_id) {
maybe_add_file(files_in_use, file);
}
}
Ok(())
},
)
.query_row(query, NO_PARAMS, |row| {
let param: Params = row.get::<_, String>(0)?.parse().unwrap_or_default();
if let Some(file) = param.get(param_id) {
maybe_add_file(files_in_use, file);
}
Ok(())
})
.unwrap_or_else(|err| {
warn!(context, "sql: failed to add_from_param: {}", err);
warn!(context, 0, "sql: failed to add_from_param: {}", err);
});
}

View File

@@ -3,10 +3,10 @@ use std::borrow::Cow;
use strum::EnumProperty;
use strum_macros::EnumProperty;
use crate::constants::Event;
use crate::contact::*;
use crate::context::Context;
use crate::dc_tools::*;
use crate::events::Event;
use libc::free;
/// Stock strings
@@ -128,7 +128,7 @@ impl Context {
/// translation, then this string will be returned. Otherwise a
/// default (English) string is returned.
pub fn stock_str(&self, id: StockMessage) -> Cow<str> {
let ptr = self.call_cb(Event::GetString { id, count: 0 }) as *mut libc::c_char;
let ptr = self.call_cb(Event::GET_STRING, id as usize, 0) as *mut libc::c_char;
if ptr.is_null() {
Cow::Borrowed(id.fallback())
} else {
@@ -140,14 +140,12 @@ impl Context {
/// Return stock string, replacing placeholders with provided string.
///
/// This replaces both the *first* `%1$s`, `%1$d` and `%1$@`
/// This replaces both the *first* `%1$s` **and** `%1$d`
/// placeholders with the provided string.
/// (the `%1$@` variant is used on iOS, the other are used on Android and Desktop)
pub fn stock_string_repl_str(&self, id: StockMessage, insert: impl AsRef<str>) -> String {
self.stock_str(id)
.replacen("%1$s", insert.as_ref(), 1)
.replacen("%1$d", insert.as_ref(), 1)
.replacen("%1$@", insert.as_ref(), 1)
}
/// Return stock string, replacing placeholders with provided int.
@@ -160,10 +158,9 @@ impl Context {
/// Return stock string, replacing 2 placeholders with provided string.
///
/// This replaces both the *first* `%1$s`, `%1$d` and `%1$@`
/// This replaces both the *first* `%1$s` **and** `%1$d`
/// placeholders with the string in `insert` and does the same for
/// `%2$s`, `%2$d` and `%2$@` for `insert2`.
/// (the `%1$@` variant is used on iOS, the other are used on Android and Desktop)
/// `%2$s` and `%2$d` for `insert2`.
fn stock_string_repl_str2(
&self,
id: StockMessage,
@@ -173,10 +170,8 @@ impl Context {
self.stock_str(id)
.replacen("%1$s", insert.as_ref(), 1)
.replacen("%1$d", insert.as_ref(), 1)
.replacen("%1$@", insert.as_ref(), 1)
.replacen("%2$s", insert2.as_ref(), 1)
.replacen("%2$d", insert2.as_ref(), 1)
.replacen("%2$@", insert2.as_ref(), 1)
}
/// Return some kind of stock message
@@ -237,8 +232,11 @@ mod tests {
use super::*;
use crate::test_utils::*;
use std::ffi::CString;
use crate::constants::DC_CONTACT_ID_SELF;
use libc::uintptr_t;
use crate::context::dc_context_new;
use crate::types::uintptr_t;
use num_traits::ToPrimitive;
@@ -255,32 +253,36 @@ mod tests {
#[test]
fn test_stock_str() {
let t = dummy_context();
assert_eq!(t.ctx.stock_str(StockMessage::NoMessages), "No messages.");
let ctx = dc_context_new(None, std::ptr::null_mut(), None);
assert_eq!(ctx.stock_str(StockMessage::NoMessages), "No messages.");
}
fn test_stock_str_no_fallback_cb(_ctx: &Context, evt: Event) -> uintptr_t {
match evt {
Event::GetString {
id: StockMessage::NoMessages,
..
} => unsafe { "Hello there".strdup() as usize },
_ => 0,
unsafe extern "C" fn test_stock_str_no_fallback_cb(
_ctx: &Context,
evt: Event,
d1: uintptr_t,
_d2: uintptr_t,
) -> uintptr_t {
if evt == Event::GET_STRING && d1 == StockMessage::NoMessages.to_usize().unwrap() {
let tmp = CString::new("Hello there").unwrap();
dc_strdup(tmp.as_ptr()) as usize
} else {
0
}
}
#[test]
fn test_stock_str_no_fallback() {
let t = test_context(Some(Box::new(test_stock_str_no_fallback_cb)));
let t = test_context(Some(test_stock_str_no_fallback_cb));
assert_eq!(t.ctx.stock_str(StockMessage::NoMessages), "Hello there");
}
#[test]
fn test_stock_string_repl_str() {
let t = dummy_context();
let ctx = dc_context_new(None, std::ptr::null_mut(), None);
// uses %1$s substitution
assert_eq!(
t.ctx.stock_string_repl_str(StockMessage::Member, "42"),
ctx.stock_string_repl_str(StockMessage::Member, "42"),
"42 member(s)"
);
// We have no string using %1$d to test...
@@ -288,42 +290,40 @@ mod tests {
#[test]
fn test_stock_string_repl_int() {
let t = dummy_context();
let ctx = dc_context_new(None, std::ptr::null_mut(), None);
assert_eq!(
t.ctx.stock_string_repl_int(StockMessage::Member, 42),
ctx.stock_string_repl_int(StockMessage::Member, 42),
"42 member(s)"
);
}
#[test]
fn test_stock_string_repl_str2() {
let t = dummy_context();
let ctx = dc_context_new(None, std::ptr::null_mut(), None);
assert_eq!(
t.ctx
.stock_string_repl_str2(StockMessage::ServerResponse, "foo", "bar"),
ctx.stock_string_repl_str2(StockMessage::ServerResponse, "foo", "bar"),
"Response from foo: bar"
);
}
#[test]
fn test_stock_system_msg_simple() {
let t = dummy_context();
let ctx = dc_context_new(None, std::ptr::null_mut(), None);
assert_eq!(
t.ctx
.stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0),
ctx.stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0),
"Location streaming enabled."
)
}
#[test]
fn test_stock_system_msg_add_member_by_me() {
let t = dummy_context();
let ctx = dc_context_new(None, std::ptr::null_mut(), None);
assert_eq!(
t.ctx.stock_system_msg(
ctx.stock_system_msg(
StockMessage::MsgAddMember,
"alice@example.com",
"",
DC_CONTACT_ID_SELF
DC_CONTACT_ID_SELF as u32
),
"Member alice@example.com added by me."
)
@@ -338,7 +338,7 @@ mod tests {
StockMessage::MsgAddMember,
"alice@example.com",
"",
DC_CONTACT_ID_SELF
DC_CONTACT_ID_SELF as u32
),
"Member Alice (alice@example.com) added by me."
);
@@ -373,7 +373,7 @@ mod tests {
StockMessage::MsgGrpName,
"Some chat",
"Other chat",
DC_CONTACT_ID_SELF
DC_CONTACT_ID_SELF as u32
),
"Group name changed from \"Some chat\" to \"Other chat\" by me."
)

View File

@@ -2,14 +2,16 @@
//!
//! This module is only compiled for test runs.
use std::ffi::CStr;
use libc::uintptr_t;
use tempfile::{tempdir, TempDir};
use crate::config::Config;
use crate::constants::KeyType;
use crate::context::{Context, ContextCallback};
use crate::events::Event;
use crate::constants::{Event, KeyType};
use crate::context::{dc_context_new, dc_open, Context};
use crate::key;
use crate::types::dc_callback_t;
/// A Context and temporary directory.
///
@@ -26,15 +28,18 @@ pub struct TestContext {
/// "db.sqlite" in the [TestContext.dir] directory.
///
/// [Context]: crate::context::Context
pub fn test_context(callback: Option<Box<ContextCallback>>) -> TestContext {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let cb: Box<ContextCallback> = match callback {
Some(cb) => cb,
None => Box::new(|_, _| 0),
};
let ctx = Context::new(cb, "FakeOs".into(), dbfile).unwrap();
TestContext { ctx: ctx, dir: dir }
pub fn test_context(cb: Option<dc_callback_t>) -> TestContext {
unsafe {
let mut ctx = dc_context_new(cb, std::ptr::null_mut(), None);
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
assert!(
dc_open(&mut ctx, dbfile.to_str().unwrap(), None),
"Failed to open {}",
dbfile.display(),
);
TestContext { ctx: ctx, dir: dir }
}
}
/// Return a dummy [TestContext].
@@ -46,11 +51,17 @@ pub fn dummy_context() -> TestContext {
test_context(None)
}
pub fn logging_cb(_ctx: &Context, evt: Event) -> uintptr_t {
pub unsafe extern "C" fn logging_cb(
_ctx: &Context,
evt: Event,
_d1: uintptr_t,
d2: uintptr_t,
) -> uintptr_t {
let to_str = |x| CStr::from_ptr(x as *const libc::c_char).to_str().unwrap();
match evt {
Event::Info(msg) => println!("I: {}", msg),
Event::Warning(msg) => println!("W: {}", msg),
Event::Error(msg) => println!("E: {}", msg),
Event::INFO => println!("I: {}", to_str(d2)),
Event::WARNING => println!("W: {}", to_str(d2)),
Event::ERROR => println!("E: {}", to_str(d2)),
_ => (),
}
0

View File

@@ -1,57 +0,0 @@
//! Functions to read/write token from/to the database. A token is any string associated with a key.
use deltachat_derive::*;
use crate::context::Context;
use crate::dc_tools::*;
use crate::sql;
// Token namespaces
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
#[repr(i32)]
pub enum Namespace {
Unknown = 0,
Auth = 110,
InviteNumber = 100,
}
impl Default for Namespace {
fn default() -> Self {
Namespace::Unknown
}
}
pub fn save(context: &Context, namespace: Namespace, foreign_id: u32) -> String {
// foreign_id may be 0
let token = dc_create_id();
sql::execute(
context,
&context.sql,
"INSERT INTO tokens (namespc, foreign_id, token, timestamp) VALUES (?, ?, ?, ?);",
params![namespace, foreign_id as i32, &token, time()],
)
.ok();
token
}
pub fn lookup(context: &Context, namespace: Namespace, foreign_id: u32) -> Option<String> {
context.sql.query_get_value::<_, String>(
context,
"SELECT token FROM tokens WHERE namespc=? AND foreign_id=?;",
params![namespace, foreign_id as i32],
)
}
pub fn lookup_or_new(context: &Context, namespace: Namespace, foreign_id: u32) -> String {
lookup(context, namespace, foreign_id).unwrap_or_else(|| save(context, namespace, foreign_id))
}
pub fn exists(context: &Context, namespace: Namespace, token: &str) -> bool {
context
.sql
.exists(
"SELECT id FROM tokens WHERE namespc=? AND token=?;",
params![namespace, token],
)
.unwrap_or_default()
}

View File

@@ -1,26 +1,24 @@
#!/usr/bin/env python3
from pathlib import Path
import os
import re
if __name__ == "__main__":
if Path('src/top_evil_rs.py').exists():
os.chdir('src')
filestats = []
for fn in Path(".").glob("**/*.rs"):
s = fn.read_text()
s = re.sub(r"(?m)///.*$", "", s) # remove comments
unsafe = s.count("unsafe")
free = s.count("free(")
gotoblocks = s.count("ok_to_continue") + s.count('OK_TO_CONTINUE')
filestats.append((fn, unsafe, free, gotoblocks))
for fn in os.listdir():
if fn.endswith(".rs"):
s = open(fn).read()
s = re.sub(r'(?m)///.*$', '', s) # remove comments
unsafe = s.count("unsafe")
free = s.count("free(")
gotoblocks = s.count("current_block =")
filestats.append((fn, unsafe, free, gotoblocks))
sum_unsafe, sum_free, sum_gotoblocks = 0, 0, 0
for fn, unsafe, free, gotoblocks in reversed(sorted(filestats, key=lambda x: sum(x[1:]))):
print("{0: <30} unsafe: {1: >3} free: {2: >3} ok_to_continue: {3: >3}".format(str(fn), unsafe, free, gotoblocks))
print("{0: <30} unsafe: {1: >3} free: {2: >3} goto-blocks: {3: >3}".format(fn, unsafe, free, gotoblocks))
sum_unsafe += unsafe
sum_free += free
sum_gotoblocks += gotoblocks
@@ -29,5 +27,5 @@ if __name__ == "__main__":
print()
print("total unsafe:", sum_unsafe)
print("total free:", sum_free)
print("total ok_to_continue:", sum_gotoblocks)
print("total gotoblocks:", sum_gotoblocks)

43
src/types.rs Normal file
View File

@@ -0,0 +1,43 @@
#![allow(non_camel_case_types)]
use crate::constants::Event;
use crate::context::Context;
pub use mmime::clist::*;
pub use rusqlite::ffi::*;
/// Callback function that should be given to dc_context_new().
///
/// @memberof Context
/// @param context The context object as returned by dc_context_new().
/// @param event one of the @ref DC_EVENT constants
/// @param data1 depends on the event parameter
/// @param data2 depends on the event parameter
/// @return return 0 unless stated otherwise in the event parameter documentation
pub type dc_callback_t =
unsafe extern "C" fn(_: &Context, _: Event, _: uintptr_t, _: uintptr_t) -> uintptr_t;
pub type dc_receive_imf_t = unsafe fn(
_: &Context,
_: *const libc::c_char,
_: size_t,
_: &str,
_: uint32_t,
_: uint32_t,
) -> ();
/* Purpose: Reading from IMAP servers with no dependencies to the database.
Context is only used for logging and to get information about
the online state. */
pub type dc_precheck_imf_t =
unsafe fn(_: &Context, _: *const libc::c_char, _: &str, _: u32) -> libc::c_int;
pub type dc_set_config_t = fn(_: &Context, _: &str, _: Option<&str>) -> ();
pub type dc_get_config_t = fn(_: &Context, _: &str) -> Option<String>;
pub type int32_t = i32;
pub type uintptr_t = libc::uintptr_t;
pub type size_t = libc::size_t;
pub type uint32_t = libc::c_uint;
pub type uint8_t = libc::c_uchar;
pub type uint16_t = libc::c_ushort;
pub type uint64_t = u64;

View File

@@ -33,6 +33,13 @@ pub fn strndup(s: *const libc::c_char, n: libc::c_ulong) -> *mut libc::c_char {
}
}
extern "C" {
pub fn clock() -> libc::clock_t;
// -- DC Methods
pub fn dc_mprintf(format: *const libc::c_char, _: ...) -> *mut libc::c_char;
}
pub(crate) unsafe fn strcasecmp(s1: *const libc::c_char, s2: *const libc::c_char) -> libc::c_int {
let s1 = std::ffi::CStr::from_ptr(s1)
.to_string_lossy()

View File

@@ -1 +0,0 @@
xsBNBF086ewBCACmJKuoxIO6T87yi4Q3MyNpMch3Y8KrtHDQyUszU36eqM3Pmd1lFrbcCd8ZWo2pq6OJSwsM/jjRGn1zo2YOaQeJRRrC+KrKGqSUtRSYQBPrPjE2YjSXAMbu8jBI6VVUhHeghriBkK79PY9O/oRhIJC0p14IJe6CQ6OI2fTmTUHF9i/nJ3G4Wb3/K1bU3yVfyPZjTZQPYPvvh03vxHULKurtYkX5DTEMSWsF4qzLMps+l87VuLV9iQnbN7YMRLHHx2KkX5Ivi7JCefhCa54M0K3bDCCxuVWAM5wjQwNZjzR+WL+dYchwoFvuF8NrlzjM9dSv+2rn+J7f99ijSXarzPC7ABEBAAHNEzxhbGljZUBleGFtcGxlLmNvbT7CwIkEEAEIADMCGQEFAl086fgCGwMECwkIBwYVCAkKCwIDFgIBFiEE+iaix4d0doj87Q0ek6DcNkbrcegACgkQk6DcNkbrcei3ogf/cruUmQ+th52TFHTHdkw9OHUl3MrXtZ7QmHyOAFvbXE/6n5Eeh+eZoF8MWWV72m14Wbs+vTcNQkFVTdOPptkKA8e4cJqwDOHsyAnvQXZ7WNje9+BMzcoipIUawHP4ORFaPDsKLZQ0b4wBbKn8ziea6zjGF0/qljTdoxTtsYpv5wXYuhwbYklrLOqgSa5M7LXUe7E3g9mbg+9iX1GuB8m6GkquJN814Y+xny4xhZzGOfue6SeP12jJMNSjSP7416dRq7794VGnkkW9V/7oFEUKu5wO9FFbgDySOSlEjByGejSGuBmho0iJSjcPjZ7EY/j3M3orq4dpza5C82OeSvxDFc7ATQRdPOnsAQgA5oLxXRLnyugzOmNCy7dxV3JrDZscA6JNlJlDWIShT0YSs+zG9JzDeQql+sYXgUSxOoIayItuXtnFn7tstwGoOnYvadm/e5/7V5fKAQRtCtdN51av62n18Venlm0yNKpROPcZ6M/sc4m6uU6YRZ/a1idal8VGY0wLKlghjIBuIiBoVQ/RnoW+/fhmwIg08dQ5m8hQe3GEOZEeLrTWL/9awPlXK7Y+DmJOoR4qbHWEcRfbzS6q4zW8vk2ztB8ngwbnqYy8zrN1DCICN1gYdlU++uVw6Bb1XfY8Cdldh1VLKpF35mAmjxLZfVDcoObFH3Cv2GB7BEYxv86KC2Y6T74Q/wARAQABwsB2BBgBCAAgBQJdPOn4AhsMFiEE+iaix4d0doj87Q0ek6DcNkbrcegACgkQk6DcNkbrcegLxwf/dXshJnoWqEnRsf6rVb9/Mc66ti+NVQLfNd275dybh/QIJdK3NmSxdnTPIEJRVspJywJoupwXFNrnHG2Ff6QPvHqI+/oNu986r9d7Z1oQibbLHKt8t6kpOfg/xGxotagJuiCQvR9mMjD1DqsdB37SjDxGupZOOJSXWi6KX60IE+uM+QOBfeOZziQwuFmA5wV6RDXIeYJfqrcbeXeR1d0nfNpPHQR1gBiqmxNb6KBbdXD2+EXW60axC7D2b1APmzMlammDliPwsK9U1rc9nuquEBvGDOJf4K+Dzn+mDCqRpP6uAuQ7RKHyim4uyN0wwKObzPqgJCGwjTglkixw+aSTXw==

View File

@@ -1,21 +0,0 @@
Return-Path: <x@testrun.org>
Received: from hq5.merlinux.eu
by hq5.merlinux.eu (Dovecot) with LMTP id yRKOBakcfV1AewAAPzvFDg
; Sat, 14 Sep 2019 19:00:25 +0200
Received: from localhost (unknown 7.165.105.24])
by hq5.merlinux.eu (Postfix) with ESMTPSA id 8D9844E023;
Sat, 14 Sep 2019 19:00:22 +0200 (CEST)
DKIM-Signature: v=1; a=rsa-sha256; c=simple/simple; d=testrun.org;
s=testrun; t=1568480425;
bh=GrziQ9bkbioILHZ1tEn81B53bDgaZK+ENbW4MUBiMUM=;
h=Date:From:To:In-Reply-To:References:Subject:From;
b=eHP/GdBQD1cq601Gaqpz7fd+nQvBJLH7c+bk9KGGC3BKZrRAzehEDOaFlGP2+9oFq
cl+y11nGowenVAw7M5ljjxCpOVy0Aa+atn3e7iZeufgOJPr5Hggg2nPe4qfyP6OmOJ
i9RazyXN1rp4fY7ku9x29g818UL8aWiloJHJ9wqDhMe8OXtIAhJA0gpHDCT1SLgo/a
Kv7/Yj4CA8XS/mRxkbBmpWgcF7SluNjuXU9Npo3aTfjQQ5xSzn72Jsai4n0ppLcQ5k
KobttFy+gkmfQRAe8v7vx51qn82BSsGRlxZWevOB9zit/uk7zb8ZbBqPMeCoWsihRM
LdSqwIpokmYmQ==
Date: Sat, 14 Sep 2019 19:00:13 +0200
From: lmn <x@tux.org>
To: abc <abc@bcd.com>, def <def@def.de>,
jik

View File

@@ -1,22 +1,25 @@
//! Stress some functions for testing; if used as a lib, this file is obsolete.
use std::collections::HashSet;
use std::path::PathBuf;
use std::ptr;
use std::ffi::CString;
use mmime::mailimf_types::*;
use tempfile::{tempdir, TempDir};
use deltachat::chat::{self, Chat};
use deltachat::config;
use deltachat::constants::*;
use deltachat::contact::*;
use deltachat::context::*;
use deltachat::dc_imex::*;
use deltachat::dc_mimeparser::*;
use deltachat::dc_tools::*;
use deltachat::key::*;
use deltachat::keyring::*;
use deltachat::oauth2::*;
use deltachat::pgp::*;
use deltachat::types::*;
use deltachat::x::*;
use deltachat::Event;
use libc;
/* some data used for testing
@@ -30,67 +33,103 @@ static mut S_EM_SETUPFILE: *const libc::c_char =
as *const u8 as *const libc::c_char;
unsafe fn stress_functions(context: &Context) {
if dc_file_exist(context, "$BLOBDIR/foobar")
|| dc_file_exist(context, "$BLOBDIR/dada")
|| dc_file_exist(context, "$BLOBDIR/foobar.dadada")
|| dc_file_exist(context, "$BLOBDIR/foobar-folder")
{
dc_delete_file(context, "$BLOBDIR/foobar");
dc_delete_file(context, "$BLOBDIR/dada");
dc_delete_file(context, "$BLOBDIR/foobar.dadada");
dc_delete_file(context, "$BLOBDIR/foobar-folder");
}
assert!(dc_write_file(context, "$BLOBDIR/foobar", b"content"));
assert!(dc_file_exist(context, "$BLOBDIR/foobar",));
assert!(!dc_file_exist(context, "$BLOBDIR/foobarx"));
assert_eq!(dc_get_filebytes(context, "$BLOBDIR/foobar"), 7);
let abs_path = context
.get_blobdir()
.join("foobar")
.to_string_lossy()
.to_string();
assert!(dc_is_blobdir_path(context, &abs_path));
assert!(dc_is_blobdir_path(context, "$BLOBDIR/fofo",));
assert!(!dc_is_blobdir_path(context, "/BLOBDIR/fofo",));
assert!(dc_file_exist(context, &abs_path));
assert!(dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada",));
assert_eq!(dc_get_filebytes(context, "$BLOBDIR/dada",), 7);
let mut buf: *mut libc::c_void = ptr::null_mut();
let mut buf_bytes: libc::size_t = 0;
assert_eq!(
dc_read_file(
if 0 != dc_is_open(context) {
if dc_file_exist(context, "$BLOBDIR/foobar")
|| dc_file_exist(context, "$BLOBDIR/dada")
|| dc_file_exist(context, "$BLOBDIR/foobar.dadada")
|| dc_file_exist(context, "$BLOBDIR/foobar-folder")
{
dc_delete_file(context, "$BLOBDIR/foobar");
dc_delete_file(context, "$BLOBDIR/dada");
dc_delete_file(context, "$BLOBDIR/foobar.dadada");
dc_delete_file(context, "$BLOBDIR/foobar-folder");
}
dc_write_file(
context,
b"$BLOBDIR/dada\x00" as *const u8 as *const libc::c_char,
&mut buf,
&mut buf_bytes,
),
1
);
assert_eq!(buf_bytes, 7);
assert_eq!(
std::str::from_utf8(std::slice::from_raw_parts(buf as *const u8, buf_bytes)).unwrap(),
"content"
);
b"$BLOBDIR/foobar\x00" as *const u8 as *const libc::c_char,
b"content\x00" as *const u8 as *const libc::c_char as *const libc::c_void,
7i32 as size_t,
);
assert!(dc_file_exist(context, "$BLOBDIR/foobar",));
assert!(!dc_file_exist(context, "$BLOBDIR/foobarx"));
assert_eq!(
dc_get_filebytes(context, "$BLOBDIR/foobar",),
7i32 as libc::c_ulonglong
);
free(buf as *mut _);
assert!(dc_delete_file(context, "$BLOBDIR/foobar"));
assert!(dc_delete_file(context, "$BLOBDIR/dada"));
assert!(dc_create_folder(context, "$BLOBDIR/foobar-folder"));
assert!(dc_file_exist(context, "$BLOBDIR/foobar-folder",));
assert!(!dc_delete_file(context, "$BLOBDIR/foobar-folder"));
let fn0 = dc_get_fine_path_filename(context, "$BLOBDIR", "foobar.dadada");
assert_eq!(fn0, PathBuf::from("$BLOBDIR/foobar.dadada"));
let abs_path: *mut libc::c_char = dc_mprintf(
b"%s/%s\x00" as *const u8 as *const libc::c_char,
context.get_blobdir(),
b"foobar\x00" as *const u8 as *const libc::c_char,
);
assert!(dc_is_blobdir_path(context, as_str(abs_path)));
assert!(dc_is_blobdir_path(context, "$BLOBDIR/fofo",));
assert!(!dc_is_blobdir_path(context, "/BLOBDIR/fofo",));
assert!(dc_file_exist(context, as_path(abs_path)));
free(abs_path as *mut libc::c_void);
assert!(dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada",));
assert_eq!(dc_get_filebytes(context, "$BLOBDIR/dada",), 7);
assert!(dc_write_file(context, &fn0, b"content"));
let fn1 = dc_get_fine_path_filename(context, "$BLOBDIR", "foobar.dadada");
assert_eq!(fn1, PathBuf::from("$BLOBDIR/foobar-1.dadada"));
let mut buf: *mut libc::c_void = 0 as *mut libc::c_void;
let mut buf_bytes: size_t = 0;
assert!(dc_delete_file(context, &fn0));
assert_eq!(
dc_read_file(
context,
b"$BLOBDIR/dada\x00" as *const u8 as *const libc::c_char,
&mut buf,
&mut buf_bytes,
),
1
);
assert_eq!(buf_bytes, 7);
assert_eq!(
std::str::from_utf8(std::slice::from_raw_parts(buf as *const u8, buf_bytes)).unwrap(),
"content"
);
free(buf as *mut _);
assert!(dc_delete_file(context, "$BLOBDIR/foobar"));
assert!(dc_delete_file(context, "$BLOBDIR/dada"));
assert!(dc_create_folder(context, "$BLOBDIR/foobar-folder"));
assert!(dc_file_exist(context, "$BLOBDIR/foobar-folder",));
assert!(dc_delete_file(context, "$BLOBDIR/foobar-folder"));
let fn0: *mut libc::c_char = dc_get_fine_pathNfilename(
context,
b"$BLOBDIR\x00" as *const u8 as *const libc::c_char,
b"foobar.dadada\x00" as *const u8 as *const libc::c_char,
);
assert!(!fn0.is_null());
assert_eq!(
strcmp(
fn0,
b"$BLOBDIR/foobar.dadada\x00" as *const u8 as *const libc::c_char,
),
0
);
dc_write_file(
context,
fn0,
b"content\x00" as *const u8 as *const libc::c_char as *const libc::c_void,
7i32 as size_t,
);
let fn1: *mut libc::c_char = dc_get_fine_pathNfilename(
context,
b"$BLOBDIR\x00" as *const u8 as *const libc::c_char,
b"foobar.dadada\x00" as *const u8 as *const libc::c_char,
);
assert!(!fn1.is_null());
assert_eq!(
strcmp(
fn1,
b"$BLOBDIR/foobar-1.dadada\x00" as *const u8 as *const libc::c_char,
),
0
);
assert!(dc_delete_file(context, as_path(fn0)));
free(fn0 as *mut libc::c_void);
free(fn1 as *mut libc::c_void);
}
let res = context.get_config(config::Config::SysConfigKeys).unwrap();
@@ -124,10 +163,10 @@ unsafe fn stress_functions(context: &Context) {
assert!(res.contains(" configured_server_flags "));
let mut buf_0: *mut libc::c_char;
let mut headerline: *const libc::c_char = ptr::null();
let mut setupcodebegin: *const libc::c_char = ptr::null();
let mut preferencrypt: *const libc::c_char = ptr::null();
let mut base64: *const libc::c_char = ptr::null();
let mut headerline: *const libc::c_char = 0 as *const libc::c_char;
let mut setupcodebegin: *const libc::c_char = 0 as *const libc::c_char;
let mut preferencrypt: *const libc::c_char = 0 as *const libc::c_char;
let mut base64: *const libc::c_char = 0 as *const libc::c_char;
buf_0 = strdup(
b"-----BEGIN PGP MESSAGE-----\nNoVal:\n\ndata\n-----END PGP MESSAGE-----\x00" as *const u8
as *const libc::c_char,
@@ -136,7 +175,7 @@ unsafe fn stress_functions(context: &Context) {
buf_0,
&mut headerline,
&mut setupcodebegin,
ptr::null_mut(),
0 as *mut *const libc::c_char,
&mut base64,
);
assert!(ok);
@@ -161,7 +200,7 @@ unsafe fn stress_functions(context: &Context) {
buf_0,
&mut headerline,
&mut setupcodebegin,
ptr::null_mut(),
0 as *mut *const libc::c_char,
&mut base64,
);
@@ -188,7 +227,7 @@ unsafe fn stress_functions(context: &Context) {
buf_0,
&mut headerline,
&mut setupcodebegin,
ptr::null_mut(),
0 as *mut *const libc::c_char,
&mut base64,
);
@@ -213,7 +252,7 @@ unsafe fn stress_functions(context: &Context) {
buf_0,
&mut headerline,
&mut setupcodebegin,
ptr::null_mut(),
0 as *mut *const libc::c_char,
&mut base64,
);
@@ -226,7 +265,7 @@ unsafe fn stress_functions(context: &Context) {
buf_0,
&mut headerline,
&mut setupcodebegin,
ptr::null_mut(),
0 as *mut *const libc::c_char,
&mut base64,
);
assert!(ok);
@@ -259,7 +298,7 @@ unsafe fn stress_functions(context: &Context) {
let ok = dc_split_armored_data(
buf_0,
&mut headerline,
ptr::null_mut(),
0 as *mut *const libc::c_char,
&mut preferencrypt,
&mut base64,
);
@@ -315,16 +354,16 @@ unsafe fn stress_functions(context: &Context) {
);
free(norm as *mut libc::c_void);
let mut buf_1: *mut libc::c_char;
let mut headerline_0: *const libc::c_char = ptr::null();
let mut setupcodebegin_0: *const libc::c_char = ptr::null();
let mut preferencrypt_0: *const libc::c_char = ptr::null();
let mut headerline_0: *const libc::c_char = 0 as *const libc::c_char;
let mut setupcodebegin_0: *const libc::c_char = 0 as *const libc::c_char;
let mut preferencrypt_0: *const libc::c_char = 0 as *const libc::c_char;
buf_1 = strdup(S_EM_SETUPFILE);
assert!(dc_split_armored_data(
buf_1,
&mut headerline_0,
&mut setupcodebegin_0,
&mut preferencrypt_0,
ptr::null_mut(),
0 as *mut *const libc::c_char,
));
assert!(!headerline_0.is_null());
assert_eq!(
@@ -350,7 +389,7 @@ unsafe fn stress_functions(context: &Context) {
&mut headerline_0,
&mut setupcodebegin_0,
&mut preferencrypt_0,
ptr::null_mut(),
0 as *mut *const libc::c_char,
));
assert!(!headerline_0.is_null());
assert_eq!(
@@ -378,16 +417,16 @@ unsafe fn stress_functions(context: &Context) {
// let setupcode_c = CString::yolo(setupcode.clone());
// let setupfile = dc_render_setup_file(context, &setupcode).unwrap();
// let setupfile_c = CString::yolo(setupfile);
// let mut headerline_2: *const libc::c_char = ptr::null();
// let mut headerline_2: *const libc::c_char = 0 as *const libc::c_char;
// let payload = dc_decrypt_setup_file(context, setupcode_c.as_ptr(), setupfile_c.as_ptr());
// assert!(payload.is_null());
// assert!(!dc_split_armored_data(
// payload,
// &mut headerline_2,
// ptr::null_mut(),
// ptr::null_mut(),
// ptr::null_mut(),
// 0 as *mut *const libc::c_char,
// 0 as *mut *const libc::c_char,
// 0 as *mut *const libc::c_char,
// ));
// assert!(!headerline_2.is_null());
// assert_eq!(
@@ -424,114 +463,175 @@ unsafe fn stress_functions(context: &Context) {
#[test]
#[ignore] // is too expensive
fn test_encryption_decryption() {
let (public_key, private_key) = dc_pgp_create_keypair("foo@bar.de").unwrap();
unsafe {
let mut bad_data: [libc::c_uchar; 4096] = [0; 4096];
let mut i_0: libc::c_int = 0i32;
while i_0 < 4096i32 {
bad_data[i_0 as usize] = (i_0 & 0xffi32) as libc::c_uchar;
i_0 += 1
}
let mut j: libc::c_int = 0i32;
private_key.split_key().unwrap();
while j < 4096 / 40 {
let bad_key = Key::from_binary(
&mut *bad_data.as_mut_ptr().offset(j as isize) as *const u8,
4096 / 2 + j,
if 0 != j & 1 {
KeyType::Public
} else {
KeyType::Private
},
);
let (public_key2, private_key2) = dc_pgp_create_keypair("two@zwo.de").unwrap();
assert!(bad_key.is_none());
j += 1
}
assert_ne!(public_key, public_key2);
let (public_key, private_key) = dc_pgp_create_keypair("foo@bar.de").unwrap();
let original_text = b"This is a test";
let mut keyring = Keyring::default();
keyring.add_owned(public_key.clone());
keyring.add_ref(&public_key2);
private_key.split_key().unwrap();
let ctext_signed = dc_pgp_pk_encrypt(original_text, &keyring, Some(&private_key)).unwrap();
assert!(!ctext_signed.is_empty());
assert!(ctext_signed.starts_with("-----BEGIN PGP MESSAGE-----"));
let (public_key2, private_key2) = dc_pgp_create_keypair("two@zwo.de").unwrap();
let ctext_unsigned = dc_pgp_pk_encrypt(original_text, &keyring, None).unwrap();
assert!(!ctext_unsigned.is_empty());
assert!(ctext_unsigned.starts_with("-----BEGIN PGP MESSAGE-----"));
assert_ne!(public_key, public_key2);
let mut keyring = Keyring::default();
keyring.add_owned(private_key);
let original_text: *const libc::c_char =
b"This is a test\x00" as *const u8 as *const libc::c_char;
let mut keyring = Keyring::default();
keyring.add_owned(public_key.clone());
keyring.add_ref(&public_key2);
let mut public_keyring = Keyring::default();
public_keyring.add_ref(&public_key);
let ctext = dc_pgp_pk_encrypt(
original_text as *const libc::c_void,
strlen(original_text),
&keyring,
Some(&private_key),
)
.unwrap();
let mut public_keyring2 = Keyring::default();
public_keyring2.add_owned(public_key2.clone());
assert!(!ctext.is_empty());
assert!(ctext.starts_with("-----BEGIN PGP MESSAGE-----"));
let mut valid_signatures: HashSet<String> = Default::default();
let ctext_signed_bytes = ctext.len();
let ctext_signed = CString::yolo(ctext);
let plain = dc_pgp_pk_decrypt(
ctext_signed.as_bytes(),
&keyring,
&public_keyring,
Some(&mut valid_signatures),
)
.unwrap();
let ctext = dc_pgp_pk_encrypt(
original_text as *const libc::c_void,
strlen(original_text),
&keyring,
None,
)
.unwrap();
assert!(!ctext.is_empty());
assert!(ctext.starts_with("-----BEGIN PGP MESSAGE-----"));
assert_eq!(plain, original_text,);
assert_eq!(valid_signatures.len(), 1);
let ctext_unsigned_bytes = ctext.len();
let ctext_unsigned = CString::yolo(ctext);
valid_signatures.clear();
let mut keyring = Keyring::default();
keyring.add_owned(private_key);
let empty_keyring = Keyring::default();
let plain = dc_pgp_pk_decrypt(
ctext_signed.as_bytes(),
&keyring,
&empty_keyring,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(plain, original_text);
assert_eq!(valid_signatures.len(), 0);
let mut public_keyring = Keyring::default();
public_keyring.add_ref(&public_key);
valid_signatures.clear();
let mut public_keyring2 = Keyring::default();
public_keyring2.add_owned(public_key2.clone());
let plain = dc_pgp_pk_decrypt(
ctext_signed.as_bytes(),
&keyring,
&public_keyring2,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(plain, original_text);
assert_eq!(valid_signatures.len(), 0);
let mut valid_signatures: HashSet<String> = Default::default();
valid_signatures.clear();
let plain = dc_pgp_pk_decrypt(
ctext_signed.as_ptr() as *const _,
ctext_signed_bytes,
&keyring,
&public_keyring,
Some(&mut valid_signatures),
)
.unwrap();
public_keyring2.add_ref(&public_key);
assert_eq!(std::str::from_utf8(&plain).unwrap(), as_str(original_text),);
assert_eq!(valid_signatures.len(), 1);
let plain = dc_pgp_pk_decrypt(
ctext_signed.as_bytes(),
&keyring,
&public_keyring2,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(plain, original_text);
assert_eq!(valid_signatures.len(), 1);
valid_signatures.clear();
valid_signatures.clear();
let empty_keyring = Keyring::default();
let plain = dc_pgp_pk_decrypt(
ctext_signed.as_ptr() as *const _,
ctext_signed_bytes,
&keyring,
&empty_keyring,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(std::str::from_utf8(&plain).unwrap(), as_str(original_text),);
assert_eq!(valid_signatures.len(), 0);
let plain = dc_pgp_pk_decrypt(
ctext_unsigned.as_bytes(),
&keyring,
&public_keyring,
Some(&mut valid_signatures),
)
.unwrap();
valid_signatures.clear();
assert_eq!(plain, original_text);
let plain = dc_pgp_pk_decrypt(
ctext_signed.as_ptr() as *const _,
ctext_signed_bytes,
&keyring,
&public_keyring2,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(std::str::from_utf8(&plain).unwrap(), as_str(original_text),);
assert_eq!(valid_signatures.len(), 0);
valid_signatures.clear();
valid_signatures.clear();
let mut keyring = Keyring::default();
keyring.add_ref(&private_key2);
let mut public_keyring = Keyring::default();
public_keyring.add_ref(&public_key);
public_keyring2.add_ref(&public_key);
let plain =
dc_pgp_pk_decrypt(ctext_signed.as_bytes(), &keyring, &public_keyring, None).unwrap();
let plain = dc_pgp_pk_decrypt(
ctext_signed.as_ptr() as *const _,
ctext_signed_bytes,
&keyring,
&public_keyring2,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(std::str::from_utf8(&plain).unwrap(), as_str(original_text),);
assert_eq!(valid_signatures.len(), 1);
assert_eq!(plain, original_text);
valid_signatures.clear();
let plain = dc_pgp_pk_decrypt(
ctext_unsigned.as_ptr() as *const _,
ctext_unsigned_bytes,
&keyring,
&public_keyring,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(std::str::from_utf8(&plain).unwrap(), as_str(original_text),);
valid_signatures.clear();
let mut keyring = Keyring::default();
keyring.add_ref(&private_key2);
let mut public_keyring = Keyring::default();
public_keyring.add_ref(&public_key);
let plain = dc_pgp_pk_decrypt(
ctext_signed.as_ptr() as *const _,
ctext_signed_bytes,
&keyring,
&public_keyring,
None,
)
.unwrap();
assert_eq!(std::str::from_utf8(&plain).unwrap(), as_str(original_text),);
}
}
fn cb(_context: &Context, _event: Event) -> libc::uintptr_t {
unsafe extern "C" fn cb(
_context: &Context,
_event: Event,
_data1: uintptr_t,
_data2: uintptr_t,
) -> uintptr_t {
0
}
@@ -541,16 +641,52 @@ struct TestContext {
dir: TempDir,
}
fn create_test_context() -> TestContext {
unsafe fn create_test_context() -> TestContext {
let mut ctx = dc_context_new(Some(cb), std::ptr::null_mut(), None);
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let ctx = Context::new(Box::new(cb), "FakeOs".into(), dbfile).unwrap();
assert!(
dc_open(&mut ctx, dbfile.to_str().unwrap(), None),
"Failed to open {}",
dbfile.display()
);
TestContext { ctx: ctx, dir: dir }
}
#[test]
fn test_dc_mimeparser_with_context() {
unsafe {
let context = create_test_context();
let mut mimeparser = dc_mimeparser_new(&context.ctx);
let raw: *const libc::c_char =
b"Content-Type: multipart/mixed; boundary=\"==break==\";\nSubject: outer-subject\nX-Special-A: special-a\nFoo: Bar\nChat-Version: 0.0\n\n--==break==\nContent-Type: text/plain; protected-headers=\"v1\";\nSubject: inner-subject\nX-Special-B: special-b\nFoo: Xy\nChat-Version: 1.0\n\ntest1\n\n--==break==--\n\n\x00"
as *const u8 as *const libc::c_char;
dc_mimeparser_parse(&mut mimeparser, raw, strlen(raw));
assert_eq!(
as_str(mimeparser.subject as *const libc::c_char),
"inner-subject",
);
let mut of: *mut mailimf_optional_field =
dc_mimeparser_lookup_optional_field(&mimeparser, "X-Special-A");
assert_eq!(as_str((*of).fld_value as *const libc::c_char), "special-a",);
of = dc_mimeparser_lookup_optional_field(&mimeparser, "Foo");
assert_eq!(as_str((*of).fld_value as *const libc::c_char), "Bar",);
of = dc_mimeparser_lookup_optional_field(&mimeparser, "Chat-Version");
assert_eq!(as_str((*of).fld_value as *const libc::c_char), "1.0",);
assert_eq!(mimeparser.parts.len(), 1);
dc_mimeparser_unref(&mut mimeparser);
}
}
#[test]
fn test_dc_get_oauth2_url() {
let ctx = create_test_context();
let ctx = unsafe { create_test_context() };
let addr = "dignifiedquire@gmail.com";
let redirect_uri = "chat.delta:/com.b44t.messenger";
let res = dc_get_oauth2_url(&ctx.ctx, addr, redirect_uri);
@@ -560,7 +696,7 @@ fn test_dc_get_oauth2_url() {
#[test]
fn test_dc_get_oauth2_addr() {
let ctx = create_test_context();
let ctx = unsafe { create_test_context() };
let addr = "dignifiedquire@gmail.com";
let code = "fail";
let res = dc_get_oauth2_addr(&ctx.ctx, addr, code);
@@ -570,10 +706,10 @@ fn test_dc_get_oauth2_addr() {
#[test]
fn test_dc_get_oauth2_token() {
let ctx = create_test_context();
let ctx = unsafe { create_test_context() };
let addr = "dignifiedquire@gmail.com";
let code = "fail";
let res = dc_get_oauth2_access_token(&ctx.ctx, addr, code, false);
let res = dc_get_oauth2_access_token(&ctx.ctx, addr, code, 0);
// this should fail as it is an invalid password
assert_eq!(res, None);
}
@@ -588,33 +724,50 @@ fn test_stress_tests() {
#[test]
fn test_get_contacts() {
let context = create_test_context();
let contacts = Contact::get_all(&context.ctx, 0, Some("some2")).unwrap();
assert_eq!(contacts.len(), 0);
unsafe {
let context = create_test_context();
let contacts = Contact::get_all(&context.ctx, 0, Some("some2")).unwrap();
assert_eq!(contacts.len(), 0);
let id = Contact::create(&context.ctx, "bob", "bob@mail.de").unwrap();
assert_ne!(id, 0);
let id = Contact::create(&context.ctx, "bob", "bob@mail.de").unwrap();
assert_ne!(id, 0);
let contacts = Contact::get_all(&context.ctx, 0, Some("bob")).unwrap();
assert_eq!(contacts.len(), 1);
let contacts = Contact::get_all(&context.ctx, 0, Some("bob")).unwrap();
assert_eq!(contacts.len(), 1);
let contacts = Contact::get_all(&context.ctx, 0, Some("alice")).unwrap();
assert_eq!(contacts.len(), 0);
let contacts = Contact::get_all(&context.ctx, 0, Some("alice")).unwrap();
assert_eq!(contacts.len(), 0);
}
}
#[test]
fn test_chat() {
let context = create_test_context();
let contact1 = Contact::create(&context.ctx, "bob", "bob@mail.de").unwrap();
assert_ne!(contact1, 0);
unsafe {
let context = create_test_context();
let contact1 = Contact::create(&context.ctx, "bob", "bob@mail.de").unwrap();
assert_ne!(contact1, 0);
let chat_id = chat::create_by_contact_id(&context.ctx, contact1).unwrap();
assert!(chat_id > 9, "chat_id too small {}", chat_id);
let chat = Chat::load_from_db(&context.ctx, chat_id).unwrap();
let chat_id = chat::create_by_contact_id(&context.ctx, contact1).unwrap();
assert!(chat_id > 9, "chat_id too small {}", chat_id);
let chat = Chat::load_from_db(&context.ctx, chat_id).unwrap();
let chat2_id = chat::create_by_contact_id(&context.ctx, contact1).unwrap();
assert_eq!(chat2_id, chat_id);
let chat2 = Chat::load_from_db(&context.ctx, chat2_id).unwrap();
let chat2_id = chat::create_by_contact_id(&context.ctx, contact1).unwrap();
assert_eq!(chat2_id, chat_id);
let chat2 = Chat::load_from_db(&context.ctx, chat2_id).unwrap();
assert_eq!(chat2.name, chat.name);
assert_eq!(chat2.name, chat.name);
}
}
#[test]
fn test_wrong_db() {
unsafe {
let mut ctx = dc_context_new(Some(cb), std::ptr::null_mut(), None);
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
std::fs::write(&dbfile, b"123").unwrap();
let res = dc_open(&mut ctx, dbfile.to_str().unwrap(), None);
assert!(!res);
}
}