Compare commits

..

17 Commits

Author SHA1 Message Date
Simon Laux
5b5c900cb9 also export qoutes, message state and is_forwarded 2021-06-01 23:58:04 +02:00
Simon Laux
b87ca6e747 restructure code 2021-06-01 23:58:04 +02:00
Simon Laux
850f7e1174 output daymarker and message error 2021-06-01 23:58:04 +02:00
Simon Laux
9f2b5feda2 move getting and writing message info to packing-
step, this could decrease memory usage
2021-06-01 23:58:04 +02:00
Simon Laux
b8cbcc6648 add export_timestamp to chat.json 2021-06-01 23:58:04 +02:00
Simon Laux
584d28f807 update code that it compiles with current core
make contact deduping more reliable
2021-06-01 23:58:04 +02:00
Simon Laux
241111470f export locations 2021-06-01 23:58:03 +02:00
Simon Laux
4bf07ccc71 repl export-chat-requires destination path
and combind the public accessible functions into one
2021-06-01 23:58:03 +02:00
Simon Laux
897d2f4a08 add some ducumentation 2021-06-01 23:58:03 +02:00
Simon Laux
a81096aa36 rename files in exported zip 2021-06-01 23:58:03 +02:00
Simon Laux
b1c9342631 save raw mime headers under the eml
extention instead of the text extention
2021-06-01 23:58:03 +02:00
Simon Laux
0c8aad2102 export message mime-headers when availible 2021-06-01 23:58:03 +02:00
Simon Laux
82253e1e30 fix deduplication of blob files 2021-06-01 23:58:03 +02:00
Simon Laux
aa953687bf add message info 2021-06-01 23:58:03 +02:00
Simon Laux
9f2f2ca1c0 [works again] export json, adjust to work in async
and remove html stuff
2021-06-01 23:58:03 +02:00
Simon Laux
54637004cd [still broken]* A bit progress on:
- transforming the export format to json
- async
- cleanup cod
(*broken state, just a commit to save progress)
2021-06-01 23:58:03 +02:00
Simon Laux
da9f45d9ff state of previous export chat pr 2021-06-01 23:58:01 +02:00
108 changed files with 3556 additions and 6285 deletions

77
.circleci/config.yml Normal file
View File

@@ -0,0 +1,77 @@
version: 2.1
executors:
default:
docker:
- image: filecoin/rust:latest
working_directory: /mnt/crate
doxygen:
docker:
- image: hrektts/doxygen
jobs:
build_doxygen:
executor: doxygen
steps:
- checkout
- run: bash scripts/run-doxygen.sh
- run: mkdir -p workspace/c-docs
- run: cp -av deltachat-ffi/{html,xml} workspace/c-docs/
- persist_to_workspace:
root: workspace
paths:
- c-docs
remote_python_packaging:
machine: true
steps:
- checkout
# the following commands on success produces
# workspace/{wheelhouse,py-docs} as artefact directories
- run:
# building aarch64 packages under qemu is very slow
no_output_timeout: 60m
command: bash scripts/remote_python_packaging.sh
- persist_to_workspace:
root: workspace
paths:
# - c-docs
- py-docs
- wheelhouse
upload_docs_wheels:
machine: true
steps:
- checkout
- attach_workspace:
at: workspace
- run: ls -laR workspace
- run: scripts/ci_upload.sh workspace/py-docs workspace/wheelhouse workspace/c-docs
workflows:
version: 2.1
test:
jobs:
- remote_python_packaging:
filters:
tags:
only: /.+/
branches:
ignore: /.*/
- build_doxygen:
filters:
tags:
only: /.+/
branches:
ignore: /.*/
- upload_docs_wheels:
requires:
- remote_python_packaging
- build_doxygen
filters:
tags:
only: /.+/
branches:
ignore: /.*/

View File

@@ -18,7 +18,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
toolchain: 1.50.0
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
@@ -32,7 +32,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
toolchain: 1.50.0
components: clippy
override: true
- uses: actions-rs/clippy-check@v1
@@ -68,23 +68,12 @@ jobs:
strategy:
matrix:
include:
# Currently used Rust version, same as in `rust-toolchain` file.
- os: ubuntu-latest
rust: 1.54.0
python: 3.9
rust: 1.50.0
python: 3.6
- os: windows-latest
rust: 1.54.0
rust: 1.50.0
python: false # Python bindings compilation on Windows is not supported.
# Minimum Supported Rust Version = 1.48.0
# This is the Debian "bullseye" release version of Rust.
#
# Minimum Supported Python Version = 3.7
# This is the minimum version for which manylinux Python wheels are
# built.
- os: ubuntu-latest
rust: 1.48.0
python: 3.7
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@master

View File

@@ -1,158 +1,5 @@
# Changelog
## 1.59.0
### Added
- add quota information to `dc_get_connectivity_html()`
### Changes
- refactorings #2592 #2570 #2581
- add 'device chat about' to now existing status #2613
- update provider database #2608
### Fixes
- provider database supports socket=PLAIN and dotless domains now #2604 #2608
- add migrated accounts to events emitter #2607
- fix forwarding quote-only mails #2600
- do not set WantsMdn param for outgoing messages #2603
- set timestamps for system messages #2593
- do not treat gmail labels as folders #2587
- avoid timing problems in `dc_maybe_network_lost()` #2551
- only set smtp to "connected" if the last message was actually sent #2541
## 1.58.0
### Fixes
- move WAL file together with database
and avoid using data if the database was not closed correctly before #2583
## 1.57.0
### API Changes
- breaking change: removed deaddrop chat #2514 #2563
Contact request chats are not merged into a single virtual
"deaddrop" chat anymore. Instead, they are shown in the chatlist the
same way as other chats, but sending of messages to them is not
allowed and MDNs are not sent automatically until the chat is
"accepted" by the user.
New API:
- `dc_chat_is_contact_request()`: returns true if chat is a contact
request. In this case an option to accept the chat via
`dc_accept_chat()` should be shown in the UI.
- `dc_accept_chat()`: unblock the chat or accept contact request
- `dc_block_chat()`: block the chat, currently works only for mailing
lists.
Removed API:
- `dc_create_chat_by_msg_id()`: deprecated 2021-02-07 in favor of
`dc_decide_on_contact_request()`
- `dc_marknoticed_contact()`: deprecated 2021-02-07 in favor of
`dc_decide_on_contact_request()`
- `dc_decide_on_contact_request()`: this call requires a message ID
from deaddrop chat as input. As deaddrop chat is removed, this
call can't be used anymore.
- `dc_msg_get_real_chat_id()`: use `dc_msg_get_chat_id()` instead, the
only difference between these calls was in handling of deaddrop
chat
- removed `DC_CHAT_ID_DEADDROP` and `DC_STR_DEADDROP` constants
- breaking change: removed `DC_EVENT_ERROR_NETWORK` and `DC_STR_SERVER_RESPONSE`
Instead, there is a new api `dc_get_connectivity()`
and `dc_get_connectivity_html()`;
`DC_EVENT_CONNECTIVITY_CHANGED` is emitted on changes
- breaking change: removed `dc_accounts_import_account()`
Instead you need to add an account and call `dc_imex(DC_IMEX_IMPORT_BACKUP)`
on its context
- update account api, 2 new methods:
`int dc_all_work_done (dc_context_t* context);`
`int dc_accounts_all_work_done (dc_accounts_t* accounts);`
- add api to check if a message was `Auto-Submitted`
cffi: `int dc_msg_is_bot (const dc_msg_t* msg);`
python: `Message.is_bot()`
- `dc_context_t* dc_accounts_get_selected_account (dc_accounts_t* accounts);`
now returns `NULL` if there is no selected account
- added `dc_accounts_maybe_network_lost()` for systems core cannot find out
connectivity loss on its own (eg. iOS) #2550
### Added
- use Auto-Submitted: auto-generated header to identify bots #2502
- allow sending stickers via repl tool
- chat: make `get_msg_cnt()` and `get_fresh_msg_cnt()` work for deaddrop chat #2493
- withdraw/revive own qr-codes #2512
- add Connectivity view (a better api for getting the connection status) #2319 #2549 #2542
### Changes
- updated spec: new `Chat-User-Avatar` usage, `Chat-Content: sticker`, structure, copyright year #2480
- update documentation #2548 #2561 #2569
- breaking: `Accounts::create` does not also create an default account anymore #2500
- remove "forwarded" from stickers, as the primary way of getting stickers
is by asking a bot and then forwarding them currently #2526
- mimeparser: use mailparse to parse RFC 2231 filenames #2543
- allow email addresses without dot in the domain part #2112
- allow installing lib and include under different prefixes #2558
- remove counter from name provided by `DC_CHAT_ID_ARCHIVED_LINK` #2566
- improve tests #2487 #2491 #2497
- refactorings #2492 #2503 #2504 #2506 #2515 #2520 #2567 #2575 #2577 #2579
- improve ci #2494
- update provider-database #2565
### Removed
- remove `dc_accounts_import_account()` api #2521
- remove `DC_EVENT_ERROR_NETWORK` and `DC_STR_SERVER_RESPONSE` #2319
### Fixes
- allow stickers with gif-images #2481
- fix database migration #2486
- do not count hidden messages in get_msg_cnt(). #2493
- improve drafts detection #2489
- fix panic when removing last, selected account from account manager #2500
- set_draft's message-changed-event returns now draft's msg id instead of 0 #2304
- avoid hiding outgoing classic emails #2505
- fixes for message timestamps #2517
- do not process names, avatars, location XMLs, message signature etc.
for duplicate messages #2513
- fix `can_send` for users not in group #2479
- fix receiving events for accounts added by `dc_accounts_add_account()` #2559
- fix which chats messages are assigned to #2465
- fix: don't create chats when MDNs are received #2578
## 1.56.0
- fix downscaling images #2469
- fix outgoing messages popping up in selfchat #2456
- securejoin: display error reason if there is any #2470
- do not allow deleting contacts with ongoing chats #2458
- fix: ignore drafts folder when scanning #2454
- fix: scan folders also when inbox is not watched #2446
- more robust In-Reply-To parsing #2182
- update dependencies #2441 #2438 #2439 #2440 #2447 #2448 #2449 #2452 #2453 #2460 #2464 #2466
- update provider-database #2471
- refactorings #2459 #2457
- improve tests and ci #2445 #2450 #2451
## 1.55.0
- fix panic when receiving some HTML messages #2434

View File

@@ -8,11 +8,7 @@ add_custom_command(
"target/release/libdeltachat.a"
"target/release/libdeltachat.so"
"target/release/pkgconfig/deltachat.pc"
COMMAND
PREFIX=${CMAKE_INSTALL_PREFIX}
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
${CARGO} build --release --no-default-features
COMMAND PREFIX=${CMAKE_INSTALL_PREFIX} ${CARGO} build --release --no-default-features
# Build in `deltachat-ffi` directory instead of using
# `--package deltachat_ffi` to avoid feature resolver version

256
Cargo.lock generated
View File

@@ -1,7 +1,5 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "addr2line"
version = "0.15.1"
@@ -140,9 +138,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.42"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486"
checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b"
[[package]]
name = "arrayvec"
@@ -238,7 +236,8 @@ dependencies = [
[[package]]
name = "async-imap"
version = "0.5.0"
source = "git+https://github.com/async-email/async-imap#4ce7da455618c387b87b2905a80935107bc69afc"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbb2df4b37a99456360a9ab475b723e3a499d51e060ab1bdd8d7565d23dcb74b"
dependencies = [
"async-native-tls",
"async-std",
@@ -249,7 +248,7 @@ dependencies = [
"imap-proto",
"lazy_static",
"log",
"nom 6.2.1",
"nom 5.1.2",
"pin-utils",
"rental",
"stop-token",
@@ -324,19 +323,19 @@ dependencies = [
[[package]]
name = "async-smtp"
version = "0.4.0"
source = "git+https://github.com/async-email/async-smtp?branch=master#2c21f5fb643e9a24c1097f13db4dfcd7818ada06"
version = "0.3.4"
source = "git+https://github.com/async-email/async-smtp?rev=2275fd8d13e39b2c58d6605c786ff06ff9e05708#2275fd8d13e39b2c58d6605c786ff06ff9e05708"
dependencies = [
"async-native-tls",
"async-std",
"async-trait",
"base64 0.13.0",
"base64 0.12.3",
"bufstream",
"fast-socks5",
"fast_chemail",
"hostname 0.1.5",
"log",
"nom 5.1.2",
"pin-project 1.0.5",
"pin-project 0.4.27",
"pin-utils",
"serde",
"serde_derive",
@@ -518,21 +517,9 @@ checksum = "46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719"
[[package]]
name = "bitflags"
version = "1.3.1"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2da1976d75adbe5fbc88130ecd119529cf1cc6a93ae1546d8696ee66f0d21af1"
[[package]]
name = "bitvec"
version = "0.19.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321"
dependencies = [
"funty",
"radium",
"tap",
"wyz",
]
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "block-buffer"
@@ -650,6 +637,27 @@ version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bzip2"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42b7c3cbf0fa9c1b82308d57191728ca0256cb821220f4e2fd410a72ade26e3b"
dependencies = [
"bzip2-sys",
"libc",
]
[[package]]
name = "bzip2-sys"
version = "0.1.10+1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17fa3d1ac1ca21c5c4e36a97f3c3eb25084576f6fc47bf0139c1123434216c6c"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]]
name = "cache-padded"
version = "1.1.1"
@@ -839,9 +847,9 @@ checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
[[package]]
name = "cpufeatures"
version = "0.1.5"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef"
checksum = "dec1028182c380cc45a2e2c5ec841134f2dfd0f8f5f0a5bcd68004f81b5efdf4"
dependencies = [
"libc",
]
@@ -878,7 +886,7 @@ dependencies = [
"clap",
"criterion-plot",
"csv",
"itertools 0.10.1",
"itertools 0.10.0",
"lazy_static",
"num-traits",
"oorandom",
@@ -1121,7 +1129,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.59.0"
version = "1.55.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -1136,6 +1144,7 @@ dependencies = [
"base64 0.13.0",
"bitflags",
"byteorder",
"charset",
"chrono",
"criterion",
"deltachat_derive",
@@ -1143,14 +1152,12 @@ dependencies = [
"email",
"encoded-words",
"escaper",
"fast-socks5",
"futures",
"futures-lite",
"hex",
"humansize",
"image",
"indexmap",
"itertools 0.10.1",
"itertools 0.10.0",
"kamadak-exif",
"lettre_email",
"libc",
@@ -1189,6 +1196,7 @@ dependencies = [
"toml",
"url",
"uuid",
"zip",
]
[[package]]
@@ -1201,7 +1209,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.59.0"
version = "1.55.0"
dependencies = [
"anyhow",
"async-std",
@@ -1517,19 +1525,6 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fast-socks5"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c1955b65d95243f547eb1d1ee6e5ce75ecf6daaeb72b08cd6c66e549d6d88e1"
dependencies = [
"anyhow",
"async-std",
"futures",
"log",
"thiserror",
]
[[package]]
name = "fast_chemail"
version = "0.9.6"
@@ -1613,17 +1608,11 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "funty"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
[[package]]
name = "futures"
version = "0.3.16"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1adc00f486adfc9ce99f77d717836f0c5aa84965eb0b4f051f4e83f7cab53f8b"
checksum = "0e7e43a803dae2fa37c1f6a8fe121e1f7bf9548b4dfc0522a42f34145dadfc27"
dependencies = [
"futures-channel",
"futures-core",
@@ -1636,9 +1625,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.16"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9"
checksum = "e682a68b29a882df0545c143dc3646daefe80ba479bcdede94d5a703de2871e2"
dependencies = [
"futures-core",
"futures-sink",
@@ -1646,15 +1635,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.16"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99"
checksum = "0402f765d8a89a26043b889b26ce3c4679d268fa6bb22cd7c6aad98340e179d1"
[[package]]
name = "futures-executor"
version = "0.3.16"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d0d535a57b87e1ae31437b892713aee90cd2d7b0ee48727cd11fc72ef54761c"
checksum = "badaa6a909fac9e7236d0620a2f57f7664640c56575b71a7552fbd68deafab79"
dependencies = [
"futures-core",
"futures-task",
@@ -1663,15 +1652,15 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.16"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582"
checksum = "acc499defb3b348f8d8f3f66415835a9131856ff7714bf10dadfc4ec4bdb29a1"
[[package]]
name = "futures-lite"
version = "1.12.0"
version = "1.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48"
checksum = "b4481d0cd0de1d204a4fa55e7d45f07b1d958abcb06714b3446438e2eff695fb"
dependencies = [
"fastrand",
"futures-core",
@@ -1684,9 +1673,9 @@ dependencies = [
[[package]]
name = "futures-macro"
version = "0.3.16"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c54913bae956fb8df7f4dc6fc90362aa72e69148e3f39041fbe8742d21e0ac57"
checksum = "a4c40298486cdf52cc00cd6d6987892ba502c7656a16a4192a9992b1ccedd121"
dependencies = [
"autocfg 1.0.1",
"proc-macro-hack",
@@ -1697,21 +1686,21 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.16"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0f30aaa67363d119812743aa5f33c201a7a66329f97d1a887022971feea4b53"
checksum = "a57bead0ceff0d6dde8f465ecd96c9338121bb7717d3e7b108059531870c4282"
[[package]]
name = "futures-task"
version = "0.3.16"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2"
checksum = "8a16bef9fc1a4dddb5bee51c989e3fbba26569cbb0e31f5b303c184e3dd33dae"
[[package]]
name = "futures-util"
version = "0.3.16"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78"
checksum = "feb5c238d27e2bf94ffdfd27b2c29e3df4a68c4193bb6427384259e2bf191967"
dependencies = [
"autocfg 1.0.1",
"futures-channel",
@@ -1805,6 +1794,12 @@ version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62aca2aba2d62b4a7f5b33f3712cb1b0692779a56fb510499d5c0aa594daeaf3"
[[package]]
name = "hashbrown"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
[[package]]
name = "hashbrown"
version = "0.11.2"
@@ -1820,7 +1815,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf"
dependencies = [
"hashbrown",
"hashbrown 0.11.2",
]
[[package]]
@@ -1949,12 +1944,6 @@ dependencies = [
"uuid",
]
[[package]]
name = "humansize"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026"
[[package]]
name = "humantime"
version = "1.3.0"
@@ -2000,21 +1989,21 @@ dependencies = [
[[package]]
name = "imap-proto"
version = "0.14.3"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ad9b46a79efb6078e578ae04e51463d7c3e8767864687f7e63095b3cbefafbb"
checksum = "3091b99ee5b80f9b010eb6f962af9495ad06561bf662126b077e8ca30e463182"
dependencies = [
"nom 6.2.1",
"nom 5.1.2",
]
[[package]]
name = "indexmap"
version = "1.7.0"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3"
dependencies = [
"autocfg 1.0.1",
"hashbrown",
"hashbrown 0.9.1",
]
[[package]]
@@ -2061,9 +2050,9 @@ dependencies = [
[[package]]
name = "itertools"
version = "0.10.1"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf"
checksum = "37d572918e350e82412fe766d24b15e6682fb2ed2bbe018280caa810397cb319"
dependencies = [
"either",
]
@@ -2161,9 +2150,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.98"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790"
checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36"
[[package]]
name = "libm"
@@ -2218,9 +2207,9 @@ dependencies = [
[[package]]
name = "mailparse"
version = "0.13.5"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06f526fc13a50f46a3689a6f438cb833c59817c898bb40a3954f341ddf74ce1"
checksum = "62db73ff1a42b0e3a8858cf0d5c183bdfc23491f7294ae4a8200c83577457386"
dependencies = [
"base64 0.13.0",
"charset",
@@ -2382,19 +2371,6 @@ dependencies = [
"version_check 0.9.3",
]
[[package]]
name = "nom"
version = "6.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c5c51b9083a3c620fa67a2a635d1ce7d95b897e957d6b28ff9a5da960a103a6"
dependencies = [
"bitvec",
"funty",
"lexical-core",
"memchr",
"version_check 0.9.3",
]
[[package]]
name = "num-bigint"
version = "0.2.6"
@@ -2495,9 +2471,9 @@ checksum = "1a5b3dd1c072ee7963717671d1ca129f1048fda25edea6b752bfc71ac8854170"
[[package]]
name = "once_cell"
version = "1.8.0"
version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3"
[[package]]
name = "oorandom"
@@ -2926,12 +2902,6 @@ dependencies = [
"rusqlite",
]
[[package]]
name = "radium"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8"
[[package]]
name = "radix_trie"
version = "0.2.1"
@@ -3350,9 +3320,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "serde"
version = "1.0.127"
version = "1.0.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8"
checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03"
dependencies = [
"serde_derive",
]
@@ -3369,9 +3339,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.127"
version = "1.0.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc"
checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43"
dependencies = [
"proc-macro2",
"quote",
@@ -3380,9 +3350,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.66"
version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127"
checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
dependencies = [
"itoa",
"ryu",
@@ -3415,9 +3385,9 @@ dependencies = [
[[package]]
name = "sha-1"
version = "0.9.7"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a0c8611594e2ab4ebbf06ec7cbbf0a99450b8570e96cbf5188b5d5f6ef18d81"
checksum = "8c4cfa741c5832d0ef7fab46cabed29c2aae926db0b11bb2069edd8db5e64e16"
dependencies = [
"block-buffer",
"cfg-if 1.0.0",
@@ -3645,15 +3615,15 @@ checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
[[package]]
name = "strum"
version = "0.21.0"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2"
checksum = "7318c509b5ba57f18533982607f24070a55d353e90d4cae30c467cdb2ad5ac5c"
[[package]]
name = "strum_macros"
version = "0.21.1"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec"
checksum = "ee8bc6b87a5112aeeab1f4a9f7ab634fe6cbefc4850006df31267f4cfb9e3149"
dependencies = [
"heck",
"proc-macro2",
@@ -3688,9 +3658,9 @@ dependencies = [
[[package]]
name = "syn"
version = "1.0.74"
version = "1.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c"
checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82"
dependencies = [
"proc-macro2",
"quote",
@@ -3709,12 +3679,6 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tempfile"
version = "3.2.0"
@@ -3749,18 +3713,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.26"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2"
checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.26"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745"
checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d"
dependencies = [
"proc-macro2",
"quote",
@@ -4230,12 +4194,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "wyz"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
[[package]]
name = "x25519-dalek"
version = "1.1.0"
@@ -4276,3 +4234,17 @@ dependencies = [
"syn",
"synstructure",
]
[[package]]
name = "zip"
version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c83dc9b784d252127720168abd71ea82bf8c3d96b17dc565b5e2a02854f2b27"
dependencies = [
"byteorder",
"bzip2",
"crc32fast",
"flate2",
"thiserror",
"time 0.1.44",
]

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.59.0"
version = "1.55.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"
@@ -15,38 +15,39 @@ lto = true
deltachat_derive = { path = "./deltachat_derive" }
ansi_term = { version = "0.12.1", optional = true }
anyhow = "1.0.42"
async-imap = { git = "https://github.com/async-email/async-imap" }
anyhow = "1.0.40"
async-imap = "0.5.0"
async-native-tls = { version = "0.3.3" }
async-smtp = { git = "https://github.com/async-email/async-smtp", branch="master", features = ["socks5"] }
async-smtp = { git = "https://github.com/async-email/async-smtp", rev="2275fd8d13e39b2c58d6605c786ff06ff9e05708" }
async-std-resolver = "0.20.3"
async-std = { version = "~1.9.0", features = ["unstable"] }
async-tar = "0.3.0"
async-trait = "0.1.50"
backtrace = "0.3.59"
base64 = "0.13"
bitflags = "1.3.1"
bitflags = "1.1.0"
byteorder = "1.3.1"
charset = "0.1"
chrono = "0.4.6"
dirs = { version = "3.0.2", optional=true }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
escaper = "0.1.1"
futures = "0.3.16"
futures = "0.3.15"
hex = "0.4.0"
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
indexmap = "1.7.0"
itertools = "0.10.1"
indexmap = "1.3.0"
itertools = "0.10.0"
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = "0.2.98"
libc = "0.2.95"
log = {version = "0.4.8", optional = true }
mailparse = "0.13.5"
mailparse = "0.13.4"
native-tls = "0.2.3"
num_cpus = "1.13.0"
num-derive = "0.3.0"
num-traits = "0.2.6"
once_cell = "1.8.0"
once_cell = "1.4.1"
percent-encoding = "2.0"
pgp = { version = "0.7.0", default-features = false }
pretty_env_logger = { version = "0.4.0", optional = true }
@@ -61,25 +62,24 @@ rustyline = { version = "8.2.0", optional = true }
sanitize-filename = "0.3.0"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
sha-1 = "0.9.7"
sha-1 = "0.9.6"
sha2 = "0.9.5"
smallvec = "1.0.0"
stop-token = "0.2.0"
strum = "0.21.0"
strum_macros = "0.21.1"
strum = "0.20.0"
strum_macros = "0.20.1"
surf = { version = "2.0.0-alpha.4", default-features = false, features = ["h1-client"] }
thiserror = "1.0.26"
thiserror = "1.0.25"
toml = "0.5.6"
url = "2.2.2"
uuid = { version = "0.8", features = ["serde", "v4"] }
fast-socks5 = "0.4.2"
humansize = "1.1.1"
zip = "0.5.12"
[dev-dependencies]
ansi_term = "0.12.0"
async-std = { version = "1.9.0", features = ["unstable", "attributes"] }
criterion = "0.3"
futures-lite = "1.12.0"
futures-lite = "1.7.0"
log = "0.4.11"
pretty_assertions = "0.7.2"
pretty_env_logger = "0.4.0"
@@ -116,7 +116,7 @@ name = "search_msgs"
harness = false
[features]
default = ["vendored"]
default = []
internals = []
repl = ["internals", "rustyline", "log", "pretty_env_logger", "ansi_term", "dirs"]
vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored", "rusqlite/bundled"]

View File

@@ -3,6 +3,7 @@
> Deltachat-core written in Rust
[![Rust CI](https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml/badge.svg)](https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml)
[![CircleCI](https://circleci.com/gh/deltachat/deltachat-core-rust.svg?style=shield)](https://circleci.com/gh/deltachat/deltachat-core-rust/)
## Installing Rust and Cargo
@@ -79,16 +80,6 @@ For more commands type:
> help
```
## Installing libdeltachat system wide
```
$ git clone https://github.com/deltachat/deltachat-core-rust.git
$ cd deltachat-core-rust
$ cmake -B build . -DCMAKE_INSTALL_PREFIX=/usr
$ cmake --build build
$ sudo cmake --install build
```
## Development
```sh

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.59.0"
version = "1.55.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
@@ -21,8 +21,8 @@ human-panic = "1.0.1"
num-traits = "0.2.6"
serde_json = "1.0"
async-std = "1.9.0"
anyhow = "1.0.42"
thiserror = "1.0.26"
anyhow = "1.0.40"
thiserror = "1.0.25"
rand = "0.7.3"
[features]

View File

@@ -23,13 +23,11 @@ fn main() {
version = env::var("CARGO_PKG_VERSION").unwrap(),
libs_priv = libs_priv,
prefix = env::var("PREFIX").unwrap_or_else(|_| "/usr/local".to_string()),
libdir = env::var("LIBDIR").unwrap_or_else(|_| "/usr/local/lib".to_string()),
includedir = env::var("INCLUDEDIR").unwrap_or_else(|_| "/usr/local/include".to_string()),
);
fs::create_dir_all(target_path.join("pkgconfig")).unwrap();
fs::File::create(target_path.join("pkgconfig").join("deltachat.pc"))
.unwrap()
.write_all(pkg_config.as_bytes())
.write_all(&pkg_config.as_bytes())
.unwrap();
}

View File

@@ -271,11 +271,6 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `send_port` = SMTP-port, guessed if left out
* - `send_security`= SMTP-socket, one of @ref DC_SOCKET, defaults to #DC_SOCKET_AUTO
* - `server_flags` = IMAP-/SMTP-flags as a combination of @ref DC_LP flags, guessed if left out
* - `socks5_enabled` = SOCKS5 enabled
* - `socks5_host` = SOCKS5 proxy server host
* - `socks5_port` = SOCKS5 proxy server port
* - `socks5_user` = SOCKS5 proxy username
* - `socks5_password` = SOCKS5 proxy password
* - `imap_certificate_checks` = how to check IMAP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
* - `smtp_certificate_checks` = how to check SMTP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
* - `displayname` = Own name to use when sending messages. MUAs are allowed to spread this way e.g. using CC, defaults to empty
@@ -310,7 +305,7 @@ char* dc_get_blobdir (const dc_context_t* context);
* DC_SHOW_EMAILS_ACCEPTED_CONTACTS (1)=
* also show all mails of confirmed contacts,
* DC_SHOW_EMAILS_ALL (2)=
* also show mails of unconfirmed contacts.
* also show mails of unconfirmed contacts in the deaddrop.
* - `key_gen_type` = DC_KEY_GEN_DEFAULT (0)=
* generate recommended key type (default),
* DC_KEY_GEN_RSA2048 (1)=
@@ -345,9 +340,7 @@ char* dc_get_blobdir (const dc_context_t* context);
* https://github.com/cracker0dks/basicwebrtc which some UIs have native support for.
* The type `jitsi:` may be handled by external apps.
* If no type is prefixed, the videochat is handled completely in a browser.
* - `bot` = Set to "1" if this is a bot.
* Prevents adding the "Device messages" and "Saved messages" chats,
* adds Auto-Submitted header to outgoing messages.
* - `bot` = Set to "1" if this is a bot. E.g. prevents adding the "Device messages" and "Saved messages" chats.
* - `fetch_existing_msgs` = 1=fetch most recent existing messages on configure (default),
* 0=do not fetch existing messages on configure.
* In both cases, existing recipients are added to the contact database.
@@ -468,66 +461,11 @@ char* dc_get_info (const dc_context_t* context);
char* dc_get_oauth2_url (dc_context_t* context, const char* addr, const char* redirect_uri);
#define DC_CONNECTIVITY_NOT_CONNECTED 1000
#define DC_CONNECTIVITY_CONNECTING 2000
#define DC_CONNECTIVITY_WORKING 3000
#define DC_CONNECTIVITY_CONNECTED 4000
/**
* Get the current connectivity, i.e. whether the device is connected to the IMAP server.
* One of:
* - DC_CONNECTIVITY_NOT_CONNECTED (1000-1999): Show e.g. the string "Not connected" or a red dot
* - DC_CONNECTIVITY_CONNECTING (2000-2999): Show e.g. the string "Connecting…" or a yellow dot
* - DC_CONNECTIVITY_WORKING (3000-3999): Show e.g. the string "Getting new messages" or a spinning wheel
* - DC_CONNECTIVITY_CONNECTED (>=4000): Show e.g. the string "Connected" or a green dot
*
* We don't use exact values but ranges here so that we can split up
* states into multiple states in the future.
*
* Meant as a rough overview that can be shown
* e.g. in the title of the main screen.
*
* If the connectivity changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
*
* @memberof dc_context_t
* @param context The context object.
* @return The current connectivity.
*/
int dc_get_connectivity (dc_context_t* context);
/**
* Get an overview of the current connectivity, and possibly more statistics.
* Meant to give the user more insight about the current status than
* the basic connectivity info returned by dc_get_connectivity(); show this
* e.g., if the user taps on said basic connectivity info.
*
* If this page changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
*
* This comes as an HTML from the core so that we can easily improve it
* and the improvement instantly reaches all UIs.
*
* @memberof dc_context_t
* @param context The context object.
* @return An HTML page with some info about the current connectivity and status.
*/
char* dc_get_connectivity_html (dc_context_t* context);
/**
* Standalone version of dc_accounts_all_work_done().
* Only used by the python tests.
*/
int dc_all_work_done (dc_context_t* context);
// connect
/**
* Configure a context.
* During configuration IO must not be started,
* if needed stop IO using dc_accounts_stop_io() or dc_stop_io() first.
* During configuration IO must not be started, if needed stop IO using dc_stop_io() first.
* If the context is already configured,
* this function will try to change the configuration.
*
@@ -687,6 +625,12 @@ int dc_preconfigure_keypair (dc_context_t* context, const cha
*
* By default, the function adds some special entries to the list.
* These special entries can be identified by the ID returned by dc_chatlist_get_chat_id():
* - DC_CHAT_ID_DEADDROP (1) - this special chat is present if there are
* messages from addresses that have no relationship to the configured account.
* The last of these messages is represented by DC_CHAT_ID_DEADDROP and you can retrieve details
* about it with dc_chatlist_get_msg_id(). Typically, the UI asks the user "Do you want to chat with NAME?"
* and offers the options "Start chat", "Block" or "Not now".
* Call dc_decide_on_contact_request() when the user selected one of these options.
* - DC_CHAT_ID_ARCHIVED_LINK (6) - this special chat is present if the user has
* archived _any_ chat using dc_set_chat_visibility(). The UI should show a link as
* "Show archived chats", if the user clicks this item, the UI should show a
@@ -704,10 +648,10 @@ int dc_preconfigure_keypair (dc_context_t* context, const cha
* the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are _any_ archived
* chats
* - the flag DC_GCL_FOR_FORWARDING sorts "Saved messages" to the top of the chatlist
* and hides the "Device chat" and contact requests.
* and hides the "Device chat" and the deaddrop.
* typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
* to also hide the archive link.
* - if the flag DC_GCL_NO_SPECIALS is set, archive link is not added
* - if the flag DC_GCL_NO_SPECIALS is set, deaddrop and archive link are not added
* to the list (may be used e.g. for selecting chats on forwarding, the flag is
* not needed when DC_GCL_ARCHIVED_ONLY is already set)
* - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT
@@ -727,12 +671,42 @@ dc_chatlist_t* dc_get_chatlist (dc_context_t* context, int flags,
// handle chats
/**
* Create a normal chat or a group chat by a messages ID that comes typically
* from the deaddrop, DC_CHAT_ID_DEADDROP (1).
*
* If the given message ID already belongs to a normal chat or to a group chat,
* the chat ID of this chat is returned and no new chat is created.
* If a new chat is created, the given message ID is moved to this chat, however,
* there may be more messages moved to the chat from the deaddrop. To get the
* chat messages, use dc_get_chat_msgs().
*
* If the user is asked before creation, he should be
* asked whether he wants to chat with the _contact_ belonging to the message;
* the group names may be really weird when taken from the subject of implicit
* groups and this may look confusing.
*
* Moreover, this function also scales up the origin of the contact belonging
* to the message and, depending on the contacts origin, messages from the
* same group may be shown or not - so, all in all, it is fine to show the
* contact name only.
*
* @deprecated Deprecated 2021-02-07, use dc_decide_on_contact_request() instead
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param msg_id The message ID to create the chat for.
* @return The created or reused chat ID on success. 0 on errors.
*/
uint32_t dc_create_chat_by_msg_id (dc_context_t* context, uint32_t msg_id);
/**
* Create a normal chat with a single user. To create group chats,
* see dc_create_group_chat().
*
* If a chat already exists, this ID is returned, otherwise a new chat is created;
* to get the chat messages, use dc_get_chat_msgs().
* this new chat may already contain messages, e.g. from the deaddrop, to get the
* chat messages, use dc_get_chat_msgs().
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
@@ -1104,7 +1078,7 @@ int dc_estimate_deletion_cnt (dc_context_t* context, int from_ser
* or badge counters eg. on the app-icon.
* The list is already sorted and starts with the most recent fresh message.
*
* Messages belonging to muted chats or to the contact requests are not returned;
* Messages belonging to muted chats or to the deaddrop are not returned;
* these messages should not be notified
* and also badge counters should not include these messages.
*
@@ -1130,7 +1104,8 @@ dc_array_t* dc_get_fresh_msgs (dc_context_t* context);
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The chat ID of which all messages should be marked as being noticed.
* @param chat_id The chat ID of which all messages should be marked as being noticed
* (this also works for the virtual chat ID DC_CHAT_ID_DEADDROP).
*/
void dc_marknoticed_chat (dc_context_t* context, uint32_t chat_id);
@@ -1239,31 +1214,6 @@ void dc_set_chat_visibility (dc_context_t* context, uint32_t ch
*/
void dc_delete_chat (dc_context_t* context, uint32_t chat_id);
/**
* Block a chat.
*
* Blocking 1:1 chats blocks the corresponding contact. Blocking
* mailing lists creates a pseudo-contact in the list of blocked
* contacts, so blocked mailing lists can be discovered and unblocked
* the same way as the contacts. Blocking group chats deletes the
* chat without blocking any contacts, so it may pop up again later.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The ID of the chat to block.
*/
void dc_block_chat (dc_context_t* context, uint32_t chat_id);
/**
* Accept a contact request chat.
*
* Use it to accept "contact request" chats as indicated by dc_chat_is_contact_request().
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The ID of the chat to accept.
*/
void dc_accept_chat (dc_context_t* context, uint32_t chat_id);
/**
* Get contact IDs belonging to a chat.
@@ -1275,6 +1225,8 @@ void dc_accept_chat (dc_context_t* context, uint32_t ch
* explicitly as it may happen that oneself gets removed from a still existing
* group
*
* - for the deaddrop, the list is empty
*
* - for mailing lists, the behavior is not documented currently, we will decide on that later.
* for now, the UI should not show the list for mailing lists.
* (we do not know all members and there is not always a global mailing list address,
@@ -1622,6 +1574,25 @@ void dc_delete_msgs (dc_context_t* context, const uint3
void dc_forward_msgs (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt, uint32_t chat_id);
/**
* Mark all messages sent by the given contact as _noticed_.
* This function is typically used to ignore a user in the deaddrop temporarily ("Not now" button).
*
* The contact is expected to belong to the deaddrop;
* only one #DC_EVENT_MSGS_NOTICED with chat_id=DC_CHAT_ID_DEADDROP may be emitted.
*
* See also dc_marknoticed_chat() and dc_markseen_msgs()
*
* @deprecated Deprecated 2021-02-07, use dc_decide_on_contact_request() if the user just hit "Not now" on a button in the deaddrop,
* dc_marknoticed_chat() if the user has entered a chat
* and dc_markseen_msgs() if the user actually _saw_ a message.
* @memberof dc_context_t
* @param context The context object.
* @param contact_id The contact ID of which all messages should be marked as noticed.
*/
void dc_marknoticed_contact (dc_context_t* context, uint32_t contact_id);
/**
* Mark messages as presented to the user.
* Typically, UIs call this function on scrolling through the chatlist,
@@ -1633,12 +1604,12 @@ void dc_forward_msgs (dc_context_t* context, const uint3
* (if dc_set_config()-options `mdns_enabled` is set)
* and the internal state is changed to DC_STATE_IN_SEEN to reflect these actions.
*
* - For contact requests, no IMAP or MDNs is done
* and the internal state is not changed therefore.
* - For the deaddrop, no IMAP or MNDs is done
* and the internal change is not changed therefore.
* See also dc_marknoticed_chat().
*
* Moreover, timer is started for incoming ephemeral messages.
* This also happens for contact requests chats.
* This also happens for messages in the deaddrop.
*
* One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat.
*
@@ -1665,6 +1636,53 @@ void dc_markseen_msgs (dc_context_t* context, const uint3
dc_msg_t* dc_get_msg (dc_context_t* context, uint32_t msg_id);
#define DC_DECISION_START_CHAT 0
#define DC_DECISION_BLOCK 1
#define DC_DECISION_NOT_NOW 2
/**
* Call this when the user decided about a deaddrop message ("Do you want to chat with NAME?").
*
* Possible decisions are:
* - DC_DECISION_START_CHAT (0)
* - This will create a new chat and return the chat id.
* - DC_DECISION_BLOCK (1)
* - This will block the sender.
* - When a new message from the sender arrives,
* that will not result in a new contact request.
* - The blocked sender will be returned by dc_get_blocked_contacts()
* typically, the UI offers an option to unblock senders from there.
* - DC_DECISION_NOT_NOW (2)
* - This will mark all messages from this sender as noticed.
* - That the contact request is removed from the chat list.
* - When a new message from the sender arrives,
* a new contact request with the new message will pop up in the chatlist.
* - The contact request stays available in the explicit deaddrop.
* - If the contact request is already noticed, nothing happens.
*
* If the message belongs to a mailing list,
* the function makes sure that all messages
* from the mailing list are blocked or marked as noticed.
*
* The user should be asked whether they want to chat with the _contact_ belonging to the message;
* the group names may be really weird when taken from the subject of implicit (= ad-hoc)
* groups and this may look confusing. Moreover, this function also scales up the origin of the contact.
*
* If the chat belongs to a mailing list, you can also ask
* "Would you like to read MAILING LIST NAME?"
* (use dc_msg_get_real_chat_id() to get the chat-id for the contact request
* and then dc_chat_is_mailing_list(), dc_chat_get_name() and so on)
*
* @memberof dc_context_t
* @param context The context object.
* @param msg_id ID of Message to decide on.
* @param decision One of the DC_DECISION_* values.
* @return The chat id of the created chat, if any.
*/
uint32_t dc_decide_on_contact_request (dc_context_t* context, uint32_t msg_id, int decision);
// handle contacts
/**
@@ -1858,8 +1876,7 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
/**
* Import/export things.
* During backup import/export IO must not be started,
* if needed stop IO using dc_accounts_stop_io() or dc_stop_io() first.
* During backup import/export IO must not be started, if needed stop IO using dc_stop_io() first.
* What to do is defined by the _what_ parameter which may be one of the following:
*
* - **DC_IMEX_EXPORT_BACKUP** (11) - Export a backup to the directory given as `param1`.
@@ -2057,80 +2074,27 @@ void dc_stop_ongoing_process (dc_context_t* context);
#define DC_QR_TEXT 330 // text1=text
#define DC_QR_URL 332 // text1=URL
#define DC_QR_ERROR 400 // text1=error string
#define DC_QR_WITHDRAW_VERIFYCONTACT 500
#define DC_QR_WITHDRAW_VERIFYGROUP 502 // text1=groupname
#define DC_QR_REVIVE_VERIFYCONTACT 510
#define DC_QR_REVIVE_VERIFYGROUP 512 // text1=groupname
/**
* Check a scanned QR code.
* The function should be called after a QR code is scanned.
* The function takes the raw text scanned and checks what can be done with it.
*
* The UI is supposed to show the result to the user.
* In case there are further actions possible,
* the UI has to ask the user before doing further steps.
*
* The QR code state is returned in dc_lot_t::state as:
*
* - DC_QR_ASK_VERIFYCONTACT with dc_lot_t::id=Contact ID:
* ask whether to verify the contact;
* if so, start the protocol with dc_join_securejoin().
*
* - DC_QR_ASK_VERIFYGROUP withdc_lot_t::text1=Group name:
* ask whether to join the group;
* if so, start the protocol with dc_join_securejoin().
*
* - DC_QR_FPR_OK with dc_lot_t::id=Contact ID:
* contact fingerprint verified,
* ask the user if they want to start chatting;
* if so, call dc_create_chat_by_contact_id().
*
* - DC_QR_FPR_MISMATCH with dc_lot_t::id=Contact ID:
* scanned fingerprint does not match last seen fingerprint.
*
* - DC_QR_ASK_VERIFYCONTACT with dc_lot_t::id=Contact ID
* - DC_QR_ASK_VERIFYGROUP withdc_lot_t::text1=Group name
* - DC_QR_FPR_OK with dc_lot_t::id=Contact ID
* - DC_QR_FPR_MISMATCH with dc_lot_t::id=Contact ID
* - DC_QR_FPR_WITHOUT_ADDR with dc_lot_t::test1=Formatted fingerprint
* the scanned QR code contains a fingerprint but no email address;
* suggest the user to establish an encrypted connection first.
*
* - DC_QR_ACCOUNT dc_lot_t::text1=domain:
* ask the user if they want to create an account on the given domain,
* if so, call dc_set_config_from_qr() and then dc_configure().
*
* - DC_QR_WEBRTC_INSTANCE with dc_lot_t::text1=domain:
* ask the user if they want to use the given service for video chats;
* if so, call dc_set_config_from_qr().
*
* - DC_QR_ADDR with dc_lot_t::id=Contact ID:
* email-address scanned,
* ask the user if they want to start chatting;
* if so, call dc_create_chat_by_contact_id()
*
* - DC_QR_TEXT with dc_lot_t::text1=Text:
* Text scanned,
* ask the user eg. if they want copy to clipboard.
*
* - DC_QR_URL with dc_lot_t::text1=URL:
* URL scanned,
* ask the user eg. if they want to open a browser or copy to clipboard.
*
* - DC_QR_ERROR with dc_lot_t::text1=Error string:
* show the error to the user.
*
* - DC_QR_WITHDRAW_VERIFYCONTACT:
* ask the user if they want to withdraw the their own qr-code;
* if so, call dc_set_config_from_qr().
*
* - DC_QR_WITHDRAW_VERIFYGROUP with text1=groupname:
* ask the user if they want to withdraw the group-invite code;
* if so, call dc_set_config_from_qr().
*
* - DC_QR_REVIVE_VERIFYCONTACT:
* ask the user if they want to revive their withdrawn qr-code;
* if so, call dc_set_config_from_qr().
*
* - DC_QR_REVIVE_VERIFYGROUP with text1=groupname:
* ask the user if they want to revive the withdrawn group-invite code;
* if so, call dc_set_config_from_qr().
* - DC_QR_ACCOUNT allows creation of an account, dc_lot_t::text1=domain
* - DC_QR_WEBRTC_INSTANCE - a shared webrtc-instance
* that will be set if dc_set_config_from_qr() is called with the qr-code,
* dc_lot_t::text1=domain could be used to ask the user
* - DC_QR_ADDR with dc_lot_t::id=Contact ID
* - DC_QR_TEXT with dc_lot_t::text1=Text
* - DC_QR_URL with dc_lot_t::text1=URL
* - DC_QR_ERROR with dc_lot_t::text1=Error string
*
* @memberof dc_context_t
* @param context The context object.
@@ -2393,7 +2357,7 @@ void dc_str_unref (char* str);
* The account manager takes an directory
* where all context-databases are placed in.
* To add a context to the account manager,
* use dc_accounts_add_account() or dc_accounts_migrate_account().
* use dc_accounts_add_account(), dc_accounts_import_account or dc_accounts_migrate_account().
* All account information are persisted.
* To remove a context from the account manager,
* use dc_accounts_remove_account().
@@ -2438,6 +2402,21 @@ void dc_accounts_unref (dc_accounts_t* accounts);
uint32_t dc_accounts_add_account (dc_accounts_t* accounts);
/**
* Import a tarfile-backup to the account manager.
* On success, a new account is added to the account-manager,
* with all the data provided by the backup-file.
* Moreover, the newly created account will be the selected one.
*
* @memberof dc_accounts_t
* @param accounts Account manager as created by dc_accounts_new().
* @param tarfile Backup as created by dc_imex().
* @return Account-id, use dc_accounts_get_account() to get the context object.
* On errors, 0 is returned.
*/
uint32_t dc_accounts_import_account (dc_accounts_t* accounts, const char* tarfile);
/**
* Migrate independent accounts into accounts managed by the account manager.
* This will _move_ the database-file and all blob-files to the directory managed
@@ -2507,7 +2486,6 @@ dc_context_t* dc_accounts_get_account (dc_accounts_t* accounts, uint32
* unmanaged account-context as created by dc_context_new().
* Once you do no longer need the context-object, you have to call dc_context_unref() on it,
* which, however, will not close the account but only decrease a reference counter.
* If there is no selected account, NULL is returned.
*/
dc_context_t* dc_accounts_get_selected_account (dc_accounts_t* accounts);
@@ -2523,23 +2501,6 @@ dc_context_t* dc_accounts_get_selected_account (dc_accounts_t* accounts);
int dc_accounts_select_account (dc_accounts_t* accounts, uint32_t account_id);
/**
* This is meant especially for iOS, because iOS needs to tell the system when its background work is done.
*
* iOS can:
* - call dc_start_io() (in case IO was not running)
* - call dc_maybe_network()
* - while dc_accounts_all_work_done() returns false:
* - Wait for #DC_EVENT_CONNECTIVITY_CHANGED
*
* @memberof dc_accounts_t
* @param accounts Account manager as created by dc_accounts_new().
* @return Whether all accounts finished their background work.
* #DC_EVENT_CONNECTIVITY_CHANGED will be sent when this turns to true.
*/
int dc_accounts_all_work_done (dc_accounts_t* accounts);
/**
* Start job and IMAP/SMTP tasks for all accounts managed by the account manager.
* If IO is already running, nothing happens.
@@ -2575,21 +2536,6 @@ void dc_accounts_stop_io (dc_accounts_t* accounts);
void dc_accounts_maybe_network (dc_accounts_t* accounts);
/**
* This function can be called when there is a hint that the network is lost.
* This is similar to dc_accounts_maybe_network(), however,
* it does not retry job processing.
*
* dc_accounts_maybe_network_lost() is needed only on systems
* where the core cannot find out the connectivity loss on its own, eg. iOS.
* The function is not needed on Android, MacOS, Windows or Linux.
*
* @memberof dc_accounts_t
* @param accounts Account manager as created by dc_accounts_new().
*/
void dc_accounts_maybe_network_lost (dc_accounts_t* accounts);
/**
* Create the event emitter that is used to receive events.
*
@@ -2809,9 +2755,15 @@ int dc_array_search_id (const dc_array_t* array, uint32_t
* and for each messages that is scrolled into view, dc_get_msg() is called then.
*
* Why no listflags?
* Without listflags, dc_get_chatlist() adds
* the archive "link" automatically as needed.
* Without listflags, dc_get_chatlist() adds the deaddrop
* and the archive "link" automatically as needed.
* The UI can just render these items differently then.
* Although the deaddrop link is currently always the 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.
*/
@@ -2952,6 +2904,7 @@ char* dc_chat_get_info_json (dc_context_t* context, size_t chat
*/
#define DC_CHAT_ID_DEADDROP 1 // virtual chat showing all messages belonging to chats flagged with chats.blocked=2
#define DC_CHAT_ID_TRASH 3 // 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)
#define DC_CHAT_ID_ARCHIVED_LINK 6 // only an indicator in a chatlist
#define DC_CHAT_ID_ALLDONE_HINT 7 // only an indicator in a chatlist
@@ -2978,6 +2931,7 @@ void dc_chat_unref (dc_chat_t* chat);
* Get chat ID. The chat ID is the ID under which the chat is filed in the database.
*
* Special IDs:
* - DC_CHAT_ID_DEADDROP (1) - Virtual chat containing messages which senders are not confirmed by the user.
* - DC_CHAT_ID_ARCHIVED_LINK (6) - A link at the end of the chatlist, if present the UI should show the button "Archived chats"-
*
* "Normal" chat IDs are larger than these special IDs (larger than DC_CHAT_ID_LAST_SPECIAL).
@@ -3069,25 +3023,6 @@ uint32_t dc_chat_get_color (const dc_chat_t* chat);
int dc_chat_get_visibility (const dc_chat_t* chat);
/**
* Check if a chat is a contact request chat.
*
* UI should display such chats with a [New] badge in the chatlist.
*
* When such chat is opened, user should be presented with a set of
* options instead of the message composition area, for example:
* - Accept chat (dc_accept_chat())
* - Block chat (dc_block_chat())
* - Delete chat (dc_delete_chat())
*
* @memberof dc_chat_t
* @param chat The chat object.
* @return 1=chat is a contact request chat
* 0=chat is not a contact request chat
*/
int dc_chat_is_contact_request (const dc_chat_t* chat);
/**
* Check if a group chat is still unpromoted.
*
@@ -3141,7 +3076,7 @@ int dc_chat_is_device_talk (const dc_chat_t* chat);
/**
* Check if messages can be sent to a given chat.
* This is not true e.g. for contact requests or for the device-talk, cmp. dc_chat_is_device_talk().
* This is not true e.g. for the deaddrop or for the device-talk, cmp. dc_chat_is_device_talk().
*
* Calling dc_send_msg() for these chats will fail
* and the UI may decide to hide input controls therefore.
@@ -3225,6 +3160,10 @@ int64_t dc_chat_get_remaining_mute_duration (const dc_chat_t* chat);
#define DC_STATE_OUT_MDN_RCVD 28
#define DC_MAX_GET_TEXT_LEN 30000 // approx. max. length returned by dc_msg_get_text()
#define DC_MAX_GET_INFO_LEN 100000 // approx. max. length returned by dc_get_msg_info()
/**
* Create new message object. Message objects are needed e.g. for sending messages using
* dc_send_msg(). Moreover, they are returned e.g. from dc_get_msg(),
@@ -3281,6 +3220,9 @@ uint32_t dc_msg_get_from_id (const dc_msg_t* msg);
/**
* Get the ID of chat the message belongs to.
* To get details about the chat, pass the returned ID to dc_get_chat().
* If a message is still in the deaddrop, the ID DC_CHAT_ID_DEADDROP is returned
* although internally another ID is used.
* (to get that internal id, use dc_msg_get_real_chat_id())
*
* @memberof dc_msg_t
* @param msg The message object.
@@ -3289,6 +3231,19 @@ uint32_t dc_msg_get_from_id (const dc_msg_t* msg);
uint32_t dc_msg_get_chat_id (const dc_msg_t* msg);
/**
* Get the ID of chat the message belongs to.
* To get details about the chat, pass the returned ID to dc_get_chat().
* In contrast to dc_msg_get_chat_id(), this function returns the chat-id also
* for messages in the deaddrop.
*
* @memberof dc_msg_t
* @param msg The message object.
* @return The ID of the chat the message belongs to, 0 on errors.
*/
uint32_t dc_msg_get_real_chat_id (const dc_msg_t* msg);
/**
* Get the type of the message.
*
@@ -3538,16 +3493,6 @@ int dc_msg_get_duration (const dc_msg_t* msg);
*/
int dc_msg_get_showpadlock (const dc_msg_t* msg);
/**
* Check if incoming message is a bot message, i.e. automatically submitted.
*
* Return value for outgoing messages is unspecified.
*
* @memberof dc_msg_t
* @param msg The message object.
* @return 1=message is submitted automatically, 0=message is not automatically submitted.
*/
int dc_msg_is_bot (const dc_msg_t* msg);
/**
* Get ephemeral timer duration for message.
@@ -3903,54 +3848,6 @@ int dc_msg_get_videochat_type (const dc_msg_t* msg);
int dc_msg_has_html (dc_msg_t* msg);
/**
* Check if the message is completely downloaded
* or if some further action is needed.
*
* The function returns one of:
* - @ref DC_DOWNLOAD_NO_URL - The message does not need any further download action
* and should be rendered as usual.
* - @ref DC_DOWNLOAD_AVAILABLE - There is additional content to download.
* Tn addition to the usual message rendering,
* the UI shall show a download button that starts dc_schedule_download()
* - @ref DC_DOWNLOAD_IN_PROGRESS - Download was started with dc_schedule_download() and is still in progress.
* On progress changes and if the download fails or succeeds,
* the event @ref DC_EVENT_DOWNLOAD_PROGRESS will be emitted.
* - @ref DC_DOWNLOAD_DONE - Download finished successfully
* - @ref DC_DOWNLOAD_FAILURE - Download error, the user may start over calling dc_schedule_download() again.
*
* @memberof dc_msg_t
* @param msg The message object.
* @return One of the @ref DC_DOWNLOAD values
*/
int dc_msg_download_status(const dc_msg_t* msg);
/**
* Advices the core to start downloading a message.
* This function is typically called when the user hits the "Download" button
* that is shown by the UI in case dc_msg_download_status()
* returns @ref DC_DOWNLOAD_AVAILABLE or @ref DC_DOWNLOAD_FAILURE.
*
* The UI may want to show a file selector and let the user chose a download location.
* The file name in the file selector may be prefilled using dc_msg_get_filename().
*
* During the download, the progress, errors and success
* are reported using @ref DC_EVENT_DOWNLOAD_PROGRESS.
*
* Once the @ref DC_EVENT_DOWNLOAD_PROGRESS reports success,
* The file can be accessed as usual using dc_msg_get_file().
*
* @memberof dc_context_t
* @param context The context object.
* @param path Path to the destination file.
* You can specify NULL here to download
* to a reasonable file name in the internal blob-directory.
* @param msg_id Message-ID to download the content for.
*/
void dc_schedule_download(dc_context_t* context, int msg_id, const char* path);
/**
* Set the text of a message object.
* This does not alter any information in the database; this may be done by dc_send_msg() later.
@@ -5005,6 +4902,26 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_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.
*
* 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 0
* @param data2 (char*) Error string, always set, never NULL.
*/
#define DC_EVENT_ERROR_NETWORK 401
/**
* An action cannot be performed because the user is not in the group.
* Reported e.g. after a call to
@@ -5189,28 +5106,6 @@ void dc_event_unref(dc_event_t* event);
*/
#define DC_EVENT_SECUREJOIN_JOINER_PROGRESS 2061
/**
* The connectivity to the server changed.
* This means that you should refresh the connectivity view
* and possibly the connectivtiy HTML; see dc_get_connectivity() and
* dc_get_connectivity_html() for details.
*
* @param data1 0
* @param data2 0
*/
#define DC_EVENT_CONNECTIVITY_CHANGED 2100
/**
* Inform about the progress of a download started by dc_schedule_download().
*
* @param data1 (int) Message-ID the progress is reported for.
* @param data2 (int) 0=error, 1-999=progress in permille, 1000=success and done
*/
#define DC_EVENT_DOWNLOAD_PROGRESS 2120
/**
* @}
*/
@@ -5341,31 +5236,6 @@ void dc_event_unref(dc_event_t* event);
/**
* @defgroup DC_DOWNLOAD DC_DOWNLOAD
*
* These constants describe the download state of a message.
* The download state can be retrieved using dc_msg_download_status()
* and usually changes after calling dc_schedule_download().
*
* @addtogroup DC_DOWNLOAD
* @{
*/
#define DC_DOWNLOAD_NO_URL 10 ///< Download not needed, see dc_msg_download_status() for details.
#define DC_DOWNLOAD_AVAILABLE 20 ///< Download available, see dc_msg_download_status() for details.
#define DC_DOWNLOAD_IN_PROGRESS 30 ///< Download in progress, see dc_msg_download_status() for details.
#define DC_DOWNLOAD_DONE 40 ///< Download done, see dc_msg_download_status() for details.
#define DC_DOWNLOAD_FAILURE 50 ///< Download failed, see dc_msg_download_status() for details.
/**
* @}
*/
/*
* TODO: Strings need some doumentation about used placeholders.
*
* @defgroup DC_STR DC_STR
*
* These constants are used to define strings using dc_set_stock_translation().
@@ -5404,6 +5274,11 @@ void dc_event_unref(dc_event_t* event);
/// Used in summaries.
#define DC_STR_VOICEMESSAGE 7
/// "Contact requests"
///
/// Used as the name for the corresponding chat.
#define DC_STR_DEADDROP 8
/// "Image"
///
/// Used in summaries.
@@ -5558,6 +5433,13 @@ void dc_event_unref(dc_event_t* event);
/// - %1$s will be replaced by the failing login name
#define DC_STR_CANNOT_LOGIN 60
/// "Could not connect to %1$s: %2$s"
///
/// Used in error strings.
/// - %1$s will be replaced by the failing server
/// - %2$s by a the error message as returned from the server
#define DC_STR_SERVER_RESPONSE 61
/// "%1$s by %2$s"
///
/// Used to concretize actions,

View File

@@ -1,6 +1,6 @@
prefix={prefix}
libdir={libdir}
includedir={includedir}
libdir=${{prefix}}/lib
includedir=${{prefix}}/include
Name: {name}
Description: {description}

File diff suppressed because it is too large Load Diff

View File

@@ -17,12 +17,15 @@ use std::ptr;
/// }
/// ```
unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
let ret: *mut libc::c_char = if !s.is_null() {
libc::strdup(s)
let ret: *mut libc::c_char;
if !s.is_null() {
ret = libc::strdup(s);
assert!(!ret.is_null());
} else {
libc::calloc(1, 1) as *mut libc::c_char
};
assert!(!ret.is_null());
ret = libc::calloc(1, 1) as *mut libc::c_char;
assert!(!ret.is_null());
}
ret
}
@@ -167,20 +170,15 @@ pub(crate) trait Strdup {
unsafe fn strdup(&self) -> *mut libc::c_char;
}
impl Strdup for str {
impl<T: AsRef<str>> Strdup for T {
unsafe fn strdup(&self) -> *mut libc::c_char {
let tmp = CString::new_lossy(self);
let tmp = CString::new_lossy(self.as_ref());
dc_strdup(tmp.as_ptr())
}
}
impl Strdup for String {
unsafe fn strdup(&self) -> *mut libc::c_char {
let s: &str = self;
s.strdup()
}
}
// We can not implement for AsRef<OsStr> because we already implement
// AsRev<str> and this conflicts. So implement for Path directly.
impl Strdup for std::path::Path {
unsafe fn strdup(&self) -> *mut libc::c_char {
let tmp = self.to_c_string().unwrap_or_else(|_| CString::default());
@@ -188,13 +186,6 @@ impl Strdup for std::path::Path {
}
}
impl Strdup for [u8] {
unsafe fn strdup(&self) -> *mut libc::c_char {
let tmp = CString::new_lossy(self);
dc_strdup(tmp.as_ptr())
}
}
/// Convenience methods to turn optional strings into C strings.
///
/// This is the same as the [Strdup] trait but a different trait name

View File

@@ -9,5 +9,5 @@ license = "MPL-2.0"
proc-macro = true
[dependencies]
syn = "1.0.74"
syn = "1.0.72"
quote = "1.0.2"

View File

@@ -13,11 +13,13 @@ use deltachat::contact::*;
use deltachat::context::*;
use deltachat::dc_receive_imf::*;
use deltachat::dc_tools::*;
use deltachat::error::Error;
use deltachat::export_chat::export_chat_to_zip;
use deltachat::imex::*;
use deltachat::location;
use deltachat::log::LogExt;
use deltachat::lot::LotState;
use deltachat::message::{self, Message, MessageState, MsgId};
use deltachat::message::{self, ContactRequestDecision, Message, MessageState, MsgId};
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::sql;
@@ -351,7 +353,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
configure\n\
connect\n\
disconnect\n\
connectivity\n\
maybenetwork\n\
housekeeping\n\
help imex (Import/Export)\n\
@@ -373,7 +374,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
getlocations [<contact-id>]\n\
send <text>\n\
sendimage <file> [<text>]\n\
sendsticker <file> [<text>]\n\
sendfile <file> [<text>]\n\
sendhtml <file for html-part> [<text for plain-part>]\n\
videochat\n\
@@ -389,8 +389,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
protect <chat-id>\n\
unprotect <chat-id>\n\
delchat <chat-id>\n\
accept <chat-id>\n\
decline <chat-id>\n\
export-chat <chat-id> <destination-file>\n\
===========================Contact requests==\n\
decidestartchat <msg-id>\n\
decideblock <msg-id>\n\
decidenotnow <msg-id>\n\
===========================Message commands==\n\
listmsgs <query>\n\
msginfo <msg-id>\n\
@@ -510,20 +513,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"info" => {
println!("{:#?}", context.get_info().await);
}
"connectivity" => {
let file = dirs::home_dir()
.unwrap_or_default()
.join("connectivity.html");
match context.get_connectivity_html().await {
Ok(html) => {
fs::write(&file, html)?;
println!("Report written to: {:#?}", file);
}
Err(err) => {
bail!("Failed to get connectivity html: {}", err);
}
}
}
"maybenetwork" => {
context.maybe_network().await;
}
@@ -551,7 +540,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
for i in (0..cnt).rev() {
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)).await?;
println!(
"{}#{}: {} [{} fresh] {}{}{}{}",
"{}#{}: {} [{} fresh] {}{}{}",
chat_prefix(&chat),
chat.get_id(),
chat.get_name(),
@@ -563,13 +552,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ChatVisibility::Pinned => "📌",
},
if chat.is_protected() { "🛡️" } else { "" },
if chat.is_contact_request() {
"🆕"
} else {
""
},
);
let lot = chatlist.get_summary(&context, i, Some(&chat)).await?;
let lot = chatlist.get_summary(&context, i, Some(&chat)).await;
let statestr = if chat.visibility == ChatVisibility::Archived {
" [Archived]"
} else {
@@ -698,6 +682,35 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!("Single#{} created successfully.", chat_id,);
}
"decidestartchat" | "createchatbymsg" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing");
let msg_id = MsgId::new(arg1.parse()?);
match message::decide_on_contact_request(
&context,
msg_id,
ContactRequestDecision::StartChat,
)
.await
{
Some(chat_id) => {
let chat = Chat::load_from_db(&context, chat_id).await?;
println!("{}#{} created successfully.", chat_prefix(&chat), chat_id);
}
None => println!("Cannot crate chat."),
}
}
"decidenotnow" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing");
let msg_id = MsgId::new(arg1.parse()?);
message::decide_on_contact_request(&context, msg_id, ContactRequestDecision::NotNow)
.await;
}
"decideblock" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing");
let msg_id = MsgId::new(arg1.parse()?);
message::decide_on_contact_request(&context, msg_id, ContactRequestDecision::Block)
.await;
}
"creategroup" => {
ensure!(!arg1.is_empty(), "Argument <name> missing.");
let chat_id =
@@ -855,14 +868,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(sel_chat.is_some(), "No chat selected.");
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), "".into()).await?;
}
"sendimage" | "sendsticker" | "sendfile" => {
"sendimage" | "sendfile" => {
ensure!(sel_chat.is_some(), "No chat selected.");
ensure!(!arg1.is_empty(), "No file given.");
let mut msg = Message::new(if arg0 == "sendimage" {
Viewtype::Image
} else if arg0 == "sendsticker" {
Viewtype::Sticker
} else {
Viewtype::File
});
@@ -1014,15 +1025,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let chat_id = ChatId::new(arg1.parse()?);
chat_id.delete(&context).await?;
}
"accept" => {
"export-chat" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
ensure!(!arg2.is_empty(), "Argument <destination file> missing.");
let chat_id = ChatId::new(arg1.parse()?);
chat_id.accept(&context).await?;
}
"blockchat" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);
chat_id.block(&context).await?;
// todo check if path is valid (dest dir exists) and ends in .zip
export_chat_to_zip(&context, chat_id, arg2).await;
}
"msginfo" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
@@ -1136,12 +1144,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"block" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id = arg1.parse()?;
Contact::block(&context, contact_id).await?;
Contact::block(&context, contact_id).await;
}
"unblock" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id = arg1.parse()?;
Contact::unblock(&context, contact_id).await?;
Contact::unblock(&context, contact_id).await;
}
"listblocked" => {
let contacts = Contact::get_all_blocked(&context).await?;
@@ -1168,10 +1176,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"providerinfo" => {
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
let socks5_enabled = context
.get_config_bool(config::Config::Socks5Enabled)
.await?;
match provider::get_provider_info(arg1, socks5_enabled).await {
match provider::get_provider_info(arg1).await {
Some(info) => {
println!("Information for provider belonging to {}:", arg1);
println!("status: {}", info.status as u32);

View File

@@ -57,6 +57,9 @@ fn receive_event(event: EventType) {
EventType::Error(msg) => {
error!("{}", msg);
}
EventType::ErrorNetwork(msg) => {
error!("[NETWORK] msg={}", msg);
}
EventType::ErrorSelfNotInGroup(msg) => {
error!("[SELF_NOT_IN_GROUP] {}", msg);
}
@@ -154,7 +157,7 @@ const IMEX_COMMANDS: [&str; 12] = [
"stop",
];
const DB_COMMANDS: [&str; 10] = [
const DB_COMMANDS: [&str; 9] = [
"info",
"set",
"get",
@@ -162,16 +165,18 @@ const DB_COMMANDS: [&str; 10] = [
"configure",
"connect",
"disconnect",
"connectivity",
"maybenetwork",
"housekeeping",
];
const CHAT_COMMANDS: [&str; 33] = [
const CHAT_COMMANDS: [&str; 35] = [
"listchats",
"listarchived",
"chat",
"createchat",
"decidestartchat",
"decideblock",
"decidenotnow",
"creategroup",
"createverified",
"addmember",
@@ -199,8 +204,7 @@ const CHAT_COMMANDS: [&str; 33] = [
"protect",
"unprotect",
"delchat",
"accept",
"blockchat",
"export-chat",
];
const MESSAGE_COMMANDS: [&str; 6] = [
"listmsgs",
@@ -392,7 +396,7 @@ async fn handle_cmd(
"oauth2" => {
if let Some(addr) = ctx.get_config(config::Config::Addr).await? {
let oauth2_url =
dc_get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?;
dc_get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await;
if oauth2_url.is_none() {
println!("OAuth2 not available for {}.", &addr);
} else {

View File

@@ -19,7 +19,7 @@ fn cb(event: EventType) {
EventType::Warning(msg) => {
log::warn!("{}", msg);
}
EventType::Error(msg) => {
EventType::Error(msg) | EventType::ErrorNetwork(msg) => {
log::error!("{}", msg);
}
event => {
@@ -86,7 +86,7 @@ async fn main() {
let chats = Chatlist::try_load(&ctx, 0, None, None).await.unwrap();
for i in 0..chats.len() {
let msg = Message::load_from_db(&ctx, chats.get_msg_id(i).unwrap().unwrap())
let msg = Message::load_from_db(&ctx, chats.get_msg_id(i).unwrap())
.await
.unwrap();
log::info!("[{}] msg: {:?}", i, msg);

View File

@@ -58,13 +58,12 @@ end-to-end tests that require accounts on real e-mail servers.
running "live" tests with temporary accounts
---------------------------------------------
If you want to run live functional tests you can set ``DCC_NEW_TMP_EMAIL`` to a URL that creates e-mail accounts. Most developers use https://testrun.org URLS created and managed by [mailadm](https://mailadm.readthedocs.io/en/latest/).
If you want to run live functional tests you can set ``DCC_NEW_TMP_EMAIL``::
Please feel free to contact us through a github issue or by e-mail and we'll send you a URL that you can then use for functional tests like this:
export DCC_NEW_TMP_EMAIL=https://testrun.org/new_email?t=1h_4w4r8h7y9nmcdsy
export DCC_NEW_TMP_EMAIL=<URL you got from us>
With this account-creation setting, pytest runs create ephemeral e-mail accounts on the http://testrun.org server. These accounts exists only for one hour and then are removed completely.
With this, pytest runs create ephemeral e-mail accounts on the http://testrun.org server.
These accounts exists for one 1hour and then are removed completely.
One hour is enough to invoke pytest and run all offline and online tests:
pytest

View File

@@ -150,7 +150,6 @@ def extract_defines(flags):
| DC_PROVIDER
| DC_KEY_GEN
| DC_IMEX
| DC_CONNECTIVITY
) # End of prefix matching
_[\w_]+ # Match the suffix, e.g. _RSA2048 in DC_KEY_GEN_RSA2048
) # Close the capturing group, this contains

View File

@@ -330,6 +330,9 @@ class Account(object):
""" Create a 1:1 chat with Account, Contact or e-mail address. """
return self.create_contact(obj).create_chat()
def _create_chat_by_message_id(self, msg_id):
return Chat(self, lib.dc_create_chat_by_msg_id(self._dc_context, msg_id))
def create_group_chat(self, name, contacts=None, verified=False):
""" create a new group chat object.
@@ -364,6 +367,9 @@ class Account(object):
chatlist.append(Chat(self, chat_id))
return chatlist
def get_deaddrop_chat(self):
return Chat(self, const.DC_CHAT_ID_DEADDROP)
def get_device_chat(self):
return Contact(self, const.DC_CONTACT_ID_DEVICE).create_chat()
@@ -568,15 +574,6 @@ class Account(object):
""" Stop ongoing securejoin, configuration or other core jobs. """
lib.dc_stop_ongoing_process(self._dc_context)
def get_connectivity(self):
return lib.dc_get_connectivity(self._dc_context)
def get_connectivity_html(self):
return from_dc_charpointer(lib.dc_get_connectivity_html(self._dc_context))
def all_work_done(self):
return lib.dc_all_work_done(self._dc_context)
def start_io(self):
""" start this account's IO scheduling (Rust-core async scheduler)

View File

@@ -50,14 +50,6 @@ class Chat(object):
"""
lib.dc_delete_chat(self.account._dc_context, self.id)
def block(self):
"""Block this chat."""
lib.dc_block_chat(self.account._dc_context, self.id)
def accept(self):
"""Accept this contact request chat."""
lib.dc_accept_chat(self.account._dc_context, self.id)
# ------ chat status/metadata API ------------------------------
def is_group(self):
@@ -67,6 +59,13 @@ class Chat(object):
"""
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP
def is_deaddrop(self):
""" return true if this chat is a deaddrop chat.
:returns: True if chat is the deaddrop chat, False otherwise.
"""
return self.id == const.DC_CHAT_ID_DEADDROP
def is_muted(self):
""" return true if this chat is muted.
@@ -74,13 +73,6 @@ class Chat(object):
"""
return lib.dc_chat_is_muted(self._dc_chat)
def is_contact_request(self):
""" return True if this chat is a contact request chat.
:returns: True if chat is a contact request chat, False otherwise.
"""
return lib.dc_chat_is_contact_request(self._dc_chat)
def is_promoted(self):
""" return True if this chat is promoted, i.e.
the member contacts are aware of their membership,
@@ -92,7 +84,7 @@ class Chat(object):
def can_send(self):
"""Check if messages can be sent to a give chat.
This is not true eg. for the contact requests or for the device-talk
This is not true eg. for the deaddrop or for the device-talk
:returns: True if the chat is writable, False otherwise
"""

View File

@@ -251,16 +251,7 @@ class DirectImap:
return res
def append(self, folder, msg):
"""Upload a message to *folder*.
Trailing whitespace or a linebreak at the beginning will be removed automatically.
"""
if msg.startswith("\n"):
msg = msg[1:]
msg = '\n'.join([s.lstrip() for s in msg.splitlines()])
self.conn.append(folder, msg)
def get_uid_by_message_id(self, message_id):
msgs = self.conn.search(['HEADER', 'MESSAGE-ID', message_id])
if len(msgs) == 0:
raise Exception("Did not find message " + message_id + ", maybe you forgot to select the correct folder?")
return msgs[0]

View File

@@ -111,33 +111,6 @@ class FFIEventTracker:
if m is not None:
return m.groups()
def wait_for_connectivity(self, connectivity):
"""Wait for the specified connectivity.
This only works reliably if the connectivity doesn't change
again too quickly, otherwise we might miss it."""
while 1:
if self.account.get_connectivity() == connectivity:
return
self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
def wait_for_connectivity_change(self, previous, expected_next):
"""Wait until the connectivity changes to `expected_next`.
Fails the test if it changes to something else."""
while 1:
current = self.account.get_connectivity()
if current == expected_next:
return
elif current != previous:
raise Exception("Expected connectivity " + str(expected_next) + " but got " + str(current))
self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
def wait_for_all_work_done(self):
while 1:
if self.account.all_work_done():
return
self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
def ensure_event_not_queued(self, event_name_regex):
__tracebackhide__ = True
rex = re.compile("(?:{}).*".format(event_name_regex))

View File

@@ -43,7 +43,7 @@ class PerAccount:
@account_hookspec
def ac_incoming_message(self, message):
""" Called on any incoming message (both existing chats and contact requests). """
""" Called on any incoming message (to deaddrop or chat). """
@account_hookspec
def ac_outgoing_message(self, message):

View File

@@ -61,13 +61,16 @@ class Message(object):
def create_chat(self):
""" create or get an existing chat (group) object for this message.
If the message is a contact request
If the message is a deaddrop contact request
the sender will become an accepted contact.
:returns: a :class:`deltachat.chat.Chat` object.
"""
self.chat.accept()
return self.chat
from .chat import Chat
chat_id = lib.dc_create_chat_by_msg_id(self.account._dc_context, self.id)
ctx = self.account._dc_context
self._dc_msg = ffi.gc(lib.dc_get_msg(ctx, self.id), lib.dc_msg_unref)
return Chat(self.account, chat_id)
@props.with_doc
def id(self):
@@ -138,10 +141,6 @@ class Message(object):
""" return True if this message was encrypted. """
return bool(lib.dc_msg_get_showpadlock(self._dc_msg))
def is_bot(self):
""" return True if this message is submitted automatically. """
return bool(lib.dc_msg_is_bot(self._dc_msg))
def is_forwarded(self):
""" return True if this message was forwarded. """
return bool(lib.dc_msg_is_forwarded(self._dc_msg))

View File

@@ -909,11 +909,12 @@ class TestOnlineAccount:
msg_in = ac2.get_message_by_id(msg_out.id)
assert msg_in.text == "message2"
lp.sec("ac2: check that the message arrived in a chat")
lp.sec("ac2: check that the message arrive in deaddrop")
chat2 = msg_in.chat
assert msg_in in chat2.get_messages()
assert not msg_in.is_forwarded()
assert chat2.is_contact_request()
assert chat2.is_deaddrop()
assert chat2 == ac2.get_deaddrop_chat()
lp.sec("ac2: create new chat and forward message to it")
chat3 = ac2.create_group_chat("newgroup")
@@ -978,16 +979,16 @@ class TestOnlineAccount:
assert not msg2.is_forwarded()
assert msg2.get_sender_contact().display_name == ac1.get_config("displayname")
lp.sec("check the message arrived in contact request chat")
lp.sec("check the message arrived in contact-requests/deaddrop")
chat2 = msg2.chat
assert msg2 in chat2.get_messages()
assert chat2.is_contact_request()
assert chat2.count_fresh_messages() == 1
assert chat2.is_deaddrop()
assert chat2.count_fresh_messages() == 0
assert msg2.time_received >= msg1.time_sent
lp.sec("create new chat with contact and verify it's proper")
chat2b = msg2.create_chat()
assert not chat2b.is_contact_request()
assert not chat2b.is_deaddrop()
assert chat2b.count_fresh_messages() == 1
lp.sec("mark chat as noticed")
@@ -1318,11 +1319,9 @@ class TestOnlineAccount:
assert not device_chat.can_send()
assert device_chat.get_draft() is None
def test_dont_show_emails_in_draft_folder(self, acfactory, lp):
def test_dont_show_emails_in_draft_folder(self, acfactory):
"""Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them.
So: If it's outgoing AND there is no Received header AND it's not in the sentbox, then ignore the email.
If the draft email is sent out later (i.e. moved to "Sent"), it must be shown."""
So: If it's outgoing AND there is no Received header AND it's not in the sentbox, then ignore the email."""
ac1 = acfactory.get_online_configuring_account()
ac1.set_config("show_emails", "2")
ac1.create_contact("alice@example.com").create_chat()
@@ -1343,7 +1342,7 @@ class TestOnlineAccount:
Message-ID: <aepiors@example.org>
Content-Type: text/plain; charset=utf-8
message in Drafts that is moved to Sent later
message in Drafts
""".format(ac1.get_config("configured_addr")))
ac1.direct_imap.append("Sent", """
From: ac1 <{}>
@@ -1356,7 +1355,6 @@ class TestOnlineAccount:
""".format(ac1.get_config("configured_addr")))
ac1.set_config("scan_all_folders_debounce_secs", "0")
lp.sec("All prepared, now let DC find the message")
ac1.start_io()
msg = ac1._evtracker.wait_next_messages_changed()
@@ -1367,18 +1365,6 @@ class TestOnlineAccount:
assert msg.text == "subj message in Sent"
assert len(msg.chat.get_messages()) == 1
ac1.stop_io()
lp.sec("'Send out' the draft, i.e. move it to the Sent folder, and wait for DC to display it this time")
ac1.direct_imap.select_folder("Drafts")
uid = ac1.direct_imap.get_uid_by_message_id("aepiors@example.org")
ac1.direct_imap.conn.move(uid, "Sent")
ac1.start_io()
msg2 = ac1._evtracker.wait_next_messages_changed()
assert msg2.text == "subj message in Drafts that is moved to Sent later"
assert len(msg.chat.get_messages()) == 2
def test_prefer_encrypt(self, acfactory, lp):
"""Test quorum rule for encryption preference in 1:1 and group chat."""
ac1, ac2, ac3 = acfactory.get_many_online_accounts(3)
@@ -1429,33 +1415,6 @@ class TestOnlineAccount:
# Majority prefers encryption now
assert msg5.is_encrypted()
def test_bot(self, acfactory, lp):
"""Test that bot messages can be identified as such"""
ac1, ac2 = acfactory.get_two_online_accounts()
ac1.set_config("bot", "0")
ac2.set_config("bot", "1")
lp.sec("ac1: create chat with ac2")
chat = acfactory.get_accepted_chat(ac1, ac2)
lp.sec("sending a message from ac1 to ac2")
text1 = "hello"
chat.send_text(text1)
lp.sec("wait for ac2 to receive a message")
msg_in = ac2._evtracker.wait_next_incoming_message()
assert msg_in.text == text1
assert not msg_in.is_bot()
lp.sec("sending a message from ac2 to ac1")
text2 = "reply"
msg_in.chat.send_text(text2)
lp.sec("wait for ac1 to receive a message")
msg_in = ac1._evtracker.wait_next_incoming_message()
assert msg_in.text == text2
assert msg_in.is_bot()
def test_quote_encrypted(self, acfactory, lp):
"""Test that replies to encrypted messages with quotes are encrypted."""
ac1, ac2 = acfactory.get_two_online_accounts()
@@ -1852,7 +1811,7 @@ class TestOnlineAccount:
lp.sec("ac2: wait for receiving message and avatar from ac1")
msg2 = ac2._evtracker.wait_next_messages_changed()
assert msg2.chat.is_contact_request()
assert msg2.chat.is_deaddrop()
received_path = msg2.get_sender_contact().get_profile_image()
assert open(received_path, "rb").read() == open(p, "rb").read()
@@ -1927,8 +1886,6 @@ class TestOnlineAccount:
ev = in_list.get(timeout=10)
assert ev.action == "chat-modified"
ev = in_list.get(timeout=10)
assert ev.action == "chat-modified"
ev = in_list.get(timeout=10)
assert ev.action == "added"
assert ev.message.get_sender_contact().addr == ac1_addr
assert ev.contact.addr == "devnull@testrun.org"
@@ -2043,84 +2000,6 @@ class TestOnlineAccount:
assert msg_back.chat == chat
assert chat.get_profile_image() is None
@pytest.mark.parametrize("inbox_watch", ["0", "1"])
def test_connectivity(self, acfactory, lp, inbox_watch):
ac1, ac2 = acfactory.get_two_online_accounts()
ac1.set_config("inbox_watch", inbox_watch)
ac1.set_config("scan_all_folders_debounce_secs", "0")
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTED)
lp.sec("Test stop_io() and start_io()")
ac1.stop_io()
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
ac1.start_io()
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTING)
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTING, const.DC_CONNECTIVITY_CONNECTED)
lp.sec("Test that after calling start_io(), maybe_network() and waiting for `all_work_done()`, " +
"all messages are fetched")
ac1.direct_imap.select_config_folder("inbox")
ac1.direct_imap.idle_start()
ac2.create_chat(ac1).send_text("Hi")
ac1.direct_imap.idle_check(terminate=False)
ac1.maybe_network()
ac1._evtracker.wait_for_all_work_done()
msgs = ac1.create_chat(ac2).get_messages()
assert len(msgs) == 1
assert msgs[0].text == "Hi"
lp.sec("Test that the connectivity changes to WORKING while new messages are fetched")
ac2.create_chat(ac1).send_text("Hi 2")
ac1.direct_imap.idle_check(terminate=True)
ac1.maybe_network()
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTED, const.DC_CONNECTIVITY_WORKING)
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_WORKING, const.DC_CONNECTIVITY_CONNECTED)
msgs = ac1.create_chat(ac2).get_messages()
assert len(msgs) == 2
assert msgs[1].text == "Hi 2"
lp.sec("Test that the connectivity doesn't flicker to WORKING if there are no new messages")
ac1.maybe_network()
while 1:
assert ac1.get_connectivity() == const.DC_CONNECTIVITY_CONNECTED
if ac1.all_work_done():
break
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
lp.sec("Test that the connectivity doesn't flicker to WORKING if the sender of the message is blocked")
ac1.create_contact(ac2).block()
ac1.direct_imap.select_config_folder("inbox")
ac1.direct_imap.idle_start()
ac2.create_chat(ac1).send_text("Hi")
ac1.direct_imap.idle_check(terminate=True)
ac1.maybe_network()
while 1:
assert ac1.get_connectivity() == const.DC_CONNECTIVITY_CONNECTED
if ac1.all_work_done():
break
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
lp.sec("Test that the connectivity is NOT_CONNECTED if the password is wrong")
ac1.set_config("configured_mail_pw", "abc")
ac1.stop_io()
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
ac1.start_io()
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTING)
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
def test_fetch_deleted_msg(self, acfactory, lp):
"""This is a regression test: Messages with \\Deleted flag were downloaded again and again,
hundreds of times, because uid_next was not updated.
@@ -2837,6 +2716,7 @@ class TestOnlineConfigureFails:
configtracker = ac1.configure()
configtracker.wait_progress(500)
configtracker.wait_progress(0)
ac1._evtracker.ensure_event_not_queued("DC_EVENT_ERROR_NETWORK")
def test_invalid_user(self, acfactory):
ac1, configdict = acfactory.get_online_config()
@@ -2844,6 +2724,7 @@ class TestOnlineConfigureFails:
configtracker = ac1.configure()
configtracker.wait_progress(500)
configtracker.wait_progress(0)
ac1._evtracker.ensure_event_not_queued("DC_EVENT_ERROR_NETWORK")
def test_invalid_domain(self, acfactory):
ac1, configdict = acfactory.get_online_config()
@@ -2851,3 +2732,4 @@ class TestOnlineConfigureFails:
configtracker = ac1.configure()
configtracker.wait_progress(500)
configtracker.wait_progress(0)
ac1._evtracker.ensure_event_not_queued("DC_EVENT_ERROR_NETWORK")

View File

@@ -1 +1 @@
1.54.0
1.50.0

View File

@@ -1,13 +1,16 @@
# Continuous Integration Scripts for Delta Chat
Continuous Integration, run through [GitHub
Actions](https://docs.github.com/actions)
and an own build machine.
Actions](https://docs.github.com/actions),
[CircleCI](https://app.circleci.com/) and an own build machine.
## Description of scripts
- `../.github/workflows` contains jobs run by GitHub Actions.
- `../.circleci/config.yml` describing the build jobs that are run
by CircleCI.
- `remote_tests_python.sh` rsyncs to a build machine and runs
`run-python-test.sh` remotely on the build machine.

75
scripts/ci_upload.sh Executable file
View File

@@ -0,0 +1,75 @@
#!/bin/bash
if [ -z "$DEVPI_LOGIN" ] ; then
echo "required: password for 'dc' user on https://m.devpi/net/dc index"
exit 0
fi
set -xe
PYDOCDIR=${1:?directory with python docs}
WHEELHOUSEDIR=${2:?directory with pre-built wheels}
DOXYDOCDIR=${3:?directory where doxygen docs to be found}
SSHTARGET=ci@b1.delta.chat
# if CIRCLE_BRANCH is not set we are called for a tag with empty CIRCLE_BRANCH variable.
export BRANCH=${CIRCLE_BRANCH:master}
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}/wheelhouse
# python docs to py.delta.chat
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null delta@py.delta.chat mkdir -p build/${BRANCH}
rsync -avz \
--delete \
-e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
"$PYDOCDIR/html/" \
delta@py.delta.chat:build/${BRANCH}
# C docs to c.delta.chat
ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null delta@c.delta.chat mkdir -p build-c/${BRANCH}
rsync -avz \
--delete \
-e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
"$DOXYDOCDIR/html/" \
delta@c.delta.chat:build-c/${BRANCH}
echo -----------------------
echo upload wheels
echo -----------------------
# Bundle external shared libraries into the wheels
ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $SSHTARGET mkdir -p $BUILDDIR
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null scripts/cleanup_devpi_indices.py $SSHTARGET:$BUILDDIR
rsync -avz \
-e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
$WHEELHOUSEDIR \
$SSHTARGET:$BUILDDIR
ssh $SSHTARGET <<_HERE
set +x -e
# make sure all processes exit when ssh dies
shopt -s huponexit
# we rely on the "venv" virtualenv on the remote account to exist
source venv/bin/activate
cd $BUILDDIR
devpi use https://m.devpi.net
devpi login dc --password $DEVPI_LOGIN
N_BRANCH=${BRANCH//[\/]}
devpi use dc/\$N_BRANCH || {
devpi index -c \$N_BRANCH
devpi use dc/\$N_BRANCH
}
devpi index \$N_BRANCH bases=/root/pypi
devpi upload wheelhouse/deltachat*
# remove devpi non-master dc indices if thy are too old
# this script was copied above
python cleanup_devpi_indices.py
_HERE

View File

@@ -1,21 +0,0 @@
# Concourse CI pipeline
`docs_wheels.yml` is a pipeline for [Concourse CI](https://concourse-ci.org/)
that builds C documentation, Python documentation, Python wheels for `x86_64`
and `aarch64` and Python source packages, and uploads them.
To setup the pipeline run
```
fly -t <your-target> set-pipeline -c docs_wheels.yml -p docs_wheels -l secret.yml
```
where `secret.yml` contains the following secrets:
```
c.delta.chat:
private_key: |
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
devpi:
login: dc
password: ...
```

View File

@@ -1,232 +0,0 @@
resources:
- name: deltachat-core-rust
type: git
icon: github
source:
branch: master
uri: https://github.com/deltachat/deltachat-core-rust.git
- name: deltachat-core-rust-release
type: git
icon: github
source:
branch: master
uri: https://github.com/deltachat/deltachat-core-rust.git
tag_filter: "py-*"
jobs:
- name: doxygen
plan:
- get: deltachat-core-rust
trigger: true
# Build Doxygen documentation
- task: build-doxygen
config:
inputs:
- name: deltachat-core-rust
outputs:
- name: c-docs
image_resource:
source:
repository: hrektts/doxygen
type: registry-image
platform: linux
run:
path: bash
args:
- -exc
- |
cd deltachat-core-rust
bash scripts/run-doxygen.sh
cd ..
cp -av deltachat-core-rust/deltachat-ffi/{html,xml} c-docs/
- task: upload-c-docs
config:
inputs:
- name: c-docs
image_resource:
type: registry-image
source:
repository: alpine
platform: linux
run:
path: sh
args:
- -ec
- |
apk add --no-cache rsync openssh-client
mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo "(("c.delta.chat".private_key))" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
rsync -e "ssh -o StrictHostKeyChecking=no" -avz --delete c-docs/html/ delta@c.delta.chat:build-c/master
- name: python-x86_64
plan:
- get: deltachat-core-rust
- get: deltachat-core-rust-release
trigger: true
# Build manylinux image with additional dependencies
- task: build-coredeps
privileged: true
config:
inputs:
# Building the latest, not tagged coredeps
- name: deltachat-core-rust
image_resource:
source:
repository: vito/oci-build-task
type: registry-image
outputs:
- name: coredeps-image
path: image
params:
CONTEXT: deltachat-core-rust/scripts/docker-coredeps
UNPACK_ROOTFS: "true"
platform: linux
caches:
- path: cache
run:
path: build
# Use built image to build python wheels
- task: build-wheels
image: coredeps-image
config:
inputs:
- name: deltachat-core-rust-release
path: .
outputs:
- name: py-docs
path: ./python/doc/_build/
# Source packages
- name: py-dist
path: ./python/.docker-tox/dist/
# Binary wheels
- name: py-wheels
path: ./python/.docker-tox/wheelhouse/
platform: linux
run:
path: bash
args:
- -exc
- |
scripts/run_all.sh
# Upload python docs to py.delta.chat
- task: upload-py-docs
config:
inputs:
- name: py-docs
image_resource:
type: registry-image
source:
repository: alpine
platform: linux
run:
path: sh
args:
- -ec
- |
apk add --no-cache rsync openssh-client
mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo "(("c.delta.chat".private_key))" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
rsync -e "ssh -o StrictHostKeyChecking=no" -avz --delete py-docs/html/ delta@py.delta.chat:build/master
# Upload x86_64 wheels and source packages
- task: upload-wheels
config:
inputs:
- name: py-wheels
- name: py-dist
image_resource:
type: registry-image
source:
repository: debian
platform: linux
run:
path: sh
args:
- -ec
- |
apt-get update -y
apt-get install -y --no-install-recommends python3-pip python3-setuptools
pip3 install devpi
devpi use https://m.devpi.net/dc/master
devpi login ((devpi.login)) --password ((devpi.password))
devpi upload py-wheels/*manylinux201*
devpi upload py-dist/*
- name: python-aarch64
plan:
- get: deltachat-core-rust
- get: deltachat-core-rust-release
trigger: true
# Build manylinux image with additional dependencies
- task: build-coredeps
privileged: true
config:
inputs:
# Building the latest, not tagged coredeps
- name: deltachat-core-rust
image_resource:
source:
repository: vito/oci-build-task
type: registry-image
outputs:
- name: coredeps-image
path: image
params:
CONTEXT: deltachat-core-rust/scripts/docker-coredeps-arm64
UNPACK_ROOTFS: "true"
platform: linux
caches:
- path: cache
run:
path: build
# Use built image to build python wheels
- task: build-wheels
image: coredeps-image
config:
inputs:
- name: deltachat-core-rust-release
path: .
outputs:
- name: py-wheels
path: ./python/.docker-tox/wheelhouse/
platform: linux
run:
path: bash
args:
- -exc
- |
scripts/run_all.sh
# Upload aarch64 wheels
- task: upload-wheels
config:
inputs:
- name: py-wheels
image_resource:
type: registry-image
source:
repository: debian
platform: linux
run:
path: sh
args:
- -ec
- |
apt-get update -y
apt-get install -y --no-install-recommends python3-pip python3-setuptools
pip3 install devpi
devpi use https://m.devpi.net/dc/master
devpi login ((devpi.login)) --password ((devpi.password))
devpi upload py-wheels/*manylinux201*

View File

@@ -1,7 +1,7 @@
#!/bin/bash
PERL_VERSION=5.34.0
# PERL_SHA256=551efc818b968b05216024fb0b727ef2ad4c100f8cb6b43fab615fa78ae5be9a
PERL_VERSION=5.30.0
# PERL_SHA256=7e929f64d4cb0e9d1159d4a59fc89394e27fa1f7004d0836ca0d514685406ea8
curl -O https://www.cpan.org/src/5.0/perl-${PERL_VERSION}.tar.gz
# echo "${PERL_SHA256} perl-${PERL_VERSION}.tar.gz" | sha256sum -c -
tar -xzf perl-${PERL_VERSION}.tar.gz

View File

@@ -8,11 +8,9 @@ set -e -x
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.54.0
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu.tar.gz" | tar xz
cd "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"
curl "https://static.rust-lang.org/dist/rust-1.52.1-$(uname -m)-unknown-linux-gnu.tar.gz" | tar xz
cd "rust-1.52.1-$(uname -m)-unknown-linux-gnu"
./install.sh --prefix=/usr --components=rustc,cargo,"rust-std-$(uname -m)-unknown-linux-gnu"
rustc --version
cd ..
rm -fr "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"
rm -fr "rust-1.52.1-$(uname -m)-unknown-linux-gnu"

View File

@@ -1,7 +1,7 @@
#!/bin/bash
PERL_VERSION=5.34.0
# PERL_SHA256=551efc818b968b05216024fb0b727ef2ad4c100f8cb6b43fab615fa78ae5be9a
PERL_VERSION=5.30.0
# PERL_SHA256=7e929f64d4cb0e9d1159d4a59fc89394e27fa1f7004d0836ca0d514685406ea8
curl -O https://www.cpan.org/src/5.0/perl-${PERL_VERSION}.tar.gz
# echo "${PERL_SHA256} perl-${PERL_VERSION}.tar.gz" | sha256sum -c -
tar -xzf perl-${PERL_VERSION}.tar.gz

View File

@@ -3,16 +3,9 @@
set -e -x
# Install Rust
#
# Path from https://forge.rust-lang.org/infra/other-installation-methods.html
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.54.0
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu.tar.gz" | tar xz
cd "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"
./install.sh --prefix=/usr --components=rustc,cargo,"rust-std-$(uname -m)-unknown-linux-gnu"
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain "1.50.0-$(uname -m)-unknown-linux-gnu" -y
export PATH=/root/.cargo/bin:$PATH
rustc --version
cd ..
rm -fr "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"
# remove some 300-400 MB that we don't need for automated builds
rm -rf "/root/.rustup/toolchains/1.50.0-$(uname -m)-unknown-linux-gnu/share"

View File

@@ -0,0 +1,67 @@
#!/bin/bash
export BRANCH=${CIRCLE_BRANCH:-master}
export REPONAME=${CIRCLE_PROJECT_REPONAME:-deltachat-core-rust}
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
set -xe
ssh -oBatchMode=yes -oStrictHostKeyChecking=no $SSHTARGET mkdir -p "$BUILDDIR"
git ls-files >.rsynclist
# we seem to need .git for setuptools_scm versioning
find .git >>.rsynclist
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR-arm64"
set +x
# we have to create a remote file for the remote-docker run
# so we can do a simple ssh command with a TTY
# so that when our job dies, all container-runs are aborted.
# sidenote: the circle-ci machinery will kill ongoing jobs
# if there are new commits and we want to ensure that
# everything is terminated/cleaned up and we have no orphaned
# useless still-running docker-containers consuming resources.
for arch in "" "-arm64"; do
ssh $SSHTARGET bash -c "cat >${BUILDDIR}${arch}/exec_docker_run" <<_HERE
set +x -e
shopt -s huponexit
cd ${BUILDDIR}${arch}
export DCC_NEW_TMP_EMAIL=$DCC_NEW_TMP_EMAIL
set -x
# run everything else inside docker
docker run -e DCC_NEW_TMP_EMAIL \
--rm -it -v \$(pwd):/mnt -w /mnt \
deltachat/coredeps${arch} scripts/run_all.sh
_HERE
done
echo "--- Running $CIRCLE_JOB remotely"
echo "--- Building aarch64 wheels"
ssh -o ServerAliveInterval=30 -t $SSHTARGET bash "$BUILDDIR-arm64/exec_docker_run"
echo "--- Building x86_64 wheels"
ssh -o ServerAliveInterval=30 -t $SSHTARGET bash "$BUILDDIR/exec_docker_run"
mkdir -p workspace
# Wheels
for arch in "" "-arm64"; do
rsync -avz "$SSHTARGET:$BUILDDIR${arch}/python/.docker-tox/wheelhouse/*manylinux201*" workspace/wheelhouse/
done
# Source packages
rsync -avz "$SSHTARGET:$BUILDDIR${arch}/python/.docker-tox/dist/*" workspace/wheelhouse/
# Documentation
rsync -avz "$SSHTARGET:$BUILDDIR/python/doc/_build/" workspace/py-docs

63
spec.md
View File

@@ -1,6 +1,6 @@
# chat-mail specification
Version: 0.33.0
Version: 0.32.0
Status: In-progress
Format: [Semantic Line Breaks](https://sembr.org/)
@@ -301,9 +301,9 @@ to add a `Chat-Group-Avatar` only on image changes.
A user MAY have a profile-image that MAY be distributed to their contacts.
To change or set the profile-image,
the messenger MUST add the header `Chat-User-Avatar: base64:IMAGEDATA`.
To bypass limits of headers, it is recommended not to use the outer header
and to limit the size to 20k.
the messenger MUST attach an image file to a message
and MUST add the header `Chat-User-Avatar`
with the value set to the image name.
To remove the profile-image,
the messenger MUST add the header `Chat-User-Avatar: 0`.
@@ -320,14 +320,19 @@ The messenger SHOULD NOT send an explicit mail to normal MUAs.
From: sender@domain
To: rcpt@domain
Chat-Version: 1.0
Chat-User-Avatar: photo.jpg
Subject: Chat: Hello, ...
Content-Type: multipart/mixed; boundary="==break=="
--==break==
Content-Type: text/plain
Chat-User-Avatar: base64:AKCgkJi3j4l5kjoldfUAKCgkJi3j4lldfHjgWICwgIEBQY ...
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.
@@ -337,11 +342,6 @@ in the same message.
To save data, it is RECOMMENDED to add a `Chat-User-Avatar` header
only on image changes.
In older specs, the profile-image was sent as an attachment
and `Chat-User-Avatar:` specified its name.
However, it turned out that these attachements are kind of unuexpected to users,
therefore the profile-image go to the header now.
# Locations
@@ -401,41 +401,9 @@ it is fine if the location is detected on forwarding etc.
</kml>
# Stickers
# Miscellaneous
Stickers are send as normal images
with the additional header `Chat-Content: sticker`.
It is discouraged to send stickers together with user generated text,
however, stickers can be used as a reply to a message
and also the footer should be set as usual.
From: alice@example.org
To: bob@example.com
Chat-Version: 1.0
Chat-Content: sticker
Message-ID: Mr.12345uvwxyZ.0005@example.org
Subject: Message from Alice
Content-Type: multipart/mixed; boundary="==break=="
--==break==
Content-Type: text/plain
--
Hi there! I am using this new messenger!
--==break==
Content-Type: image/png
Content-Disposition: attachment; filename="sticker.png"
R0lGODlhpAGkAfe9AP+zd2eQkZhrI//z9v++PMb///+scrdDT3BtbtrZ2f/LQSsREcdIVf9 ...
--==break==--
Typical sticker formats are `image/png`, `image/gif` and `image/webp`.
Animated stickers are supported
by just using an image format that supports animation.
# Voice messages
Messengers SHOULD use the header `In-Reply-To` as usual.
Messengers SHOULD add a `Chat-Voice-message: 1` header
if an attached audio file is a voice message.
@@ -449,11 +417,6 @@ This allows the receiver to show the time without knowing the file format.
Chat-Voice-Message: 1
Chat-Duration: 10000
# Miscellaneous
Messengers SHOULD use the header `In-Reply-To` as usual.
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))
@@ -474,4 +437,4 @@ as the sending time of the message as indicated by its Date header,
or the time of first receipt if that date is in the future or unavailable.
Copyright © 2017-2021 Delta Chat contributors.
Copyright © 2017-2020 Delta Chat contributors.

View File

@@ -1,8 +1,5 @@
//! # Account manager module.
use std::collections::BTreeMap;
use async_std::channel::{Receiver, Sender};
use async_std::fs;
use async_std::path::PathBuf;
use async_std::prelude::*;
@@ -21,7 +18,6 @@ pub struct Accounts {
dir: PathBuf,
config: Config,
accounts: Arc<RwLock<BTreeMap<u32, Context>>>,
emitter: EventEmitter,
}
impl Accounts {
@@ -34,13 +30,19 @@ impl Accounts {
Accounts::open(dir).await
}
/// Creates a new default structure.
/// Creates a new default structure, including a default account.
pub async fn create(os_name: String, dir: &PathBuf) -> Result<()> {
fs::create_dir_all(dir)
.await
.context("failed to create folder")?;
Config::new(os_name.clone(), dir).await?;
// create default account
let config = Config::new(os_name.clone(), dir).await?;
let account_config = config.new_account(dir).await?;
Context::new(os_name, account_config.dbfile().into(), account_config.id)
.await
.context("failed to create default account")?;
Ok(())
}
@@ -56,16 +58,10 @@ impl Accounts {
let config = Config::from_file(config_file).await?;
let accounts = config.load_accounts().await?;
let emitter = EventEmitter::new();
for account in accounts.values() {
emitter.add_account(account).await?;
}
Ok(Self {
dir,
config,
accounts: Arc::new(RwLock::new(accounts)),
emitter,
})
}
@@ -75,17 +71,14 @@ impl Accounts {
}
/// Get the currently selected account.
pub async fn get_selected_account(&self) -> Option<Context> {
pub async fn get_selected_account(&self) -> Context {
let id = self.config.get_selected_account().await;
self.accounts.read().await.get(&id).cloned()
}
/// Returns the currently selected account's id or None if no account is selected.
pub async fn get_selected_account_id(&self) -> Option<u32> {
match self.config.get_selected_account().await {
0 => None,
id => Some(id),
}
self.accounts
.read()
.await
.get(&id)
.cloned()
.expect("inconsistent state")
}
/// Select the given account.
@@ -101,7 +94,6 @@ impl Accounts {
let account_config = self.config.new_account(&self.dir).await?;
let ctx = Context::new(os_name, account_config.dbfile().into(), account_config.id).await?;
self.emitter.add_account(&ctx).await?;
self.accounts.write().await.insert(account_config.id, ctx);
Ok(account_config.id)
@@ -128,7 +120,6 @@ impl Accounts {
/// Migrate an existing account into this structure.
pub async fn migrate_account(&self, dbfile: PathBuf) -> Result<u32> {
let blobdir = Context::derive_blobdir(&dbfile);
let walfile = Context::derive_walfile(&dbfile);
ensure!(
dbfile.exists().await,
@@ -152,7 +143,6 @@ impl Accounts {
let new_dbfile = account_config.dbfile().into();
let new_blobdir = Context::derive_blobdir(&new_dbfile);
let new_walfile = Context::derive_walfile(&new_dbfile);
let res = {
fs::create_dir_all(&account_config.dir)
@@ -164,11 +154,6 @@ impl Accounts {
fs::rename(&blobdir, &new_blobdir)
.await
.context("failed to rename blobdir")?;
if walfile.exists().await {
fs::rename(&walfile, &new_walfile)
.await
.context("failed to rename walfile")?;
}
Ok(())
};
@@ -181,7 +166,6 @@ impl Accounts {
account_config.id,
)
.await?;
self.emitter.add_account(&ctx).await?;
self.accounts.write().await.insert(account_config.id, ctx);
Ok(account_config.id)
}
@@ -206,23 +190,23 @@ impl Accounts {
self.accounts.read().await.keys().copied().collect()
}
/// This is meant especially for iOS, because iOS needs to tell the system when its background work is done.
///
/// Returns whether all accounts finished their background work.
/// DC_EVENT_CONNECTIVITY_CHANGED will be sent when this turns to true.
///
/// iOS can:
/// - call dc_start_io() (in case IO was not running)
/// - call dc_maybe_network()
/// - while dc_accounts_all_work_done() returns false:
/// - Wait for DC_EVENT_CONNECTIVITY_CHANGED
pub async fn all_work_done(&self) -> bool {
for account in self.accounts.read().await.values() {
if !account.all_work_done().await {
return false;
/// Import a backup using a new account and selects it.
pub async fn import_account(&self, file: PathBuf) -> Result<u32> {
let old_id = self.config.get_selected_account().await;
let id = self.add_account().await?;
let ctx = self.get_account(id).await.expect("just added");
match crate::imex::imex(&ctx, crate::imex::ImexMode::ImportBackup, &file).await {
Ok(_) => Ok(id),
Err(err) => {
// remove temp account
self.remove_account(id).await?;
// set selection back
self.select_account(old_id).await?;
Err(err)
}
}
true
}
pub async fn start_io(&self) {
@@ -246,70 +230,32 @@ impl Accounts {
}
}
pub async fn maybe_network_lost(&self) {
let accounts = &*self.accounts.read().await;
for account in accounts.values() {
account.maybe_network_lost().await;
}
}
/// Returns unified event emitter.
/// Unified event emitter.
pub async fn get_event_emitter(&self) -> EventEmitter {
self.emitter.clone()
let emitters: Vec<_> = self
.accounts
.read()
.await
.iter()
.map(|(_id, a)| a.get_event_emitter())
.collect();
EventEmitter(futures::stream::select_all(emitters))
}
}
/// Unified event emitter for multiple accounts.
#[derive(Debug, Clone)]
pub struct EventEmitter {
/// Aggregate stream of events from all accounts.
stream: Arc<RwLock<futures::stream::SelectAll<crate::events::EventEmitter>>>,
/// Sender for the channel where new account emitters will be pushed.
sender: Sender<crate::events::EventEmitter>,
/// Receiver for the channel where new account emitters will be pushed.
receiver: Receiver<crate::events::EventEmitter>,
}
#[derive(Debug)]
pub struct EventEmitter(futures::stream::SelectAll<crate::events::EventEmitter>);
impl EventEmitter {
pub fn new() -> Self {
let (sender, receiver) = async_std::channel::unbounded();
Self {
stream: Arc::new(RwLock::new(futures::stream::SelectAll::new())),
sender,
receiver,
}
}
/// Blocking recv of an event. Return `None` if all `Sender`s have been droped.
pub fn recv_sync(&mut self) -> Option<Event> {
async_std::task::block_on(self.recv()).unwrap_or_default()
async_std::task::block_on(self.recv())
}
/// Async recv of an event. Return `None` if all `Sender`s have been dropped.
pub async fn recv(&mut self) -> Result<Option<Event>> {
let mut stream = self.stream.write().await;
loop {
match futures::future::select(self.receiver.recv(), stream.next()).await {
futures::future::Either::Left((emitter, _)) => {
stream.push(emitter?);
}
futures::future::Either::Right((ev, _)) => return Ok(ev),
}
}
}
/// Add event emitter of a new account to the aggregate event emitter.
pub async fn add_account(&self, context: &Context) -> Result<()> {
self.sender.send(context.get_event_emitter()).await?;
Ok(())
}
}
impl Default for EventEmitter {
fn default() -> Self {
Self::new()
/// Async recv of an event. Return `None` if all `Sender`s have been droped.
pub async fn recv(&mut self) -> Option<Event> {
self.0.next().await
}
}
@@ -320,14 +266,13 @@ impl async_std::stream::Stream for EventEmitter {
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
std::pin::Pin::new(&mut self).poll_next(cx)
std::pin::Pin::new(&mut self.0).poll_next(cx)
}
}
pub const CONFIG_NAME: &str = "accounts.toml";
pub const DB_NAME: &str = "dc.db";
/// Account manager configuration file.
#[derive(Debug, Clone)]
pub struct Config {
file: PathBuf,
@@ -402,7 +347,7 @@ impl Config {
}
/// Create a new account in the given root directory.
async fn new_account(&self, dir: &PathBuf) -> Result<AccountConfig> {
pub async fn new_account(&self, dir: &PathBuf) -> Result<AccountConfig> {
let id = {
let inner = &mut self.inner.write().await;
let id = inner.next_id;
@@ -442,7 +387,7 @@ impl Config {
self.sync().await
}
async fn get_account(&self, id: u32) -> Option<AccountConfig> {
pub async fn get_account(&self, id: u32) -> Option<AccountConfig> {
self.inner
.read()
.await
@@ -473,9 +418,8 @@ impl Config {
}
}
/// Configuration of a single account.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct AccountConfig {
pub struct AccountConfig {
/// Unique id.
pub id: u32,
/// Root directory for all data for this account.
@@ -500,8 +444,6 @@ mod tests {
let p: PathBuf = dir.path().join("accounts1").into();
let accounts1 = Accounts::new("my_os".into(), p.clone()).await.unwrap();
accounts1.add_account().await.unwrap();
let accounts2 = Accounts::open(p).await.unwrap();
assert_eq!(accounts1.accounts.read().await.len(), 1);
@@ -524,11 +466,7 @@ mod tests {
let p: PathBuf = dir.path().join("accounts").into();
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
assert_eq!(accounts.accounts.read().await.len(), 0);
assert_eq!(accounts.config.get_selected_account().await, 0);
let id = accounts.add_account().await.unwrap();
assert_eq!(id, 1);
assert_eq!(accounts.accounts.read().await.len(), 1);
assert_eq!(accounts.config.get_selected_account().await, 1);
@@ -545,35 +483,14 @@ mod tests {
assert_eq!(accounts.accounts.read().await.len(), 1);
}
#[async_std::test]
async fn test_accounts_remove_last() -> Result<()> {
let dir = tempfile::tempdir()?;
let p: PathBuf = dir.path().join("accounts").into();
let accounts = Accounts::new("my_os".into(), p.clone()).await?;
assert!(accounts.get_selected_account().await.is_none());
assert_eq!(accounts.config.get_selected_account().await, 0);
let id = accounts.add_account().await?;
assert!(accounts.get_selected_account().await.is_some());
assert_eq!(id, 1);
assert_eq!(accounts.accounts.read().await.len(), 1);
assert_eq!(accounts.config.get_selected_account().await, id);
accounts.remove_account(id).await?;
assert!(accounts.get_selected_account().await.is_none());
Ok(())
}
#[async_std::test]
async fn test_migrate_account() {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts").into();
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
assert_eq!(accounts.accounts.read().await.len(), 0);
assert_eq!(accounts.config.get_selected_account().await, 0);
assert_eq!(accounts.accounts.read().await.len(), 1);
assert_eq!(accounts.config.get_selected_account().await, 1);
let extern_dbfile: PathBuf = dir.path().join("other").into();
let ctx = Context::new("my_os".into(), extern_dbfile.clone(), 0)
@@ -589,10 +506,10 @@ mod tests {
.migrate_account(extern_dbfile.clone())
.await
.unwrap();
assert_eq!(accounts.accounts.read().await.len(), 1);
assert_eq!(accounts.config.get_selected_account().await, 1);
assert_eq!(accounts.accounts.read().await.len(), 2);
assert_eq!(accounts.config.get_selected_account().await, 2);
let ctx = accounts.get_selected_account().await.unwrap();
let ctx = accounts.get_selected_account().await;
assert_eq!(
"me@mail.com",
ctx.get_config(crate::config::Config::Addr)
@@ -610,7 +527,7 @@ mod tests {
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
for expected_id in 1..10 {
for expected_id in 2..10 {
let id = accounts.add_account().await.unwrap();
assert_eq!(id, expected_id);
}
@@ -620,86 +537,4 @@ mod tests {
assert_eq!(ids.get(i), Some(&expected_id));
}
}
#[async_std::test]
async fn test_accounts_ids_unique_increasing_and_persisted() -> Result<()> {
let dir = tempfile::tempdir()?;
let p: PathBuf = dir.path().join("accounts").into();
let dummy_accounts = 10;
let (id0, id1, id2) = {
let accounts = Accounts::new("my_os".into(), p.clone()).await?;
accounts.add_account().await?;
let ids = accounts.get_all().await;
assert_eq!(ids.len(), 1);
let id0 = *ids.get(0).unwrap();
let ctx = accounts.get_account(id0).await.unwrap();
ctx.set_config(crate::config::Config::Addr, Some("one@example.org"))
.await?;
let id1 = accounts.add_account().await?;
let ctx = accounts.get_account(id1).await.unwrap();
ctx.set_config(crate::config::Config::Addr, Some("two@example.org"))
.await?;
// add and remove some accounts and force a gap (ids must not be reused)
for _ in 0..dummy_accounts {
let to_delete = accounts.add_account().await?;
accounts.remove_account(to_delete).await?;
}
let id2 = accounts.add_account().await?;
let ctx = accounts.get_account(id2).await.unwrap();
ctx.set_config(crate::config::Config::Addr, Some("three@example.org"))
.await?;
accounts.select_account(id1).await?;
(id0, id1, id2)
};
assert!(id0 > 0);
assert!(id1 > id0);
assert!(id2 > id1 + dummy_accounts);
let (id0_reopened, id1_reopened, id2_reopened) = {
let accounts = Accounts::new("my_os".into(), p.clone()).await?;
let ctx = accounts.get_selected_account().await.unwrap();
assert_eq!(
ctx.get_config(crate::config::Config::Addr).await?,
Some("two@example.org".to_string())
);
let ids = accounts.get_all().await;
assert_eq!(ids.len(), 3);
let id0 = *ids.get(0).unwrap();
let ctx = accounts.get_account(id0).await.unwrap();
assert_eq!(
ctx.get_config(crate::config::Config::Addr).await?,
Some("one@example.org".to_string())
);
let id1 = *ids.get(1).unwrap();
let t = accounts.get_account(id1).await.unwrap();
assert_eq!(
t.get_config(crate::config::Config::Addr).await?,
Some("two@example.org".to_string())
);
let id2 = *ids.get(2).unwrap();
let ctx = accounts.get_account(id2).await.unwrap();
assert_eq!(
ctx.get_config(crate::config::Config::Addr).await?,
Some("three@example.org".to_string())
);
(id0, id1, id2)
};
assert_eq!(id0, id0_reopened);
assert_eq!(id1, id1_reopened);
assert_eq!(id2, id2_reopened);
Ok(())
}
}

View File

@@ -1,4 +1,4 @@
//! # Autocrypt header module.
//! # Autocrypt header module
//!
//! Parse and create [Autocrypt-headers](https://autocrypt.org/en/latest/level1.html#the-autocrypt-header).

View File

@@ -1,4 +1,4 @@
//! # Blob directory management.
//! # Blob directory management
use core::cmp::max;
use std::ffi::OsStr;
@@ -74,7 +74,7 @@ impl<'a> BlobObject<'a> {
// workaround a bug in async-std
// (the executor does not handle blocking operation in Drop correctly,
// see <https://github.com/async-rs/async-std/issues/900>)
// see https://github.com/async-rs/async-std/issues/900 )
let _ = file.flush().await;
let blob = BlobObject {
@@ -452,7 +452,7 @@ impl<'a> BlobObject<'a> {
img.write_to(encoded, image::ImageFormat::Jpeg)?;
Ok(())
}
fn encoded_img_exceeds_bytes(
fn encode_img_exceeds_bytes(
context: &Context,
img: &DynamicImage,
max_bytes: Option<usize>,
@@ -477,7 +477,7 @@ impl<'a> BlobObject<'a> {
let exceeds_width = img.width() > img_wh || img.height() > img_wh;
let do_scale =
exceeds_width || encoded_img_exceeds_bytes(context, &img, max_bytes, &mut encoded)?;
exceeds_width || encode_img_exceeds_bytes(context, &img, max_bytes, &mut encoded)?;
let do_rotate = matches!(orientation, Ok(90) | Ok(180) | Ok(270));
if do_scale || do_rotate {
@@ -500,7 +500,7 @@ impl<'a> BlobObject<'a> {
loop {
let new_img = img.thumbnail(img_wh, img_wh);
if encoded_img_exceeds_bytes(context, &new_img, max_bytes, &mut encoded)? {
if encode_img_exceeds_bytes(context, &new_img, max_bytes, &mut encoded)? {
if img_wh < 20 {
return Err(format_err!(
"Failed to scale image to below {}B",
@@ -511,10 +511,6 @@ impl<'a> BlobObject<'a> {
img_wh = img_wh * 2 / 3;
} else {
if encoded.is_empty() {
encode_img(&new_img, &mut encoded)?;
}
info!(
context,
"Final scaled-down image size: {}B ({}px)",
@@ -621,8 +617,7 @@ mod tests {
use super::*;
use crate::{message::Message, test_utils::TestContext};
use image::Pixel;
use crate::test_utils::TestContext;
#[async_std::test]
async fn test_create() {
@@ -920,148 +915,4 @@ mod tests {
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
}
#[async_std::test]
async fn test_recode_image() {
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
// BALANCED_IMAGE_SIZE > 1000, the original image size, so the image is not scaled down:
send_image_check_mediaquality(Some("0"), bytes, 1000, 1000, 0, 1000, 1000)
.await
.unwrap();
send_image_check_mediaquality(
Some("1"),
bytes,
1000,
1000,
0,
WORSE_IMAGE_SIZE,
WORSE_IMAGE_SIZE,
)
.await
.unwrap();
// The "-rotated" files are rotated by 270 degrees using the Exif metadata
let bytes = include_bytes!("../test-data/image/rectangle2000x1800-rotated.jpg");
let img_rotated = send_image_check_mediaquality(
Some("0"),
bytes,
2000,
1800,
270,
BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE,
)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
let mut bytes = vec![];
img_rotated
.write_to(&mut bytes, image::ImageFormat::Jpeg)
.unwrap();
let img_rotated = send_image_check_mediaquality(
Some("0"),
&bytes,
BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE,
0,
BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE,
)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
let img_rotated = send_image_check_mediaquality(
Some("1"),
&bytes,
BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE,
0,
WORSE_IMAGE_SIZE * 1800 / 2000,
WORSE_IMAGE_SIZE,
)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg");
let img_rotated = send_image_check_mediaquality(Some("0"), bytes, 200, 180, 270, 180, 200)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg");
let img_rotated = send_image_check_mediaquality(Some("1"), bytes, 200, 180, 270, 180, 200)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
}
fn assert_correct_rotation(img: &DynamicImage) {
// The test images are black in the bottom left corner after correctly applying
// the EXIF orientation
let [luma] = img.get_pixel(10, 10).to_luma().0;
assert_eq!(luma, 255);
let [luma] = img.get_pixel(img.width() - 10, 10).to_luma().0;
assert_eq!(luma, 255);
let [luma] = img
.get_pixel(img.width() - 10, img.height() - 10)
.to_luma()
.0;
assert_eq!(luma, 255);
let [luma] = img.get_pixel(10, img.height() - 10).to_luma().0;
assert_eq!(luma, 0);
}
async fn send_image_check_mediaquality(
media_quality_config: Option<&str>,
bytes: &[u8],
original_width: u32,
original_height: u32,
orientation: i32,
compressed_width: u32,
compressed_height: u32,
) -> anyhow::Result<DynamicImage> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
alice
.set_config(Config::MediaQuality, media_quality_config)
.await?;
let file = alice.get_blobdir().join("file.jpg");
File::create(&file).await?.write_all(bytes).await?;
let img = image::open(&file)?;
assert_eq!(img.width(), original_width);
assert_eq!(img.height(), original_height);
let blob = BlobObject::new_from_path(&alice, &file).await?;
assert_eq!(blob.get_exif_orientation(&alice).unwrap_or(0), orientation);
let mut msg = Message::new(Viewtype::Image);
msg.set_file(file.to_str().unwrap(), None);
let chat = alice.create_chat(&bob).await;
let sent = alice.send_msg(chat.id, &mut msg).await;
let alice_msg = alice.get_last_msg().await;
assert_eq!(alice_msg.get_width() as u32, compressed_width);
assert_eq!(alice_msg.get_height() as u32, compressed_height);
let img = image::open(alice_msg.get_file(&alice).unwrap())?;
assert_eq!(img.width() as u32, compressed_width);
assert_eq!(img.height() as u32, compressed_height);
bob.recv_msg(&sent).await;
let bob_msg = bob.get_last_msg().await;
assert_eq!(bob_msg.get_width() as u32, compressed_width);
assert_eq!(bob_msg.get_height() as u32, compressed_height);
let file = bob_msg.get_file(&bob).unwrap();
let blob = BlobObject::new_from_path(&bob, &file).await?;
assert_eq!(blob.get_exif_orientation(&bob).unwrap_or(0), 0);
let img = image::open(file)?;
assert_eq!(img.width() as u32, compressed_width);
assert_eq!(img.height() as u32, compressed_height);
Ok(img)
}
}

View File

@@ -1,4 +1,4 @@
//! # Chat module.
//! # Chat module
use std::convert::{TryFrom, TryInto};
use std::str::FromStr;
@@ -8,17 +8,19 @@ use anyhow::{bail, ensure, format_err, Context as _, Result};
use async_std::path::{Path, PathBuf};
use deltachat_derive::{FromSql, ToSql};
use itertools::Itertools;
use num_traits::FromPrimitive;
use serde::{Deserialize, Serialize};
use crate::aheader::EncryptPreference;
use crate::blob::{BlobError, BlobObject};
use crate::chatlist::dc_get_archived_cnt;
use crate::color::str_to_color;
use crate::config::Config;
use crate::constants::{
Blocked, Chattype, Viewtype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK,
DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_INFO,
DC_CONTACT_ID_LAST_SPECIAL, DC_CONTACT_ID_SELF, DC_GCM_ADDDAYMARKER, DC_GCM_INFO_ONLY,
DC_RESEND_USER_AVATAR_DAYS,
Blocked, Chattype, ShowEmails, Viewtype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK,
DC_CHAT_ID_DEADDROP, DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_CONTACT_ID_DEVICE,
DC_CONTACT_ID_INFO, DC_CONTACT_ID_LAST_SPECIAL, DC_CONTACT_ID_SELF, DC_GCM_ADDDAYMARKER,
DC_GCM_INFO_ONLY, DC_RESEND_USER_AVATAR_DAYS,
};
use crate::contact::{addr_cmp, Contact, Origin, VerifiedStatus};
use crate::context::Context;
@@ -31,7 +33,7 @@ use crate::ephemeral::{delete_expired_messages, schedule_ephemeral_task, Timer a
use crate::events::EventType;
use crate::html::new_html_mimepart;
use crate::job::{self, Action};
use crate::message::{self, Message, MessageState, MsgId};
use crate::message::{self, InvalidMsgId, Message, MessageState, MsgId};
use crate::mimeparser::SystemMessage;
use crate::param::{Param, Params};
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
@@ -112,6 +114,15 @@ impl ChatId {
(0..=DC_CHAT_ID_LAST_SPECIAL.0).contains(&self.0)
}
/// Chat ID which represents the deaddrop chat.
///
/// This is a virtual chat showing all messages belonging to chats
/// flagged with [Blocked::Deaddrop]. Usually the UI will show
/// these messages as contact requests.
pub fn is_deaddrop(self) -> bool {
self == DC_CHAT_ID_DEADDROP
}
/// Chat ID for messages which need to be deleted.
///
/// Messages which should be deleted get this chat ID and are
@@ -170,11 +181,14 @@ impl ChatId {
///
/// This should be used when **a user action** creates a chat 1:1, it ensures the chat
/// exists and is unblocked and scales the [`Contact`]'s origin.
///
/// If a chat was in the deaddrop unblocking is how it becomes a normal chat and it will
/// look to the user like the chat was newly created.
pub async fn create_for_contact(context: &Context, contact_id: u32) -> Result<Self> {
let chat_id = match ChatIdBlocked::lookup_by_contact(context, contact_id).await? {
Some(chat) => {
if chat.blocked != Blocked::Not {
chat.id.unblock(context).await?;
chat.id.unblock(context).await;
}
chat.id
}
@@ -214,90 +228,23 @@ impl ChatId {
Ok(())
}
/// Updates chat blocked status.
///
/// Returns true if the value was modified.
async fn set_blocked(self, context: &Context, new_blocked: Blocked) -> Result<bool> {
pub async fn set_blocked(self, context: &Context, new_blocked: Blocked) -> bool {
if self.is_special() {
bail!("ignoring setting of Block-status for {}", self);
warn!(context, "ignoring setting of Block-status for {}", self);
return false;
}
let count = context
context
.sql
.execute(
"UPDATE chats SET blocked=?1 WHERE id=?2 AND blocked != ?1",
"UPDATE chats SET blocked=? WHERE id=?;",
paramsv![new_blocked, self],
)
.await?;
Ok(count > 0)
.await
.is_ok()
}
/// Blocks the chat as a result of explicit user action.
pub async fn block(self, context: &Context) -> Result<()> {
let chat = Chat::load_from_db(context, self).await?;
match chat.typ {
Chattype::Undefined => bail!("Can't block chat of undefined chattype"),
Chattype::Single => {
for contact_id in get_chat_contacts(context, self).await? {
if contact_id != DC_CONTACT_ID_SELF {
info!(
context,
"Blocking the contact {} to block 1:1 chat", contact_id
);
Contact::block(context, contact_id).await?;
}
}
}
Chattype::Group => {
info!(context, "Can't block groups yet, deleting the chat");
self.delete(context).await?;
}
Chattype::Mailinglist => {
if self.set_blocked(context, Blocked::Manually).await? {
context.emit_event(EventType::ChatModified(self));
}
}
}
Ok(())
}
/// Unblocks the chat.
pub async fn unblock(self, context: &Context) -> Result<()> {
self.set_blocked(context, Blocked::Not).await?;
Ok(())
}
/// Accept the contact request.
///
/// Unblocks the chat and scales up origin of contacts.
pub async fn accept(self, context: &Context) -> Result<()> {
let chat = Chat::load_from_db(context, self).await?;
match chat.typ {
Chattype::Undefined => bail!("Can't accept chat of undefined chattype"),
Chattype::Single | Chattype::Group => {
// User has "created a chat" with all these contacts.
//
// Previously accepting a chat literally created a chat because unaccepted chats
// went to "contact requests" list rather than normal chatlist.
for contact_id in get_chat_contacts(context, self).await? {
if contact_id != DC_CONTACT_ID_SELF {
Contact::scaleup_origin_by_id(context, contact_id, Origin::CreateChat)
.await;
}
}
}
Chattype::Mailinglist => {
// If the message is from a mailing list, the contacts are not counted as "known"
}
}
if self.set_blocked(context, Blocked::Not).await? {
context.emit_event(EventType::ChatModified(self));
}
Ok(())
pub async fn unblock(self, context: &Context) {
self.set_blocked(context, Blocked::Not).await;
}
/// Sets protection without sending a message.
@@ -381,14 +328,7 @@ impl ChatId {
msg.param.set_cmd(cmd);
send_msg(context, self, &mut msg).await?;
} else {
add_info_msg_with_cmd(
context,
self,
msg_text,
cmd,
dc_create_smeared_timestamp(context).await,
)
.await?;
add_info_msg_with_cmd(context, self, msg_text, cmd).await?;
}
Ok(())
@@ -513,12 +453,12 @@ impl ChatId {
/// Sets draft message.
///
/// Passing `None` as message just deletes the draft
pub async fn set_draft(self, context: &Context, mut msg: Option<&mut Message>) -> Result<()> {
pub async fn set_draft(self, context: &Context, msg: Option<&mut Message>) -> Result<()> {
if self.is_special() {
return Ok(());
}
let changed = match &mut msg {
let changed = match msg {
None => self.maybe_delete_draft(context).await?,
Some(msg) => self.set_draft_raw(context, msg).await?,
};
@@ -526,14 +466,7 @@ impl ChatId {
if changed {
context.emit_event(EventType::MsgsChanged {
chat_id: self,
msg_id: if msg.is_some() {
match self.get_draft_msg_id(context).await? {
Some(msg_id) => msg_id,
None => MsgId::new(0),
}
} else {
MsgId::new(0)
},
msg_id: MsgId::new(0),
});
}
@@ -605,7 +538,7 @@ impl ChatId {
}
let chat = Chat::load_from_db(context, self).await?;
if !chat.can_send(context).await {
if !chat.can_send() {
bail!("Can't set a draft: Can't send");
}
@@ -643,10 +576,7 @@ impl ChatId {
pub async fn get_msg_cnt(self, context: &Context) -> Result<usize> {
let count = context
.sql
.count(
"SELECT COUNT(*) FROM msgs WHERE hidden=0 AND chat_id=?",
paramsv![self],
)
.count("SELECT COUNT(*) FROM msgs WHERE chat_id=?", paramsv![self])
.await?;
Ok(count as usize)
}
@@ -667,10 +597,10 @@ impl ChatId {
.count(
"SELECT COUNT(*)
FROM msgs
WHERE state=?
WHERE state=10
AND hidden=0
AND chat_id=?;",
paramsv![MessageState::InFresh, self],
paramsv![self],
)
.await?;
Ok(count as usize)
@@ -810,7 +740,9 @@ impl ChatId {
impl std::fmt::Display for ChatId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.is_trash() {
if self.is_deaddrop() {
write!(f, "Chat#Deadrop")
} else if self.is_trash() {
write!(f, "Chat#Trash")
} else if self.is_archived_link() {
write!(f, "Chat#ArchivedLink")
@@ -897,8 +829,12 @@ impl Chat {
.await
.context(format!("Failed loading chat {} from database", chat_id))?;
if chat.id.is_archived_link() {
chat.name = stock_str::archived_chats(context).await;
if chat.id.is_deaddrop() {
chat.name = stock_str::dead_drop(context).await;
} else if chat.id.is_archived_link() {
let tempname = stock_str::archived_chats(context).await;
let cnt = dc_get_archived_cnt(context).await?;
chat.name = format!("{} ({})", tempname, cnt);
} else {
if chat.typ == Chattype::Single {
let mut chat_name = "Err [Name not found]".to_owned();
@@ -940,13 +876,8 @@ impl Chat {
}
/// Returns true if user can send messages to this chat.
pub async fn can_send(&self, context: &Context) -> bool {
!self.id.is_special()
&& !self.is_device_talk()
&& !self.is_mailing_list()
&& !self.is_contact_request()
&& (self.typ == Chattype::Single
|| is_contact_in_chat(context, self.id, DC_CONTACT_ID_SELF).await)
pub fn can_send(&self) -> bool {
!self.id.is_special() && !self.is_device_talk() && !self.is_mailing_list()
}
pub async fn update_param(&mut self, context: &Context) -> Result<()> {
@@ -975,7 +906,6 @@ impl Chat {
&self.name
}
/// Returns profile image path for the chat.
pub async fn get_profile_image(&self, context: &Context) -> Result<Option<PathBuf>> {
if let Some(image_rel) = self.param.get(Param::ProfileImage) {
if !image_rel.is_empty() {
@@ -1047,14 +977,6 @@ impl Chat {
self.visibility
}
/// Returns true if chat is a contact request.
///
/// Messages cannot be sent to such chat and read receipts are not
/// sent until the chat is manually unblocked.
pub fn is_contact_request(&self) -> bool {
self.blocked == Blocked::Request
}
pub fn is_unpromoted(&self) -> bool {
self.param.get_int(Param::Unpromoted).unwrap_or_default() == 1
}
@@ -1165,7 +1087,7 @@ impl Chat {
}
// the whole list of messages referenced may be huge;
// only use the oldest and the parent message
// only use the oldest and and the parent message
let parent_references = parent_references
.find(' ')
.and_then(|n| parent_references.get(..n))
@@ -1321,7 +1243,7 @@ impl rusqlite::types::FromSql for ChatVisibility {
2 => ChatVisibility::Pinned,
1 => ChatVisibility::Archived,
0 => ChatVisibility::Normal,
// fallback to Normal for unknown values, may happen eg. on imports created by a newer version.
// fallback to to Normal for unknown values, may happen eg. on imports created by a newer version.
_ => ChatVisibility::Normal,
}
})
@@ -1389,12 +1311,58 @@ pub struct ChatInfo {
/// Ephemeral message timer.
pub ephemeral_timer: EphemeralTimer,
// ToDo:
// - [ ] deaddrop,
// - [ ] summary,
// - [ ] lastUpdated,
// - [ ] freshMessageCounter,
// - [ ] email
}
/// Create a chat from a message ID.
///
/// Typically you'd do this for a message ID found in the
/// [DC_CHAT_ID_DEADDROP] which turns the chat the message belongs to
/// into a normal chat. The chat can be a 1:1 chat or a group chat
/// and all messages belonging to the chat will be moved from the
/// deaddrop to the normal chat.
///
/// In reality the messages already belong to this chat as receive_imf
/// always creates chat IDs appropriately, so this function really
/// only unblocks the chat and "scales up" the origin of the contact
/// the message is from.
///
/// If prompting the user before calling this function, they should be
/// asked whether they want to chat with the **contact** the message
/// is from and **not** the group name since this can be really weird
/// and confusing when taken from subject of implicit groups.
///
/// # Returns
///
/// The "created" chat ID is returned.
pub async fn create_by_msg_id(context: &Context, msg_id: MsgId) -> Result<ChatId> {
let msg = Message::load_from_db(context, msg_id).await?;
let chat = Chat::load_from_db(context, msg.chat_id).await?;
ensure!(
!chat.id.is_special(),
"Message can not belong to a special chat"
);
if chat.blocked != Blocked::Not {
chat.id.unblock(context).await;
// Sending with 0s as data since multiple messages may have changed.
context.emit_event(EventType::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
}
// If the message is from a mailing list, the contacts are not counted as "known"
if !chat.is_mailing_list() {
Contact::scaleup_origin_by_id(context, msg.from_id, Origin::CreateChat).await;
}
Ok(chat.id)
}
pub(crate) async fn update_saved_messages_icon(context: &Context) -> Result<()> {
// if there is no saved-messages chat, there is nothing to update. this is no error.
if let Some(chat_id) = ChatId::lookup_by_contact(context, DC_CONTACT_ID_SELF).await? {
@@ -1529,7 +1497,6 @@ impl ChatIdBlocked {
_ => (),
}
let created_timestamp = dc_create_smeared_timestamp(context).await;
let chat_id = context
.sql
.transaction(move |transaction| {
@@ -1542,7 +1509,7 @@ impl ChatIdBlocked {
chat_name,
params.to_string(),
create_blocked as u8,
created_timestamp,
time(),
],
)?;
let chat_id = ChatId::new(
@@ -1668,7 +1635,7 @@ async fn prepare_msg_common(
chat_id.unarchive(context).await?;
let mut chat = Chat::load_from_db(context, chat_id).await?;
ensure!(chat.can_send(context).await, "cannot send to {}", chat_id);
ensure!(chat.can_send(), "cannot send to {}", chat_id);
// The OutPreparing state is set by dc_prepare_msg() before it
// calls this function and the message is left in the OutPreparing
@@ -1717,7 +1684,11 @@ pub async fn send_msg(context: &Context, chat_id: ChatId, msg: &mut Message) ->
let forwards = msg.param.get(Param::PrepForwards);
if let Some(forwards) = forwards {
for forward in forwards.split(' ') {
if let Ok(msg_id) = forward.parse::<u32>().map(MsgId::new) {
if let Ok(msg_id) = forward
.parse::<u32>()
.map_err(|_| InvalidMsgId)
.map(MsgId::new)
{
if let Ok(mut msg) = Message::load_from_db(context, msg_id).await {
send_msg_inner(context, chat_id, &mut msg).await?;
};
@@ -1932,7 +1903,35 @@ pub async fn get_chat_msgs(
Ok(ret)
};
let items = if (flags & DC_GCM_INFO_ONLY) != 0 {
let items = if chat_id.is_deaddrop() {
let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?)
.unwrap_or_default();
context
.sql
.query_map(
"SELECT m.id AS id, m.timestamp AS timestamp
FROM msgs m
LEFT JOIN chats
ON m.chat_id=chats.id
LEFT JOIN contacts
ON m.from_id=contacts.id
WHERE m.from_id!=1 -- 1=DC_CONTACT_ID_SELF
AND m.from_id!=2 -- 2=DC_CONTACT_ID_INFO
AND m.hidden=0
AND chats.blocked=2
AND contacts.blocked=0
AND m.msgrmsg>=?
ORDER BY m.timestamp,m.id;",
paramsv![if show_emails == ShowEmails::All {
0i32
} else {
1i32
}],
process_row,
process_rows,
)
.await?
} else if (flags & DC_GCM_INFO_ONLY) != 0 {
context
.sql
.query_map(
@@ -1990,8 +1989,32 @@ pub(crate) async fn marknoticed_chat_if_older_than(
Ok(())
}
/// Marks all messages in the chat as noticed.
pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()> {
// for the virtual deaddrop chat-id,
// mark all messages that will appear in the deaddrop as noticed
if chat_id.is_deaddrop() {
if context
.sql
.execute(
"UPDATE msgs
SET state=?1
WHERE state=?2
AND hidden=0
AND chat_id IN (SELECT id FROM chats WHERE blocked=?3);",
paramsv![
MessageState::InNoticed,
MessageState::InFresh,
Blocked::Deaddrop
],
)
.await?
> 0
{
context.emit_event(EventType::MsgsNoticed(chat_id));
}
return Ok(());
}
// "WHERE" below uses the index `(state, hidden, chat_id)`, see get_fresh_msg_cnt() for reasoning
// the additional SELECT statement may speed up things as no write-blocking is needed.
let exists = context
@@ -2053,7 +2076,15 @@ pub async fn get_chat_media(
},
],
|row| row.get::<_, MsgId>(0),
|ids| Ok(ids.flatten().collect()),
|ids| {
let mut ret = Vec::new();
for id in ids {
if let Ok(msg_id) = id {
ret.push(msg_id)
}
}
Ok(ret)
},
)
.await?;
Ok(list)
@@ -2111,11 +2142,17 @@ pub async fn get_next_media(
Ok(ret)
}
/// Returns a vector of contact IDs for given chat ID.
pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec<u32>> {
// Normal chats do not include SELF. Group chats do (as it may happen that one is deleted from a
// groupchat but the chats stays visible, moreover, this makes displaying lists easier)
if chat_id.is_deaddrop() {
return Ok(Vec::new());
}
// we could also create a list for all contacts in the deaddrop by searching contacts belonging to chats with
// chats.blocked=2, however, currently this is not needed
let list = context
.sql
.query_map(
@@ -2134,7 +2171,6 @@ pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec
Ok(list)
}
/// Creates a group chat with a given `name`.
pub async fn create_group_chat(
context: &Context,
protect: ProtectionStatus,
@@ -2152,12 +2188,7 @@ pub async fn create_group_chat(
"INSERT INTO chats
(type, name, grpid, param, created_timestamp)
VALUES(?, ?, ?, \'U=1\', ?);",
paramsv![
Chattype::Group,
chat_name,
grpid,
dc_create_smeared_timestamp(context).await,
],
paramsv![Chattype::Group, chat_name, grpid, time(),],
)
.await?;
@@ -2182,7 +2213,7 @@ pub async fn create_group_chat(
Ok(chat_id)
}
/// Adds a contact to the `chats_contacts` table.
/// add a contact to the chats_contact table
pub(crate) async fn add_to_chat_contacts_table(
context: &Context,
chat_id: ChatId,
@@ -2570,7 +2601,6 @@ pub(crate) async fn is_group_explicitly_left(
Ok(exists)
}
/// Sets group or mailing list chat name.
pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -> Result<()> {
let new_name = improve_single_line_input(new_name);
/* the function only sets the names of group chats; normal chats get their names from the contacts */
@@ -2708,7 +2738,7 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
chat_id.unarchive(context).await?;
if let Ok(mut chat) = Chat::load_from_db(context, chat_id).await {
ensure!(chat.can_send(context).await, "cannot send to {}", chat_id);
ensure!(chat.can_send(), "cannot send to {}", chat_id);
curr_timestamp = dc_create_smeared_timestamps(context, msg_ids.len()).await;
let ids = context
.sql
@@ -2735,11 +2765,8 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
// we tested a sort of broadcast
// by not marking own forwarded messages as such,
// however, this turned out to be to confusing and unclear.
if msg.get_viewtype() != Viewtype::Sticker {
msg.param
.set_int(Param::Forwarded, src_msg_id.to_u32() as i32);
}
msg.param
.set_int(Param::Forwarded, src_msg_id.to_u32() as i32);
msg.param.remove(Param::GuaranteeE2ee);
msg.param.remove(Param::ForcePlaintext);
@@ -2820,10 +2847,10 @@ pub(crate) async fn get_chat_cnt(context: &Context) -> Result<usize> {
pub(crate) async fn get_chat_id_by_grpid(
context: &Context,
grpid: impl AsRef<str>,
) -> Result<Option<(ChatId, bool, Blocked)>> {
) -> Result<(ChatId, bool, Blocked)> {
context
.sql
.query_row_optional(
.query_row(
"SELECT id, blocked, protected FROM chats WHERE grpid=?;",
paramsv![grpid.as_ref()],
|row| {
@@ -2944,7 +2971,6 @@ pub async fn add_device_msg_with_importance(
Ok(msg_id)
}
/// Adds a message to device chat.
pub async fn add_device_msg(
context: &Context,
label: Option<&str>,
@@ -2994,7 +3020,6 @@ pub(crate) async fn add_info_msg_with_cmd(
chat_id: ChatId,
text: impl AsRef<str>,
cmd: SystemMessage,
timestamp: i64,
) -> Result<MsgId> {
let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device");
let ephemeral_timer = chat_id.get_ephemeral_timer(context).await?;
@@ -3011,7 +3036,7 @@ pub(crate) async fn add_info_msg_with_cmd(
chat_id,
DC_CONTACT_ID_INFO,
DC_CONTACT_ID_INFO,
timestamp,
dc_create_smeared_timestamp(context).await,
Viewtype::Text,
MessageState::InNoticed,
text.as_ref().to_string(),
@@ -3026,16 +3051,8 @@ pub(crate) async fn add_info_msg_with_cmd(
Ok(msg_id)
}
/// Adds info message with a given text and `timestamp` to the chat.
pub(crate) async fn add_info_msg(
context: &Context,
chat_id: ChatId,
text: impl AsRef<str>,
timestamp: i64,
) {
if let Err(e) =
add_info_msg_with_cmd(context, chat_id, text, SystemMessage::Unknown, timestamp).await
{
pub(crate) async fn add_info_msg(context: &Context, chat_id: ChatId, text: impl AsRef<str>) {
if let Err(e) = add_info_msg_with_cmd(context, chat_id, text, SystemMessage::Unknown).await {
warn!(context, "Could not add info msg: {}", e);
}
}
@@ -3044,13 +3061,11 @@ pub(crate) async fn add_info_msg(
mod tests {
use super::*;
use crate::chatlist::{dc_get_archived_cnt, Chatlist};
use crate::chatlist::Chatlist;
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS};
use crate::contact::Contact;
use crate::dc_receive_imf::dc_receive_imf;
use crate::test_utils::TestContext;
use async_std::fs::File;
use async_std::prelude::*;
#[async_std::test]
async fn test_chat_info() {
@@ -3167,11 +3182,24 @@ mod tests {
assert!(chat.is_self_talk());
assert!(chat.visibility == ChatVisibility::Normal);
assert!(!chat.is_device_talk());
assert!(chat.can_send(&t).await);
assert!(chat.can_send());
assert_eq!(chat.name, stock_str::saved_messages(&t).await);
assert!(chat.get_profile_image(&t).await.unwrap().is_some());
}
#[async_std::test]
async fn test_deaddrop_chat() {
let t = TestContext::new().await;
let chat = Chat::load_from_db(&t, DC_CHAT_ID_DEADDROP).await.unwrap();
assert_eq!(DC_CHAT_ID_DEADDROP.0, 1);
assert!(chat.id.is_deaddrop());
assert!(!chat.is_self_talk());
assert!(chat.visibility == ChatVisibility::Normal);
assert!(!chat.is_device_talk());
assert!(!chat.can_send());
assert_eq!(chat.name, stock_str::dead_drop(&t).await);
}
#[async_std::test]
async fn test_add_device_msg_unlabelled() {
let t = TestContext::new().await;
@@ -3246,7 +3274,7 @@ mod tests {
assert_eq!(chat.get_type(), Chattype::Single);
assert!(chat.is_device_talk());
assert!(!chat.is_self_talk());
assert!(!chat.can_send(&t).await);
assert!(!chat.can_send());
assert_eq!(chat.name, stock_str::device_messages(&t).await);
assert!(chat.get_profile_image(&t).await.unwrap().is_some());
@@ -3659,7 +3687,7 @@ mod tests {
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo")
.await
.unwrap();
add_info_msg(&t, chat_id, "foo info", 200000).await;
add_info_msg(&t, chat_id, "foo info").await;
let msg = t.get_last_msg_in(chat_id).await;
assert_eq!(msg.get_chat_id(), chat_id);
@@ -3680,7 +3708,6 @@ mod tests {
chat_id,
"foo bar info",
SystemMessage::EphemeralTimerChanged,
10000,
)
.await
.unwrap();
@@ -3853,7 +3880,7 @@ mod tests {
);
send_text_msg(&alice, alice_chat_id, "hi!".to_string())
.await
.unwrap();
.ok();
assert_eq!(
get_chat_msgs(&alice, alice_chat_id, 0, None)
.await
@@ -3877,14 +3904,11 @@ mod tests {
let bob_chat = Chat::load_from_db(&bob, msg.chat_id).await.unwrap();
assert_eq!(bob_chat.grpid, alice_chat.grpid);
// Bob accepts contact request.
bob_chat.id.unblock(&bob).await.unwrap();
// Bob answers - simulate a normal MUA by not setting `Chat-*`-headers;
// moreover, Bob's SMTP-server also replaces the `Message-ID:`-header
send_text_msg(&bob, bob_chat.id, "ho!".to_string())
.await
.unwrap();
.ok();
let msg = bob.pop_sent_msg().await.payload();
let msg = msg.replace("Message-ID: <Gr.", "Message-ID: <XXX");
let msg = msg.replace("Chat-", "XXXX-");
@@ -3929,6 +3953,7 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await?;
assert_eq!(chats.len(), 1);
assert_eq!(chats.get_chat_id(0), chat.id);
assert_ne!(chats.get_chat_id(0), DC_CHAT_ID_DEADDROP);
assert_eq!(chat.id.get_fresh_msg_cnt(&t).await?, 1);
assert_eq!(t.get_fresh_msgs().await?.len(), 1);
@@ -3954,7 +3979,7 @@ mod tests {
}
#[async_std::test]
async fn test_contact_request_fresh_messages() -> Result<()> {
async fn test_marknoticed_deaddrop_chat() -> Result<()> {
let t = TestContext::new_alice().await;
let chats = Chatlist::try_load(&t, 0, None, None).await?;
@@ -3977,14 +4002,8 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await?;
assert_eq!(chats.len(), 1);
let chat_id = chats.get_chat_id(0);
assert!(Chat::load_from_db(&t, chat_id)
.await
.unwrap()
.is_contact_request());
assert_eq!(chat_id.get_msg_cnt(&t).await?, 1);
assert_eq!(chat_id.get_fresh_msg_cnt(&t).await?, 1);
let msgs = get_chat_msgs(&t, chat_id, 0, None).await?;
assert_eq!(chats.get_chat_id(0), DC_CHAT_ID_DEADDROP);
let msgs = get_chat_msgs(&t, DC_CHAT_ID_DEADDROP, 0, None).await?;
assert_eq!(msgs.len(), 1);
let msg_id = match msgs.first().unwrap() {
ChatItem::Message { msg_id } => *msg_id,
@@ -3992,249 +4011,16 @@ mod tests {
};
let msg = message::Message::load_from_db(&t, msg_id).await?;
assert_eq!(msg.state, MessageState::InFresh);
assert_eq!(t.get_fresh_msgs().await?.len(), 0); // deaddrop is excluded from global badge
// Contact requests are excluded from global badge.
assert_eq!(t.get_fresh_msgs().await?.len(), 0);
marknoticed_chat(&t, DC_CHAT_ID_DEADDROP).await?;
let chats = Chatlist::try_load(&t, 0, None, None).await?;
assert_eq!(chats.len(), 1);
assert_eq!(chats.len(), 0);
let msg = message::Message::load_from_db(&t, msg_id).await?;
assert_eq!(msg.state, MessageState::InFresh);
assert_eq!(msg.state, MessageState::InNoticed);
assert_eq!(t.get_fresh_msgs().await?.len(), 0);
Ok(())
}
#[async_std::test]
async fn test_contact_request_archive() -> Result<()> {
let t = TestContext::new_alice().await;
dc_receive_imf(
&t,
b"From: bob@example.org\n\
To: alice@example.com\n\
Message-ID: <2@example.org>\n\
Chat-Version: 1.0\n\
Date: Sun, 22 Mar 2021 19:37:57 +0000\n\
\n\
hello\n",
"INBOX",
1,
false,
)
.await?;
let chats = Chatlist::try_load(&t, 0, None, None).await?;
assert_eq!(chats.len(), 1);
let chat_id = chats.get_chat_id(0);
assert!(Chat::load_from_db(&t, chat_id).await?.is_contact_request());
assert_eq!(dc_get_archived_cnt(&t).await?, 0);
// archive request without accepting or blocking
chat_id.set_visibility(&t, ChatVisibility::Archived).await?;
let chats = Chatlist::try_load(&t, 0, None, None).await?;
assert_eq!(chats.len(), 1);
let chat_id = chats.get_chat_id(0);
assert!(chat_id.is_archived_link());
assert_eq!(dc_get_archived_cnt(&t).await?, 1);
let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None).await?;
assert_eq!(chats.len(), 1);
let chat_id = chats.get_chat_id(0);
assert!(Chat::load_from_db(&t, chat_id).await?.is_contact_request());
Ok(())
}
#[async_std::test]
async fn test_classic_email_chat() -> Result<()> {
let alice = TestContext::new_alice().await;
// Alice enables receiving classic emails.
alice
.set_config(Config::ShowEmails, Some("2"))
.await
.unwrap();
// Alice receives a classic (non-chat) message from Bob.
dc_receive_imf(
&alice,
b"From: bob@example.org\n\
To: alice@example.com\n\
Message-ID: <1@example.org>\n\
Date: Sun, 22 Mar 2021 19:37:57 +0000\n\
\n\
hello\n",
"INBOX",
1,
false,
)
.await?;
let msg = alice.get_last_msg().await;
let chat_id = msg.chat_id;
assert_eq!(chat_id.get_fresh_msg_cnt(&alice).await?, 1);
let msgs = get_chat_msgs(&alice, chat_id, 0, None).await?;
assert_eq!(msgs.len(), 1);
// Alice disables receiving classic emails.
alice
.set_config(Config::ShowEmails, Some("0"))
.await
.unwrap();
// Already received classic email should still be in the chat.
assert_eq!(chat_id.get_fresh_msg_cnt(&alice).await?, 1);
let msgs = get_chat_msgs(&alice, chat_id, 0, None).await?;
assert_eq!(msgs.len(), 1);
Ok(())
}
async fn test_sticker(filename: &str, bytes: &[u8], w: i32, h: i32) -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat = alice.create_chat(&bob).await;
let bob_chat = bob.create_chat(&alice).await;
let file = alice.get_blobdir().join(filename);
File::create(&file).await?.write_all(bytes).await?;
let mut msg = Message::new(Viewtype::Sticker);
msg.set_file(file.to_str().unwrap(), None);
let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
let mime = sent_msg.payload();
assert_eq!(mime.match_indices("Chat-Content: sticker").count(), 1);
bob.recv_msg(&sent_msg).await;
let msg = bob.get_last_msg().await;
assert_eq!(msg.chat_id, bob_chat.id);
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
assert_eq!(msg.get_filename(), Some(filename.to_string()));
assert_eq!(msg.get_width(), w);
assert_eq!(msg.get_height(), h);
assert!(msg.get_filebytes(&bob).await > 250);
Ok(())
}
#[async_std::test]
async fn test_sticker_png() -> Result<()> {
test_sticker(
"sticker.png",
include_bytes!("../test-data/image/avatar64x64.png"),
64,
64,
)
.await
}
#[async_std::test]
async fn test_sticker_jpeg() -> Result<()> {
test_sticker(
"sticker.jpg",
include_bytes!("../test-data/image/avatar1000x1000.jpg"),
1000,
1000,
)
.await
}
#[async_std::test]
async fn test_sticker_gif() -> Result<()> {
test_sticker(
"sticker.gif",
include_bytes!("../test-data/image/image100x50.gif"),
100,
50,
)
.await
}
#[async_std::test]
async fn test_sticker_forward() -> Result<()> {
// create chats
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat = alice.create_chat(&bob).await;
let bob_chat = bob.create_chat(&alice).await;
// create sticker
let file_name = "sticker.jpg";
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
let file = alice.get_blobdir().join(file_name);
File::create(&file).await?.write_all(bytes).await?;
let mut msg = Message::new(Viewtype::Sticker);
msg.set_file(file.to_str().unwrap(), None);
// send sticker to bob
let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await;
bob.recv_msg(&sent_msg).await;
let msg = bob.get_last_msg().await;
// forward said sticker to alice
forward_msgs(&bob, &[msg.id], bob_chat.get_id()).await?;
let forwarded_msg = bob.pop_sent_msg().await;
alice.recv_msg(&forwarded_msg).await;
// retrieve forwarded sticker which should not have forwarded-flag
let msg = alice.get_last_msg().await;
assert!(!msg.is_forwarded());
Ok(())
}
#[async_std::test]
async fn test_forward() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat = alice.create_chat(&bob).await;
let bob_chat = bob.create_chat(&alice).await;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("Hi Bob".to_owned()));
let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await;
bob.recv_msg(&sent_msg).await;
let msg = bob.get_last_msg().await;
forward_msgs(&bob, &[msg.id], bob_chat.get_id()).await?;
let forwarded_msg = bob.pop_sent_msg().await;
alice.recv_msg(&forwarded_msg).await;
let msg = alice.get_last_msg().await;
assert!(msg.get_text().unwrap() == "Hi Bob");
assert!(msg.is_forwarded());
Ok(())
}
#[async_std::test]
async fn test_can_send_group() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = Contact::create(&alice, "", "bob@f.br").await?;
let chat_id = ChatId::create_for_contact(&alice, bob).await?;
let chat = Chat::load_from_db(&alice, chat_id).await?;
assert!(chat.can_send(&alice).await);
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
assert_eq!(
Chat::load_from_db(&alice, chat_id)
.await?
.can_send(&alice)
.await,
true
);
remove_contact_from_chat(&alice, chat_id, DC_CONTACT_ID_SELF).await?;
assert_eq!(
Chat::load_from_db(&alice, chat_id)
.await?
.can_send(&alice)
.await,
false
);
Ok(())
}
}

View File

@@ -1,12 +1,12 @@
//! # Chat list module.
//! # Chat list module
use anyhow::{bail, ensure, Result};
use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
use crate::constants::{
Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CONTACT_ID_DEVICE,
DC_CONTACT_ID_SELF, DC_CONTACT_ID_UNDEFINED, DC_GCL_ADD_ALLDONE_HINT, DC_GCL_ARCHIVED_ONLY,
DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS,
Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CHAT_ID_DEADDROP,
DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_SELF, DC_CONTACT_ID_UNDEFINED, DC_GCL_ADD_ALLDONE_HINT,
DC_GCL_ARCHIVED_ONLY, DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS,
};
use crate::contact::Contact;
use crate::context::Context;
@@ -34,12 +34,15 @@ use crate::stock_str;
/// and for each messages that is scrolled into view, dc_get_msg() is called then.
///
/// Why no listflags?
/// Without listflags, dc_get_chatlist() adds the archive "link" automatically as needed.
/// The UI can just render these items differently then.
/// Without listflags, dc_get_chatlist() adds the deaddrop and the archive "link" automatically as needed.
/// The UI can just render these items differently then. Although the deaddrop link is currently always the
/// 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 {
/// Stores pairs of `chat_id, message_id`
ids: Vec<(ChatId, Option<MsgId>)>,
ids: Vec<(ChatId, MsgId)>,
}
impl Chatlist {
@@ -55,6 +58,12 @@ impl Chatlist {
///
/// By default, the function adds some special entries to the list.
/// These special entries can be identified by the ID returned by chatlist.get_chat_id():
/// - DC_CHAT_ID_DEADDROP (1) - this special chat is present if there are
/// messages from addresses that have no relationship to the configured account.
/// The last of these messages is represented by DC_CHAT_ID_DEADDROP and you can retrieve details
/// about it with chatlist.get_msg_id(). Typically, the UI asks the user "Do you want to chat with NAME?"
/// and offers the options "Start chat", "Block" and "Not now";
/// The decision should be passed to dc_decide_on_contact_request().
/// - DC_CHAT_ID_ARCHIVED_LINK (6) - this special chat is present if the user has
/// archived *any* chat using dc_set_chat_visibility(). The UI should show a link as
/// "Show archived chats", if the user clicks this item, the UI should show a
@@ -70,9 +79,9 @@ impl Chatlist {
/// the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are *any* archived
/// chats
/// - the flag DC_GCL_FOR_FORWARDING sorts "Saved messages" to the top of the chatlist
/// and hides the device-chat and contact requests
/// and hides the device-chat,
/// typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
/// - if the flag DC_GCL_NO_SPECIALS is set, archive link is not added
/// - if the flag DC_GCL_NO_SPECIALS is set, deaddrop and archive link are not added
/// to the list (may be used eg. for selecting chats on forwarding, the flag is
/// not needed when DC_GCL_ARCHIVED_ONLY is already set)
/// - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT
@@ -102,7 +111,7 @@ impl Chatlist {
let process_row = |row: &rusqlite::Row| {
let chat_id: ChatId = row.get(0)?;
let msg_id: Option<MsgId> = row.get(1)?;
let msg_id: MsgId = row.get(1).unwrap_or_default();
Ok((chat_id, msg_id))
};
@@ -127,8 +136,13 @@ impl Chatlist {
// timestamp
// - the list starts with the newest chats
//
// The query shows messages from blocked contacts in
// groups. Otherwise it would be hard to follow conversations.
// nb: the query currently shows messages from blocked
// contacts in groups. however, for normal-groups, this is
// okay as the message is also returned by dc_get_chat_msgs()
// (otherwise it would be hard to follow conversations, wa and
// tg do the same) for the deaddrop, however, they should
// really be hidden, however, _currently_ the deaddrop is not
// shown at all permanent in the chatlist.
let mut ids = if let Some(query_contact_id) = query_contact_id {
// show chats shared with a given contact
context.sql.query_map(
@@ -143,7 +157,7 @@ impl Chatlist {
AND (hidden=0 OR state=?1)
ORDER BY timestamp DESC, id DESC LIMIT 1)
WHERE c.id>9
AND c.blocked!=1
AND c.blocked=0
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
GROUP BY c.id
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
@@ -170,7 +184,7 @@ impl Chatlist {
AND (hidden=0 OR state=?)
ORDER BY timestamp DESC, id DESC LIMIT 1)
WHERE c.id>9
AND c.blocked!=1
AND c.blocked=0
AND c.archived=1
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
@@ -204,7 +218,7 @@ impl Chatlist {
AND (hidden=0 OR state=?1)
ORDER BY timestamp DESC, id DESC LIMIT 1)
WHERE c.id>9 AND c.id!=?2
AND c.blocked!=1
AND c.blocked=0
AND c.name LIKE ?3
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
@@ -222,7 +236,7 @@ impl Chatlist {
} else {
ChatId::new(0)
};
let ids = context.sql.query_map(
let mut ids = context.sql.query_map(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
@@ -234,15 +248,22 @@ impl Chatlist {
AND (hidden=0 OR state=?1)
ORDER BY timestamp DESC, id DESC LIMIT 1)
WHERE c.id>9 AND c.id!=?2
AND (c.blocked=0 OR (c.blocked=2 AND NOT ?3))
AND NOT c.archived=?4
AND c.blocked=0
AND NOT c.archived=?3
GROUP BY c.id
ORDER BY c.id=?5 DESC, c.archived=?6 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
paramsv![MessageState::OutDraft, skip_id, flag_for_forwarding, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned],
ORDER BY c.id=?4 DESC, c.archived=?5 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
paramsv![MessageState::OutDraft, skip_id, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned],
process_row,
process_rows,
).await?;
if !flag_no_specials {
if let Some(last_deaddrop_fresh_msg_id) =
get_last_deaddrop_fresh_msg(context).await?
{
if !flag_for_forwarding {
ids.insert(0, (DC_CHAT_ID_DEADDROP, last_deaddrop_fresh_msg_id));
}
}
add_archived_link_item = true;
}
ids
@@ -250,9 +271,9 @@ impl Chatlist {
if add_archived_link_item && dc_get_archived_cnt(context).await? > 0 {
if ids.is_empty() && flag_add_alldone_hint {
ids.push((DC_CHAT_ID_ALLDONE_HINT, None));
ids.push((DC_CHAT_ID_ALLDONE_HINT, MsgId::new(0)));
}
ids.push((DC_CHAT_ID_ARCHIVED_LINK, None));
ids.push((DC_CHAT_ID_ARCHIVED_LINK, MsgId::new(0)));
}
Ok(Chatlist { ids })
@@ -281,7 +302,7 @@ impl Chatlist {
/// Get a single message ID of a chatlist.
///
/// To get the message object from the message ID, use dc_get_msg().
pub fn get_msg_id(&self, index: usize) -> Result<Option<MsgId>> {
pub fn get_msg_id(&self, index: usize) -> Result<MsgId> {
match self.ids.get(index) {
Some((_chat_id, msg_id)) => Ok(*msg_id),
None => bail!("Chatlist index out of range"),
@@ -302,19 +323,18 @@ 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 async fn get_summary(
&self,
context: &Context,
index: usize,
chat: Option<&Chat>,
) -> Result<Lot> {
pub async fn get_summary(&self, context: &Context, index: usize, chat: Option<&Chat>) -> 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".
// Also, sth. as "No messages" would not work if the summary comes from a message.
let (chat_id, lastmsg_id) = match self.ids.get(index) {
Some(ids) => ids,
None => bail!("Chatlist index out of range"),
None => {
let mut ret = Lot::new();
ret.text2 = Some("ErrBadChatlistIndex".to_string());
return Lot::new();
}
};
Chatlist::get_summary2(context, *chat_id, *lastmsg_id, chat).await
@@ -323,50 +343,50 @@ impl Chatlist {
pub async fn get_summary2(
context: &Context,
chat_id: ChatId,
lastmsg_id: Option<MsgId>,
lastmsg_id: MsgId,
chat: Option<&Chat>,
) -> Result<Lot> {
) -> Lot {
let mut ret = Lot::new();
let chat_loaded: Chat;
let chat = if let Some(chat) = chat {
chat
} else {
let chat = Chat::load_from_db(context, chat_id).await?;
} else if let Ok(chat) = Chat::load_from_db(context, chat_id).await {
chat_loaded = chat;
&chat_loaded
} else {
return ret;
};
let (lastmsg, lastcontact) = if let Some(lastmsg_id) = lastmsg_id {
let lastmsg = Message::load_from_db(context, lastmsg_id).await?;
if lastmsg.from_id == DC_CONTACT_ID_SELF {
(Some(lastmsg), None)
} else {
match chat.typ {
Chattype::Group | Chattype::Mailinglist => {
let lastcontact =
Contact::load_from_db(context, lastmsg.from_id).await.ok();
(Some(lastmsg), lastcontact)
let (lastmsg, lastcontact) =
if let Ok(lastmsg) = Message::load_from_db(context, lastmsg_id).await {
if lastmsg.from_id == DC_CONTACT_ID_SELF {
(Some(lastmsg), None)
} else {
match chat.typ {
Chattype::Group | Chattype::Mailinglist => {
let lastcontact =
Contact::load_from_db(context, lastmsg.from_id).await.ok();
(Some(lastmsg), lastcontact)
}
Chattype::Single | Chattype::Undefined => (Some(lastmsg), None),
}
Chattype::Single | Chattype::Undefined => (Some(lastmsg), None),
}
}
} else {
(None, None)
};
} else {
(None, None)
};
if chat.id.is_archived_link() {
ret.text2 = None;
} else if let Some(mut lastmsg) =
lastmsg.filter(|msg| msg.from_id != DC_CONTACT_ID_UNDEFINED)
} else if lastmsg.is_none() || lastmsg.as_ref().unwrap().from_id == DC_CONTACT_ID_UNDEFINED
{
ret.fill(&mut lastmsg, chat, lastcontact.as_ref(), context)
.await;
} else {
ret.text2 = Some(stock_str::no_messages(context).await);
} else {
ret.fill(&mut lastmsg.unwrap(), chat, lastcontact.as_ref(), context)
.await;
}
Ok(ret)
ret
}
pub fn get_index_for_id(&self, id: ChatId) -> Option<usize> {
@@ -379,13 +399,35 @@ pub async fn dc_get_archived_cnt(context: &Context) -> Result<usize> {
let count = context
.sql
.count(
"SELECT COUNT(*) FROM chats WHERE blocked!=? AND archived=?;",
paramsv![Blocked::Manually, ChatVisibility::Archived],
"SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;",
paramsv![],
)
.await?;
Ok(count)
}
async fn get_last_deaddrop_fresh_msg(context: &Context) -> Result<Option<MsgId>> {
// We have an index over the state-column, this should be
// sufficient as there are typically only few fresh messages.
let id = context
.sql
.query_get_value(
concat!(
"SELECT m.id",
" FROM msgs m",
" LEFT JOIN chats c",
" ON c.id=m.chat_id",
" WHERE m.state=10",
" AND m.hidden=0",
" AND c.blocked=2",
" ORDER BY m.timestamp DESC, m.id DESC;"
),
paramsv![],
)
.await?;
Ok(id)
}
#[cfg(test)]
mod tests {
use super::*;
@@ -393,6 +435,8 @@ mod tests {
use crate::chat::{create_group_chat, get_chat_contacts, ProtectionStatus};
use crate::constants::Viewtype;
use crate::dc_receive_imf::dc_receive_imf;
use crate::message;
use crate::message::ContactRequestDecision;
use crate::stock_str::StockMessage;
use crate::test_utils::TestContext;
@@ -502,7 +546,7 @@ mod tests {
async fn test_search_single_chat() -> anyhow::Result<()> {
let t = TestContext::new_alice().await;
// receive a one-to-one-message
// receive a one-to-one-message, accept contact request
dc_receive_imf(
&t,
b"From: Bob Authname <bob@example.org>\n\
@@ -520,13 +564,15 @@ mod tests {
.await?;
let chats = Chatlist::try_load(&t, 0, Some("Bob Authname"), None).await?;
// Contact request should be searchable
assert_eq!(chats.len(), 1);
assert_eq!(chats.len(), 0);
let msg = t.get_last_msg().await;
let chat_id = msg.get_chat_id();
chat_id.accept(&t).await.unwrap();
assert_eq!(msg.get_chat_id(), DC_CHAT_ID_DEADDROP);
let chat_id =
message::decide_on_contact_request(&t, msg.get_id(), ContactRequestDecision::StartChat)
.await
.unwrap();
let contacts = get_chat_contacts(&t, chat_id).await?;
let contact_id = *contacts.first().unwrap();
let chat = Chat::load_from_db(&t, chat_id).await?;
@@ -564,7 +610,7 @@ mod tests {
async fn test_search_single_chat_without_authname() -> anyhow::Result<()> {
let t = TestContext::new_alice().await;
// receive a one-to-one-message without authname set
// receive a one-to-one-message without authname set, accept contact request
dc_receive_imf(
&t,
b"From: bob@example.org\n\
@@ -582,8 +628,10 @@ mod tests {
.await?;
let msg = t.get_last_msg().await;
let chat_id = msg.get_chat_id();
chat_id.accept(&t).await.unwrap();
let chat_id =
message::decide_on_contact_request(&t, msg.get_id(), ContactRequestDecision::StartChat)
.await
.unwrap();
let contacts = get_chat_contacts(&t, chat_id).await?;
let contact_id = *contacts.first().unwrap();
let chat = Chat::load_from_db(&t, chat_id).await?;
@@ -636,7 +684,7 @@ mod tests {
chat_id1.set_draft(&t, Some(&mut msg)).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
let summary = chats.get_summary(&t, 0, None).await.unwrap();
let summary = chats.get_summary(&t, 0, None).await;
assert_eq!(summary.get_text2().unwrap(), "foo: bar test"); // the linebreak should be removed from summary
}
}

View File

@@ -1,4 +1,4 @@
//! Implementation of Consistent Color Generation.
//! Implementation of Consistent Color Generation
//!
//! Consistent Color Generation is defined in XEP-0392.
//!
@@ -42,7 +42,7 @@ mod tests {
#[test]
fn test_str_to_angle() {
// Test against test vectors from
// <https://xmpp.org/extensions/xep-0392.html#testvectors-fullrange-no-cvd>
// https://xmpp.org/extensions/xep-0392.html#testvectors-fullrange-no-cvd
assert!((str_to_angle("Romeo") - 327.255249).abs() < 1e-6);
assert!((str_to_angle("juliet@capulet.lit") - 209.410400).abs() < 1e-6);
assert!((str_to_angle("😺") - 331.199341).abs() < 1e-6);

View File

@@ -1,4 +1,4 @@
//! # Key-value configuration management.
//! # Key-value configuration management
use anyhow::Result;
use strum::{EnumProperty, IntoEnumIterator};
@@ -18,18 +18,7 @@ use crate::stock_str;
/// The available configuration keys.
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Display,
EnumString,
AsRefStr,
EnumIter,
EnumProperty,
PartialOrd,
Ord,
Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, AsRefStr, EnumIter, EnumProperty,
)]
#[strum(serialize_all = "snake_case")]
pub enum Config {
@@ -48,12 +37,6 @@ pub enum Config {
SmtpCertificateChecks,
ServerFlags,
Socks5Enabled,
Socks5Host,
Socks5Port,
Socks5User,
Socks5Password,
Displayname,
Selfstatus,
Selfavatar,

View File

@@ -1,4 +1,4 @@
//! Email accounts autoconfiguration process module.
//! Email accounts autoconfiguration process module
mod auto_mozilla;
mod auto_outlook;
@@ -14,7 +14,6 @@ use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use crate::dc_tools::EmailAddress;
use crate::imap::Imap;
use crate::login_param::Socks5Config;
use crate::login_param::{LoginParam, ServerLoginParam};
use crate::message::Message;
use crate::oauth2::dc_get_oauth2_addr;
@@ -171,17 +170,12 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
DC_LP_AUTH_NORMAL as i32
};
let socks5_config = param.socks5_config.clone();
let socks5_enabled = socks5_config.is_some();
let ctx2 = ctx.clone();
let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await });
// Step 1: Load the parameters and check email-address and password
// Do oauth2 only if socks5 is disabled. As soon as we have a http library that can do
// socks5 requests, this can work with socks5 too
if oauth2 && !socks5_enabled {
if oauth2 {
// the used oauth2 addr may differ, check this.
// if dc_get_oauth2_addr() is not available in the oauth2 implementation, just use the given one.
progress!(ctx, 10);
@@ -223,7 +217,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
"checking internal provider-info for offline autoconfig"
);
if let Some(provider) = provider::get_provider_info(&param_domain, socks5_enabled).await {
if let Some(provider) = provider::get_provider_info(&param_domain).await {
param.provider = Some(provider);
match provider.status {
provider::Status::Ok | provider::Status::Preparation => {
@@ -262,16 +256,9 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
}
}
} else {
// Try receiving autoconfig
info!(ctx, "no offline autoconfig found");
param_autoconfig = if socks5_enabled {
// Currently we can't do http requests through socks5, to not leak
// the ip, just don't do online autoconfig
info!(ctx, "socks5 enabled, skipping autoconfig");
None
} else {
get_autoconfig(ctx, param, &param_domain, &param_addr_urlencoded).await
}
param_autoconfig =
get_autoconfig(ctx, param, &param_domain, &param_addr_urlencoded).await;
}
} else {
param_autoconfig = None;
@@ -333,7 +320,6 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
match try_smtp_one_param(
&context_smtp,
&smtp_param,
&socks5_config,
&smtp_addr,
oauth2,
provider_strict_tls,
@@ -359,8 +345,10 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
progress!(ctx, 600);
// Configure IMAP
let (_s, r) = async_std::channel::bounded(1);
let mut imap = Imap::new(r);
let mut imap: Option<Imap> = None;
let mut imap_configured = false;
let imap_servers: Vec<&ServerParams> = servers
.iter()
.filter(|params| params.protocol == Protocol::Imap)
@@ -376,15 +364,15 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
match try_imap_one_param(
ctx,
&param.imap,
&param.socks5_config,
&param.addr,
oauth2,
provider_strict_tls,
&mut imap,
)
.await
{
Ok(configured_imap) => {
imap = Some(configured_imap);
Ok(_) => {
imap_configured = true;
break;
}
Err(e) => errors.push(e),
@@ -394,10 +382,9 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
600 + (800 - 600) * (1 + imap_server_index) / imap_servers_count
);
}
let mut imap = match imap {
Some(imap) => imap,
None => bail!(nicer_configuration_error(ctx, errors).await),
};
if !imap_configured {
bail!(nicer_configuration_error(ctx, errors).await);
}
progress!(ctx, 850);
@@ -476,7 +463,7 @@ async fn get_autoconfig(
if let Ok(res) = moz_autoconfigure(
ctx,
// the doc does not mention `emailaddress=`, however, Thunderbird adds it, see <https://releases.mozilla.org/pub/thunderbird/>, which makes some sense
// the doc does not mention `emailaddress=`, however, Thunderbird adds it, see https://releases.mozilla.org/pub/thunderbird/ , which makes some sense
&format!(
"https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}",
&param_domain, &param_addr_urlencoded
@@ -530,88 +517,48 @@ async fn get_autoconfig(
async fn try_imap_one_param(
context: &Context,
param: &ServerLoginParam,
socks5_config: &Option<Socks5Config>,
addr: &str,
oauth2: bool,
provider_strict_tls: bool,
) -> Result<Imap, ConfigurationError> {
imap: &mut Imap,
) -> Result<(), ConfigurationError> {
let inf = format!(
"imap: {}@{}:{} security={} certificate_checks={} oauth2={}",
param.user, param.server, param.port, param.security, param.certificate_checks, oauth2
);
info!(context, "Trying: {}", inf);
let (_s, r) = async_std::channel::bounded(1);
let mut imap = match Imap::new(
param,
socks5_config.clone(),
addr,
oauth2,
provider_strict_tls,
r,
)
.await
if let Err(err) = imap
.connect(context, param, addr, oauth2, provider_strict_tls)
.await
{
Err(err) => {
info!(context, "failure: {}", err);
return Err(ConfigurationError {
config: inf,
msg: err.to_string(),
});
}
Ok(imap) => imap,
};
match imap.connect(context).await {
Err(err) => {
info!(context, "failure: {}", err);
Err(ConfigurationError {
config: inf,
msg: err.to_string(),
})
}
Ok(()) => {
info!(context, "success: {}", inf);
Ok(imap)
}
info!(context, "failure: {}", err);
Err(ConfigurationError {
config: inf,
msg: err.to_string(),
})
} else {
info!(context, "success: {}", inf);
Ok(())
}
}
async fn try_smtp_one_param(
context: &Context,
param: &ServerLoginParam,
socks5_config: &Option<Socks5Config>,
addr: &str,
oauth2: bool,
provider_strict_tls: bool,
smtp: &mut Smtp,
) -> Result<(), ConfigurationError> {
let inf = format!(
"smtp: {}@{}:{} security={} certificate_checks={} oauth2={} socks5_config={}",
param.user,
param.server,
param.port,
param.security,
param.certificate_checks,
oauth2,
if let Some(socks5_config) = socks5_config {
socks5_config.to_string()
} else {
"None".to_string()
}
"smtp: {}@{}:{} security={} certificate_checks={} oauth2={}",
param.user, param.server, param.port, param.security, param.certificate_checks, oauth2
);
info!(context, "Trying: {}", inf);
if let Err(err) = smtp
.connect(
context,
param,
socks5_config,
addr,
oauth2,
provider_strict_tls,
)
.connect(context, param, addr, oauth2, provider_strict_tls)
.await
{
info!(context, "failure: {}", err);
@@ -669,10 +616,10 @@ pub enum Error {
},
#[error("Failed to get URL: {0}")]
ReadUrl(#[from] self::read_url::Error),
ReadUrlError(#[from] self::read_url::Error),
#[error("Number of redirection is exceeded")]
Redirection,
RedirectionError,
}
#[cfg(test)]

View File

@@ -1,6 +1,6 @@
//! # Thunderbird's Autoconfiguration implementation
//!
//! Documentation: <https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration>
//! Documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration
use quick_xml::events::{BytesStart, Event};
use std::io::BufRead;

View File

@@ -15,27 +15,27 @@ use super::{Error, ServerParams};
/// Result of parsing a single `Protocol` tag.
///
/// <https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/protocol-pox>
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/protocol-pox
#[derive(Debug)]
struct ProtocolTag {
/// Server type, such as "IMAP", "SMTP" or "POP3".
///
/// <https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/type-pox>
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/type-pox
pub typ: String,
/// Server identifier, hostname or IP address for IMAP and SMTP.
///
/// <https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/server-pox>
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/server-pox
pub server: String,
/// Network port.
///
/// <https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/port-pox>
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/port-pox
pub port: u16,
/// Whether connection should be secure, "on" or "off", default is "on".
///
/// <https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/ssl-pox>
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/ssl-pox
pub ssl: bool,
}
@@ -203,7 +203,7 @@ pub(crate) async fn outlk_autodiscover(
}
}
}
Err(Error::Redirection)
Err(Error::RedirectionError)
}
#[cfg(test)]

View File

@@ -25,20 +25,16 @@ pub(crate) struct ServerParams {
}
impl ServerParams {
fn expand_usernames(self, addr: &str) -> Vec<ServerParams> {
fn expand_usernames(mut self, addr: &str) -> Vec<ServerParams> {
let mut res = Vec::new();
if self.username.is_empty() {
res.push(Self {
username: addr.to_string(),
..self.clone()
});
self.username = addr.to_string();
res.push(self.clone());
if let Some(at) = addr.find('@') {
res.push(Self {
username: addr.split_at(at).0.to_string(),
..self
});
self.username = addr.split_at(at).0.to_string();
res.push(self);
}
} else {
res.push(self)
@@ -46,28 +42,24 @@ impl ServerParams {
res
}
fn expand_hostnames(self, param_domain: &str) -> Vec<ServerParams> {
fn expand_hostnames(mut self, param_domain: &str) -> Vec<ServerParams> {
let mut res = Vec::new();
if self.hostname.is_empty() {
vec![
Self {
hostname: param_domain.to_string(),
..self.clone()
},
Self {
hostname: match self.protocol {
Protocol::Imap => "imap.".to_string() + param_domain,
Protocol::Smtp => "smtp.".to_string() + param_domain,
},
..self.clone()
},
Self {
hostname: "mail.".to_string() + param_domain,
..self
},
]
self.hostname = param_domain.to_string();
res.push(self.clone());
self.hostname = match self.protocol {
Protocol::Imap => "imap.".to_string() + param_domain,
Protocol::Smtp => "smtp.".to_string() + param_domain,
};
res.push(self.clone());
self.hostname = "mail.".to_string() + param_domain;
res.push(self);
} else {
vec![self]
res.push(self);
}
res
}
fn expand_ports(mut self) -> Vec<ServerParams> {
@@ -86,47 +78,39 @@ impl ServerParams {
}
}
let mut res = Vec::new();
if self.port == 0 {
// Neither port nor security is set.
//
// Try common secure combinations.
vec![
// Try STARTTLS
Self {
socket: Socket::Starttls,
port: match self.protocol {
Protocol::Imap => 143,
Protocol::Smtp => 587,
},
..self.clone()
},
// Try TLS
Self {
socket: Socket::Ssl,
port: match self.protocol {
Protocol::Imap => 993,
Protocol::Smtp => 465,
},
..self
},
]
// Try STARTTLS
self.socket = Socket::Starttls;
self.port = match self.protocol {
Protocol::Imap => 143,
Protocol::Smtp => 587,
};
res.push(self.clone());
// Try TLS
self.socket = Socket::Ssl;
self.port = match self.protocol {
Protocol::Imap => 993,
Protocol::Smtp => 465,
};
res.push(self);
} else if self.socket == Socket::Automatic {
vec![
// Try TLS over user-provided port.
Self {
socket: Socket::Ssl,
..self.clone()
},
// Try STARTTLS over user-provided port.
Self {
socket: Socket::Starttls,
..self
},
]
// Try TLS over user-provided port.
self.socket = Socket::Ssl;
res.push(self.clone());
// Try STARTTLS over user-provided port.
self.socket = Socket::Starttls;
res.push(self);
} else {
vec![self]
res.push(self);
}
res
}
}

View File

@@ -1,4 +1,4 @@
//! # Constants.
//! # Constants
use deltachat_derive::{FromSql, ToSql};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
@@ -25,7 +25,7 @@ pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION")
pub enum Blocked {
Not = 0,
Manually = 1,
Request = 2,
Deaddrop = 2,
}
impl Default for Blocked {
@@ -123,6 +123,8 @@ pub const DC_RESEND_USER_AVATAR_DAYS: i64 = 14;
// do not use too small value that will annoy users checking for nonexistant updates.
pub const DC_OUTDATED_WARNING_DAYS: i64 = 365;
/// virtual chat showing all messages belonging to chats flagged with chats.blocked=2
pub const DC_CHAT_ID_DEADDROP: ChatId = ChatId::new(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: ChatId = ChatId::new(3);
/// only an indicator in a chatlist
@@ -165,18 +167,36 @@ pub const DC_MSG_ID_MARKER1: u32 = 1;
pub const DC_MSG_ID_DAYMARKER: u32 = 9;
pub const DC_MSG_ID_LAST_SPECIAL: u32 = 9;
/// String that indicates that something is left out or truncated.
pub const DC_ELLIPSIS: &str = "[...]";
/// string that indicates sth. is left out or truncated
pub const DC_ELLIPSE: &str = "[...]";
/// Message length limit.
/// to keep bubbles and chat flow usable,
/// and to avoid problems with controls using very long texts,
/// we limit the text length to DC_DESIRED_TEXT_LEN.
/// if the text is longer, the full text can be retrieved usind has_html()/get_html().
///
/// To keep bubbles and chat flow usable and to avoid problems with controls using very long texts,
/// we limit the text length to `DC_DESIRED_TEXT_LEN`. If the text is longer, the full text can be
/// retrieved using has_html()/get_html().
/// we are using a bit less than DC_MAX_GET_TEXT_LEN to avoid cutting twice
/// (a bit less as truncation may not be exact and ellipses may be added).
///
/// Note that for simplicity maximum length is defined as the number of Unicode Scalar Values (Rust
/// `char`s), not Unicode Grapheme Clusters.
pub const DC_DESIRED_TEXT_LEN: usize = 5000;
/// note, that DC_DESIRED_TEXT_LEN and DC_MAX_GET_TEXT_LEN
/// define max. number of bytes, _not_ unicode graphemes.
/// in general, that seems to be okay for such an upper limit,
/// esp. as calculating the number of graphemes is not simple
/// (one graphemes may be a sequence of code points which is a sequence of bytes).
/// also even if we have the exact number of graphemes,
/// that would not always help on getting an idea about the screen space used
/// (to keep bubbles and chat flow usable).
///
/// therefore, the number of bytes is only a very rough estimation,
/// however, the ~30K seems to work okayish for a while,
/// if it turns out, it is too few for some alphabet, we can still increase.
pub const DC_DESIRED_TEXT_LEN: usize = 29_000;
/// approx. max. length (number of bytes) returned by dc_msg_get_text()
pub const DC_MAX_GET_TEXT_LEN: usize = 30_000;
/// approx. max. length returned by dc_get_msg_info()
pub const DC_MAX_GET_INFO_LEN: usize = 100_000;
pub const DC_CONTACT_ID_UNDEFINED: u32 = 0;
pub const DC_CONTACT_ID_SELF: u32 = 1;
@@ -384,7 +404,7 @@ mod tests {
assert_eq!(Blocked::Not, Blocked::default());
assert_eq!(Blocked::Not, Blocked::from_i32(0).unwrap());
assert_eq!(Blocked::Manually, Blocked::from_i32(1).unwrap());
assert_eq!(Blocked::Request, Blocked::from_i32(2).unwrap());
assert_eq!(Blocked::Deaddrop, Blocked::from_i32(2).unwrap());
}
#[test]

View File

@@ -14,8 +14,8 @@ use crate::chat::ChatId;
use crate::color::str_to_color;
use crate::config::Config;
use crate::constants::{
Blocked, Chattype, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_DEVICE_ADDR, DC_CONTACT_ID_LAST_SPECIAL,
DC_CONTACT_ID_SELF, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY,
Blocked, Chattype, DC_CHAT_ID_DEADDROP, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_DEVICE_ADDR,
DC_CONTACT_ID_LAST_SPECIAL, DC_CONTACT_ID_SELF, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY,
};
use crate::context::Context;
use crate::dc_tools::{dc_get_abs_path, improve_single_line_input, EmailAddress};
@@ -218,7 +218,6 @@ impl Contact {
} else if contact_id == DC_CONTACT_ID_DEVICE {
contact.name = stock_str::device_messages(context).await;
contact.addr = DC_CONTACT_ID_DEVICE_ADDR.to_string();
contact.status = stock_str::device_messages_hint(context).await;
}
Ok(contact)
}
@@ -237,13 +236,13 @@ impl Contact {
}
/// Block the given contact.
pub async fn block(context: &Context, id: u32) -> Result<()> {
set_block_contact(context, id, true).await
pub async fn block(context: &Context, id: u32) {
set_block_contact(context, id, true).await;
}
/// Unblock the given contact.
pub async fn unblock(context: &Context, id: u32) -> Result<()> {
set_block_contact(context, id, false).await
pub async fn unblock(context: &Context, id: u32) {
set_block_contact(context, id, false).await;
}
/// Add a single contact as a result of an _explicit_ user action.
@@ -271,22 +270,27 @@ impl Contact {
}
}
if blocked {
Contact::unblock(context, contact_id).await?;
Contact::unblock(context, contact_id).await;
}
Ok(contact_id)
}
/// Mark messages from a contact as noticed.
pub async fn mark_noticed(context: &Context, id: u32) -> Result<()> {
context
/// The contact is expected to belong to the deaddrop,
/// therefore, DC_EVENT_MSGS_NOTICED(DC_CHAT_ID_DEADDROP) is emitted.
pub async fn mark_noticed(context: &Context, id: u32) {
if context
.sql
.execute(
"UPDATE msgs SET state=? WHERE from_id=? AND state=?;",
paramsv![MessageState::InNoticed, id as i32, MessageState::InFresh],
)
.await?;
Ok(())
.await
.is_ok()
{
context.emit_event(EventType::MsgsNoticed(DC_CHAT_ID_DEADDROP));
}
}
/// Check if an e-mail address belongs to a known and unblocked contact.
@@ -856,7 +860,7 @@ impl Contact {
"Can not delete special contact"
);
let count_chats = context
let count_contacts = context
.sql
.count(
"SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;",
@@ -864,7 +868,19 @@ impl Contact {
)
.await?;
if count_chats == 0 {
let count_msgs = if count_contacts > 0 {
context
.sql
.count(
"SELECT COUNT(*) FROM msgs WHERE from_id=? OR to_id=?;",
paramsv![contact_id as i32, contact_id as i32],
)
.await?
} else {
0
};
if count_msgs == 0 {
match context
.sql
.execute(
@@ -886,9 +902,9 @@ impl Contact {
info!(
context,
"could not delete contact {}, there are {} chats with it", contact_id, count_chats
"could not delete contact {}, there are {} messages with it", contact_id, count_msgs
);
bail!("Could not delete contact with ongoing chats");
bail!("Could not delete contact with messages in it");
}
/// Get a single contact object. For a list, see eg. dc_get_contacts().
@@ -1158,59 +1174,56 @@ fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
}
}
async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: bool) -> Result<()> {
ensure!(
contact_id > DC_CONTACT_ID_LAST_SPECIAL,
"Can't block special contact {}",
contact_id
);
async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: bool) {
if contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
return;
}
let contact = Contact::load_from_db(context, contact_id).await?;
if contact.blocked != new_blocking {
context
.sql
.execute(
"UPDATE contacts SET blocked=? WHERE id=?;",
paramsv![new_blocking as i32, contact_id as i32],
)
.await?;
// also (un)block all chats with _only_ this contact - we do not delete them to allow a
// non-destructive blocking->unblocking.
// (Maybe, beside normal chats (type=100) we should also block group chats with only this user.
// However, I'm not sure about this point; it may be confusing if the user wants to add other people;
// this would result in recreating the same group...)
if context
.sql
.execute(
r#"
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
if contact.blocked != new_blocking
&& context
.sql
.execute(
"UPDATE contacts SET blocked=? WHERE id=?;",
paramsv![new_blocking as i32, contact_id as i32],
)
.await
.is_ok()
{
// also (un)block all chats with _only_ this contact - we do not delete them to allow a
// non-destructive blocking->unblocking.
// (Maybe, beside normal chats (type=100) we should also block group chats with only this user.
// However, I'm not sure about this point; it may be confusing if the user wants to add other people;
// this would result in recreating the same group...)
if context
.sql
.execute(
r#"
UPDATE chats
SET blocked=?
WHERE type=? AND id IN (
SELECT chat_id FROM chats_contacts WHERE contact_id=?
);
"#,
paramsv![new_blocking, Chattype::Single, contact_id],
)
.await
.is_ok()
{
Contact::mark_noticed(context, contact_id).await?;
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
}
// also unblock mailinglist
// if the contact is a mailinglist address explicitly created to allow unblocking
if !new_blocking && contact.origin == Origin::MailinglistAddress {
if let Some((chat_id, _, _)) = chat::get_chat_id_by_grpid(context, contact.addr).await?
paramsv![new_blocking, Chattype::Single, contact_id],
)
.await
.is_ok()
{
chat_id.unblock(context).await?;
Contact::mark_noticed(context, contact_id).await;
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
}
// also unblock mailinglist
// if the contact is a mailinglist address explicitly created to allow unblocking
if !new_blocking && contact.origin == Origin::MailinglistAddress {
if let Ok((chat_id, _, _)) = chat::get_chat_id_by_grpid(context, contact.addr).await
{
chat_id.set_blocked(context, Blocked::Not).await;
}
}
}
}
Ok(())
}
/// Set profile image for a contact.
@@ -1387,12 +1400,12 @@ mod tests {
assert_eq!(may_be_valid_addr("user@domain.tld"), true);
assert_eq!(may_be_valid_addr("uuu"), false);
assert_eq!(may_be_valid_addr("dd.tt"), false);
assert_eq!(may_be_valid_addr("tt.dd@uu"), true);
assert_eq!(may_be_valid_addr("u@d"), true);
assert_eq!(may_be_valid_addr("u@d."), true);
assert_eq!(may_be_valid_addr("u@d.t"), true);
assert_eq!(may_be_valid_addr("tt.dd@uu"), false);
assert_eq!(may_be_valid_addr("u@d"), false);
assert_eq!(may_be_valid_addr("u@d."), false);
assert_eq!(may_be_valid_addr("u@d.t"), false);
assert_eq!(may_be_valid_addr("u@d.tt"), true);
assert_eq!(may_be_valid_addr("u@.tt"), true);
assert_eq!(may_be_valid_addr("u@.tt"), false);
assert_eq!(may_be_valid_addr("@d.tt"), false);
assert_eq!(may_be_valid_addr("<da@d.tt"), false);
assert_eq!(may_be_valid_addr("sk <@d.tt>"), false);
@@ -1605,34 +1618,6 @@ mod tests {
assert!(!contact.is_blocked());
}
#[async_std::test]
async fn test_delete() -> Result<()> {
let alice = TestContext::new_alice().await;
assert!(Contact::delete(&alice, DC_CONTACT_ID_SELF).await.is_err());
// Create Bob contact
let (contact_id, _) =
Contact::add_or_lookup(&alice, "Bob", "bob@example.net", Origin::ManuallyCreated)
.await
.unwrap();
let chat = alice
.create_chat_with_contact("Bob", "bob@example.net")
.await;
// Can't delete a contact with ongoing chats.
assert!(Contact::delete(&alice, contact_id).await.is_err());
// Delete chat.
chat.get_id().delete(&alice).await?;
// Can delete contact now.
Contact::delete(&alice, contact_id).await?;
Ok(())
}
#[async_std::test]
async fn test_remote_authnames() {
let t = TestContext::new().await;
@@ -1836,6 +1821,9 @@ mod tests {
assert!(Contact::create(&t, "", "dskjfdslk@sadklj.dk>")
.await
.is_err());
assert!(Contact::create(&t, "", "dskjf@dslk@sadkljdk")
.await
.is_err());
assert!(Contact::create(&t, "", "dskjf dslk@d.e").await.is_err());
assert!(Contact::create(&t, "", "<dskjf dslk@sadklj.dk")
.await

View File

@@ -1,4 +1,4 @@
//! Context module.
//! Context module
use std::collections::{BTreeMap, HashMap};
use std::ffi::OsString;
@@ -22,7 +22,6 @@ use crate::events::{Event, EventEmitter, EventType, Events};
use crate::key::{DcKey, SignedPublicKey};
use crate::login_param::LoginParam;
use crate::message::{self, MessageState, MsgId};
use crate::quota::QuotaInfo;
use crate::scheduler::Scheduler;
use crate::securejoin::Bob;
use crate::sql::Sql;
@@ -63,10 +62,6 @@ pub struct InnerContext {
pub(crate) scheduler: RwLock<Scheduler>,
pub(crate) ephemeral_task: RwLock<Option<task::JoinHandle<()>>>,
/// Recently loaded quota information, if any.
/// Set to `None` if quota was never tried to load.
pub(crate) quota: RwLock<Option<QuotaInfo>>,
pub(crate) last_full_folder_scan: Mutex<Option<Instant>>,
/// ID for this `Context` in the current process.
@@ -144,7 +139,6 @@ impl Context {
events: Events::default(),
scheduler: RwLock::new(Scheduler::Stopped),
ephemeral_task: RwLock::new(None),
quota: RwLock::new(None),
creation_time: std::time::SystemTime::now(),
last_full_folder_scan: Mutex::new(None),
};
@@ -167,9 +161,7 @@ impl Context {
{
let l = &mut *self.inner.scheduler.write().await;
if let Err(err) = l.start(self.clone()).await {
error!(self, "Failed to start IO: {}", err)
}
l.start(self.clone()).await;
}
}
@@ -285,11 +277,10 @@ impl Context {
let l2 = LoginParam::from_database(self, "configured_").await?;
let displayname = self.get_config(Config::Displayname).await?;
let chats = get_chat_cnt(self).await? as usize;
let unblocked_msgs = message::get_unblocked_msg_cnt(self).await as usize;
let request_msgs = message::get_request_msg_cnt(self).await as usize;
let real_msgs = message::get_real_msg_cnt(self).await as usize;
let deaddrop_msgs = message::get_deaddrop_msg_cnt(self).await as usize;
let contacts = Contact::get_real_cnt(self).await? as usize;
let is_configured = self.get_config_int(Config::Configured).await?;
let socks5_enabled = self.get_config_int(Config::Socks5Enabled).await?;
let dbversion = self
.sql
.get_raw_config_int("dbversion")
@@ -343,8 +334,8 @@ impl Context {
// insert values
res.insert("bot", self.get_config_int(Config::Bot).await?.to_string());
res.insert("number_of_chats", chats.to_string());
res.insert("number_of_chat_messages", unblocked_msgs.to_string());
res.insert("messages_in_contact_requests", request_msgs.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());
@@ -358,7 +349,6 @@ impl Context {
.unwrap_or_else(|| "<unset>".to_string()),
);
res.insert("is_configured", is_configured.to_string());
res.insert("socks5_enabled", socks5_enabled.to_string());
res.insert("entered_account_settings", l.to_string());
res.insert("used_account_settings", l2.to_string());
res.insert(
@@ -430,7 +420,7 @@ impl Context {
Ok(res)
}
/// Get a list of fresh, unmuted messages in unblocked chats.
/// Get a list of fresh, unmuted messages in any chat but deaddrop.
///
/// The list starts with the most recent message
/// and is typically used to show notifications.
@@ -548,16 +538,19 @@ impl Context {
pub async fn is_sentbox(&self, folder_name: &str) -> Result<bool> {
let sentbox = self.get_config(Config::ConfiguredSentboxFolder).await?;
Ok(sentbox.as_deref() == Some(folder_name))
}
pub async fn is_mvbox(&self, folder_name: &str) -> Result<bool> {
let mvbox = self.get_config(Config::ConfiguredMvboxFolder).await?;
Ok(mvbox.as_deref() == Some(folder_name))
}
pub async fn is_spam_folder(&self, folder_name: &str) -> Result<bool> {
let spam = self.get_config(Config::ConfiguredSpamFolder).await?;
Ok(spam.as_deref() == Some(folder_name))
}
@@ -567,13 +560,6 @@ impl Context {
blob_fname.push("-blobs");
dbfile.with_file_name(blob_fname)
}
pub fn derive_walfile(dbfile: &PathBuf) -> PathBuf {
let mut wal_fname = OsString::new();
wal_fname.push(dbfile.file_name().unwrap_or_default());
wal_fname.push("-wal");
dbfile.with_file_name(wal_fname)
}
}
impl InnerContext {
@@ -880,10 +866,6 @@ mod tests {
"send_security",
"server_flags",
"smtp_certificate_checks",
"socks5_host",
"socks5_port",
"socks5_user",
"socks5_password",
];
let t = TestContext::new().await;
let info = t.get_info().await.unwrap();

File diff suppressed because it is too large Load Diff

View File

@@ -17,7 +17,7 @@ use chrono::{Local, TimeZone};
use rand::{thread_rng, Rng};
use crate::chat::{add_device_msg, add_device_msg_with_importance};
use crate::constants::{Viewtype, DC_ELLIPSIS, DC_OUTDATED_WARNING_DAYS};
use crate::constants::{Viewtype, DC_ELLIPSE, DC_OUTDATED_WARNING_DAYS};
use crate::context::Context;
use crate::events::EventType;
use crate::message::Message;
@@ -29,7 +29,7 @@ use crate::stock_str;
#[allow(clippy::indexing_slicing)]
pub(crate) fn dc_truncate(buf: &str, approx_chars: usize) -> Cow<str> {
let count = buf.chars().count();
if count > approx_chars + DC_ELLIPSIS.len() {
if approx_chars > 0 && count > approx_chars + DC_ELLIPSE.len() {
let end_pos = buf
.char_indices()
.nth(approx_chars)
@@ -37,9 +37,9 @@ pub(crate) fn dc_truncate(buf: &str, approx_chars: usize) -> Cow<str> {
.unwrap_or_default();
if let Some(index) = buf[..end_pos].rfind(|c| c == ' ' || c == '\n') {
Cow::Owned(format!("{}{}", &buf[..=index], DC_ELLIPSIS))
Cow::Owned(format!("{}{}", &buf[..=index], DC_ELLIPSE))
} else {
Cow::Owned(format!("{}{}", &buf[..end_pos], DC_ELLIPSIS))
Cow::Owned(format!("{}{}", &buf[..end_pos], DC_ELLIPSE))
}
} else {
Cow::Borrowed(buf)
@@ -608,8 +608,19 @@ impl FromStr for EmailAddress {
if local.is_empty() {
return err("empty string is not valid for local part");
}
if domain.is_empty() {
return err("missing domain after '@'");
if domain.len() <= 3 {
return err("domain is too short");
}
let dot = domain.find('.');
match dot {
None => {
return err("invalid domain");
}
Some(dot_idx) => {
if dot_idx >= domain.len() - 2 {
return err("invalid domain");
}
}
}
Ok(EmailAddress {
local: (*local).to_string(),
@@ -655,7 +666,7 @@ pub fn remove_subject_prefix(last_subject: &str) -> String {
0
} else {
// "Antw:" is the longest abbreviation in
// <https://en.wikipedia.org/wiki/List_of_email_subject_abbreviations#Abbreviations_in_other_languages>,
// https://en.wikipedia.org/wiki/List_of_email_subject_abbreviations#Abbreviations_in_other_languages,
// so look at the first _5_ characters:
match last_subject.chars().take(5).position(|c| c == ':') {
Some(prefix_end) => prefix_end + 1,
@@ -711,7 +722,10 @@ mod tests {
assert_eq!(dc_truncate("\n hello \n world", 4), "\n [...]");
assert_eq!(dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 1), "𐠈[...]");
assert_eq!(dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 0), "[...]");
assert_eq!(
dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 0),
"𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ"
);
// 9 characters, so no truncation
assert_eq!(dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠", 6), "𑒀ὐ¢🜀\u{1e01b}A a🟠",);
@@ -811,19 +825,12 @@ mod tests {
domain: "domain.tld".into(),
}
);
assert_eq!(
"user@localhost".parse::<EmailAddress>().unwrap(),
EmailAddress {
local: "user".into(),
domain: "localhost".into()
}
);
assert_eq!("uuu".parse::<EmailAddress>().is_ok(), false);
assert_eq!("dd.tt".parse::<EmailAddress>().is_ok(), false);
assert!("tt.dd@uu".parse::<EmailAddress>().is_ok());
assert!("u@d".parse::<EmailAddress>().is_ok());
assert!("u@d.".parse::<EmailAddress>().is_ok());
assert!("u@d.t".parse::<EmailAddress>().is_ok());
assert_eq!("tt.dd@uu".parse::<EmailAddress>().is_ok(), false);
assert_eq!("u@d".parse::<EmailAddress>().is_ok(), false);
assert_eq!("u@d.".parse::<EmailAddress>().is_ok(), false);
assert_eq!("u@d.t".parse::<EmailAddress>().is_ok(), false);
assert_eq!(
"u@d.tt".parse::<EmailAddress>().unwrap(),
EmailAddress {
@@ -831,7 +838,7 @@ mod tests {
domain: "d.tt".into(),
}
);
assert!("u@tt".parse::<EmailAddress>().is_ok());
assert_eq!("u@tt".parse::<EmailAddress>().is_ok(), false);
assert_eq!("@d.tt".parse::<EmailAddress>().is_ok(), false);
}

View File

@@ -1,4 +1,4 @@
//! De-HTML.
//! De-HTML
//!
//! A module to remove HTML tags from the email text

View File

@@ -178,9 +178,7 @@ pub async fn try_decrypt(
let mut signatures = HashSet::default();
if let Some(ref mut peerstate) = peerstate {
peerstate
.handle_fingerprint_change(context, message_time)
.await?;
peerstate.handle_fingerprint_change(context).await?;
if let Some(key) = &peerstate.public_key {
public_keyring_for_validate.add(key.clone());
} else if let Some(key) = &peerstate.gossip_key {
@@ -335,7 +333,7 @@ fn has_decrypted_pgp_armor(input: &[u8]) -> bool {
false
}
/// Checks if a MIME structure contains a multipart/report part.
/// Check if a MIME structure contains a multipart/report part.
///
/// As reports are often unencrypted, we do not reset the Autocrypt header in
/// this case.

View File

@@ -1,4 +1,4 @@
//! # Ephemeral messages.
//! # Ephemeral messages
//!
//! Ephemeral messages are messages that have an Ephemeral-Timer
//! header attached to them, which specifies time in seconds after

View File

@@ -1,4 +1,4 @@
//! # Events specification.
//! # Events specification
use std::ops::Deref;
@@ -185,6 +185,21 @@ pub enum EventType {
#[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.
///
/// 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.
#[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(),
@@ -315,11 +330,4 @@ pub enum EventType {
/// (Bob has verified alice and waits until Alice does the same for him)
#[strum(props(id = "2061"))]
SecurejoinJoinerProgress { contact_id: u32, progress: usize },
/// The connectivity to the server changed.
/// This means that you should refresh the connectivity view
/// and possibly the connectivtiy HTML; see dc_get_connectivity() and
/// dc_get_connectivity_html() for details.
#[strum(props(id = "2100"))]
ConnectivityChanged,
}

339
src/export_chat.rs Normal file
View File

@@ -0,0 +1,339 @@
//! Export chats module
//!
//! ## Export Format
//! The format of an exported chat is a zip file with the following structure:
//! ```text
//! ├── blobs/ # all files that are referenced by the chat
//! ├── msg_info/
//! │ └── [msg_id].txt # message info
//! ├── msg_source/
//! │ └── [msg_id].eml # email sourcecode of messages if availible¹
//! └── chat.json # chat info, messages and message authors
//! ```
//! ##### ¹ Saving Mime header
//! To save the mime header you need to have the config option [`SaveMimeHeaders`] enabled.
//! This option saves the mime headers on future messages. Normaly the original email source code is discarded to save space.
//! You can use the repl tool to do this job:
//! ```sh
//! $ cargo run --example repl --features=repl /path/to/account/db.sqlite
//! > set save_mime_headers 1
//! ```
//! [`SaveMimeHeaders`]: ../config/enum.Config.html#variant.SaveMimeHeaders
use crate::chat::*;
use crate::constants::Viewtype;
use crate::constants::DC_GCM_ADDDAYMARKER;
use crate::contact::*;
use crate::context::Context;
// use crate::error::Error;
use crate::dc_tools::time;
use crate::message::*;
use std::collections::HashMap;
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;
use zip::write::FileOptions;
use crate::location::Location;
use serde::Serialize;
#[derive(Debug)]
struct ExportChatResult {
chat_json: String,
// locations_geo_json: String,
message_ids: Vec<MsgId>,
referenced_blobs: Vec<String>,
}
pub async fn export_chat_to_zip(context: &Context, chat_id: ChatId, filename: &str) {
let res = export_chat_data(&context, chat_id).await;
let destination = std::path::Path::new(filename);
let pack_res = pack_exported_chat(&context, res, destination).await;
match &pack_res {
Ok(()) => println!("Exported chat successfully to {}", filename),
Err(err) => println!("Error {:?}", err),
};
}
async fn pack_exported_chat(
context: &Context,
artifact: ExportChatResult,
destination: &Path,
) -> zip::result::ZipResult<()> {
let file = std::fs::File::create(&destination).unwrap();
let mut zip = zip::ZipWriter::new(file);
zip.start_file("chat.json", Default::default())?;
zip.write_all(artifact.chat_json.as_bytes())?;
zip.add_directory("blobs/", Default::default())?;
let options = FileOptions::default();
for blob_name in artifact.referenced_blobs {
let path = context.get_blobdir().join(&blob_name);
// println!("adding file {:?} as {:?} ...", path, &blob_name);
zip.start_file(format!("blobs/{}", &blob_name), options)?;
let mut f = File::open(path)?;
let mut buffer = Vec::new();
f.read_to_end(&mut buffer)?;
zip.write_all(&*buffer)?;
buffer.clear();
}
zip.add_directory("msg_info/", Default::default())?;
zip.add_directory("msg_source/", Default::default())?;
for id in artifact.message_ids {
zip.start_file(format!("msg_info/{}.txt", id.to_u32()), options)?;
zip.write_all((get_msg_info(&context, id).await).as_bytes())?;
if let Some(mime_headers) = get_mime_headers(&context, id).await {
zip.start_file(format!("msg_source/{}.eml", id.to_u32()), options)?;
zip.write_all((mime_headers).as_bytes())?;
}
}
zip.finish()?;
Ok(())
}
#[derive(Serialize)]
struct ChatJSON {
chat_json_version: u8,
export_timestamp: i64,
name: String,
color: String,
profile_img: Option<String>,
contacts: HashMap<u32, ContactJSON>,
referenced_external_messages:Vec<ChatItemJSON>,
messages: Vec<ChatItemJSON>,
locations: Vec<Location>,
}
#[derive(Serialize)]
struct ContactJSON {
name: String,
email: String,
color: String,
profile_img: Option<String>,
}
#[derive(Serialize)]
struct FileReference {
name: String,
filesize: u64,
mime: String,
path: String,
}
#[derive(Serialize)]
struct Qoute {
quoted_text: String,
message_id: Option<u32>,
}
#[derive(Serialize)]
#[serde(tag = "type")]
enum ChatItemJSON {
Message {
id: u32,
author_id: u32, // from_id
view_type: Viewtype,
timestamp_sort: i64,
timestamp_sent: i64,
timestamp_rcvd: i64,
text: Option<String>,
attachment: Option<FileReference>,
location_id: Option<u32>,
is_info_message: bool,
show_padlock: bool,
state: MessageState,
is_forwarded: bool,
quote: Option<Qoute>
},
MessageError {
id: u32,
error: String,
},
DayMarker {
timestamp: i64,
},
}
impl ChatItemJSON {
pub async fn from_message(message: &Message, context: &Context) -> ChatItemJSON {
let msg_id = message.get_id();
ChatItemJSON::Message {
id: msg_id.to_u32(),
author_id: message.get_from_id(), // from_id
view_type: message.get_viewtype(),
timestamp_sort: message.timestamp_sort,
timestamp_sent: message.timestamp_sent,
timestamp_rcvd: message.timestamp_rcvd,
text: message.get_text(),
attachment: match message.get_file(context) {
Some(file) => Some(FileReference {
name: message.get_filename().unwrap_or_else(|| "".to_owned()),
filesize: message.get_filebytes(context).await,
mime: message.get_filemime().unwrap_or_else(|| "".to_owned()),
path: format!(
"blobs/{}",
file.file_name()
.unwrap_or_else(|| std::ffi::OsStr::new(""))
.to_str()
.unwrap()
),
}),
None => None,
},
location_id: match message.has_location() {
true => Some(message.location_id),
false => None,
},
is_info_message: message.is_info(),
show_padlock: message.get_showpadlock(),
state: message.get_state(),
is_forwarded: message.is_forwarded(),
quote: match message.quoted_text() {
Some(text) => match message.quoted_message(&context).await {
Ok(Some(msg)) => Some(Qoute {
quoted_text: text,
message_id: Some(msg.get_id().to_u32())
}),
Err(_) | Ok(None) => Some(Qoute {
quoted_text: text,
message_id: None
})
}
None => None
}
}
}
}
async fn export_chat_data(context: &Context, chat_id: ChatId) -> ExportChatResult {
let mut blobs = Vec::new();
let mut chat_author_ids = Vec::new();
// message_ids var is used for writing message info to files
let mut message_ids: Vec<MsgId> = Vec::new();
let mut message_json: Vec<ChatItemJSON> = Vec::new();
let mut referenced_external_messages: Vec<ChatItemJSON> = Vec::new();
for item in get_chat_msgs(context, chat_id, DC_GCM_ADDDAYMARKER, None).await {
if let Some(json_item) = match item {
ChatItem::Message { msg_id } => match Message::load_from_db(context, msg_id).await {
Ok(message) => {
let filename = message.get_filename();
if let Some(file) = filename {
// push referenced blobs (attachments)
blobs.push(file);
}
message_ids.push(message.id);
// populate contactid list
chat_author_ids.push(message.from_id);
if let Ok(Some(ex_msg)) = message.quoted_message(&context).await {
if ex_msg.get_chat_id() != chat_id {
// if external add it to the file
referenced_external_messages.push(ChatItemJSON::from_message(&ex_msg, &context).await)
// contacts don't need to be referenced, because these should only be private replies
}
}
Some(ChatItemJSON::from_message(&message, &context).await)
}
Err(error_message) => Some(ChatItemJSON::MessageError {
id: msg_id.to_u32(),
error: error_message.to_string(),
}),
},
ChatItem::DayMarker { timestamp } => Some(ChatItemJSON::DayMarker { timestamp }),
ChatItem::Marker1 => None,
} {
message_json.push(json_item)
}
}
// deduplicate contact list and load the contacts
chat_author_ids.sort();
chat_author_ids.dedup();
// load information about the authors
let mut chat_authors: HashMap<u32, ContactJSON> = HashMap::new();
chat_authors.insert(
0,
ContactJSON {
name: "Err: Contact not found".to_owned(),
email: "error@localhost".to_owned(),
profile_img: None,
color: "grey".to_owned(),
},
);
for author_id in chat_author_ids {
let contact = Contact::get_by_id(context, author_id).await;
if let Ok(c) = contact {
let profile_img_path: String;
if let Some(path) = c.get_profile_image(context).await {
profile_img_path = path
.file_name()
.unwrap_or_else(|| std::ffi::OsStr::new(""))
.to_str()
.unwrap()
.to_owned();
// push referenced blobs (avatars)
blobs.push(profile_img_path.clone());
} else {
profile_img_path = "".to_owned();
}
chat_authors.insert(
author_id,
ContactJSON {
name: c.get_display_name().to_owned(),
email: c.get_addr().to_owned(),
profile_img: match profile_img_path != "" {
true => Some(profile_img_path),
false => None,
},
color: format!("{:#}", c.get_color()), // TODO
},
);
}
}
// Load information about the chat
let chat: Chat = Chat::load_from_db(context, chat_id).await.unwrap();
let chat_avatar = match chat.get_profile_image(context).await {
Some(img) => {
let path = img
.file_name()
.unwrap_or_else(|| std::ffi::OsStr::new(""))
.to_str()
.unwrap()
.to_owned();
blobs.push(path.clone());
Some(format!("blobs/{}", path))
}
None => None,
};
let chat_json = ChatJSON {
chat_json_version: 1,
export_timestamp: time(),
name: chat.get_name().to_owned(),
color: format!("{:#}", chat.get_color(&context).await),
profile_img: chat_avatar,
contacts: chat_authors,
referenced_external_messages,
messages: message_json,
locations: crate::location::get_range(&context, chat_id, 0, 0, crate::dc_tools::time())
.await,
};
blobs.sort();
blobs.dedup();
ExportChatResult {
chat_json: serde_json::to_string(&chat_json).unwrap(),
message_ids,
referenced_blobs: blobs,
}
}

View File

@@ -1,13 +1,13 @@
//! # format=flowed support.
//!
//! Format=flowed is defined in
//! [RFC 3676](https://tools.ietf.org/html/rfc3676).
//!
//! Older [RFC 2646](https://tools.ietf.org/html/rfc2646) is used
//! during formatting, i.e., DelSp parameter introduced in RFC 3676
//! is assumed to be set to "no".
//!
//! For received messages, DelSp parameter is honoured.
///! # format=flowed support
///!
///! Format=flowed is defined in
///! [RFC 3676](https://tools.ietf.org/html/rfc3676).
///!
///! Older [RFC 2646](https://tools.ietf.org/html/rfc2646) is used
///! during formatting, i.e., DelSp parameter introduced in RFC 3676
///! is assumed to be set to "no".
///!
///! For received messages, DelSp parameter is honoured.
/// Wraps line to 72 characters using format=flowed soft breaks.
///
@@ -104,13 +104,13 @@ pub fn unformat_flowed(text: &str, delsp: bool) -> String {
for line in text.split('\n') {
// Revert space-stuffing
let line = line.strip_prefix(' ').unwrap_or(line);
let line = line.strip_prefix(" ").unwrap_or(line);
if !skip_newline {
result.push('\n');
}
if let Some(line) = line.strip_suffix(' ') {
if let Some(line) = line.strip_suffix(" ") {
// Flowed line
result += line;
if !delsp {

View File

@@ -1,5 +1,3 @@
//! # List of email headers.
use crate::strum::AsStaticRef;
use mailparse::{MailHeader, MailHeaderMap};
@@ -27,12 +25,6 @@ pub enum HeaderDef {
/// we need to check that header as well.
XMicrosoftOriginalMessageId,
/// Thunderbird header used to store Draft information.
///
/// Thunderbird 78.11.0 does not set \Draft flag on messages saved as "Template", but sets this
/// header, so it can be used to ignore such messages.
XMozillaDraftInfo,
ListId,
References,
InReplyTo,

View File

@@ -1,12 +1,11 @@
//! # Get message as HTML.
//!
//! Use `Message.has_html()` to check if the UI shall render a
//! corresponding button and `MsgId.get_html()` to get the full message.
//!
//! Even when the original mime-message is not HTML,
//! `MsgId.get_html()` will return HTML -
//! this allows nice quoting, handling linebreaks properly etc.
///! # Get message as HTML.
///!
///! Use `Message.has_html()` to check if the UI shall render a
///! corresponding button and `MsgId.get_html()` to get the full message.
///!
///! Even when the original mime-message is not HTML,
///! `MsgId.get_html()` will return HTML -
///! this allows nice quoting, handling linebreaks properly etc.
use futures::future::FutureExt;
use std::future::Future;
use std::pin::Pin;
@@ -249,7 +248,7 @@ impl MsgId {
let rawmime = message::get_mime_headers(context, self).await?;
if !rawmime.is_empty() {
match HtmlMsgParser::from_bytes(context, &rawmime).await {
match HtmlMsgParser::from_bytes(context, rawmime.as_bytes()).await {
Err(err) => {
warn!(context, "get_html: parser error: {}", err);
Ok(None)
@@ -425,10 +424,10 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
}
#[async_std::test]
async fn test_get_html_invalid_msgid() {
async fn test_get_html_empty() {
let t = TestContext::new().await;
let msg_id = MsgId::new(100);
assert!(msg_id.get_html(&t).await.is_err())
assert!(msg_id.get_html(&t).await.unwrap().is_none())
}
#[async_std::test]
@@ -551,26 +550,4 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
let html = msg.get_id().get_html(&bob).await.unwrap().unwrap();
assert!(html.contains("<b>html</b> text"));
}
#[async_std::test]
async fn test_cp1252_html() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await?;
dc_receive_imf(
&t,
include_bytes!("../test-data/message/cp1252-html.eml"),
"INBOX",
0,
false,
)
.await?;
let msg = t.get_last_msg().await;
assert_eq!(msg.viewtype, Viewtype::Text);
assert!(msg.text.as_ref().unwrap().contains("foo bar ä ö ü ß"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&t).await?.unwrap();
println!("{}", html);
assert!(html.contains("foo bar ä ö ü ß"));
Ok(())
}
}

View File

@@ -1,19 +1,21 @@
//! # IMAP handling module.
//! # Imap handling module
//!
//! uses [async-email/async-imap](https://github.com/async-email/async-imap)
//! to implement connect, fetch, delete functionality with standard IMAP servers.
use std::{cmp, cmp::max, collections::BTreeMap};
use anyhow::{anyhow, bail, format_err, Context as _, Result};
use anyhow::{bail, format_err, Context as _, Result};
use async_imap::{
error::Result as ImapResult,
types::{Fetch, Flag, Mailbox, Name, NameAttribute, Quota, QuotaRoot, UnsolicitedResponse},
types::{Capability, Fetch, Flag, Mailbox, Name, NameAttribute},
};
use async_std::channel::Receiver;
use async_std::prelude::*;
use num_traits::FromPrimitive;
use crate::chat;
use crate::config::Config;
use crate::constants::{
Chattype, ShowEmails, Viewtype, DC_FETCH_EXISTING_MSGS_COUNT, DC_FOLDERS_CONFIGURED_VERSION,
DC_LP_AUTH_OAUTH2,
@@ -27,7 +29,6 @@ use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::job::{self, Action};
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam};
use crate::login_param::{ServerAddress, Socks5Config};
use crate::message::{self, update_server_uid, MessageState};
use crate::mimeparser;
use crate::oauth2::dc_get_oauth2_access_token;
@@ -35,8 +36,6 @@ use crate::param::Params;
use crate::provider::Socket;
use crate::scheduler::InterruptInfo;
use crate::stock_str;
use crate::{chat, constants::DC_CONTACT_ID_SELF};
use crate::{config::Config, scheduler::connectivity::ConnectivityStore};
mod client;
mod idle;
@@ -93,12 +92,6 @@ pub struct Imap {
interrupt: Option<stop_token::StopSource>,
should_reconnect: bool,
login_failed_once: bool,
/// True if CAPABILITY command was run successfully once and config.can_* contain correct
/// values.
capabilities_determined: bool,
pub(crate) connectivity: ConnectivityStore,
}
#[derive(Debug)]
@@ -118,145 +111,87 @@ impl async_imap::Authenticator for OAuth2 {
}
}
#[derive(Debug, PartialEq, Clone, Copy)]
#[derive(Debug, PartialEq)]
enum FolderMeaning {
Unknown,
Spam,
Sent,
Drafts,
SentObjects,
Other,
}
impl FolderMeaning {
fn to_config(self) -> Option<Config> {
match self {
FolderMeaning::Unknown => None,
FolderMeaning::Spam => Some(Config::ConfiguredSpamFolder),
FolderMeaning::Sent => Some(Config::ConfiguredSentboxFolder),
FolderMeaning::Drafts => None,
FolderMeaning::Other => None,
}
}
}
#[derive(Debug)]
struct ImapConfig {
pub addr: String,
pub lp: ServerLoginParam,
pub socks5_config: Option<Socks5Config>,
pub strict_tls: bool,
pub oauth2: bool,
pub selected_folder: Option<String>,
pub selected_mailbox: Option<Mailbox>,
pub selected_folder_needs_expunge: bool,
pub can_idle: bool,
/// True if the server has MOVE capability as defined in
/// <https://tools.ietf.org/html/rfc6851>
/// https://tools.ietf.org/html/rfc6851
pub can_move: bool,
/// True if the server has QUOTA capability as defined in
/// <https://tools.ietf.org/html/rfc2087>
pub can_check_quota: bool,
}
impl Imap {
/// Creates new disconnected IMAP client using the specific login parameters.
///
/// `addr` is used to renew token if OAuth2 authentication is used.
pub async fn new(
lp: &ServerLoginParam,
socks5_config: Option<Socks5Config>,
addr: &str,
oauth2: bool,
provider_strict_tls: bool,
idle_interrupt: Receiver<InterruptInfo>,
) -> Result<Self> {
if lp.server.is_empty() || lp.user.is_empty() || lp.password.is_empty() {
bail!("Incomplete IMAP connection parameters");
}
let strict_tls = match lp.certificate_checks {
CertificateChecks::Automatic => provider_strict_tls,
CertificateChecks::Strict => true,
CertificateChecks::AcceptInvalidCertificates
| CertificateChecks::AcceptInvalidCertificates2 => false,
};
let config = ImapConfig {
addr: addr.to_string(),
lp: lp.clone(),
socks5_config,
strict_tls,
oauth2,
impl Default for ImapConfig {
fn default() -> Self {
ImapConfig {
addr: "".into(),
lp: Default::default(),
strict_tls: false,
oauth2: false,
selected_folder: None,
selected_mailbox: None,
selected_folder_needs_expunge: false,
can_idle: false,
can_move: false,
can_check_quota: false,
};
}
}
}
let imap = Imap {
impl Imap {
pub fn new(idle_interrupt: Receiver<InterruptInfo>) -> Self {
Imap {
idle_interrupt,
config,
session: None,
connected: false,
interrupt: None,
should_reconnect: false,
login_failed_once: false,
connectivity: Default::default(),
capabilities_determined: false,
};
Ok(imap)
config: Default::default(),
session: Default::default(),
connected: Default::default(),
interrupt: Default::default(),
should_reconnect: Default::default(),
login_failed_once: Default::default(),
}
}
/// Creates new disconnected IMAP client using configured parameters.
pub async fn new_configured(
context: &Context,
idle_interrupt: Receiver<InterruptInfo>,
) -> Result<Self> {
if !context.is_configured().await? {
bail!("IMAP Connect without configured params");
}
pub fn is_connected(&self) -> bool {
self.connected
}
let param = LoginParam::from_database(context, "configured_").await?;
// the trailing underscore is correct
pub fn should_reconnect(&self) -> bool {
self.should_reconnect
}
let imap = Self::new(
&param.imap,
param.socks5_config.clone(),
&param.addr,
param.server_flags & DC_LP_AUTH_OAUTH2 != 0,
param.provider.map_or(false, |provider| provider.strict_tls),
idle_interrupt,
)
.await?;
Ok(imap)
pub fn trigger_reconnect(&mut self) {
self.should_reconnect = true;
}
/// Connects or reconnects if needed.
///
/// It is safe to call this function if already connected, actions are performed only as needed.
///
/// Calling this function is not enough to perform IMAP operations. Use [`Imap::prepare`]
/// instead if you are going to actually use connection rather than trying connection
/// parameters.
pub async fn connect(&mut self, context: &Context) -> Result<()> {
/// It is safe to call this function if already connected, actions
/// are performed only as needed.
async fn try_setup_handle(&mut self, context: &Context) -> Result<()> {
if self.config.lp.server.is_empty() {
bail!("IMAP operation attempted while it is torn down");
}
if self.should_reconnect() {
self.disconnect(context).await;
self.unsetup_handle(context).await;
self.should_reconnect = false;
} else if self.is_connected() {
return Ok(());
}
self.connectivity.set_connecting(context).await;
let oauth2 = self.config.oauth2;
let connection_res: ImapResult<Client> = if self.config.lp.security == Socket::Starttls
@@ -266,20 +201,7 @@ impl Imap {
let imap_server: &str = config.lp.server.as_ref();
let imap_port = config.lp.port;
let connection = if let Some(socks5_config) = &config.socks5_config {
Client::connect_insecure_socks5(
&ServerAddress {
host: imap_server.to_string(),
port: imap_port,
},
socks5_config.clone(),
)
.await
} else {
Client::connect_insecure((imap_server, imap_port)).await
};
match connection {
match Client::connect_insecure((imap_server, imap_port)).await {
Ok(client) => {
if config.lp.security == Socket::Starttls {
client.secure(imap_server, config.strict_tls).await
@@ -294,20 +216,7 @@ impl Imap {
let imap_server: &str = config.lp.server.as_ref();
let imap_port = config.lp.port;
if let Some(socks5_config) = &config.socks5_config {
Client::connect_secure_socks5(
&ServerAddress {
host: imap_server.to_string(),
port: imap_port,
},
config.strict_tls,
socks5_config.clone(),
)
.await
} else {
Client::connect_secure((imap_server, imap_port), imap_server, config.strict_tls)
.await
}
Client::connect_secure((imap_server, imap_port), imap_server, config.strict_tls).await
};
let login_res = match connection_res {
@@ -347,10 +256,6 @@ impl Imap {
self.connected = true;
self.session = Some(session);
self.login_failed_once = false;
emit_event!(
context,
EventType::ImapConnected(format!("IMAP-LOGIN as {}", self.config.lp.user))
);
Ok(())
}
@@ -381,53 +286,26 @@ impl Imap {
self.login_failed_once = true;
}
self.trigger_reconnect(context).await;
self.trigger_reconnect();
Err(format_err!("{}\n\n{}", message, err))
}
}
}
/// Determine server capabilities if not done yet.
async fn determine_capabilities(&mut self) -> Result<()> {
if self.capabilities_determined {
return Ok(());
}
match &mut self.session {
Some(ref mut session) => match session.capabilities().await {
Ok(caps) => {
self.config.can_idle = caps.has_str("IDLE");
self.config.can_move = caps.has_str("MOVE");
self.config.can_check_quota = caps.has_str("QUOTA");
self.capabilities_determined = true;
Ok(())
}
Err(err) => {
bail!("CAPABILITY command error: {}", err);
}
},
None => {
bail!("Can't determine server capabilities because connection was not established")
}
}
}
/// Prepare for IMAP operation.
/// Connects or reconnects if not already connected.
///
/// Ensure that IMAP client is connected, folders are created and IMAP capabilities are
/// determined.
pub async fn prepare(&mut self, context: &Context) -> Result<()> {
if let Err(err) = self.connect(context).await {
self.connectivity.set_err(context, &err).await;
return Err(err);
/// This function emits network error if it fails. It should not
/// be used during configuration to avoid showing failed attempt
/// errors to the user.
async fn setup_handle(&mut self, context: &Context) -> Result<()> {
let res = self.try_setup_handle(context).await;
if let Err(ref err) = res {
emit_event!(context, EventType::ErrorNetwork(err.to_string()));
}
self.ensure_configured_folders(context, true).await?;
self.determine_capabilities().await?;
Ok(())
res
}
async fn disconnect(&mut self, context: &Context) {
async fn unsetup_handle(&mut self, context: &Context) {
// Close folder if messages should be expunged
if let Err(err) = self.close_folder(context).await {
warn!(context, "failed to close folder: {:?}", err);
@@ -440,22 +318,139 @@ impl Imap {
}
}
self.connected = false;
self.capabilities_determined = false;
self.config.selected_folder = None;
self.config.selected_mailbox = None;
}
pub fn is_connected(&self) -> bool {
self.connected
async fn free_connect_params(&mut self) {
let mut cfg = &mut self.config;
cfg.addr = "".into();
cfg.lp = Default::default();
cfg.can_idle = false;
cfg.can_move = false;
}
pub fn should_reconnect(&self) -> bool {
self.should_reconnect
/// Connects to IMAP account using already-configured parameters.
///
/// Emits network error if connection fails.
pub async fn connect_configured(&mut self, context: &Context) -> Result<()> {
if self.is_connected() && !self.should_reconnect() {
return Ok(());
}
if !context.is_configured().await? {
bail!("IMAP Connect without configured params");
}
let param = LoginParam::from_database(context, "configured_").await?;
// the trailing underscore is correct
if let Err(err) = self
.connect(
context,
&param.imap,
&param.addr,
param.server_flags & DC_LP_AUTH_OAUTH2 != 0,
param.provider.map_or(false, |provider| provider.strict_tls),
)
.await
{
bail!("IMAP Connection Failed with params {}: {}", param, err);
} else {
self.ensure_configured_folders(context, true).await
}
}
pub async fn trigger_reconnect(&mut self, context: &Context) {
self.connectivity.set_connecting(context).await;
self.should_reconnect = true;
/// Tries connecting to imap account using the specific login parameters.
///
/// `addr` is used to renew token if OAuth2 authentication is used.
///
/// Does not emit network errors, can be used to try various
/// parameters during autoconfiguration.
pub async fn connect(
&mut self,
context: &Context,
lp: &ServerLoginParam,
addr: &str,
oauth2: bool,
provider_strict_tls: bool,
) -> Result<()> {
if lp.server.is_empty() || lp.user.is_empty() || lp.password.is_empty() {
bail!("Incomplete IMAP connection parameters");
}
{
let mut config = &mut self.config;
config.addr = addr.to_string();
config.lp = lp.clone();
config.strict_tls = match lp.certificate_checks {
CertificateChecks::Automatic => provider_strict_tls,
CertificateChecks::Strict => true,
CertificateChecks::AcceptInvalidCertificates
| CertificateChecks::AcceptInvalidCertificates2 => false,
};
config.oauth2 = oauth2;
}
if let Err(err) = self.try_setup_handle(context).await {
warn!(context, "try_setup_handle: {}", err);
self.free_connect_params().await;
return Err(err);
}
let teardown = match &mut self.session {
Some(ref mut session) => match session.capabilities().await {
Ok(caps) => {
if !context.sql.is_open().await {
warn!(context, "IMAP-LOGIN as {} ok but ABORTING", lp.user,);
true
} else {
let can_idle = caps.has_str("IDLE");
let can_move = caps.has_str("MOVE");
let caps_list = caps.iter().fold(String::new(), |s, c| {
if let Capability::Atom(x) = c {
s + &format!(" {}", x)
} else {
s + &format!(" {:?}", c)
}
});
self.config.can_idle = can_idle;
self.config.can_move = can_move;
self.connected = true;
emit_event!(
context,
EventType::ImapConnected(format!(
"IMAP-LOGIN as {}, capabilities: {}",
lp.user, caps_list,
))
);
false
}
}
Err(err) => {
info!(context, "CAPABILITY command error: {}", err);
true
}
},
None => true,
};
if teardown {
self.disconnect(context).await;
warn!(
context,
"IMAP disconnected immediately after connecting due to error"
);
}
Ok(())
}
pub async fn disconnect(&mut self, context: &Context) {
self.unsetup_handle(context).await;
self.free_connect_params().await;
}
pub async fn fetch(&mut self, context: &Context, watch_folder: &str) -> Result<()> {
@@ -463,7 +458,7 @@ impl Imap {
// probably shutdown
bail!("IMAP operation attempted while it is torn down");
}
self.prepare(context).await?;
self.setup_handle(context).await?;
while self
.fetch_new_messages(context, &watch_folder, false)
@@ -701,7 +696,6 @@ impl Imap {
current_uid,
&headers,
&msg_id,
msg.flags(),
folder,
show_emails,
)
@@ -715,10 +709,6 @@ impl Imap {
}
}
if !uids.is_empty() {
self.connectivity.set_working(context).await;
}
let (largest_uid_processed, error_cnt) = self
.fetch_many_msgs(context, folder, uids, fetch_existing_msgs)
.await;
@@ -825,7 +815,7 @@ impl Imap {
// at least one UID, even if last_seen_uid+1 is past
// the last UID in the mailbox. It happens because
// uid:* is interpreted the same way as *:uid.
// See <https://tools.ietf.org/html/rfc3501#page-61> for
// See https://tools.ietf.org/html/rfc3501#page-61 for
// standard reference. Therefore, sometimes we receive
// already seen messages and have to filter them out.
let new_msgs = msgs.split_off(&uid_next);
@@ -886,7 +876,7 @@ impl Imap {
if self.session.is_none() {
// we could not get a valid imap session, this should be retried
self.trigger_reconnect(context).await;
self.trigger_reconnect();
warn!(context, "Could not get IMAP session");
return (None, server_uids.len());
}
@@ -984,6 +974,10 @@ impl Imap {
(last_uid, read_errors)
}
pub async fn can_move(&self) -> bool {
self.config.can_move
}
pub async fn mv(
&mut self,
context: &Context,
@@ -1008,7 +1002,7 @@ impl Imap {
let set = format!("{}", uid);
let display_folder_id = format!("{}/{}", folder, uid);
if self.config.can_move {
if self.can_move().await {
if let Some(ref mut session) = &mut self.session {
match session.uid_mv(&set, &dest_folder).await {
Ok(_) => {
@@ -1134,7 +1128,7 @@ impl Imap {
// TODO: make INBOX/SENT/MVBOX perform the jobs on their
// respective folders to avoid select_folder network traffic
// and the involved error states
if let Err(err) = self.prepare(context).await {
if let Err(err) = self.connect_configured(context).await {
warn!(context, "prepare_imap_op failed: {}", err);
return Some(ImapActionResult::RetryLater);
}
@@ -1306,8 +1300,9 @@ impl Imap {
let mut delimiter = ".".to_string();
let mut delimiter_is_default = true;
let mut sentbox_folder = None;
let mut spam_folder = None;
let mut mvbox_folder = None;
let mut folder_configs = BTreeMap::new();
let mut fallback_folder = get_fallback_folder(&delimiter);
while let Some(folder) = folders.next().await {
@@ -1326,26 +1321,31 @@ impl Imap {
let folder_meaning = get_folder_meaning(&folder);
let folder_name_meaning = get_folder_meaning_by_name(folder.name());
if folder.name() == "DeltaChat" {
// Always takes precedence
// Always takes precendent
mvbox_folder = Some(folder.name().to_string());
} else if folder.name() == fallback_folder {
// only set if none has been already set
// only set iff none has been already set
if mvbox_folder.is_none() {
mvbox_folder = Some(folder.name().to_string());
}
} else if let Some(config) = folder_meaning.to_config() {
// Always takes precedence
folder_configs.insert(config, folder.name().to_string());
} else if let Some(config) = folder_name_meaning.to_config() {
// only set if none has been already set
folder_configs
.entry(config)
.or_insert_with(|| folder.name().to_string());
} else if folder_meaning == FolderMeaning::SentObjects {
// Always takes precedent
sentbox_folder = Some(folder.name().to_string());
} else if folder_meaning == FolderMeaning::Spam {
spam_folder = Some(folder.name().to_string());
} else if folder_name_meaning == FolderMeaning::SentObjects {
// only set iff none has been already set
if sentbox_folder.is_none() {
sentbox_folder = Some(folder.name().to_string());
}
} else if folder_name_meaning == FolderMeaning::Spam && spam_folder.is_none() {
spam_folder = Some(folder.name().to_string());
}
}
drop(folders);
info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
info!(context, "sentbox folder is {:?}", sentbox_folder);
if mvbox_folder.is_none() && create_mvbox {
info!(context, "Creating MVBOX-folder \"DeltaChat\"...",);
@@ -1393,8 +1393,15 @@ impl Imap {
.set_config(Config::ConfiguredMvboxFolder, Some(mvbox_folder))
.await?;
}
for (config, name) in folder_configs {
context.set_config(config, Some(&name)).await?;
if let Some(ref sentbox_folder) = sentbox_folder {
context
.set_config(Config::ConfiguredSentboxFolder, Some(sentbox_folder))
.await?;
}
if let Some(ref spam_folder) = spam_folder {
context
.set_config(Config::ConfiguredSpamFolder, Some(spam_folder))
.await?;
}
context
.sql
@@ -1404,47 +1411,6 @@ impl Imap {
info!(context, "FINISHED configuring IMAP-folders.");
Ok(())
}
/// Return whether the server sent an unsolicited EXISTS response.
/// Drains all responses from `session.unsolicited_responses` in the process.
/// If this returns `true`, this means that new emails arrived and you should
/// fetch again, even if you just fetched.
fn server_sent_unsolicited_exists(&self, context: &Context) -> bool {
let session = match &self.session {
Some(s) => s,
None => return false,
};
let mut unsolicited_exists = false;
while let Ok(response) = session.unsolicited_responses.try_recv() {
match response {
UnsolicitedResponse::Exists(_) => {
info!(
context,
"Need to fetch again, got unsolicited EXISTS {:?}", response
);
unsolicited_exists = true;
}
_ => info!(context, "ignoring unsolicited response {:?}", response),
}
}
unsolicited_exists
}
pub fn can_check_quota(&self) -> bool {
self.config.can_check_quota
}
pub async fn get_quota_roots(
&mut self,
mailbox_name: &str,
) -> Result<(Vec<QuotaRoot>, Vec<Quota>)> {
if let Some(session) = self.session.as_mut() {
let quota_roots = session.get_quota_root(mailbox_name).await?;
Ok(quota_roots)
} else {
Err(anyhow!("Not connected to IMAP, no session"))
}
}
}
/// Try to get the folder meaning by the name of the folder only used if the server does not support XLIST.
@@ -1454,7 +1420,7 @@ impl Imap {
// CAVE: if possible, take care not to add a name here that is "sent" in one language
// but sth. different in others - a hard job.
fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
// source: <https://stackoverflow.com/questions/2185391/localized-gmail-imap-folders>
// source: https://stackoverflow.com/questions/2185391/localized-gmail-imap-folders
const SENT_NAMES: &[&str] = &[
"sent",
"sentmail",
@@ -1508,50 +1474,29 @@ fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
"迷惑メール",
"스팸",
];
const DRAFT_NAMES: &[&str] = &[
"Drafts",
"Kladder",
"Entw?rfe",
"Borradores",
"Brouillons",
"Bozze",
"Concepten",
"Wersje robocze",
"Rascunhos",
"Entwürfe",
"Koncepty",
"Kopie robocze",
"Taslaklar",
"Utkast",
"Πρόχειρα",
"Черновики",
"下書き",
"草稿",
"임시보관함",
];
let lower = folder_name.to_lowercase();
if SENT_NAMES.iter().any(|s| s.to_lowercase() == lower) {
FolderMeaning::Sent
FolderMeaning::SentObjects
} else if SPAM_NAMES.iter().any(|s| s.to_lowercase() == lower) {
FolderMeaning::Spam
} else if DRAFT_NAMES.iter().any(|s| s.to_lowercase() == lower) {
FolderMeaning::Drafts
} else {
FolderMeaning::Unknown
}
}
fn get_folder_meaning(folder_name: &Name) -> FolderMeaning {
let special_names = vec!["\\Trash", "\\Drafts"];
for attr in folder_name.attributes() {
if let NameAttribute::Custom(ref label) = attr {
match label.as_ref() {
"\\Trash" => return FolderMeaning::Other,
"\\Sent" => return FolderMeaning::Sent,
"\\Spam" | "\\Junk" => return FolderMeaning::Spam,
"\\Drafts" => return FolderMeaning::Drafts,
_ => {}
};
if special_names.iter().any(|s| *s == label) {
return FolderMeaning::Other;
} else if label == "\\Sent" {
return FolderMeaning::SentObjects;
} else if label == "\\Spam" || label == "\\Junk" {
return FolderMeaning::Spam;
}
}
}
FolderMeaning::Unknown
@@ -1669,7 +1614,6 @@ fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Result<String>
pub(crate) async fn prefetch_should_download(
context: &Context,
headers: &[mailparse::MailHeader<'_>],
mut flags: impl Iterator<Item = Flag<'_>>,
show_emails: ShowEmails,
) -> Result<bool> {
let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion).is_some();
@@ -1677,7 +1621,7 @@ pub(crate) async fn prefetch_should_download(
let is_reply_to_chat_message = parent.is_some();
if let Some(parent) = &parent {
let chat = chat::Chat::load_from_db(context, parent.get_chat_id()).await?;
if chat.typ == Chattype::Group && !chat.id.is_special() {
if chat.typ == Chattype::Group {
// This might be a group command, like removing a group member.
// We really need to fetch this to avoid inconsistent group state.
return Ok(true);
@@ -1689,7 +1633,7 @@ pub(crate) async fn prefetch_should_download(
// deleted from the database or has not arrived yet.
if let Some(rfc724_mid) = headers.get_header_value(HeaderDef::MessageId) {
if let Some(group_id) = dc_extract_grpid_from_rfc724_mid(&rfc724_mid) {
if get_chat_id_by_grpid(context, group_id).await?.is_some() {
if let Ok((_chat_id, _, _)) = get_chat_id_by_grpid(context, group_id).await {
return Ok(true);
}
}
@@ -1707,17 +1651,12 @@ pub(crate) async fn prefetch_should_download(
.get_header_value(HeaderDef::AutocryptSetupMessage)
.is_some();
let (from_id, blocked_contact, origin) =
let (_contact_id, blocked_contact, origin) =
from_field_to_contact_id(context, &mimeparser::get_from(headers), true).await?;
// prevent_rename=true as this might be a mailing list message and in this case it would be bad if we rename the contact.
// (prevent_rename is the last argument of from_field_to_contact_id())
if flags.any(|f| f == Flag::Draft) && from_id == DC_CONTACT_ID_SELF {
info!(context, "Ignoring draft message");
return Ok(false);
}
let accepted_contact = origin.is_known();
let show = is_autocrypt_setup_message
|| match show_emails {
ShowEmails::Off => is_chat_message || is_reply_to_chat_message,
@@ -1736,7 +1675,6 @@ async fn message_needs_processing(
current_uid: u32,
headers: &[mailparse::MailHeader<'_>],
msg_id: &str,
flags: impl Iterator<Item = Flag<'_>>,
folder: &str,
show_emails: ShowEmails,
) -> bool {
@@ -1760,7 +1698,7 @@ async fn message_needs_processing(
// we do not know the message-id
// or the message-id is missing (in this case, we create one in the further process)
// or some other error happened
let show = match prefetch_should_download(context, headers, flags, show_emails).await {
let show = match prefetch_should_download(context, headers, show_emails).await {
Ok(show) => show,
Err(err) => {
warn!(context, "prefetch_should_download error: {}", err);
@@ -1784,7 +1722,7 @@ fn get_fallback_folder(delimiter: &str) -> String {
}
/// uid_next is the next unique identifier value from the last time we fetched a folder
/// See <https://tools.ietf.org/html/rfc3501#section-2.3.1.1>
/// See https://tools.ietf.org/html/rfc3501#section-2.3.1.1
/// This function is used to update our uid_next after fetching messages.
pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32) -> Result<()> {
context
@@ -1799,7 +1737,7 @@ pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32)
}
/// uid_next is the next unique identifier value from the last time we fetched a folder
/// See <https://tools.ietf.org/html/rfc3501#section-2.3.1.1>
/// See https://tools.ietf.org/html/rfc3501#section-2.3.1.1
/// This method returns the uid_next from the last time we fetched messages.
/// We can compare this to the current uid_next to find out whether there are new messages
/// and fetch from this value on to get all new messages.
@@ -1860,7 +1798,7 @@ pub async fn get_config_last_seen_uid<S: AsRef<str>>(
}
/// Builds a list of sequence/uid sets. The returned sets have each no more than around 1000
/// characters because according to <https://tools.ietf.org/html/rfc2683#section-3.2.1.5>
/// characters because according to https://tools.ietf.org/html/rfc2683#section-3.2.1.5
/// command lines should not be much more than 1000 chars (servers should allow at least 8000 chars)
fn build_sequence_sets(mut uids: Vec<u32>) -> Vec<String> {
uids.sort_unstable();
@@ -1923,16 +1861,25 @@ mod tests {
use crate::test_utils::TestContext;
#[test]
fn test_get_folder_meaning_by_name() {
assert_eq!(get_folder_meaning_by_name("Gesendet"), FolderMeaning::Sent);
assert_eq!(get_folder_meaning_by_name("GESENDET"), FolderMeaning::Sent);
assert_eq!(get_folder_meaning_by_name("gesendet"), FolderMeaning::Sent);
assert_eq!(
get_folder_meaning_by_name("Gesendet"),
FolderMeaning::SentObjects
);
assert_eq!(
get_folder_meaning_by_name("GESENDET"),
FolderMeaning::SentObjects
);
assert_eq!(
get_folder_meaning_by_name("gesendet"),
FolderMeaning::SentObjects
);
assert_eq!(
get_folder_meaning_by_name("Messages envoyés"),
FolderMeaning::Sent
FolderMeaning::SentObjects
);
assert_eq!(
get_folder_meaning_by_name("mEsSaGes envoyÉs"),
FolderMeaning::Sent
FolderMeaning::SentObjects
);
assert_eq!(get_folder_meaning_by_name("xxx"), FolderMeaning::Unknown);
assert_eq!(get_folder_meaning_by_name("SPAM"), FolderMeaning::Spam);

View File

@@ -1,24 +1,16 @@
use std::{
ops::{Deref, DerefMut},
time::Duration,
};
use std::ops::{Deref, DerefMut};
use async_imap::{
error::{Error as ImapError, Result as ImapResult},
Client as ImapClient,
};
use async_smtp::ServerAddress;
use async_std::net::{self, TcpStream};
use super::session::Session;
use crate::login_param::{dc_build_tls, Socks5Config};
use crate::login_param::dc_build_tls;
use super::session::SessionStream;
/// IMAP write and read timeout in seconds.
const IMAP_TIMEOUT: u64 = 30;
#[derive(Debug)]
pub(crate) struct Client {
is_secure: bool,
@@ -119,63 +111,6 @@ impl Client {
})
}
pub async fn connect_secure_socks5(
target_addr: &ServerAddress,
strict_tls: bool,
socks5_config: Socks5Config,
) -> ImapResult<Self> {
let socks5_stream: Box<dyn SessionStream> = Box::new(
match socks5_config
.connect(target_addr, Some(Duration::from_secs(IMAP_TIMEOUT)))
.await
{
Ok(s) => s,
Err(e) => return ImapResult::Err(async_imap::error::Error::Bad(e.to_string())),
},
);
let tls = dc_build_tls(strict_tls);
let tls_stream: Box<dyn SessionStream> =
Box::new(tls.connect(target_addr.host.clone(), socks5_stream).await?);
let mut client = ImapClient::new(tls_stream);
let _greeting = client
.read_response()
.await
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
Ok(Client {
is_secure: true,
inner: client,
})
}
pub async fn connect_insecure_socks5(
target_addr: &ServerAddress,
socks5_config: Socks5Config,
) -> ImapResult<Self> {
let socks5_stream: Box<dyn SessionStream> = Box::new(
match socks5_config
.connect(target_addr, Some(Duration::from_secs(IMAP_TIMEOUT)))
.await
{
Ok(s) => s,
Err(e) => return ImapResult::Err(async_imap::error::Error::Bad(e.to_string())),
},
);
let mut client = ImapClient::new(socks5_stream);
let _greeting = client
.read_response()
.await
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
Ok(Client {
is_secure: false,
inner: client,
})
}
pub async fn secure(self, domain: &str, strict_tls: bool) -> ImapResult<Client> {
if self.is_secure {
Ok(self)

View File

@@ -2,6 +2,7 @@ use super::Imap;
use anyhow::{bail, format_err, Result};
use async_imap::extensions::idle::IdleResponse;
use async_imap::types::UnsolicitedResponse;
use async_std::prelude::*;
use std::time::{Duration, SystemTime};
@@ -24,18 +25,31 @@ impl Imap {
if !self.can_idle() {
bail!("IMAP server does not have IDLE capability");
}
self.prepare(context).await?;
self.setup_handle(context).await?;
self.select_folder(context, watch_folder.as_deref()).await?;
let timeout = Duration::from_secs(23 * 60);
let mut info = Default::default();
if self.server_sent_unsolicited_exists(context) {
return Ok(info);
}
if let Some(session) = self.session.take() {
// if we have unsolicited responses we directly return
let mut unsolicited_exists = false;
while let Ok(response) = session.unsolicited_responses.try_recv() {
match response {
UnsolicitedResponse::Exists(_) => {
warn!(context, "skip idle, got unsolicited EXISTS {:?}", response);
unsolicited_exists = true;
}
_ => info!(context, "ignoring unsolicited response {:?}", response),
}
}
if unsolicited_exists {
self.session = Some(session);
return Ok(info);
}
if let Ok(info) = self.idle_interrupt.try_recv() {
info!(context, "skip idle, got interrupt {:?}", info);
self.session = Some(session);
@@ -144,7 +158,7 @@ impl Imap {
// try to connect with proper login params
// (setup_handle_if_needed might not know about them if we
// never successfully connected)
if let Err(err) = self.prepare(context).await {
if let Err(err) = self.connect_configured(context).await {
warn!(context, "fake_idle: could not connect: {}", err);
continue;
}
@@ -167,7 +181,7 @@ impl Imap {
}
Err(err) => {
error!(context, "could not fetch from folder: {:#}", err);
self.trigger_reconnect(context).await;
self.trigger_reconnect()
}
}
}

View File

@@ -1,13 +1,13 @@
use std::{collections::BTreeMap, time::Instant};
use std::time::Instant;
use anyhow::{Context as _, Result};
use crate::config::Config;
use crate::context::Context;
use crate::imap::Imap;
use crate::{config::Config, log::LogExt};
use crate::{context::Context, imap::FolderMeaning};
use async_std::prelude::*;
use super::{get_folder_meaning, get_folder_meaning_by_name};
use super::{get_folder_meaning, get_folder_meaning_by_name, FolderMeaning};
impl Imap {
pub async fn scan_folders(&mut self, context: &Context) -> Result<()> {
@@ -25,13 +25,14 @@ impl Imap {
}
info!(context, "Starting full folder scan");
self.prepare(context).await?;
self.connect_configured(context).await?;
let session = self.session.as_mut();
let session = session.context("scan_folders(): IMAP No Connection established")?;
let folders: Vec<_> = session.list(Some(""), Some("*")).await?.collect().await;
let watched_folders = get_watched_folders(context).await;
let mut folder_configs = BTreeMap::new();
let mut sentbox_folder = None;
let mut spam_folder = None;
for folder in folders {
let folder = match folder {
@@ -42,67 +43,45 @@ impl Imap {
}
};
// Gmail labels are not folders and should be skipped. For example,
// emails appear in the inbox and under "All Mail" as soon as it is
// received. The code used to wrongly conclude that the email had
// already been moved and left it in the inbox.
let folder_name = folder.name();
if folder_name.starts_with("[Gmail]") {
continue;
}
let foldername = folder.name();
let folder_meaning = get_folder_meaning(&folder);
let folder_name_meaning = get_folder_meaning_by_name(folder.name());
let folder_name_meaning = get_folder_meaning_by_name(foldername);
if let Some(config) = folder_meaning.to_config() {
// Always takes precedence
folder_configs.insert(config, folder.name().to_string());
} else if let Some(config) = folder_name_meaning.to_config() {
// only set if none has been already set
folder_configs
.entry(config)
.or_insert_with(|| folder.name().to_string());
if folder_meaning == FolderMeaning::SentObjects {
// Always takes precedent
sentbox_folder = Some(folder.name().to_string());
} else if folder_meaning == FolderMeaning::Spam {
spam_folder = Some(folder.name().to_string());
} else if folder_name_meaning == FolderMeaning::SentObjects {
// only set iff none has been already set
if sentbox_folder.is_none() {
sentbox_folder = Some(folder.name().to_string());
}
} else if folder_name_meaning == FolderMeaning::Spam && spam_folder.is_none() {
spam_folder = Some(folder.name().to_string());
}
let is_drafts = folder_meaning == FolderMeaning::Drafts
|| (folder_meaning == FolderMeaning::Unknown
&& folder_name_meaning == FolderMeaning::Drafts);
// Don't scan folders that are watched anyway
if !watched_folders.contains(&folder.name().to_string()) && !is_drafts {
// Drain leftover unsolicited EXISTS messages
self.server_sent_unsolicited_exists(context);
loop {
self.fetch_new_messages(context, folder.name(), false)
.await
.ok_or_log_msg(context, "Can't fetch new msgs in scanned folder");
// If the server sent an unsocicited EXISTS during the fetch, we need to fetch again
if !self.server_sent_unsolicited_exists(context) {
break;
}
if !watched_folders.contains(&foldername.to_string()) {
if let Err(e) = self.fetch_new_messages(context, foldername, false).await {
warn!(context, "Can't fetch new msgs in scanned folder: {:#}", e);
}
}
}
// We iterate over both folder meanings to make sure that if e.g. the "Sent" folder was deleted,
// `ConfiguredSentboxFolder` is set to `None`:
for config in &[
Config::ConfiguredSentboxFolder,
Config::ConfiguredSpamFolder,
] {
context
.set_config(*config, folder_configs.get(config).map(|s| s.as_str()))
.await?;
}
context
.set_config(Config::ConfiguredSentboxFolder, sentbox_folder.as_deref())
.await?;
context
.set_config(Config::ConfiguredSpamFolder, spam_folder.as_deref())
.await?;
last_scan.replace(Instant::now());
Ok(())
}
}
pub(crate) async fn get_watched_folders(context: &Context) -> Vec<String> {
async fn get_watched_folders(context: &Context) -> Vec<String> {
let mut res = Vec::new();
let folder_watched_configured = &[
(Config::SentboxWatch, Config::ConfiguredSentboxFolder),

View File

@@ -26,7 +26,7 @@ impl Imap {
/// Issues a CLOSE command to expunge selected folder.
///
/// CLOSE is considerably faster than an EXPUNGE, see
/// <https://tools.ietf.org/html/rfc3501#section-6.4.2>
/// https://tools.ietf.org/html/rfc3501#section-6.4.2
pub(super) async fn close_folder(&mut self, context: &Context) -> Result<()> {
if let Some(ref folder) = self.config.selected_folder {
info!(context, "Expunge messages in \"{}\".", folder);
@@ -37,7 +37,7 @@ impl Imap {
info!(context, "close/expunge succeeded");
}
Err(err) => {
self.trigger_reconnect(context).await;
self.trigger_reconnect();
return Err(Error::CloseExpungeFailed(err));
}
}
@@ -70,7 +70,7 @@ impl Imap {
if self.session.is_none() {
self.config.selected_folder = None;
self.config.selected_folder_needs_expunge = false;
self.trigger_reconnect(context).await;
self.trigger_reconnect();
return Err(Error::NoSession);
}
@@ -92,7 +92,7 @@ impl Imap {
if let Some(ref mut session) = &mut self.session {
let res = session.select(folder).await;
// <https://tools.ietf.org/html/rfc3501#section-6.3.1>
// https://tools.ietf.org/html/rfc3501#section-6.3.1
// says that if the server reports select failure we are in
// authenticated (not-select) state.
@@ -103,7 +103,7 @@ impl Imap {
Ok(NewlySelected::Yes)
}
Err(async_imap::error::Error::ConnectionLost) => {
self.trigger_reconnect(context).await;
self.trigger_reconnect();
self.config.selected_folder = None;
Err(Error::ConnectionLost)
}
@@ -112,7 +112,7 @@ impl Imap {
}
Err(err) => {
self.config.selected_folder = None;
self.trigger_reconnect(context).await;
self.trigger_reconnect();
Err(Error::Other(err.to_string()))
}
}

View File

@@ -3,7 +3,6 @@ use std::ops::{Deref, DerefMut};
use async_imap::Session as ImapSession;
use async_native_tls::TlsStream;
use async_std::net::TcpStream;
use fast_socks5::client::Socks5Stream;
#[derive(Debug)]
pub(crate) struct Session {
@@ -18,7 +17,6 @@ pub(crate) trait SessionStream:
impl SessionStream for TlsStream<Box<dyn SessionStream>> {}
impl SessionStream for TlsStream<TcpStream> {}
impl SessionStream for TcpStream {}
impl SessionStream for Socks5Stream<TcpStream> {}
impl Deref for Session {
type Target = ImapSession<Box<dyn SessionStream>>;

View File

@@ -1,4 +1,4 @@
//! # Import/export module.
//! # Import/export module
use std::any::Any;
use std::ffi::OsStr;
@@ -66,15 +66,15 @@ pub enum ImexMode {
/// Import/export things.
///
/// What to do is defined by the `what` parameter.
/// What to do is defined by the *what* parameter.
///
/// During execution of the job,
/// some events are sent out:
///
/// - A number of `DC_EVENT_IMEX_PROGRESS` events are sent and may be used to create
/// - A number of #DC_EVENT_IMEX_PROGRESS events are sent and may be used to create
/// a progress bar or stuff like that. Moreover, you'll be informed when the imex-job is done.
///
/// - For each file written on export, the function sends `DC_EVENT_IMEX_FILE_WRITTEN`
/// - For each file written on export, the function sends #DC_EVENT_IMEX_FILE_WRITTEN
///
/// Only one import-/export-progress can run at the same time.
/// To cancel an import-/export-progress, drop the future returned by this function.
@@ -184,7 +184,7 @@ pub async fn has_backup_old(context: &Context, dir_name: &Path) -> Result<String
"Found backup file {} which could not be opened: {}", name, e
);
// On some Android devices we can't open sql files that are not in our private directory
// (see <https://github.com/deltachat/deltachat-android/issues/1768>). So, compare names
// (see https://github.com/deltachat/deltachat-android/issues/1768). So, compare names
// to still find the newest backup.
let name: String = name.into();
if newest_backup_time == 0
@@ -204,7 +204,6 @@ pub async fn has_backup_old(context: &Context, dir_name: &Path) -> Result<String
}
}
/// Initiates key transfer via Autocrypt Setup Message.
pub async fn initiate_key_transfer(context: &Context) -> Result<String> {
use futures::future::FutureExt;
@@ -436,7 +435,7 @@ async fn decrypt_setup_file<T: std::io::Read + std::io::Seek>(
Ok(plain_text)
}
fn normalize_setup_code(s: &str) -> String {
pub fn normalize_setup_code(s: &str) -> String {
let mut out = String::new();
for c in s.chars() {
if ('0'..='9').contains(&c) {

View File

@@ -1,4 +1,4 @@
//! # Job module.
//! # Job module
//!
//! This module implements a job queue maintained in the SQLite database
//! and job types.
@@ -13,8 +13,9 @@ use itertools::Itertools;
use rand::{thread_rng, Rng};
use crate::blob::BlobObject;
use crate::chat::{self, ChatId};
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ChatItem};
use crate::config::Config;
use crate::constants::{Blocked, Chattype, DC_CHAT_ID_DEADDROP};
use crate::contact::{normalize_name, Contact, Modifier, Origin};
use crate::context::Context;
use crate::dc_tools::{dc_delete_file, dc_read_file, time};
@@ -95,9 +96,6 @@ pub enum Action {
FetchExistingMsgs = 110,
MarkseenMsgOnImap = 130,
// this is user initiated so it should have a fairly high priority
UpdateRecentQuota = 140,
// Moving message is prioritized lower than deletion so we don't
// bother moving message if it is already scheduled for deletion.
MoveMsg = 200,
@@ -133,7 +131,6 @@ impl From<Action> for Thread {
ResyncFolders => Thread::Imap,
MarkseenMsgOnImap => Thread::Imap,
MoveMsg => Thread::Imap,
UpdateRecentQuota => Thread::Imap,
MaybeSendLocations => Thread::Smtp,
MaybeSendLocationsEnded => Thread::Smtp,
@@ -152,6 +149,7 @@ pub struct Job {
pub added_timestamp: i64,
pub tries: u32,
pub param: Params,
pub pending_error: Option<String>,
}
impl fmt::Display for Job {
@@ -172,6 +170,7 @@ impl Job {
added_timestamp: timestamp,
tries: 0,
param,
pending_error: None,
}
}
@@ -250,16 +249,11 @@ impl Job {
info!(context, "smtp-sending out mime message:");
println!("{}", String::from_utf8_lossy(&message));
}
smtp.connectivity.set_working(context).await;
let send_result = smtp.send(context, recipients, message, job_id).await;
smtp.last_send_error = send_result.as_ref().err().map(|e| e.to_string());
let status = match send_result {
Err(crate::smtp::send::Error::SmtpSend(err)) => {
let status = match smtp.send(context, recipients, message, job_id).await {
Err(crate::smtp::send::Error::SendError(err)) => {
// Remote error, retry later.
warn!(context, "SMTP failed to send: {:?}", &err);
warn!(context, "SMTP failed to send: {:?}", err);
self.pending_error = Some(err.to_string());
let res = match err {
async_smtp::smtp::error::Error::Permanent(ref response) => {
@@ -267,13 +261,13 @@ impl Job {
// instead of temporary ones.
let maybe_transient = match response.code {
// Sometimes servers send a permanent error when actually it is a temporary error
// For documentation see <https://tools.ietf.org/html/rfc3463>
// For documentation see https://tools.ietf.org/html/rfc3463
Code {
category: Category::MailSystem,
detail: Detail::Zero,
..
} => {
// Ignore status code 5.5.0, see <https://support.delta.chat/t/every-other-message-gets-stuck/877/2>
// Ignore status code 5.5.0, see https://support.delta.chat/t/every-other-message-gets-stuck/877/2
// Maybe incorrectly configured Postfix milter with "reject" instead of "tempfail", which returns
// "550 5.5.0 Service unavailable" instead of "451 4.7.1 Service unavailable - try again later".
//
@@ -307,7 +301,7 @@ impl Job {
// Sometimes we receive transient errors that should be permanent.
// Any extended smtp status codes like x.1.1, x.1.2 or x.1.3 that we
// receive as a transient error are misconfigurations of the smtp server.
// See <https://tools.ietf.org/html/rfc3463#section-3.2>
// See https://tools.ietf.org/html/rfc3463#section-3.2
info!(context, "Smtp-job #{} Received extended status code {} for a transient error. This looks like a misconfigured smtp server, let's fail immediatly", self.job_id, first_word);
Status::Finished(Err(format_err!("Permanent SMTP error: {}", err)))
} else {
@@ -332,7 +326,7 @@ impl Job {
res
}
Err(crate::smtp::send::Error::Envelope(err)) => {
Err(crate::smtp::send::Error::EnvelopeError(err)) => {
// Local error, job is invalid, do not retry.
smtp.disconnect().await;
warn!(context, "SMTP job is invalid: {}", err);
@@ -368,7 +362,6 @@ impl Job {
// SMTP server, if not yet done
if let Err(err) = smtp.connect_configured(context).await {
warn!(context, "SMTP connection failure: {:?}", err);
smtp.last_send_error = Some(format!("SMTP connection failure: {:#}", err));
return Status::RetryLater;
}
@@ -411,8 +404,6 @@ impl Job {
}
Err(err) => {
warn!(context, "failed to check message existence: {:?}", err);
smtp.last_send_error =
Some(format!("failed to check message existence: {:#}", err));
return Status::RetryLater;
}
}
@@ -527,7 +518,6 @@ impl Job {
// connect to SMTP server, if not yet done
if let Err(err) = smtp.connect_configured(context).await {
warn!(context, "SMTP connection failure: {:?}", err);
smtp.last_send_error = Some(err.to_string());
return Status::RetryLater;
}
@@ -542,7 +532,7 @@ impl Job {
}
async fn move_msg(&mut self, context: &Context, imap: &mut Imap) -> Status {
if let Err(err) = imap.prepare(context).await {
if let Err(err) = imap.connect_configured(context).await {
warn!(context, "could not connect: {:?}", err);
return Status::RetryLater;
}
@@ -597,15 +587,14 @@ impl Job {
/// Deletes a message on the server.
///
/// `foreign_id` is a MsgId.
/// foreign_id is a MsgId pointing to a message in the trash chat
/// or a hidden message.
///
/// If the message is in the trash chat or hidden, this job
/// removes database record, otherwise it only clears the
/// `server_uid` column. If there are no more records pointing to
/// the same message on the server, the job actually removes the
/// message on the server.
/// This job removes the database record. If there are no more
/// records pointing to the same message on the server, the job
/// also removes the message on the server.
async fn delete_msg_on_imap(&mut self, context: &Context, imap: &mut Imap) -> Status {
if let Err(err) = imap.prepare(context).await {
if let Err(err) = imap.connect_configured(context).await {
warn!(context, "could not connect: {:?}", err);
return Status::RetryLater;
}
@@ -693,7 +682,7 @@ impl Job {
if job_try!(context.get_config_bool(Config::Bot).await) {
return Status::Finished(Ok(())); // Bots don't want those messages
}
if let Err(err) = imap.prepare(context).await {
if let Err(err) = imap.connect_configured(context).await {
warn!(context, "could not connect: {:?}", err);
return Status::RetryLater;
}
@@ -718,6 +707,40 @@ impl Job {
}
}
// Make sure that if there now is a chat with a contact (created by an outgoing
// message), then group contact requests from this contact should also be unblocked.
// See https://github.com/deltachat/deltachat-core-rust/issues/2097.
for item in job_try!(chat::get_chat_msgs(context, DC_CHAT_ID_DEADDROP, 0, None).await) {
if let ChatItem::Message { msg_id } = item {
let msg = match Message::load_from_db(context, msg_id).await {
Err(e) => {
warn!(context, "can't get msg: {:#}", e);
return Status::RetryLater;
}
Ok(m) => m,
};
let chat = match Chat::load_from_db(context, msg.chat_id).await {
Err(e) => {
warn!(context, "can't get chat: {:#}", e);
return Status::RetryLater;
}
Ok(c) => c,
};
match chat.typ {
Chattype::Group | Chattype::Mailinglist => {
if let Ok(Some(one_to_one_chat)) =
ChatIdBlocked::lookup_by_contact(context, msg.from_id).await
{
if one_to_one_chat.blocked == Blocked::Not {
chat.id.unblock(context).await;
}
}
}
Chattype::Single | Chattype::Undefined => {}
}
}
}
info!(context, "Done fetching existing messages.");
Status::Finished(Ok(()))
}
@@ -732,7 +755,7 @@ impl Job {
/// Chat in contrast to the Sent folder, which is normally managed
/// by the user via webmail or another email client.
async fn resync_folders(&mut self, context: &Context, imap: &mut Imap) -> Status {
if let Err(err) = imap.prepare(context).await {
if let Err(err) = imap.connect_configured(context).await {
warn!(context, "could not connect: {:?}", err);
return Status::RetryLater;
}
@@ -756,7 +779,7 @@ impl Job {
}
async fn markseen_msg_on_imap(&mut self, context: &Context, imap: &mut Imap) -> Status {
if let Err(err) = imap.prepare(context).await {
if let Err(err) = imap.connect_configured(context).await {
warn!(context, "could not connect: {:?}", err);
return Status::RetryLater;
}
@@ -1029,9 +1052,16 @@ pub(crate) enum Connection<'a> {
}
pub(crate) async fn load_imap_deletion_job(context: &Context) -> Result<Option<Job>> {
let res = load_imap_deletion_msgid(context)
.await?
.map(|msg_id| Job::new(Action::DeleteMsgOnImap, msg_id.to_u32(), Params::new(), 0));
let res = if let Some(msg_id) = load_imap_deletion_msgid(context).await? {
Some(Job::new(
Action::DeleteMsgOnImap,
msg_id.to_u32(),
Params::new(),
0,
))
} else {
None
};
Ok(res)
}
@@ -1152,7 +1182,6 @@ async fn perform_job_action(
sql::housekeeping(context).await.ok_or_log(context);
Status::Finished(Ok(()))
}
Action::UpdateRecentQuota => context.update_recent_quota(connection.inbox()).await,
};
info!(context, "Finished immediate try {} of job {}", tries, job);
@@ -1215,8 +1244,7 @@ pub async fn add(context: &Context, job: Job) {
| Action::ResyncFolders
| Action::MarkseenMsgOnImap
| Action::FetchExistingMsgs
| Action::MoveMsg
| Action::UpdateRecentQuota => {
| Action::MoveMsg => {
info!(context, "interrupt: imap");
context
.interrupt_inbox(InterruptInfo::new(false, None))
@@ -1330,6 +1358,7 @@ LIMIT 1;
added_timestamp: row.get("added_timestamp")?,
tries: row.get("tries")?,
param: row.get::<_, String>("param")?.parse().unwrap_or_default(),
pending_error: None,
};
Ok(job)

View File

@@ -1,25 +1,49 @@
//! Cryptographic key module.
//! Cryptographic key module
use std::collections::BTreeMap;
use std::fmt;
use std::io::Cursor;
use anyhow::{format_err, Result};
use async_trait::async_trait;
use num_traits::FromPrimitive;
use pgp::composed::Deserializable;
use pgp::ser::Serialize;
use pgp::types::{KeyTrait, SecretKeyTrait};
use thiserror::Error;
use crate::config::Config;
use crate::constants::KeyGenType;
use crate::context::Context;
use crate::dc_tools::{time, EmailAddress};
use crate::dc_tools::{time, EmailAddress, InvalidEmailError};
// Re-export key types
pub use crate::pgp::KeyPair;
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
/// Error type for deltachat key handling.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("Could not decode base64")]
Base64Decode(#[from] base64::DecodeError),
#[error("rPGP error: {}", _0)]
Pgp(#[from] pgp::errors::Error),
#[error("Failed to generate PGP key: {}", _0)]
Keygen(#[from] crate::pgp::PgpKeygenError),
#[error("Failed to save generated key: {}", _0)]
StoreKey(#[from] SaveKeyError),
#[error("No address configured")]
NoConfiguredAddr,
#[error("Configured address is invalid: {}", _0)]
InvalidConfiguredAddr(#[from] InvalidEmailError),
#[error("no data provided")]
Empty,
#[error("{0}")]
Other(#[from] anyhow::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
/// Convenience trait for working with keys.
///
/// This trait is implemented for rPGP's [SignedPublicKey] and
@@ -50,8 +74,7 @@ pub trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
/// the ASCII-armored representation.
fn from_asc(data: &str) -> Result<(Self::KeyType, BTreeMap<String, String>)> {
let bytes = data.as_bytes();
Self::KeyType::from_armor_single(Cursor::new(bytes))
.map_err(|err| format_err!("rPGP error: {}", err))
Self::KeyType::from_armor_single(Cursor::new(bytes)).map_err(Error::Pgp)
}
/// Load the users' default key from the database.
@@ -202,7 +225,7 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
let addr = context
.get_config(Config::ConfiguredAddr)
.await?
.ok_or_else(|| format_err!("No address configured"))?;
.ok_or(Error::NoConfiguredAddr)?;
let addr = EmailAddress::new(&addr)?;
let _guard = context.generating_key_mutex.lock().await;
@@ -261,6 +284,24 @@ pub enum KeyPairUse {
ReadOnly,
}
/// Error saving a keypair to the database.
#[derive(Debug, thiserror::Error)]
#[error("SaveKeyError: {message}")]
pub struct SaveKeyError {
message: String,
#[source]
cause: anyhow::Error,
}
impl SaveKeyError {
fn new(message: impl Into<String>, cause: impl Into<anyhow::Error>) -> Self {
Self {
message: message.into(),
cause: cause.into(),
}
}
}
/// Store the keypair as an owned keypair for addr in the database.
///
/// This will save the keypair as keys for the given address. The
@@ -277,7 +318,7 @@ pub async fn store_self_keypair(
context: &Context,
keypair: &KeyPair,
default: KeyPairUse,
) -> Result<()> {
) -> std::result::Result<(), SaveKeyError> {
// Everything should really be one transaction, more refactoring
// is needed for that.
let public_key = DcKey::to_bytes(&keypair.public);
@@ -289,13 +330,13 @@ pub async fn store_self_keypair(
paramsv![public_key, secret_key],
)
.await
.map_err(|err| err.context("failed to remove old use of key"))?;
.map_err(|err| SaveKeyError::new("failed to remove old use of key", err))?;
if default == KeyPairUse::Default {
context
.sql
.execute("UPDATE keypairs SET is_default=0;", paramsv![])
.await
.map_err(|err| err.context("failed to clear default"))?;
.map_err(|err| SaveKeyError::new("failed to clear default", err))?;
}
let is_default = match default {
KeyPairUse::Default => true as i32,
@@ -313,7 +354,7 @@ pub async fn store_self_keypair(
paramsv![addr, is_default, public_key, secret_key, t],
)
.await
.map_err(|err| err.context("failed to insert keypair"))?;
.map_err(|err| SaveKeyError::new("failed to insert keypair", err))?;
Ok(())
}
@@ -323,10 +364,10 @@ pub async fn store_self_keypair(
pub struct Fingerprint(Vec<u8>);
impl Fingerprint {
pub fn new(v: Vec<u8>) -> Result<Fingerprint> {
pub fn new(v: Vec<u8>) -> std::result::Result<Fingerprint, FingerprintError> {
match v.len() {
20 => Ok(Fingerprint(v)),
_ => Err(format_err!("Wrong fingerprint length")),
_ => Err(FingerprintError::WrongLength),
}
}
@@ -365,7 +406,7 @@ impl fmt::Display for Fingerprint {
/// Parse a human-readable or otherwise formatted fingerprint.
impl std::str::FromStr for Fingerprint {
type Err = anyhow::Error;
type Err = FingerprintError;
fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
let hex_repr: String = input
@@ -379,11 +420,21 @@ impl std::str::FromStr for Fingerprint {
}
}
#[derive(Debug, Error)]
pub enum FingerprintError {
#[error("Invalid hex characters")]
NotHex(#[from] hex::FromHexError),
#[error("Incorrect fingerprint lengths")]
WrongLength,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::{alice_keypair, TestContext};
use std::error::Error;
use async_std::sync::Arc;
use once_cell::sync::Lazy;
@@ -625,7 +676,13 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
.unwrap();
assert_eq!(fp, res);
assert!("1".parse::<Fingerprint>().is_err());
let err = "1".parse::<Fingerprint>().err().unwrap();
match err {
FingerprintError::NotHex(_) => (),
_ => panic!("Wrong error"),
}
let src_err = err.source().unwrap().downcast_ref::<hex::FromHexError>();
assert_eq!(src_err, Some(&hex::FromHexError::OddLength));
}
#[test]

View File

@@ -3,7 +3,7 @@
use anyhow::Result;
use crate::context::Context;
use crate::key::DcKey;
use crate::key::{self, DcKey};
/// An in-memory keyring.
///
@@ -27,14 +27,14 @@ where
}
/// Create a new keyring with the the user's secret key loaded.
pub async fn new_self(context: &Context) -> Result<Keyring<T>> {
pub async fn new_self(context: &Context) -> Result<Keyring<T>, key::Error> {
let mut keyring: Keyring<T> = Keyring::new();
keyring.load_self(context).await?;
Ok(keyring)
}
/// Load the user's key into the keyring.
pub async fn load_self(&mut self, context: &Context) -> Result<()> {
pub async fn load_self(&mut self, context: &Context) -> Result<(), key::Error> {
self.add(T::load_self(context).await?);
Ok(())
}

View File

@@ -1,5 +1,3 @@
//! # Delta Chat Core Library.
#![forbid(unsafe_code)]
#![deny(
clippy::correctness,
@@ -9,11 +7,7 @@
clippy::wildcard_imports,
clippy::needless_borrow
)]
#![allow(
clippy::match_bool,
clippy::eval_order_dependence,
clippy::bool_assert_comparison
)]
#![allow(clippy::match_bool, clippy::eval_order_dependence)]
#[macro_use]
extern crate num_derive;
@@ -57,11 +51,12 @@ pub mod contact;
pub mod context;
mod e2ee;
pub mod ephemeral;
pub mod export_chat;
mod imap;
pub mod imex;
mod scheduler;
#[macro_use]
mod job;
pub mod job;
mod format_flowed;
pub mod key;
mod keyring;
@@ -77,7 +72,6 @@ pub mod peerstate;
pub mod pgp;
pub mod provider;
pub mod qr;
pub mod quota;
pub mod securejoin;
mod simplify;
mod smtp;

View File

@@ -1,4 +1,4 @@
//! Location handling.
//! Location handling
use std::convert::TryFrom;
use anyhow::{ensure, Error};
@@ -16,9 +16,8 @@ use crate::message::{Message, MsgId};
use crate::mimeparser::SystemMessage;
use crate::param::Params;
use crate::stock_str;
/// Location record
#[derive(Debug, Clone, Default)]
use serde::Serialize;
#[derive(Debug, Clone, Default, Serialize)]
pub struct Location {
pub location_id: u32,
pub latitude: f64,
@@ -190,7 +189,7 @@ impl Kml {
}
}
/// Enables location streaming in chat identified by `chat_id` for `seconds` seconds.
// location streaming
pub async fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds: i64) {
let now = time();
if !(seconds < 0 || chat_id.is_special()) {
@@ -221,7 +220,7 @@ pub async fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds:
.unwrap_or_default();
} else if 0 == seconds && is_sending_locations_before {
let stock_str = stock_str::msg_location_disabled(context).await;
chat::add_info_msg(context, chat_id, stock_str, now).await;
chat::add_info_msg(context, chat_id, stock_str).await;
}
context.emit_event(EventType::ChatModified(chat_id));
if 0 != seconds {
@@ -716,8 +715,7 @@ pub(crate) async fn job_maybe_send_locations_ended(
.await
);
let now = time();
if !(send_begin != 0 && now <= send_until) {
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
@@ -736,7 +734,7 @@ pub(crate) async fn job_maybe_send_locations_ended(
);
let stock_str = stock_str::msg_location_disabled(context).await;
chat::add_info_msg(context, chat_id, stock_str, now).await;
chat::add_info_msg(context, chat_id, stock_str).await;
context.emit_event(EventType::ChatModified(chat_id));
}
}

View File

@@ -1,5 +1,4 @@
//! # Logging.
//! # Logging
use crate::context::Context;
#[macro_export]
@@ -43,6 +42,17 @@ macro_rules! error {
}};
}
#[macro_export]
macro_rules! error_network {
($ctx:expr, $msg:expr) => {
error_network!($ctx, $msg,)
};
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
let formatted = format!($msg, $($args),*);
emit_event!($ctx, $crate::EventType::ErrorNetwork(formatted));
}};
}
#[macro_export]
macro_rules! emit_event {
($ctx:expr, $event:expr) => {
@@ -66,7 +76,7 @@ where
/// Once it is, you can add `#[track_caller]` to helper functions that use one of the log helpers here
/// so that the location of the caller can be seen in the log. (this won't work with the macros,
/// like warn!(), since the file!() and line!() macros don't work with track_caller)
/// See <https://github.com/rust-lang/rust/issues/78840> for progress on this.
/// See https://github.com/rust-lang/rust/issues/78840 for progress on this.
#[track_caller]
fn log_err(self, context: &Context, msg: &str) -> Result<T, E> {
self.log_err_inner(context, Some(msg))

View File

@@ -1,19 +1,12 @@
//! # Login parameters.
//! # Login parameters
use std::borrow::Cow;
use std::fmt;
use std::time::Duration;
use crate::provider::{get_provider_by_id, Provider};
use crate::{context::Context, provider::Socket};
use anyhow::Result;
use async_std::io;
use async_std::net::TcpStream;
pub use async_smtp::ServerAddress;
use fast_socks5::client::Socks5Stream;
#[derive(Copy, Clone, Debug, Display, FromPrimitive, PartialEq, Eq)]
#[repr(u32)]
#[strum(serialize_all = "snake_case")]
@@ -51,81 +44,6 @@ pub struct ServerLoginParam {
pub certificate_checks: CertificateChecks,
}
#[derive(Default, Debug, Clone, PartialEq)]
pub struct Socks5Config {
pub host: String,
pub port: u16,
pub user_password: Option<(String, String)>,
}
impl Socks5Config {
/// Reads SOCKS5 configuration from the database.
pub async fn from_database(context: &Context) -> Result<Option<Self>> {
let sql = &context.sql;
let enabled = sql.get_raw_config_bool("socks5_enabled").await?;
if enabled {
let host = sql.get_raw_config("socks5_host").await?.unwrap_or_default();
let port: u16 = sql
.get_raw_config_int("socks5_port")
.await?
.unwrap_or_default() as u16;
let user = sql.get_raw_config("socks5_user").await?.unwrap_or_default();
let password = sql
.get_raw_config("socks5_password")
.await?
.unwrap_or_default();
let socks5_config = Self {
host,
port,
user_password: if !user.is_empty() {
Some((user, password))
} else {
None
},
};
Ok(Some(socks5_config))
} else {
Ok(None)
}
}
pub async fn connect(
&self,
target_addr: &ServerAddress,
timeout: Option<Duration>,
) -> io::Result<Socks5Stream<TcpStream>> {
self.to_async_smtp_socks5_config()
.connect(target_addr, timeout)
.await
}
pub fn to_async_smtp_socks5_config(&self) -> async_smtp::smtp::Socks5Config {
async_smtp::smtp::Socks5Config {
host: self.host.clone(),
port: self.port,
user_password: self.user_password.clone(),
}
}
}
impl fmt::Display for Socks5Config {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"host:{},port:{},user_password:{}",
self.host,
self.port,
if let Some(user_password) = self.user_password.clone() {
format!("user: {}, password: ***", user_password.0)
} else {
"user: None".to_string()
}
)
}
}
#[derive(Default, Debug, Clone, PartialEq)]
pub struct LoginParam {
pub addr: String,
@@ -133,7 +51,6 @@ pub struct LoginParam {
pub smtp: ServerLoginParam,
pub server_flags: i32,
pub provider: Option<&'static Provider>,
pub socks5_config: Option<Socks5Config>,
}
impl LoginParam {
@@ -213,8 +130,6 @@ impl LoginParam {
.await?
.and_then(|provider_id| get_provider_by_id(&provider_id));
let socks5_config = Socks5Config::from_database(context).await?;
Ok(LoginParam {
addr,
imap: ServerLoginParam {
@@ -235,7 +150,6 @@ impl LoginParam {
},
provider,
server_flags,
socks5_config,
})
}
@@ -420,8 +334,6 @@ mod tests {
},
server_flags: 0,
provider: get_provider_by_id("example.com"),
// socks5_config is not saved by `save_to_database`, using default value
socks5_config: None,
};
param.save_to_database(&t, "foobar_").await?;

View File

@@ -1,5 +1,3 @@
//! # Legacy generic return values for C API.
use deltachat_derive::{FromSql, ToSql};
use crate::key::Fingerprint;
@@ -112,16 +110,6 @@ pub enum LotState {
/// text1=error string
QrError = 400,
QrWithdrawVerifyContact = 500,
/// text1=groupname
QrWithdrawVerifyGroup = 502,
QrReviveVerifyContact = 510,
/// text1=groupname
QrReviveVerifyGroup = 512,
// Message States
MsgInFresh = 10,
MsgInNoticed = 13,

View File

@@ -1,4 +1,4 @@
//! # Messages and their identifiers.
//! # Messages and their identifiers
use std::collections::BTreeMap;
use std::convert::TryInto;
@@ -7,27 +7,27 @@ use anyhow::{ensure, format_err, Result};
use async_std::path::{Path, PathBuf};
use deltachat_derive::{FromSql, ToSql};
use itertools::Itertools;
use rusqlite::types::ValueRef;
use serde::{Deserialize, Serialize};
use crate::chat::{self, Chat, ChatId};
use crate::config::Config;
use crate::constants::{
Blocked, Chattype, VideochatType, Viewtype, DC_CHAT_ID_TRASH, DC_CONTACT_ID_INFO,
DC_CONTACT_ID_SELF, DC_DESIRED_TEXT_LEN, DC_MSG_ID_LAST_SPECIAL,
Blocked, Chattype, VideochatType, Viewtype, DC_CHAT_ID_DEADDROP, DC_CHAT_ID_TRASH,
DC_CONTACT_ID_INFO, DC_CONTACT_ID_LAST_SPECIAL, DC_CONTACT_ID_SELF, DC_MAX_GET_INFO_LEN,
DC_MAX_GET_TEXT_LEN, DC_MSG_ID_LAST_SPECIAL,
};
use crate::contact::{Contact, Origin};
use crate::context::Context;
use crate::dc_tools::{
dc_create_smeared_timestamp, dc_get_filebytes, dc_get_filemeta, dc_gm2local_offset,
dc_read_file, dc_timestamp_to_str, dc_truncate, time,
dc_get_filebytes, dc_get_filemeta, dc_gm2local_offset, dc_read_file, dc_timestamp_to_str,
dc_truncate, time,
};
use crate::ephemeral::Timer as EphemeralTimer;
use crate::events::EventType;
use crate::job::{self, Action};
use crate::log::LogExt;
use crate::lot::{Lot, LotState, Meaning};
use crate::mimeparser::{parse_message_id, FailureReport, SystemMessage};
use crate::mimeparser::{FailureReport, SystemMessage};
use crate::param::{Param, Params};
use crate::pgp::split_armored_data;
use crate::stock_str;
@@ -108,7 +108,7 @@ impl MsgId {
Ok(Some(ConfiguredInboxFolder))
}
} else {
// Blocked or contact request message in the spam folder, leave it there
// Blocked/deaddrop message in the spam folder, leave it there
Ok(None)
};
}
@@ -235,9 +235,9 @@ impl std::fmt::Display for MsgId {
impl rusqlite::types::ToSql for MsgId {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
if self.0 <= DC_MSG_ID_LAST_SPECIAL {
return Err(rusqlite::Error::ToSqlConversionFailure(
format_err!("Invalid MsgId").into(),
));
return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
InvalidMsgId,
)));
}
let val = rusqlite::types::Value::Integer(self.0 as i64);
let out = rusqlite::types::ToSqlOutput::Owned(val);
@@ -259,6 +259,15 @@ impl rusqlite::types::FromSql for MsgId {
}
}
/// Message ID was invalid.
///
/// This usually occurs when trying to use a message ID of
/// [DC_MSG_ID_LAST_SPECIAL] or below in a situation where this is not
/// possible.
#[derive(Debug, thiserror::Error)]
#[error("Invalid Message ID.")]
pub struct InvalidMsgId;
#[derive(
Debug,
Copy,
@@ -391,9 +400,7 @@ impl Message {
let msg = Message {
id: row.get("id")?,
rfc724_mid: row.get::<_, String>("rfc724mid")?,
in_reply_to: row
.get::<_, Option<String>>("mime_in_reply_to")?
.and_then(|in_reply_to| parse_message_id(&in_reply_to).ok()),
in_reply_to: row.get::<_, Option<String>>("mime_in_reply_to")?,
server_folder: row.get::<_, Option<String>>("server_folder")?,
server_uid: row.get("server_uid")?,
chat_id: row.get("chat_id")?,
@@ -519,8 +526,19 @@ impl Message {
self.from_id
}
/// Returns the chat ID.
/// get the chat-id,
/// if the message is a contact request, the DC_CHAT_ID_DEADDROP is returned.
pub fn get_chat_id(&self) -> ChatId {
if self.chat_blocked != Blocked::Not {
DC_CHAT_ID_DEADDROP
} else {
self.chat_id
}
}
/// get the chat-id, also when the message is still a contact request.
/// DC_CHAT_ID_DEADDROP is never returned.
pub fn get_real_chat_id(&self) -> ChatId {
self.chat_id
}
@@ -541,7 +559,9 @@ impl Message {
}
pub fn get_text(&self) -> Option<String> {
self.text.as_ref().map(|s| s.to_string())
self.text
.as_ref()
.map(|text| dc_truncate(text, DC_MAX_GET_TEXT_LEN).to_string())
}
pub fn get_subject(&self) -> &str {
@@ -579,11 +599,6 @@ impl Message {
self.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() != 0
}
/// Returns true if message is Auto-Submitted.
pub fn is_bot(&self) -> bool {
self.param.get_bool(Param::Bot).unwrap_or_default()
}
pub fn get_ephemeral_timer(&self) -> EphemeralTimer {
self.ephemeral_timer
}
@@ -740,7 +755,7 @@ impl Message {
} else if url.contains("$NOROOM") {
// there are some usecases where a separate room is not needed to use a service
// eg. if you let in people manually anyway, see discussion at
// <https://support.delta.chat/t/videochat-with-webex/1412/4>.
// https://support.delta.chat/t/videochat-with-webex/1412/4 .
// hacks as hiding the room behind `#` are not reliable, therefore,
// these services are supported by adding the string `$NOROOM` to the url.
url.replace("$NOROOM", "")
@@ -945,6 +960,13 @@ impl Message {
}
}
#[derive(Display, Debug, FromPrimitive)]
pub enum ContactRequestDecision {
StartChat = 0,
Block = 1,
NotNow = 2,
}
#[derive(
Debug,
Clone,
@@ -1126,6 +1148,76 @@ impl Lot {
}
}
/// Call this when the user decided about a deaddrop message ("Do you want to chat with NAME?").
///
/// If the decision is `StartChat`, this will create a new chat and return the chat id.
/// If the decision is `Block`, this will usually block the sender.
/// If the decision is `NotNow`, this will usually mark all messages from this sender as read.
///
/// If the message belongs to a mailing list, makes sure that all messages from this mailing list are
/// blocked or marked as noticed.
///
/// The user should be asked whether they want to chat with the _contact_ belonging to the message;
/// the group names may be really weird when taken from the subject of implicit (= ad-hoc)
/// groups and this may look confusing. Moreover, this function also scales up the origin of the contact.
///
/// If the chat belongs to a mailing list, you can also ask
/// "Would you like to read MAILING LIST NAME in Delta Chat?"
/// (use `Message.get_real_chat_id()` to get the chat-id for the contact request
/// and then `Chat.is_mailing_list()`, `Chat.get_name()` and so on)
pub async fn decide_on_contact_request(
context: &Context,
msg_id: MsgId,
decision: ContactRequestDecision,
) -> Option<ChatId> {
let msg = match Message::load_from_db(context, msg_id).await {
Ok(m) => m,
Err(e) => {
warn!(context, "Can't load message: {}", e);
return None;
}
};
let chat = match Chat::load_from_db(context, msg.chat_id).await {
Ok(c) => c,
Err(e) => {
warn!(context, "Can't load chat: {}", e);
return None;
}
};
let mut created_chat_id = None;
use ContactRequestDecision::*;
match (decision, chat.is_mailing_list()) {
(StartChat, _) => match chat::create_by_msg_id(context, msg.id).await {
Ok(id) => created_chat_id = Some(id),
Err(e) => warn!(context, "decide_on_contact_request error: {}", e),
},
(Block, false) => Contact::block(context, msg.from_id).await,
(Block, true) => {
if !msg.chat_id.set_blocked(context, Blocked::Manually).await {
warn!(context, "Block mailing list failed.")
}
}
(NotNow, false) => Contact::mark_noticed(context, msg.from_id).await,
(NotNow, true) => {
if let Err(e) = chat::marknoticed_chat(context, msg.chat_id).await {
warn!(context, "Marknoticed failed: {}", e)
}
}
}
// Multiple chats may have changed, so send 0s
// (performance is not so important because this function is not called very often)
context.emit_event(EventType::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
created_chat_id
}
pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
let msg = Message::load_from_db(context, msg_id).await?;
let rawtxt: Option<String> = context
@@ -1140,7 +1232,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
return Ok(ret);
}
let rawtxt = rawtxt.unwrap_or_default();
let rawtxt = dc_truncate(rawtxt.trim(), DC_DESIRED_TEXT_LEN);
let rawtxt = dc_truncate(rawtxt.trim(), DC_MAX_GET_INFO_LEN);
let fts = dc_timestamp_to_str(msg.get_timestamp());
ret += &format!("Sent: {}", fts);
@@ -1270,7 +1362,7 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> {
// before using viewtype other than Viewtype::File,
// make sure, all target UIs support that type in the context of the used viewer/player.
// if in doubt, it is better to default to Viewtype::File that passes handing to an external app.
// (cmp. <https://developer.android.com/guide/topics/media/media-formats>)
// (cmp. https://developer.android.com/guide/topics/media/media-formats )
"3gp" => (Viewtype::Video, "video/3gpp"),
"aac" => (Viewtype::Audio, "audio/aac"),
"avi" => (Viewtype::Video, "video/x-msvideo"),
@@ -1344,25 +1436,18 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> {
/// only if `dc_set_config(context, "save_mime_headers", "1")`
/// was called before.
///
/// Returns an empty vector if there are no headers saved for the given message,
/// Returns an empty string if there are no headers saved for the given message,
/// e.g. because of save_mime_headers is not set
/// or the message is not incoming.
pub async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result<Vec<u8>> {
pub async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result<String> {
let headers = context
.sql
.query_row(
.query_get_value(
"SELECT mime_headers FROM msgs WHERE id=?;",
paramsv![msg_id],
|row| {
row.get(0).or_else(|err| match row.get_ref(0)? {
ValueRef::Null => Ok(Vec::new()),
ValueRef::Text(text) => Ok(text.to_vec()),
ValueRef::Blob(blob) => Ok(blob.to_vec()),
ValueRef::Integer(_) | ValueRef::Real(_) => Err(err),
})
},
)
.await?;
.await?
.unwrap_or_default();
Ok(headers)
}
@@ -1414,37 +1499,33 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
}
let conn = context.sql.get_conn().await?;
let msgs = async_std::task::spawn_blocking(move || -> Result<_> {
let mut stmt = conn.prepare_cached(concat!(
"SELECT",
" m.chat_id AS chat_id,",
" m.state AS state,",
" c.blocked AS blocked",
" FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id",
" WHERE m.id=? AND m.chat_id>9"
))?;
let mut stmt = conn.prepare_cached(concat!(
"SELECT",
" m.chat_id AS chat_id,",
" m.state AS state,",
" c.blocked AS blocked",
" FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id",
" WHERE m.id=? AND m.chat_id>9"
))?;
let mut msgs = Vec::with_capacity(msg_ids.len());
for id in msg_ids.into_iter() {
let query_res = stmt.query_row(paramsv![id], |row| {
Ok((
row.get::<_, ChatId>("chat_id")?,
row.get::<_, MessageState>("state")?,
row.get::<_, Option<Blocked>>("blocked")?
.unwrap_or_default(),
))
});
if let Err(rusqlite::Error::QueryReturnedNoRows) = query_res {
continue;
}
let (chat_id, state, blocked) = query_res.map_err(Into::<anyhow::Error>::into)?;
msgs.push((id, chat_id, state, blocked));
let mut msgs = Vec::with_capacity(msg_ids.len());
for id in msg_ids.into_iter() {
let query_res = stmt.query_row(paramsv![id], |row| {
Ok((
row.get::<_, ChatId>("chat_id")?,
row.get::<_, MessageState>("state")?,
row.get::<_, Option<Blocked>>("blocked")?
.unwrap_or_default(),
))
});
if let Err(rusqlite::Error::QueryReturnedNoRows) = query_res {
continue;
}
drop(stmt);
drop(conn);
Ok(msgs)
})
.await?;
let (chat_id, state, blocked) = query_res.map_err(Into::<anyhow::Error>::into)?;
msgs.push((id, chat_id, state, blocked));
}
drop(stmt);
drop(conn);
let mut updated_chat_ids = BTreeMap::new();
@@ -1635,9 +1716,13 @@ pub async fn handle_mdn(
rfc724_mid: &str,
timestamp_sent: i64,
) -> Result<Option<(ChatId, MsgId)>> {
if from_id <= DC_CONTACT_ID_LAST_SPECIAL || rfc724_mid.is_empty() {
return Ok(None);
}
let res = context
.sql
.query_row_optional(
.query_row(
concat!(
"SELECT",
" m.id AS msg_id,",
@@ -1658,80 +1743,75 @@ pub async fn handle_mdn(
))
},
)
.await?;
.await;
if let Err(ref err) = res {
info!(context, "Failed to select MDN {:?}", err);
}
let (msg_id, chat_id, chat_type, msg_state) = if let Some(res) = res {
res
} else {
info!(
context,
"handle_mdn found no message with Message-ID {:?} sent by us in the database",
rfc724_mid
);
return Ok(None);
};
if let Ok((msg_id, chat_id, chat_type, msg_state)) = res {
let mut read_by_all = false;
let mut read_by_all = false;
if msg_state == MessageState::OutPreparing
|| msg_state == MessageState::OutPending
|| msg_state == MessageState::OutDelivered
{
let mdn_already_in_table = context
.sql
.exists(
"SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=? AND contact_id=?;",
paramsv![msg_id, from_id as i32,],
)
.await?;
if !mdn_already_in_table {
context
if msg_state == MessageState::OutPreparing
|| msg_state == MessageState::OutPending
|| msg_state == MessageState::OutDelivered
{
let mdn_already_in_table = context
.sql
.execute(
.exists(
"SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=? AND contact_id=?;",
paramsv![msg_id, from_id as i32,],
)
.await
.unwrap_or_default();
if !mdn_already_in_table {
context.sql.execute(
"INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?);",
paramsv![msg_id, from_id as i32, timestamp_sent],
)
.await?;
}
.await
.unwrap_or_default(); // TODO: better error handling
}
// Normal chat? that's quite easy.
if chat_type == Chattype::Single {
update_msg_state(context, msg_id, MessageState::OutMdnRcvd).await;
read_by_all = true;
} else {
// send event about new state
let ist_cnt = context
.sql
.count(
"SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=?;",
paramsv![msg_id],
)
.await?;
// Groupsize: Min. MDNs
// 1 S n/a
// 2 SR 1
// 3 SRR 2
// 4 SRRR 2
// 5 SRRRR 3
// 6 SRRRRR 3
//
// (S=Sender, R=Recipient)
// for rounding, SELF is already included!
let soll_cnt = (chat::get_chat_contact_cnt(context, chat_id).await? + 1) / 2;
if ist_cnt >= soll_cnt {
// Normal chat? that's quite easy.
if chat_type == Chattype::Single {
update_msg_state(context, msg_id, MessageState::OutMdnRcvd).await;
read_by_all = true;
} // else wait for more receipts
}
}
} else {
// send event about new state
let ist_cnt = context
.sql
.count(
"SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=?;",
paramsv![msg_id],
)
.await?;
if read_by_all {
Ok(Some((chat_id, msg_id)))
} else {
Ok(None)
// Groupsize: Min. MDNs
// 1 S n/a
// 2 SR 1
// 3 SRR 2
// 4 SRRR 2
// 5 SRRRR 3
// 6 SRRRRR 3
//
// (S=Sender, R=Recipient)
// for rounding, SELF is already included!
let soll_cnt = (chat::get_chat_contact_cnt(context, chat_id).await? + 1) / 2;
if ist_cnt >= soll_cnt {
update_msg_state(context, msg_id, MessageState::OutMdnRcvd).await;
read_by_all = true;
} // else wait for more receipts
}
}
return if read_by_all {
Ok(Some((chat_id, msg_id)))
} else {
Ok(None)
};
}
Ok(None)
}
/// Marks a message as failed after an ndn (non-delivery-notification) arrived.
@@ -1803,13 +1883,7 @@ async fn ndn_maybe_add_info_msg(
// Tell the user which of the recipients failed if we know that (because in
// a group, this might otherwise be unclear)
let text = stock_str::failed_sending_to(context, contact.get_display_name()).await;
chat::add_info_msg(
context,
chat_id,
text,
dc_create_smeared_timestamp(context).await,
)
.await;
chat::add_info_msg(context, chat_id, text).await;
context.emit_event(EventType::ChatModified(chat_id));
}
}
@@ -1823,8 +1897,8 @@ async fn ndn_maybe_add_info_msg(
Ok(())
}
/// The number of messages assigned to unblocked chats
pub async fn get_unblocked_msg_cnt(context: &Context) -> usize {
/// The number of messages assigned to real chat (!=deaddrop, !=trash)
pub async fn get_real_msg_cnt(context: &Context) -> usize {
match context
.sql
.count(
@@ -1837,14 +1911,13 @@ pub async fn get_unblocked_msg_cnt(context: &Context) -> usize {
{
Ok(res) => res,
Err(err) => {
error!(context, "dc_get_unblocked_msg_cnt() failed. {}", err);
error!(context, "dc_get_real_msg_cnt() failed. {}", err);
0
}
}
}
/// Returns the number of messages in contact request chats.
pub async fn get_request_msg_cnt(context: &Context) -> usize {
pub async fn get_deaddrop_msg_cnt(context: &Context) -> usize {
match context
.sql
.count(
@@ -1857,7 +1930,7 @@ pub async fn get_request_msg_cnt(context: &Context) -> usize {
{
Ok(res) => res,
Err(err) => {
error!(context, "dc_get_request_msg_cnt() failed. {}", err);
error!(context, "dc_get_deaddrop_msg_cnt() failed. {}", err);
0
}
}
@@ -2016,7 +2089,7 @@ mod tests {
];
// These are the same as above, but all messages in Spam stay in Spam
const COMBINATIONS_REQUEST: &[(&str, bool, bool, &str)] = &[
const COMBINATIONS_DEADDROP: &[(&str, bool, bool, &str)] = &[
("INBOX", false, false, "INBOX"),
("INBOX", false, true, "INBOX"),
("INBOX", true, false, "INBOX"),
@@ -2049,8 +2122,8 @@ mod tests {
}
#[async_std::test]
async fn test_needs_move_incoming_request() {
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_REQUEST {
async fn test_needs_move_incoming_deaddrop() {
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_DEADDROP {
check_needs_move_combination(
folder,
*mvbox_move,
@@ -2599,7 +2672,10 @@ mod tests {
// check chat-id of this message
let msg = alice.get_last_msg().await;
assert!(!msg.get_chat_id().is_special());
assert!(msg.get_chat_id().is_deaddrop());
assert!(msg.get_chat_id().is_special());
assert!(!msg.get_real_chat_id().is_deaddrop());
assert!(!msg.get_real_chat_id().is_special());
assert_eq!(msg.get_text().unwrap(), "hello".to_string());
}
@@ -2660,31 +2736,33 @@ mod tests {
msg.set_text(Some("this is the text!".to_string()));
// alice sends to bob,
// bob does not know alice yet and messages go to bob's deaddrop
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0);
bob.recv_msg(&alice.send_msg(alice_chat.id, &mut msg).await)
.await;
let msg1 = bob.get_last_msg().await;
let bob_chat_id = msg1.chat_id;
bob.recv_msg(&alice.send_msg(alice_chat.id, &mut msg).await)
.await;
let msg2 = bob.get_last_msg().await;
assert_eq!(msg1.chat_id, msg2.chat_id);
assert_ne!(msg1.chat_id, DC_CHAT_ID_DEADDROP);
let chats = Chatlist::try_load(&bob, 0, None, None).await?;
assert_eq!(chats.len(), 1);
let msgs = chat::get_chat_msgs(&bob, bob_chat_id, 0, None).await?;
assert_eq!(chats.get_chat_id(0), DC_CHAT_ID_DEADDROP);
let msgs = chat::get_chat_msgs(&bob, DC_CHAT_ID_DEADDROP, 0, None).await?;
assert_eq!(msgs.len(), 2);
assert_eq!(bob.get_fresh_msgs().await?.len(), 0);
// that has no effect in contact request
// that has no effect in deaddrop
markseen_msgs(&bob, vec![msg1.id, msg2.id]).await?;
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
let bob_chat = Chat::load_from_db(&bob, bob_chat_id).await?;
assert_eq!(bob_chat.blocked, Blocked::Request);
let msgs = chat::get_chat_msgs(&bob, bob_chat_id, 0, None).await?;
let msgs = chat::get_chat_msgs(&bob, DC_CHAT_ID_DEADDROP, 0, None).await?;
assert_eq!(msgs.len(), 2);
bob_chat_id.accept(&bob).await.unwrap();
let bob_chat_id =
decide_on_contact_request(&bob, msg2.get_id(), ContactRequestDecision::StartChat)
.await
.unwrap();
// bob sends to alice,
// alice knows bob and messages appear in normal chat
@@ -2784,50 +2862,4 @@ mod tests {
Ok(())
}
#[async_std::test]
async fn test_is_bot() -> Result<()> {
let alice = TestContext::new_alice().await;
// Alice receives a message from Bob the bot.
dc_receive_imf(
&alice,
b"From: Bob <bob@example.com>\n\
To: alice@example.com\n\
Chat-Version: 1.0\n\
Message-ID: <123@example.com>\n\
Auto-Submitted: auto-generated\n\
Date: Fri, 29 Jan 2021 21:37:55 +0000\n\
\n\
hello\n",
"INBOX",
1,
false,
)
.await?;
let msg = alice.get_last_msg().await;
assert_eq!(msg.get_text().unwrap(), "hello".to_string());
assert!(msg.is_bot());
// Alice receives a message from Bob who is not the bot anymore.
dc_receive_imf(
&alice,
b"From: Bob <bob@example.com>\n\
To: alice@example.com\n\
Chat-Version: 1.0\n\
Message-ID: <456@example.com>\n\
Date: Fri, 29 Jan 2021 21:37:55 +0000\n\
\n\
hello again\n",
"INBOX",
2,
false,
)
.await?;
let msg = alice.get_last_msg().await;
assert_eq!(msg.get_text().unwrap(), "hello again".to_string());
assert!(!msg.is_bot());
Ok(())
}
}

View File

@@ -1,5 +1,3 @@
//! # MIME message production.
use std::convert::TryInto;
use anyhow::{bail, ensure, format_err, Result};
@@ -493,11 +491,6 @@ impl<'a> MimeFactory<'a> {
"Auto-Submitted".to_string(),
"auto-replied".to_string(),
));
} else if context.get_config_bool(Config::Bot).await? {
headers.unprotected.push(Header::new(
"Auto-Submitted".to_string(),
"auto-generated".to_string(),
));
}
if self.req_mdn {
@@ -867,7 +860,7 @@ impl<'a> MimeFactory<'a> {
// This should prevent automatic replies,
// such as non-delivery reports.
//
// See <https://tools.ietf.org/html/rfc3834>
// See https://tools.ietf.org/html/rfc3834
//
// Adding this header without encryption leaks some
// information about the message contents, but it can
@@ -1402,7 +1395,7 @@ mod tests {
Address::new_mailbox_with_name(display_name.to_string(), addr.to_string())
);
// Addresses should not be unnecessarily be encoded, see <https://github.com/deltachat/deltachat-core-rust/issues/1575>:
// Addresses should not be unnecessarily be encoded, see https://github.com/deltachat/deltachat-core-rust/issues/1575:
assert_eq!(s, "a space <x@y.org>");
}
@@ -1808,8 +1801,9 @@ mod tests {
let chats = Chatlist::try_load(context, 0, None, None).await.unwrap();
let chat_id = chats.get_chat_id(0);
chat_id.accept(context).await.unwrap();
let chat_id = chat::create_by_msg_id(context, chats.get_msg_id(0).unwrap())
.await
.unwrap();
let mut new_msg = Message::new(Viewtype::Text);
new_msg.set_text(Some("Hi".to_string()));
@@ -1865,7 +1859,7 @@ mod tests {
#[test]
fn test_no_empty_lines_in_header() {
// See <https://github.com/deltachat/deltachat-core-rust/issues/2118>
// See https://github.com/deltachat/deltachat-core-rust/issues/2118
let to_tuples = [
("Nnnn", "nnn@ttttttttt.de"),
("😀 ttttttt", "ttttttt@rrrrrr.net"),

View File

@@ -1,18 +1,18 @@
//! # MIME message parsing module.
use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::pin::Pin;
use anyhow::{bail, Result};
use charset::Charset;
use deltachat_derive::{FromSql, ToSql};
use lettre_email::mime::{self, Mime};
use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo};
use once_cell::sync::Lazy;
use percent_encoding::percent_decode_str;
use crate::aheader::Aheader;
use crate::blob::BlobObject;
use crate::constants::{Viewtype, DC_DESIRED_TEXT_LEN, DC_ELLIPSIS};
use crate::constants::{Viewtype, DC_DESIRED_TEXT_LEN, DC_ELLIPSE};
use crate::contact::addr_normalize;
use crate::context::Context;
use crate::dc_tools::{dc_get_filemeta, dc_truncate};
@@ -198,7 +198,7 @@ impl MimeMessage {
}
// Handle any gossip headers if the mail was encrypted. See section
// "3.6 Key Gossip" of <https://autocrypt.org/autocrypt-spec-1.1.0.pdf>
// "3.6 Key Gossip" of https://autocrypt.org/autocrypt-spec-1.1.0.pdf
// but only if the mail was correctly signed:
if !signatures.is_empty() {
let gossip_headers =
@@ -220,7 +220,7 @@ impl MimeMessage {
let mut throwaway_from = from.clone();
// We do not want to allow unencrypted subject in encrypted emails because the user might falsely think that the subject is safe.
// See <https://github.com/deltachat/deltachat-core-rust/issues/1790>.
// See https://github.com/deltachat/deltachat-core-rust/issues/1790.
headers.remove("subject");
MimeMessage::merge_headers(
@@ -278,7 +278,7 @@ impl MimeMessage {
parser.maybe_remove_bad_parts();
parser.maybe_remove_inline_mailinglist_footer();
parser.heuristically_parse_ndn(context).await;
parser.parse_headers(context).await?;
parser.parse_headers(context).await;
if warn_empty_signature && parser.signatures.is_empty() {
for part in parser.parts.iter_mut() {
@@ -295,7 +295,7 @@ impl MimeMessage {
/// Parses system messages.
fn parse_system_message_headers(&mut self, context: &Context) {
if self.get_header(HeaderDef::AutocryptSetupMessage).is_some() {
if self.get(HeaderDef::AutocryptSetupMessage).is_some() {
self.parts = self
.parts
.iter()
@@ -311,7 +311,7 @@ impl MimeMessage {
} else {
warn!(context, "could not determine ASM mime-part");
}
} else if let Some(value) = self.get_header(HeaderDef::ChatContent) {
} else if let Some(value) = self.get(HeaderDef::ChatContent) {
if value == "location-streaming-enabled" {
self.is_system_message = SystemMessage::LocationStreamingEnabled;
} else if value == "ephemeral-timer-changed" {
@@ -326,19 +326,19 @@ impl MimeMessage {
/// Parses avatar action headers.
async fn parse_avatar_headers(&mut self, context: &Context) {
if let Some(header_value) = self.get_header(HeaderDef::ChatGroupAvatar).cloned() {
if let Some(header_value) = self.get(HeaderDef::ChatGroupAvatar).cloned() {
self.group_avatar = self.avatar_action_from_header(context, header_value).await;
}
if let Some(header_value) = self.get_header(HeaderDef::ChatUserAvatar).cloned() {
if let Some(header_value) = self.get(HeaderDef::ChatUserAvatar).cloned() {
self.user_avatar = self.avatar_action_from_header(context, header_value).await;
}
}
fn parse_videochat_headers(&mut self) {
if let Some(value) = self.get_header(HeaderDef::ChatContent).cloned() {
if let Some(value) = self.get(HeaderDef::ChatContent).cloned() {
if value == "videochat-invitation" {
let instance = self.get_header(HeaderDef::ChatWebrtcRoom).cloned();
let instance = self.get(HeaderDef::ChatWebrtcRoom).cloned();
if let Some(part) = self.parts.first_mut() {
part.typ = Viewtype::VideochatInvitation;
part.param
@@ -395,12 +395,11 @@ impl MimeMessage {
}
if let Some(mut part) = self.parts.pop() {
if part.typ == Viewtype::Audio && self.get_header(HeaderDef::ChatVoiceMessage).is_some()
{
if part.typ == Viewtype::Audio && self.get(HeaderDef::ChatVoiceMessage).is_some() {
part.typ = Viewtype::Voice;
}
if part.typ == Viewtype::Image || part.typ == Viewtype::Gif {
if let Some(value) = self.get_header(HeaderDef::ChatContent) {
if part.typ == Viewtype::Image {
if let Some(value) = self.get(HeaderDef::ChatContent) {
if value == "sticker" {
part.typ = Viewtype::Sticker;
}
@@ -410,7 +409,7 @@ impl MimeMessage {
|| part.typ == Viewtype::Voice
|| part.typ == Viewtype::Video
{
if let Some(field_0) = self.get_header(HeaderDef::ChatDuration) {
if let Some(field_0) = self.get(HeaderDef::ChatDuration) {
let duration_ms = field_0.parse().unwrap_or_default();
if duration_ms > 0 && duration_ms < 24 * 60 * 60 * 1000 {
part.param.set_int(Param::Duration, duration_ms);
@@ -422,7 +421,7 @@ impl MimeMessage {
}
}
async fn parse_headers(&mut self, context: &Context) -> Result<()> {
async fn parse_headers(&mut self, context: &Context) {
self.parse_system_message_headers(context);
self.parse_avatar_headers(context).await;
self.parse_videochat_headers();
@@ -467,20 +466,15 @@ impl MimeMessage {
if !self.decrypting_failed && !self.parts.is_empty() {
if let Some(ref dn_to) = self.chat_disposition_notification_to {
if let Some(from) = self.from.get(0) {
// Check that the message is not outgoing.
if !context.is_self_addr(&from.addr).await? {
if from.addr.to_lowercase() == dn_to.addr.to_lowercase() {
if let Some(part) = self.parts.last_mut() {
part.param.set_int(Param::WantsMdn, 1);
}
} else {
warn!(
context,
"{} requested a read receipt to {}, ignoring",
from.addr,
dn_to.addr
);
if from.addr.to_lowercase() == dn_to.addr.to_lowercase() {
if let Some(part) = self.parts.last_mut() {
part.param.set_int(Param::WantsMdn, 1);
}
} else {
warn!(
context,
"{} requested a read receipt to {}, ignoring", from.addr, dn_to.addr
);
}
}
}
@@ -504,14 +498,6 @@ impl MimeMessage {
self.parts.push(part);
}
if self.header.contains_key("auto-submitted") {
for part in &mut self.parts {
part.param.set(Param::Bot, "1");
}
}
Ok(())
}
async fn avatar_action_from_header(
@@ -592,12 +578,12 @@ impl MimeMessage {
}
pub(crate) fn get_subject(&self) -> Option<String> {
self.get_header(HeaderDef::Subject)
self.get(HeaderDef::Subject)
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
}
pub fn get_header(&self, headerdef: HeaderDef) -> Option<&String> {
pub fn get(&self, headerdef: HeaderDef) -> Option<&String> {
self.header.get(headerdef.get_headername())
}
@@ -745,7 +731,7 @@ impl MimeMessage {
The second body part contains the control information necessary to
verify the digital signature." We simply take the first body part and
skip the rest. (see
<https://k9mail.github.io/2016/11/24/OpenPGP-Considerations-Part-I.html>
https://k9mail.github.io/2016/11/24/OpenPGP-Considerations-Part-I.html
for background information why we use encrypted+signed) */
if let Some(first) = mail.subparts.get(0) {
any_part_added = self
@@ -906,14 +892,13 @@ impl MimeMessage {
(simplified_txt, top_quote)
};
let simplified_txt = if simplified_txt.chars().count()
> DC_DESIRED_TEXT_LEN + DC_ELLIPSIS.len()
{
self.is_mime_modified = true;
dc_truncate(&*simplified_txt, DC_DESIRED_TEXT_LEN).to_string()
} else {
simplified_txt
};
let simplified_txt =
if simplified_txt.len() > DC_DESIRED_TEXT_LEN + DC_ELLIPSE.len() {
self.is_mime_modified = true;
dc_truncate(&*simplified_txt, DC_DESIRED_TEXT_LEN).to_string()
} else {
simplified_txt
};
if !simplified_txt.is_empty() || simplified_quote.is_some() {
let mut part = Part {
@@ -1020,12 +1005,12 @@ impl MimeMessage {
}
pub(crate) fn get_mailinglist_type(&self) -> MailinglistType {
if self.get_header(HeaderDef::ListId).is_some() {
if self.get(HeaderDef::ListId).is_some() {
return MailinglistType::ListIdBased;
} else if self.get_header(HeaderDef::Sender).is_some() {
} else if self.get(HeaderDef::Sender).is_some() {
// the `Sender:`-header alone is no indicator for mailing list
// as also used for bot-impersonation via `set_override_sender_name()`
if let Some(precedence) = self.get_header(HeaderDef::Precedence) {
if let Some(precedence) = self.get(HeaderDef::Precedence) {
if precedence == "list" || precedence == "bulk" {
return MailinglistType::SenderBased;
}
@@ -1051,8 +1036,8 @@ impl MimeMessage {
}
pub fn get_rfc724_mid(&self) -> Option<String> {
self.get_header(HeaderDef::XMicrosoftOriginalMessageId)
.or_else(|| self.get_header(HeaderDef::MessageId))
self.get(HeaderDef::XMicrosoftOriginalMessageId)
.or_else(|| self.get(HeaderDef::MessageId))
.and_then(|msgid| parse_message_id(msgid).ok())
}
@@ -1239,7 +1224,7 @@ impl MimeMessage {
/// Also you should add a test in dc_receive_imf.rs (there already are lots of test_parse_ndn_* tests).
#[allow(clippy::indexing_slicing)]
async fn heuristically_parse_ndn(&mut self, context: &Context) {
let maybe_ndn = if let Some(from) = self.get_header(HeaderDef::From_) {
let maybe_ndn = if let Some(from) = self.get(HeaderDef::From_) {
let from = from.to_ascii_lowercase();
from.contains("mailer-daemon") || from.contains("mail-daemon")
} else {
@@ -1314,7 +1299,7 @@ impl MimeMessage {
/// database, returns None.
pub async fn get_parent_timestamp(&self, context: &Context) -> Result<Option<i64>> {
let parent_timestamp = if let Some(field) = self
.get_header(HeaderDef::InReplyTo)
.get(HeaderDef::InReplyTo)
.and_then(|msgid| parse_message_id(msgid).ok())
{
context
@@ -1358,9 +1343,7 @@ async fn update_gossip_peerstates(
peerstate = Some(p);
}
if let Some(peerstate) = peerstate {
peerstate
.handle_fingerprint_change(context, message_time)
.await?;
peerstate.handle_fingerprint_change(context).await?;
}
gossipped_addr.insert(header.addr.clone());
@@ -1480,7 +1463,7 @@ fn get_mime_type(mail: &mailparse::ParsedMail<'_>) -> Result<(Mime, Viewtype)> {
mime::VIDEO => Viewtype::Video,
mime::MULTIPART => Viewtype::Unknown,
mime::MESSAGE => {
// Enacapsulated messages, see <https://www.w3.org/Protocols/rfc1341/7_3_Message.html>
// Enacapsulated messages, see https://www.w3.org/Protocols/rfc1341/7_3_Message.html
// Also used as part "message/disposition-notification" of "multipart/report", which, however, will
// be handled separatedly.
// I've not seen any messages using this, so we do not attach these parts (maybe they're used to attach replies,
@@ -1521,13 +1504,55 @@ fn get_attachment_filename(
// `Content-Disposition: ... filename=...`
let mut desired_filename = ct.params.get("filename").map(|s| s.to_string());
// try to get file name from
// `Content-Disposition: ... filename*0*=... filename*1*=... filename*2*=...`
// encoded as CHARSET'LANG'test%2E%70%64%66 (key ends with `*`)
// or as "encoded-words" (key does not end with `*`)
if desired_filename.is_none() {
if let Some(name) = ct.params.get("filename*").map(|s| s.to_string()) {
// be graceful and just use the original name.
// some MUA, including Delta Chat up to core1.50,
// use `filename*` mistakenly for simple encoded-words without following rfc2231
warn!(context, "apostrophed encoding invalid: {}", name);
desired_filename = Some(name);
let mut apostrophe_encoded = false;
desired_filename = ct
.params
.iter()
.filter(|(key, _value)| key.starts_with("filename*"))
.fold(None, |acc, (key, value)| {
if key.ends_with('*') {
apostrophe_encoded = true;
}
if let Some(acc) = acc {
Some(acc + value)
} else {
Some(value.to_string())
}
});
if apostrophe_encoded {
if let Some(name) = desired_filename {
let mut parts = name.splitn(3, '\'');
desired_filename =
if let (Some(charset), Some(value)) = (parts.next(), parts.last()) {
let decoded_bytes = percent_decode_str(value);
if charset.to_lowercase() == "utf-8" {
Some(decoded_bytes.decode_utf8_lossy().to_string())
} else {
// encoded_words crate say, latin-1 is not reported; moreover, latin1 is a good default
if let Some(charset) = Charset::for_label(charset.as_bytes())
.or_else(|| Charset::for_label(b"latin1"))
{
let decoded_bytes = decoded_bytes.collect::<Vec<u8>>();
let (utf8_str, _, _) = charset.decode(&*decoded_bytes);
Some(utf8_str.into())
} else {
warn!(context, "latin1 encoding does not exist");
None
}
}
} else {
warn!(context, "apostrophed encoding invalid: {}", name);
// be graceful and just use the original name.
// some MUA, including Delta Chat up to core1.50,
// use `filename*` mistakenly for simple encoded-words without following rfc2231
Some(name)
}
}
}
}
@@ -1608,6 +1633,7 @@ mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::constants::DC_MAX_GET_TEXT_LEN;
use crate::{
chatlist::Chatlist,
config::Config,
@@ -1888,6 +1914,13 @@ mod tests {
assert_eq!(filename, Some("Maßnahmen Okt. 2020.html".to_string()))
}
#[test]
fn test_charset_latin1() {
// make sure, latin1 exists under this name
// as we're using it as default in get_attachment_filename() for non-utf-8
assert!(Charset::for_label(b"latin1").is_some());
}
#[test]
fn test_mailparse_content_type() {
let ctype =
@@ -1983,11 +2016,11 @@ mod tests {
.unwrap();
// non-overwritten headers do not bubble up
let of = mimeparser.get_header(HeaderDef::SecureJoinGroup).unwrap();
let of = mimeparser.get(HeaderDef::SecureJoinGroup).unwrap();
assert_eq!(of, "no");
// unknown headers do not bubble upwards
let of = mimeparser.get_header(HeaderDef::_TestHeader).unwrap();
let of = mimeparser.get(HeaderDef::_TestHeader).unwrap();
assert_eq!(of, "Bar");
// the following fields would bubble up
@@ -1996,15 +2029,13 @@ mod tests {
// for Chat-Version, also the case-insensivity is tested.
assert_eq!(mimeparser.get_subject(), Some("outer-subject".into()));
let of = mimeparser.get_header(HeaderDef::ChatVersion).unwrap();
let of = mimeparser.get(HeaderDef::ChatVersion).unwrap();
assert_eq!(of, "0.0");
assert_eq!(mimeparser.parts.len(), 1);
// make sure, headers that are only allowed in the encrypted part
// cannot be set from the outer part
assert!(mimeparser
.get_header(HeaderDef::SecureJoinFingerprint)
.is_none());
assert!(mimeparser.get(HeaderDef::SecureJoinFingerprint).is_none());
}
#[async_std::test]
@@ -2797,7 +2828,7 @@ On 2020-10-25, Bob wrote:
.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
let msg_id = chats.get_msg_id(0).unwrap().unwrap();
let msg_id = chats.get_msg_id(0).unwrap();
let msg = Message::load_from_db(&t.ctx, msg_id).await.unwrap();
assert_eq!(
@@ -2807,7 +2838,7 @@ On 2020-10-25, Bob wrote:
assert_eq!(msg.viewtype, Viewtype::Image);
assert_eq!(msg.error(), None);
assert_eq!(msg.is_dc_message, MessengerMessage::No);
assert_eq!(msg.chat_blocked, Blocked::Request);
assert_eq!(msg.chat_blocked, Blocked::Deaddrop);
assert_eq!(msg.state, MessageState::InFresh);
assert_eq!(msg.get_filebytes(&t).await, 2115);
assert!(msg.get_file(&t).is_some());
@@ -2885,20 +2916,21 @@ On 2020-10-25, Bob wrote:
let t = TestContext::new().await;
static REPEAT_TXT: &str = "this text with 42 chars is just repeated.\n";
static REPEAT_CNT: usize = 2000; // results in a text of 84k, should be more than DC_DESIRED_TEXT_LEN
static REPEAT_CNT: usize = 2000; // results in a text of 84k, should be more than DC_MAX_GET_TEXT_LEN
let long_txt = format!("From: alice@c.de\n\n{}", REPEAT_TXT.repeat(REPEAT_CNT));
assert!(DC_DESIRED_TEXT_LEN + DC_ELLIPSE.len() < DC_MAX_GET_TEXT_LEN);
let mimemsg = MimeMessage::from_bytes(&t, long_txt.as_ref())
.await
.unwrap();
assert_eq!(long_txt.matches("just repeated").count(), REPEAT_CNT);
assert!(long_txt.len() > DC_DESIRED_TEXT_LEN);
assert!(long_txt.len() > DC_MAX_GET_TEXT_LEN);
assert!(mimemsg.is_mime_modified);
assert!(
mimemsg.parts[0].msg.matches("just repeated").count()
<= DC_DESIRED_TEXT_LEN / REPEAT_TXT.len()
< DC_MAX_GET_TEXT_LEN / REPEAT_TXT.len()
);
assert!(mimemsg.parts[0].msg.len() <= DC_DESIRED_TEXT_LEN + DC_ELLIPSIS.len());
assert!(mimemsg.parts[0].msg.len() <= DC_MAX_GET_TEXT_LEN);
}
#[async_std::test]
@@ -2923,78 +2955,4 @@ On 2020-10-25, Bob wrote:
Some("Mr.6Dx7ITn4w38.n9j7epIcuQI@outlook.com".to_string())
);
}
#[async_std::test]
async fn test_long_in_reply_to() -> Result<()> {
let t = TestContext::new_alice().await;
// A message with a long Message-ID.
// Long message-IDs are generated by Mailjet.
let raw = br###"Date: Thu, 28 Jan 2021 00:26:57 +0000
Chat-Version: 1.0\n\
Message-ID: <ABCDEFGH.1234567_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@mailjet.com>
To: Bob <bob@example.org>
From: Alice <alice@example.org>
Subject: ...
Some quote.
"###;
dc_receive_imf(&t, raw, "INBOX", 1, false).await?;
// Delta Chat generates In-Reply-To with a starting tab when Message-ID is too long.
let raw = br###"In-Reply-To:
<ABCDEFGH.1234567_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@mailjet.com>
Date: Thu, 28 Jan 2021 00:26:57 +0000
Chat-Version: 1.0\n\
Message-ID: <foobar@example.org>
To: Alice <alice@example.org>
From: Bob <bob@example.org>
Subject: ...
> Some quote.
Some reply
"###;
dc_receive_imf(&t, raw, "INBOX", 2, false).await?;
let msg = t.get_last_msg().await;
assert_eq!(msg.get_text().unwrap(), "Some reply");
let quoted_message = msg.quoted_message(&t).await?.unwrap();
assert_eq!(quoted_message.get_text().unwrap(), "Some quote.");
Ok(())
}
// Test that WantsMdn parameter is not set on outgoing messages.
#[async_std::test]
async fn test_outgoing_wants_mdn() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let raw = br###"Date: Thu, 28 Jan 2021 00:26:57 +0000
Chat-Version: 1.0\n\
Message-ID: <foobarbaz@example.org>
To: Bob <bob@example.org>
From: Alice <alice@example.com>
Subject: subject
Chat-Disposition-Notification-To: alice@example.com
Message.
"###;
// Bob receives message.
dc_receive_imf(&bob, raw, "INBOX", 1, false).await?;
let msg = bob.get_last_msg().await;
// Message is incoming.
assert!(msg.param.get_bool(Param::WantsMdn).unwrap());
// Alice receives copy-to-self.
dc_receive_imf(&alice, raw, "INBOX", 1, false).await?;
let msg = alice.get_last_msg().await;
// Message is outgoing, don't send read receipt to self.
assert!(msg.param.get_bool(Param::WantsMdn).is_none());
Ok(())
}
}

View File

@@ -1,4 +1,4 @@
//! OAuth 2 module.
//! OAuth 2 module
use std::collections::HashMap;
@@ -6,14 +6,13 @@ use anyhow::Result;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use serde::Deserialize;
use crate::config::Config;
use crate::context::Context;
use crate::dc_tools::time;
use crate::provider;
use crate::provider::Oauth2Authorizer;
const OAUTH2_GMAIL: Oauth2 = Oauth2 {
// see <https://developers.google.com/identity/protocols/OAuth2InstalledApp>
// see https://developers.google.com/identity/protocols/OAuth2InstalledApp
client_id: "959970109878-4mvtgf6feshskf7695nfln6002mom908.apps.googleusercontent.com",
get_code: "https://accounts.google.com/o/oauth2/auth?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&response_type=code&scope=https%3A%2F%2Fmail.google.com%2F%20email&access_type=offline",
init_token: "https://accounts.google.com/o/oauth2/token?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&code=$CODE&grant_type=authorization_code",
@@ -22,7 +21,7 @@ const OAUTH2_GMAIL: Oauth2 = Oauth2 {
};
const OAUTH2_YANDEX: Oauth2 = Oauth2 {
// see <https://tech.yandex.com/oauth/doc/dg/reference/auto-code-client-docpage/>
// see https://tech.yandex.com/oauth/doc/dg/reference/auto-code-client-docpage/
client_id: "c4d0b6735fc8420a816d7e1303469341",
get_code: "https://oauth.yandex.com/authorize?client_id=$CLIENT_ID&response_type=code&scope=mail%3Aimap_full%20mail%3Asmtp&force_confirm=true",
init_token: "https://oauth.yandex.com/token?grant_type=authorization_code&code=$CODE&client_id=$CLIENT_ID&client_secret=58b8c6e94cf44fbe952da8511955dacf",
@@ -42,7 +41,7 @@ struct Oauth2 {
/// OAuth 2 Access Token Response
#[derive(Debug, Deserialize)]
struct Response {
// Should always be there according to: <https://www.oauth.com/oauth2-servers/access-tokens/access-token-response/>
// Should always be there according to: https://www.oauth.com/oauth2-servers/access-tokens/access-token-response/
// but previous code handled its abscense.
access_token: Option<String>,
token_type: String,
@@ -56,19 +55,22 @@ pub async fn dc_get_oauth2_url(
context: &Context,
addr: &str,
redirect_uri: &str,
) -> Result<Option<String>> {
let socks5_enabled = context.get_config_bool(Config::Socks5Enabled).await?;
if let Some(oauth2) = Oauth2::from_address(addr, socks5_enabled).await {
context
) -> Option<String> {
if let Some(oauth2) = Oauth2::from_address(addr).await {
if context
.sql
.set_raw_config("oauth2_pending_redirect_uri", Some(redirect_uri))
.await?;
.await
.is_err()
{
return None;
}
let oauth2_url = replace_in_uri(oauth2.get_code, "$CLIENT_ID", oauth2.client_id);
let oauth2_url = replace_in_uri(&oauth2_url, "$REDIRECT_URI", redirect_uri);
Ok(Some(oauth2_url))
Some(oauth2_url)
} else {
Ok(None)
None
}
}
@@ -78,8 +80,7 @@ pub async fn dc_get_oauth2_access_token(
code: &str,
regenerate: bool,
) -> Result<Option<String>> {
let socks5_enabled = context.get_config_bool(Config::Socks5Enabled).await?;
if let Some(oauth2) = Oauth2::from_address(addr, socks5_enabled).await {
if let Some(oauth2) = Oauth2::from_address(addr).await {
let lock = context.oauth2_mutex.lock().await;
// read generated token
@@ -128,7 +129,7 @@ pub async fn dc_get_oauth2_access_token(
};
// to allow easier specification of different configurations,
// token_url is in GET-method-format, sth. as <https://domain?param1=val1&param2=val2> -
// token_url is in GET-method-format, sth. as https://domain?param1=val1&param2=val2 -
// convert this to POST-format ...
let mut parts = token_url.splitn(2, '?');
let post_url = parts.next().unwrap_or_default();
@@ -224,8 +225,7 @@ pub async fn dc_get_oauth2_addr(
addr: &str,
code: &str,
) -> Result<Option<String>> {
let socks5_enabled = context.get_config_bool(Config::Socks5Enabled).await?;
let oauth2 = match Oauth2::from_address(addr, socks5_enabled).await {
let oauth2 = match Oauth2::from_address(addr).await {
Some(o) => o,
None => return Ok(None),
};
@@ -253,13 +253,13 @@ pub async fn dc_get_oauth2_addr(
}
impl Oauth2 {
async fn from_address(addr: &str, skip_mx: bool) -> Option<Self> {
async fn from_address(addr: &str) -> Option<Self> {
let addr_normalized = normalize_addr(addr);
if let Some(domain) = addr_normalized
.find('@')
.map(|index| addr_normalized.split_at(index + 1).1)
{
if let Some(oauth2_authorizer) = provider::get_provider_info(domain, skip_mx)
if let Some(oauth2_authorizer) = provider::get_provider_info(domain)
.await
.and_then(|provider| provider.oauth2_authorizer.as_ref())
{
@@ -357,29 +357,29 @@ mod tests {
#[async_std::test]
async fn test_oauth_from_address() {
assert_eq!(
Oauth2::from_address("hello@gmail.com", false).await,
Oauth2::from_address("hello@gmail.com").await,
Some(OAUTH2_GMAIL)
);
assert_eq!(
Oauth2::from_address("hello@googlemail.com", false).await,
Oauth2::from_address("hello@googlemail.com").await,
Some(OAUTH2_GMAIL)
);
assert_eq!(
Oauth2::from_address("hello@yandex.com", false).await,
Oauth2::from_address("hello@yandex.com").await,
Some(OAUTH2_YANDEX)
);
assert_eq!(
Oauth2::from_address("hello@yandex.ru", false).await,
Oauth2::from_address("hello@yandex.ru").await,
Some(OAUTH2_YANDEX)
);
assert_eq!(Oauth2::from_address("hello@web.de", false).await, None);
assert_eq!(Oauth2::from_address("hello@web.de").await, None);
}
#[async_std::test]
async fn test_oauth_from_mx() {
assert_eq!(
Oauth2::from_address("hello@google.com", false).await,
Oauth2::from_address("hello@google.com").await,
Some(OAUTH2_GMAIL)
);
}
@@ -399,9 +399,7 @@ mod tests {
let ctx = TestContext::new().await;
let addr = "dignifiedquire@gmail.com";
let redirect_uri = "chat.delta:/com.b44t.messenger";
let res = dc_get_oauth2_url(&ctx.ctx, addr, redirect_uri)
.await
.unwrap();
let res = dc_get_oauth2_url(&ctx.ctx, addr, redirect_uri).await;
assert_eq!(res, Some("https://accounts.google.com/o/oauth2/auth?client_id=959970109878%2D4mvtgf6feshskf7695nfln6002mom908%2Eapps%2Egoogleusercontent%2Ecom&redirect_uri=chat%2Edelta%3A%2Fcom%2Eb44t%2Emessenger&response_type=code&scope=https%3A%2F%2Fmail.google.com%2F%20email&access_type=offline".into()));
}

View File

@@ -60,9 +60,6 @@ pub enum Param {
/// For Messages
WantsMdn = b'r',
/// For Messages: a message with Auto-Submitted header ("bot").
Bot = b'b',
/// For Messages: unset or 0=not forwarded,
/// 1=forwarded from unknown msg_id, >9 forwarded from msg_id
Forwarded = b'a',

View File

@@ -1,4 +1,4 @@
//! # [Autocrypt Peer State](https://autocrypt.org/level1.html#peer-state-management) module.
//! # [Autocrypt Peer State](https://autocrypt.org/level1.html#peer-state-management) module
use std::collections::HashSet;
use std::fmt;
@@ -260,24 +260,21 @@ impl Peerstate {
}
/// Adds a warning to the chat corresponding to peerstate if fingerprint has changed.
pub(crate) async fn handle_fingerprint_change(
&self,
context: &Context,
timestamp: i64,
) -> Result<()> {
pub(crate) async fn handle_fingerprint_change(&self, context: &Context) -> Result<()> {
if self.fingerprint_changed {
if let Some(contact_id) = context
.sql
.query_get_value("SELECT id FROM contacts WHERE addr=?;", paramsv![self.addr])
.await?
{
let chat_id = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Request)
.await?
.id;
let chat_id =
ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Deaddrop)
.await?
.id;
let msg = stock_str::contact_setup_changed(context, self.addr.clone()).await;
chat::add_info_msg(context, chat_id, msg, timestamp).await;
chat::add_info_msg(context, chat_id, msg).await;
emit_event!(context, EventType::ChatModified(chat_id));
} else {
bail!("contact with peerstate.addr {:?} not found", &self.addr);
@@ -496,6 +493,12 @@ impl Peerstate {
}
}
impl From<crate::key::FingerprintError> for rusqlite::Error {
fn from(_source: crate::key::FingerprintError) -> Self {
Self::InvalidColumnType(0, "Invalid fingerprint".into(), rusqlite::types::Type::Text)
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -1,4 +1,4 @@
//! OpenPGP helper module using [rPGP facilities](https://github.com/rpgp/rpgp).
//! OpenPGP helper module using [rPGP facilities](https://github.com/rpgp/rpgp)
use std::collections::{BTreeMap, HashSet};
use std::io;
@@ -86,7 +86,7 @@ impl<'a> PublicKeyTrait for SignedPublicKeyOrSubkey<'a> {
}
}
/// Split data from PGP Armored Data as defined in <https://tools.ietf.org/html/rfc4880#section-6.2>.
/// Split data from PGP Armored Data as defined in https://tools.ietf.org/html/rfc4880#section-6.2.
///
/// Returns (type, headers, base64 encoded body).
pub fn split_armored_data(buf: &[u8]) -> Result<(BlockType, BTreeMap<String, String>, Vec<u8>)> {

View File

@@ -1,5 +1,4 @@
//! Handle plain text together with some attributes.
///! Handle plain text together with some attributes.
use crate::simplify::split_lines;
use once_cell::sync::Lazy;
@@ -65,7 +64,7 @@ impl PlainText {
// flowed text as of RFC 3676 -
// a leading space shall be removed
// and is only there to allow > at the beginning of a line that is no quote.
line = line.strip_prefix(' ').unwrap_or(&line).to_string();
line = line.strip_prefix(" ").unwrap_or(&line).to_string();
if is_quote {
line = "<em>".to_owned() + &line + "</em>";
}

View File

@@ -1,4 +1,4 @@
//! [Provider database](https://providers.delta.chat/) module.
//! [Provider database](https://providers.delta.chat/) module
mod data;
@@ -89,17 +89,15 @@ pub struct Provider {
///
/// For compatibility, email address can be passed to this function
/// instead of the domain.
pub async fn get_provider_info(domain: &str, skip_mx: bool) -> Option<&'static Provider> {
pub async fn get_provider_info(domain: &str) -> Option<&'static Provider> {
let domain = domain.rsplitn(2, '@').next()?;
if let Some(provider) = get_provider_by_domain(domain) {
return Some(provider);
}
if !skip_mx {
if let Some(provider) = get_provider_by_mx(domain).await {
return Some(provider);
}
if let Some(provider) = get_provider_by_mx(domain).await {
return Some(provider);
}
None
@@ -223,17 +221,11 @@ mod tests {
#[async_std::test]
async fn test_get_provider_info() {
assert!(get_provider_info("", false).await.is_none());
assert!(get_provider_info("google.com", false).await.unwrap().id == "gmail");
assert!(get_provider_info("").await.is_none());
assert!(get_provider_info("google.com").await.unwrap().id == "gmail");
// get_provider_info() accepts email addresses for backwards compatibility
assert!(
get_provider_info("example@google.com", false)
.await
.unwrap()
.id
== "gmail"
);
assert!(get_provider_info("example@google.com").await.unwrap().id == "gmail");
}
#[test]

View File

@@ -8,23 +8,6 @@ use std::collections::HashMap;
use once_cell::sync::Lazy;
// 163.md: 163.com
static P_163: Lazy<Provider> = Lazy::new(|| {
Provider {
id: "163",
status: Status::Broken,
before_login_hint: "163 Mail does not work since it forces the email clients to connect with an IMAP ID, which is currently not the case of Delta Chat.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/163",
server: vec![
],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
}
});
// aktivix.org.md: aktivix.org
static P_AKTIVIX_ORG: Lazy<Provider> = Lazy::new(|| Provider {
id: "aktivix.org",
@@ -585,8 +568,8 @@ static P_HEY_COM: Lazy<Provider> = Lazy::new(|| {
// i.ua.md: i.ua
static P_I_UA: Lazy<Provider> = Lazy::new(|| Provider {
id: "i.ua",
status: Status::Broken,
before_login_hint: "Протокол IMAP не предоставляется и не планируется.",
status: Status::Ok,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/i-ua",
server: vec![],
@@ -614,7 +597,8 @@ static P_I3_NET: Lazy<Provider> = Lazy::new(|| Provider {
static P_ICLOUD: Lazy<Provider> = Lazy::new(|| Provider {
id: "icloud",
status: Status::Preparation,
before_login_hint: "You must create an app-specific password for Delta Chat before login.",
before_login_hint:
"You must create an app-specific password for Delta Chat before you can login.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/icloud",
server: vec![
@@ -667,7 +651,7 @@ static P_KONTENT_COM: Lazy<Provider> = Lazy::new(|| Provider {
oauth2_authorizer: None,
});
// mail.ru.md: mail.ru, inbox.ru, internet.ru, bk.ru, list.ru
// mail.ru.md: mail.ru, inbox.ru, bk.ru, list.ru
static P_MAIL_RU: Lazy<Provider> = Lazy::new(|| {
Provider {
id: "mail.ru",
@@ -676,8 +660,6 @@ static P_MAIL_RU: Lazy<Provider> = Lazy::new(|| {
after_login_hint: "",
overview_page: "https://providers.delta.chat/mail-ru",
server: vec![
Server { protocol: Imap, socket: Ssl, hostname: "imap.mail.ru", port: 993, username_pattern: Email },
Server { protocol: Smtp, socket: Ssl, hostname: "smtp.mail.ru", port: 465, username_pattern: Email },
],
config_defaults: None,
strict_tls: true,
@@ -686,35 +668,6 @@ static P_MAIL_RU: Lazy<Provider> = Lazy::new(|| {
}
});
// mail2tor.md: mail2tor.com
static P_MAIL2TOR: Lazy<Provider> = Lazy::new(|| Provider {
id: "mail2tor",
status: Status::Preparation,
before_login_hint: "Tor is needed to connect to the email servers.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/mail2tor",
server: vec![
Server {
protocol: Imap,
socket: Plain,
hostname: "g77kjrad6bafzzyldqvffq6kxlsgphcygptxhnn4xlnktfgaqshilmyd.onion",
port: 143,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Plain,
hostname: "xc7tgk2c5onxni2wsy76jslfsitxjbbptejnqhw6gy2ft7khpevhc7ad.onion",
port: 25,
username_pattern: Email,
},
],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
// mailbox.org.md: mailbox.org, secure.mailbox.org
static P_MAILBOX_ORG: Lazy<Provider> = Lazy::new(|| Provider {
id: "mailbox.org",
@@ -820,35 +773,6 @@ static P_NAUTA_CU: Lazy<Provider> = Lazy::new(|| Provider {
oauth2_authorizer: None,
});
// naver.md: naver.com
static P_NAVER: Lazy<Provider> = Lazy::new(|| Provider {
id: "naver",
status: Status::Preparation,
before_login_hint: "Manually enabling IMAP/SMTP is required.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/naver",
server: vec![
Server {
protocol: Imap,
socket: Ssl,
hostname: "imap.naver.com",
port: 993,
username_pattern: Emaillocalpart,
},
Server {
protocol: Smtp,
socket: Starttls,
hostname: "smtp.naver.com",
port: 587,
username_pattern: Email,
},
],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
// outlook.com.md: hotmail.com, outlook.com, office365.com, outlook.com.tr, live.com
static P_OUTLOOK_COM: Lazy<Provider> = Lazy::new(|| Provider {
id: "outlook.com",
@@ -878,7 +802,7 @@ static P_OUTLOOK_COM: Lazy<Provider> = Lazy::new(|| Provider {
oauth2_authorizer: None,
});
// posteo.md: posteo.de, posteo.af, posteo.at, posteo.be, posteo.ch, posteo.cl, posteo.co, posteo.co.uk, posteo.com.br, posteo.cr, posteo.cz, posteo.dk, posteo.ee, posteo.es, posteo.eu, posteo.fi, posteo.gl, posteo.gr, posteo.hn, posteo.hr, posteo.hu, posteo.ie, posteo.in, posteo.is, posteo.it, posteo.jp, posteo.la, posteo.li, posteo.lt, posteo.lu, posteo.me, posteo.mx, posteo.my, posteo.net, posteo.nl, posteo.no, posteo.nz, posteo.org, posteo.pe, posteo.pl, posteo.pm, posteo.pt, posteo.ro, posteo.ru, posteo.se, posteo.sg, posteo.si, posteo.tn, posteo.uk, posteo.us
// posteo.md: posteo.de, posteo.af, posteo.at, posteo.be, posteo.ch, posteo.cl, posteo.co, posteo.co.uk, posteo.com.br, posteo.cr, posteo.cz, posteo.dk, posteo.ee, posteo.es, posteo.eu, posteo.fi, posteo.gl, posteo.gr, posteo.hn, posteo.hr, posteo.hu, posteo.ie, posteo.in, posteo.is, posteo.jp, posteo.la, posteo.li, posteo.lt, posteo.lu, posteo.me, posteo.mx, posteo.my, posteo.net, posteo.nl, posteo.no, posteo.nz, posteo.org, posteo.pe, posteo.pl, posteo.pm, posteo.pt, posteo.ro, posteo.ru, posteo.se, posteo.sg, posteo.si, posteo.tn, posteo.uk, posteo.us
static P_POSTEO: Lazy<Provider> = Lazy::new(|| Provider {
id: "posteo",
status: Status::Ok,
@@ -924,25 +848,6 @@ static P_PROTONMAIL: Lazy<Provider> = Lazy::new(|| {
}
});
// qq.md: qq.com, foxmail.com
static P_QQ: Lazy<Provider> = Lazy::new(|| {
Provider {
id: "qq",
status: Status::Preparation,
before_login_hint: "Manually enabling IMAP/SMTP and creating an app-specific password for Delta Chat are required.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/qq",
server: vec![
Server { protocol: Imap, socket: Ssl, hostname: "imap.qq.com", port: 993, username_pattern: Emaillocalpart },
Server { protocol: Smtp, socket: Starttls, hostname: "smtp.qq.com", port: 465, username_pattern: Email },
],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
}
});
// riseup.net.md: riseup.net
static P_RISEUP_NET: Lazy<Provider> = Lazy::new(|| Provider {
id: "riseup.net",
@@ -971,35 +876,6 @@ static P_ROGERS_COM: Lazy<Provider> = Lazy::new(|| Provider {
oauth2_authorizer: None,
});
// systemausfall.org.md: systemausfall.org, solidaris.me
static P_SYSTEMAUSFALL_ORG: Lazy<Provider> = Lazy::new(|| Provider {
id: "systemausfall.org",
status: Status::Ok,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/systemausfall-org",
server: vec![
Server {
protocol: Imap,
socket: Ssl,
hostname: "mail.systemausfall.org",
port: 993,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "mail.systemausfall.org",
port: 465,
username_pattern: Email,
},
],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
// systemli.org.md: systemli.org
static P_SYSTEMLI_ORG: Lazy<Provider> = Lazy::new(|| Provider {
id: "systemli.org",
@@ -1115,23 +991,6 @@ static P_TISCALI_IT: Lazy<Provider> = Lazy::new(|| Provider {
oauth2_authorizer: None,
});
// tutanota.md: tutanota.com, tutanota.de, tutamail.com, tuta.io, keemail.me
static P_TUTANOTA: Lazy<Provider> = Lazy::new(|| {
Provider {
id: "tutanota",
status: Status::Broken,
before_login_hint: "Tutanota does not offer the standard IMAP e-mail protocol, so you cannot log in with Delta Chat to Tutanota.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/tutanota",
server: vec![
],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
}
});
// ukr.net.md: ukr.net
static P_UKR_NET: Lazy<Provider> = Lazy::new(|| Provider {
id: "ukr.net",
@@ -1189,35 +1048,6 @@ static P_VFEMAIL: Lazy<Provider> = Lazy::new(|| Provider {
oauth2_authorizer: None,
});
// vivaldi.md: vivaldi.net
static P_VIVALDI: Lazy<Provider> = Lazy::new(|| Provider {
id: "vivaldi",
status: Status::Ok,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/vivaldi",
server: vec![
Server {
protocol: Imap,
socket: Starttls,
hostname: "imap.vivaldi.net",
port: 143,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Starttls,
hostname: "smtp.vivaldi.net",
port: 587,
username_pattern: Email,
},
],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
// vodafone.de.md: vodafone.de, vodafonemail.de
static P_VODAFONE_DE: Lazy<Provider> = Lazy::new(|| Provider {
id: "vodafone.de",
@@ -1315,25 +1145,6 @@ static P_YANDEX_RU: Lazy<Provider> = Lazy::new(|| Provider {
oauth2_authorizer: Some(Oauth2Authorizer::Yandex),
});
// yggmail.md: yggmail
static P_YGGMAIL: Lazy<Provider> = Lazy::new(|| {
Provider {
id: "yggmail",
status: Status::Preparation,
before_login_hint: "An Yggmail companion app needs to be installed on your device to access the Yggmail network.",
after_login_hint: "Make sure, the Yggmail companion app runs whenever you want to use this account. Note, that you usually cannot write from @yggmail addresses to normal e-mail-addresses (as @gmx.net). However, you can create another account in the normal e-mail-network for this purpose.",
overview_page: "https://providers.delta.chat/yggmail",
server: vec![
Server { protocol: Imap, socket: Plain, hostname: "localhost", port: 1143, username_pattern: Email },
Server { protocol: Smtp, socket: Plain, hostname: "localhost", port: 1025, username_pattern: Email },
],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
}
});
// ziggo.nl.md: ziggo.nl
static P_ZIGGO_NL: Lazy<Provider> = Lazy::new(|| Provider {
id: "ziggo.nl",
@@ -1363,38 +1174,8 @@ static P_ZIGGO_NL: Lazy<Provider> = Lazy::new(|| Provider {
oauth2_authorizer: None,
});
// zoho.md: zohomail.eu, zoho.com
static P_ZOHO: Lazy<Provider> = Lazy::new(|| Provider {
id: "zoho",
status: Status::Preparation,
before_login_hint: "To use Zoho Mail, you have to turn on IMAP in the Zoho Mail backend.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/zoho",
server: vec![
Server {
protocol: Imap,
socket: Ssl,
hostname: "imap.zoho.eu",
port: 993,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "smtp.zoho.eu",
port: 465,
username_pattern: Email,
},
],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>> = Lazy::new(|| {
[
("163.com", &*P_163),
("aktivix.org", &*P_AKTIVIX_ORG),
("aol.com", &*P_AOL),
("arcor.de", &*P_ARCOR_DE),
@@ -1440,15 +1221,12 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
("kontent.com", &*P_KONTENT_COM),
("mail.ru", &*P_MAIL_RU),
("inbox.ru", &*P_MAIL_RU),
("internet.ru", &*P_MAIL_RU),
("bk.ru", &*P_MAIL_RU),
("list.ru", &*P_MAIL_RU),
("mail2tor.com", &*P_MAIL2TOR),
("mailbox.org", &*P_MAILBOX_ORG),
("secure.mailbox.org", &*P_MAILBOX_ORG),
("mailo.com", &*P_MAILO_COM),
("nauta.cu", &*P_NAUTA_CU),
("naver.com", &*P_NAVER),
("hotmail.com", &*P_OUTLOOK_COM),
("outlook.com", &*P_OUTLOOK_COM),
("office365.com", &*P_OUTLOOK_COM),
@@ -1478,7 +1256,6 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
("posteo.ie", &*P_POSTEO),
("posteo.in", &*P_POSTEO),
("posteo.is", &*P_POSTEO),
("posteo.it", &*P_POSTEO),
("posteo.jp", &*P_POSTEO),
("posteo.la", &*P_POSTEO),
("posteo.li", &*P_POSTEO),
@@ -1506,26 +1283,16 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
("posteo.us", &*P_POSTEO),
("protonmail.com", &*P_PROTONMAIL),
("protonmail.ch", &*P_PROTONMAIL),
("qq.com", &*P_QQ),
("foxmail.com", &*P_QQ),
("riseup.net", &*P_RISEUP_NET),
("rogers.com", &*P_ROGERS_COM),
("systemausfall.org", &*P_SYSTEMAUSFALL_ORG),
("solidaris.me", &*P_SYSTEMAUSFALL_ORG),
("systemli.org", &*P_SYSTEMLI_ORG),
("t-online.de", &*P_T_ONLINE),
("magenta.de", &*P_T_ONLINE),
("testrun.org", &*P_TESTRUN),
("tiscali.it", &*P_TISCALI_IT),
("tutanota.com", &*P_TUTANOTA),
("tutanota.de", &*P_TUTANOTA),
("tutamail.com", &*P_TUTANOTA),
("tuta.io", &*P_TUTANOTA),
("keemail.me", &*P_TUTANOTA),
("ukr.net", &*P_UKR_NET),
("undernet.uy", &*P_UNDERNET_UY),
("vfemail.net", &*P_VFEMAIL),
("vivaldi.net", &*P_VIVALDI),
("vodafone.de", &*P_VODAFONE_DE),
("vodafonemail.de", &*P_VODAFONE_DE),
("web.de", &*P_WEB_DE),
@@ -1577,10 +1344,7 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
("yandex.ua", &*P_YANDEX_RU),
("ya.ru", &*P_YANDEX_RU),
("narod.ru", &*P_YANDEX_RU),
("yggmail", &*P_YGGMAIL),
("ziggo.nl", &*P_ZIGGO_NL),
("zohomail.eu", &*P_ZOHO),
("zoho.com", &*P_ZOHO),
]
.iter()
.copied()
@@ -1589,7 +1353,6 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> = Lazy::new(|| {
[
("163", &*P_163),
("aktivix.org", &*P_AKTIVIX_ORG),
("aol", &*P_AOL),
("arcor.de", &*P_ARCOR_DE),
@@ -1618,34 +1381,26 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
("kolst.com", &*P_KOLST_COM),
("kontent.com", &*P_KONTENT_COM),
("mail.ru", &*P_MAIL_RU),
("mail2tor", &*P_MAIL2TOR),
("mailbox.org", &*P_MAILBOX_ORG),
("mailo.com", &*P_MAILO_COM),
("nauta.cu", &*P_NAUTA_CU),
("naver", &*P_NAVER),
("outlook.com", &*P_OUTLOOK_COM),
("posteo", &*P_POSTEO),
("protonmail", &*P_PROTONMAIL),
("qq", &*P_QQ),
("riseup.net", &*P_RISEUP_NET),
("rogers.com", &*P_ROGERS_COM),
("systemausfall.org", &*P_SYSTEMAUSFALL_ORG),
("systemli.org", &*P_SYSTEMLI_ORG),
("t-online", &*P_T_ONLINE),
("testrun", &*P_TESTRUN),
("tiscali.it", &*P_TISCALI_IT),
("tutanota", &*P_TUTANOTA),
("ukr.net", &*P_UKR_NET),
("undernet.uy", &*P_UNDERNET_UY),
("vfemail", &*P_VFEMAIL),
("vivaldi", &*P_VIVALDI),
("vodafone.de", &*P_VODAFONE_DE),
("web.de", &*P_WEB_DE),
("yahoo", &*P_YAHOO),
("yandex.ru", &*P_YANDEX_RU),
("yggmail", &*P_YGGMAIL),
("ziggo.nl", &*P_ZIGGO_NL),
("zoho", &*P_ZOHO),
]
.iter()
.copied()
@@ -1653,4 +1408,4 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
});
pub static PROVIDER_UPDATED: Lazy<chrono::NaiveDate> =
Lazy::new(|| chrono::NaiveDate::from_ymd(2021, 8, 17));
Lazy::new(|| chrono::NaiveDate::from_ymd(2021, 4, 10));

View File

@@ -63,7 +63,7 @@ def process_data(data, file):
raise TypeError("no domains found")
for domain in data["domains"]:
domain = cleanstr(domain)
if domain == "" or domain.lower() != domain:
if domain == "" or domain.count(".") < 1 or domain.lower() != domain:
raise TypeError("bad domain: " + domain)
global domains_set
@@ -84,7 +84,7 @@ def process_data(data, file):
for s in data["server"]:
hostname = cleanstr(s.get("hostname", ""))
port = int(s.get("port", ""))
if hostname == "" or hostname.lower() != hostname or port <= 0:
if hostname == "" or hostname.count(".") < 1 or port <= 0:
raise TypeError("bad hostname or port")
protocol = s.get("type", "").upper()
@@ -96,7 +96,7 @@ def process_data(data, file):
raise TypeError("bad protocol")
socket = s.get("socket", "").upper()
if socket != "STARTTLS" and socket != "SSL" and socket != "PLAIN":
if socket != "STARTTLS" and socket != "SSL":
raise TypeError("bad socket")
username_pattern = s.get("username_pattern", "EMAIL").upper()

142
src/qr.rs
View File

@@ -1,4 +1,4 @@
//! # QR code module.
//! # QR code module
use anyhow::{bail, ensure, format_err, Error};
use once_cell::sync::Lazy;
@@ -6,18 +6,16 @@ use percent_encoding::percent_decode_str;
use serde::Deserialize;
use std::collections::BTreeMap;
use crate::chat::{self, get_chat_id_by_grpid, ChatIdBlocked};
use crate::chat::{self, ChatIdBlocked};
use crate::config::Config;
use crate::constants::Blocked;
use crate::contact::{addr_normalize, may_be_valid_addr, Contact, Origin};
use crate::context::Context;
use crate::dc_tools::time;
use crate::key::Fingerprint;
use crate::log::LogExt;
use crate::lot::{Lot, LotState};
use crate::message::Message;
use crate::peerstate::Peerstate;
use crate::token;
const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase
const DCACCOUNT_SCHEME: &str = "DCACCOUNT:";
@@ -88,7 +86,11 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Lot {
};
let fingerprint: Fingerprint = match fingerprint.parse() {
Ok(fp) => fp,
Err(err) => return err.context("Failed to parse fingerprint in QR code").into(),
Err(err) => {
return Error::new(err)
.context("Failed to parse fingerprint in QR code")
.into()
}
};
let param: BTreeMap<&str, &str> = fragment
@@ -157,17 +159,11 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Lot {
.map(|(id, _)| id)
.unwrap_or_default();
if let Ok(chat) = ChatIdBlocked::get_for_contact(context, lot.id, Blocked::Request)
if let Ok(chat) = ChatIdBlocked::get_for_contact(context, lot.id, Blocked::Deaddrop)
.await
.log_err(context, "Failed to create (new) chat for contact")
{
chat::add_info_msg(
context,
chat.id,
format!("{} verified.", peerstate.addr),
time(),
)
.await;
chat::add_info_msg(context, chat.id, format!("{} verified.", peerstate.addr)).await;
}
} else if let Some(addr) = addr {
lot.state = LotState::QrFprMismatch;
@@ -197,25 +193,6 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Lot {
lot.fingerprint = Some(fingerprint);
lot.invitenumber = invitenumber;
lot.auth = auth;
// scanning own qr-code offers withdraw/revive instead of secure-join
if context.is_self_addr(&addr).await.unwrap_or_default() {
if let Some(ref invitenumber) = lot.invitenumber {
lot.state =
if token::exists(context, token::Namespace::InviteNumber, &*invitenumber).await
{
if lot.state == LotState::QrAskVerifyContact {
LotState::QrWithdrawVerifyContact
} else {
LotState::QrWithdrawVerifyGroup
}
} else if lot.state == LotState::QrAskVerifyContact {
LotState::QrReviveVerifyContact
} else {
LotState::QrReviveVerifyGroup
}
}
}
} else {
return format_err!("Missing address").into();
}
@@ -302,8 +279,7 @@ async fn set_account_from_qr(context: &Context, qr: &str) -> Result<(), Error> {
}
pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<(), Error> {
let lot = check_qr(context, qr).await;
match lot.state {
match check_qr(context, qr).await.state {
LotState::QrAccount => set_account_from_qr(context, qr).await,
LotState::QrWebrtcInstance => {
let val = decode_webrtc_instance(context, qr).text2;
@@ -312,45 +288,6 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<(), Error
.await?;
Ok(())
}
LotState::QrWithdrawVerifyContact | LotState::QrWithdrawVerifyGroup => {
token::delete(
context,
token::Namespace::InviteNumber,
lot.invitenumber.unwrap_or_default().as_str(),
)
.await?;
token::delete(
context,
token::Namespace::Auth,
lot.auth.unwrap_or_default().as_str(),
)
.await?;
Ok(())
}
LotState::QrReviveVerifyContact | LotState::QrReviveVerifyGroup => {
let chat_id = if lot.state == LotState::QrReviveVerifyContact {
None
} else {
get_chat_id_by_grpid(context, &lot.text2.unwrap_or_default())
.await?
.map(|(chat_id, _protected, _blocked)| chat_id)
};
token::save(
context,
token::Namespace::InviteNumber,
chat_id,
&lot.invitenumber.unwrap_or_default(),
)
.await?;
token::save(
context,
token::Namespace::Auth,
chat_id,
&lot.auth.unwrap_or_default(),
)
.await?;
Ok(())
}
_ => bail!("qr code does not contain config: {}", qr),
}
}
@@ -504,12 +441,9 @@ mod tests {
use super::*;
use crate::aheader::EncryptPreference;
use crate::chat::{create_group_chat, ProtectionStatus};
use crate::key::DcKey;
use crate::peerstate::ToSave;
use crate::securejoin::dc_get_securejoin_qr;
use crate::test_utils::{alice_keypair, TestContext};
use anyhow::Result;
#[async_std::test]
async fn test_decode_http() {
@@ -786,62 +720,6 @@ mod tests {
assert_eq!(res.get_id(), 0);
}
#[async_std::test]
async fn test_withdraw_verfifycontact() -> Result<()> {
let alice = TestContext::new_alice().await;
let qr = dc_get_securejoin_qr(&alice, None).await.unwrap();
// scanning own verfify-contact code offers withdrawing
let check = check_qr(&alice, &qr).await;
assert_eq!(check.state, LotState::QrWithdrawVerifyContact);
assert!(check.text1.is_none());
set_config_from_qr(&alice, &qr).await?;
// scanning withdrawn verfify-contact code offers reviving
let check = check_qr(&alice, &qr).await;
assert_eq!(check.state, LotState::QrReviveVerifyContact);
assert!(check.text1.is_none());
set_config_from_qr(&alice, &qr).await?;
let check = check_qr(&alice, &qr).await;
assert_eq!(check.state, LotState::QrWithdrawVerifyContact);
// someone else always scans as ask-verify-contact
let bob = TestContext::new_bob().await;
let check = check_qr(&bob, &qr).await;
assert_eq!(check.state, LotState::QrAskVerifyContact);
assert!(check.text1.is_none());
assert!(set_config_from_qr(&bob, &qr).await.is_err());
Ok(())
}
#[async_std::test]
async fn test_withdraw_verfifygroup() -> Result<()> {
let alice = TestContext::new_alice().await;
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
let qr = dc_get_securejoin_qr(&alice, Some(chat_id)).await.unwrap();
// scanning own verfify-group code offers withdrawing
let check = check_qr(&alice, &qr).await;
assert_eq!(check.state, LotState::QrWithdrawVerifyGroup);
assert_eq!(check.text1, Some("foo".to_string()));
set_config_from_qr(&alice, &qr).await?;
// scanning withdrawn verfify-group code offers reviving
let check = check_qr(&alice, &qr).await;
assert_eq!(check.state, LotState::QrReviveVerifyGroup);
assert_eq!(check.text1, Some("foo".to_string()));
// someone else always scans as ask-verify-group
let bob = TestContext::new_bob().await;
let check = check_qr(&bob, &qr).await;
assert_eq!(check.state, LotState::QrAskVerifyGroup);
assert_eq!(check.text1, Some("foo".to_string()));
assert!(set_config_from_qr(&bob, &qr).await.is_err());
Ok(())
}
#[async_std::test]
async fn test_decode_account() {
let ctx = TestContext::new().await;

View File

@@ -1,102 +0,0 @@
//! # Support for IMAP QUOTA extension.
use anyhow::{anyhow, Result};
use async_imap::types::{Quota, QuotaResource};
use indexmap::IndexMap;
use crate::context::Context;
use crate::dc_tools::time;
use crate::imap::scan_folders::get_watched_folders;
use crate::imap::Imap;
use crate::job::{Action, Status};
use crate::param::Params;
use crate::{job, EventType};
/// warn about a nearly full mailbox after this usage percentage is reached.
/// quota icon is "yellow".
pub const QUOTA_WARN_THRESHOLD_PERCENTAGE: u64 = 80;
// warning is already issued at QUOTA_WARN_THRESHOLD_PERCENTAGE,
// this threshold only makes the quota icon "red".
pub const QUOTA_ERROR_THRESHOLD_PERCENTAGE: u64 = 99;
// if recent quota is older,
// it is re-fetched on dc_get_connectivity_html()
pub const QUOTA_MAX_AGE_SECONDS: i64 = 60;
#[derive(Debug)]
pub struct QuotaInfo {
/// Recently loaded quota information.
/// set to `Err()` if the provider does not support quota or on other errors,
/// set to `Ok()` for valid quota information.
/// Updated by `Action::UpdateRecentQuota`
pub(crate) recent: Result<IndexMap<String, Vec<QuotaResource>>>,
/// Timestamp when structure was modified.
pub(crate) modified: i64,
}
async fn get_unique_quota_roots_and_usage(
folders: Vec<String>,
imap: &mut Imap,
) -> Result<IndexMap<String, Vec<QuotaResource>>> {
let mut unique_quota_roots: IndexMap<String, Vec<QuotaResource>> = IndexMap::new();
for folder in folders {
let (quota_roots, quotas) = &imap.get_quota_roots(&folder).await?;
// if there are new quota roots found in this imap folder, add them to the list
for qr_entries in quota_roots {
for quota_root_name in &qr_entries.quota_root_names {
// the quota for that quota root
let quota: Quota = quotas
.iter()
.find(|q| &q.root_name == quota_root_name)
.cloned()
.ok_or_else(|| anyhow!("quota_root should have a quota"))?;
// replace old quotas, because between fetching quotaroots for folders,
// messages could be recieved and so the usage could have been changed
*unique_quota_roots
.entry(quota_root_name.clone())
.or_insert(vec![]) = quota.resources;
}
}
}
Ok(unique_quota_roots)
}
impl Context {
// Adds a job to update `quota.recent`
pub(crate) async fn schedule_quota_update(&self) {
job::kill_action(self, Action::UpdateRecentQuota).await;
job::add(
self,
job::Job::new(Action::UpdateRecentQuota, 0, Params::new(), 0),
)
.await;
}
/// Updates `quota.recent`, sets `quota.modified` to the current time
/// and emits an event to let the UIs update connectivity view.
///
/// Called in response to `Action::UpdateRecentQuota`.
pub(crate) async fn update_recent_quota(&self, imap: &mut Imap) -> Status {
if let Err(err) = imap.prepare(self).await {
warn!(self, "could not connect: {:?}", err);
return Status::RetryNow;
}
let quota = if imap.can_check_quota() {
let folders = get_watched_folders(self).await;
get_unique_quota_roots_and_usage(folders, imap).await
} else {
Err(anyhow!("Quota not supported by your provider."))
};
*self.quota.write().await = Some(QuotaInfo {
recent: quota,
modified: time(),
});
self.emit_event(EventType::ConnectivityChanged);
Status::Finished(Ok(()))
}
}

View File

@@ -1,4 +1,3 @@
use anyhow::{bail, Result};
use async_std::prelude::*;
use async_std::{
channel::{self, Receiver, Sender},
@@ -13,10 +12,6 @@ use crate::job::{self, Thread};
use crate::message::MsgId;
use crate::smtp::Smtp;
use self::connectivity::ConnectivityStore;
pub(crate) mod connectivity;
pub(crate) struct StopToken;
/// Job and connection scheduler.
@@ -39,16 +34,7 @@ pub(crate) enum Scheduler {
impl Context {
/// Indicate that the network likely has come back.
pub async fn maybe_network(&self) {
let lock = self.scheduler.read().await;
lock.maybe_network().await;
connectivity::idle_interrupted(lock).await;
}
/// Indicate that the network likely is lost.
pub async fn maybe_network_lost(&self) {
let lock = self.scheduler.read().await;
lock.maybe_network_lost().await;
connectivity::maybe_network_lost(self, lock).await;
self.scheduler.read().await.maybe_network().await;
}
pub(crate) async fn interrupt_inbox(&self, info: InterruptInfo) {
@@ -120,9 +106,6 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
} else {
if let Err(err) = connection.scan_folders(&ctx).await {
warn!(ctx, "{}", err);
connection.connectivity.set_err(&ctx, err).await;
} else {
connection.connectivity.set_not_configured(&ctx).await;
}
connection.fake_idle(&ctx, None).await
};
@@ -147,25 +130,27 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
async fn fetch(ctx: &Context, connection: &mut Imap) {
match ctx.get_config(Config::ConfiguredInboxFolder).await {
Ok(Some(watch_folder)) => {
if let Err(err) = connection.prepare(ctx).await {
warn!(ctx, "Could not connect: {}", err);
if let Err(err) = connection.connect_configured(ctx).await {
error_network!(ctx, "{}", err);
return;
}
// fetch
if let Err(err) = connection.fetch(ctx, &watch_folder).await {
connection.trigger_reconnect(ctx).await;
connection.trigger_reconnect();
warn!(ctx, "{:#}", err);
}
}
Ok(None) => {
info!(ctx, "Can not fetch inbox folder, not set");
warn!(ctx, "Can not fetch inbox folder, not set");
connection.fake_idle(ctx, None).await;
}
Err(err) => {
warn!(
ctx,
"Can not fetch inbox folder, failed to get config: {:?}", err
);
connection.fake_idle(ctx, None).await;
}
}
}
@@ -174,16 +159,15 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
match ctx.get_config(folder).await {
Ok(Some(watch_folder)) => {
// connect and fake idle if unable to connect
if let Err(err) = connection.prepare(ctx).await {
if let Err(err) = connection.connect_configured(ctx).await {
warn!(ctx, "imap connection failed: {}", err);
return connection.fake_idle(ctx, Some(watch_folder)).await;
}
// fetch
if let Err(err) = connection.fetch(ctx, &watch_folder).await {
connection.trigger_reconnect(ctx).await;
connection.trigger_reconnect();
warn!(ctx, "{:#}", err);
return InterruptInfo::new(false, None);
}
if folder == Config::ConfiguredInboxFolder {
@@ -195,25 +179,22 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
}
}
connection.connectivity.set_connected(ctx).await;
// idle
if connection.can_idle() {
match connection.idle(ctx, Some(watch_folder)).await {
Ok(v) => v,
Err(err) => {
connection.trigger_reconnect(ctx).await;
connection
.idle(ctx, Some(watch_folder))
.await
.unwrap_or_else(|err| {
connection.trigger_reconnect();
warn!(ctx, "{}", err);
InterruptInfo::new(false, None)
}
}
})
} else {
connection.fake_idle(ctx, Some(watch_folder)).await
}
}
Ok(None) => {
connection.connectivity.set_not_configured(ctx).await;
info!(ctx, "Can not watch {} folder, not set", folder);
warn!(ctx, "Can not watch {} folder, not set", folder);
connection.fake_idle(ctx, None).await
}
Err(err) => {
@@ -298,11 +279,6 @@ async fn smtp_loop(ctx: Context, started: Sender<()>, smtp_handlers: SmtpConnect
None => {
// Fake Idle
info!(ctx, "smtp fake idle - started");
match &connection.last_send_error {
None => connection.connectivity.set_connected(&ctx).await,
Some(err) => connection.connectivity.set_err(&ctx, err).await,
}
interrupt_info = idle_interrupt_receiver.recv().await.unwrap_or_default();
info!(ctx, "smtp fake idle - interrupted")
}
@@ -325,11 +301,11 @@ async fn smtp_loop(ctx: Context, started: Sender<()>, smtp_handlers: SmtpConnect
impl Scheduler {
/// Start the scheduler, panics if it is already running.
pub async fn start(&mut self, ctx: Context) -> Result<()> {
let (mvbox, mvbox_handlers) = ImapConnectionState::new(&ctx).await?;
let (sentbox, sentbox_handlers) = ImapConnectionState::new(&ctx).await?;
pub async fn start(&mut self, ctx: Context) {
let (mvbox, mvbox_handlers) = ImapConnectionState::new();
let (sentbox, sentbox_handlers) = ImapConnectionState::new();
let (smtp, smtp_handlers) = SmtpConnectionState::new();
let (inbox, inbox_handlers) = ImapConnectionState::new(&ctx).await?;
let (inbox, inbox_handlers) = ImapConnectionState::new();
let (inbox_start_send, inbox_start_recv) = channel::bounded(1);
let (mvbox_start_send, mvbox_start_recv) = channel::bounded(1);
@@ -345,7 +321,11 @@ impl Scheduler {
}))
};
if ctx.get_config_bool(Config::MvboxWatch).await? {
if ctx
.get_config_bool(Config::MvboxWatch)
.await
.unwrap_or_default()
{
let ctx = ctx.clone();
mvbox_handle = Some(task::spawn(async move {
simple_imap_loop(
@@ -361,14 +341,13 @@ impl Scheduler {
.send(())
.await
.expect("mvbox start send, missing receiver");
mvbox_handlers
.connection
.connectivity
.set_not_configured(&ctx)
.await
}
if ctx.get_config_bool(Config::SentboxWatch).await? {
if ctx
.get_config_bool(Config::SentboxWatch)
.await
.unwrap_or_default()
{
let ctx = ctx.clone();
sentbox_handle = Some(task::spawn(async move {
simple_imap_loop(
@@ -384,11 +363,6 @@ impl Scheduler {
.send(())
.await
.expect("sentbox start send, missing receiver");
sentbox_handlers
.connection
.connectivity
.set_not_configured(&ctx)
.await
}
let smtp_handle = {
@@ -417,11 +391,10 @@ impl Scheduler {
.try_join(smtp_start_recv.recv())
.await
{
bail!("failed to start scheduler: {}", err);
error!(ctx, "failed to start scheduler: {}", err);
}
info!(ctx, "scheduler is running");
Ok(())
}
async fn maybe_network(&self) {
@@ -436,18 +409,6 @@ impl Scheduler {
.await;
}
async fn maybe_network_lost(&self) {
if !self.is_running() {
return;
}
self.interrupt_inbox(InterruptInfo::new(false, None))
.join(self.interrupt_mvbox(InterruptInfo::new(false, None)))
.join(self.interrupt_sentbox(InterruptInfo::new(false, None)))
.join(self.interrupt_smtp(InterruptInfo::new(false, None)))
.await;
}
async fn interrupt_inbox(&self, info: InterruptInfo) {
if let Scheduler::Running { ref inbox, .. } = self {
inbox.interrupt(info).await;
@@ -553,8 +514,6 @@ struct ConnectionState {
stop_sender: Sender<()>,
/// Channel to interrupt idle.
idle_interrupt_sender: Sender<InterruptInfo>,
/// Mutex to pass connectivity info between IMAP/SMTP threads and the API
connectivity: ConnectivityStore,
}
impl ConnectionState {
@@ -597,7 +556,6 @@ impl SmtpConnectionState {
shutdown_receiver,
stop_sender,
idle_interrupt_sender,
connectivity: handlers.connection.connectivity.clone(),
};
let conn = SmtpConnectionState { state };
@@ -630,13 +588,13 @@ pub(crate) struct ImapConnectionState {
impl ImapConnectionState {
/// Construct a new connection.
async fn new(context: &Context) -> Result<(Self, ImapConnectionHandlers)> {
fn new() -> (Self, ImapConnectionHandlers) {
let (stop_sender, stop_receiver) = channel::bounded(1);
let (shutdown_sender, shutdown_receiver) = channel::bounded(1);
let (idle_interrupt_sender, idle_interrupt_receiver) = channel::bounded(1);
let handlers = ImapConnectionHandlers {
connection: Imap::new_configured(context, idle_interrupt_receiver).await?,
connection: Imap::new(idle_interrupt_receiver),
stop_receiver,
shutdown_sender,
};
@@ -645,12 +603,11 @@ impl ImapConnectionState {
shutdown_receiver,
stop_sender,
idle_interrupt_sender,
connectivity: handlers.connection.connectivity.clone(),
};
let conn = ImapConnectionState { state };
Ok((conn, handlers))
(conn, handlers)
}
/// Interrupt any form of idle.

View File

@@ -1,543 +0,0 @@
use core::fmt;
use std::{ops::Deref, sync::Arc};
use async_std::sync::{Mutex, RwLockReadGuard};
use crate::dc_tools::time;
use crate::events::EventType;
use crate::quota::{
QUOTA_ERROR_THRESHOLD_PERCENTAGE, QUOTA_MAX_AGE_SECONDS, QUOTA_WARN_THRESHOLD_PERCENTAGE,
};
use crate::{config::Config, dc_tools, scheduler::Scheduler};
use crate::{context::Context, log::LogExt};
use anyhow::{anyhow, Result};
use humansize::{file_size_opts, FileSize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumProperty, PartialOrd, Ord)]
pub enum Connectivity {
NotConnected = 1000,
Connecting = 2000,
/// Fetching or sending messages
Working = 3000,
Connected = 4000,
}
// The order of the connectivities is important: worse connectivities (i.e. those at
// the top) take priority. This means that e.g. if any folder has an error - usually
// because there is no internet connection - the connectivity for the whole
// account will be `Notconnected`.
#[derive(Debug, Clone, PartialEq, Eq, EnumProperty, PartialOrd)]
enum DetailedConnectivity {
Error(String),
Uninitialized,
Connecting,
Working,
InterruptingIdle,
Connected,
/// The folder was configured not to be watched or configured_*_folder is not set
NotConfigured,
}
impl Default for DetailedConnectivity {
fn default() -> Self {
DetailedConnectivity::Uninitialized
}
}
impl DetailedConnectivity {
fn to_basic(&self) -> Option<Connectivity> {
match self {
DetailedConnectivity::Error(_) => Some(Connectivity::NotConnected),
DetailedConnectivity::Uninitialized => Some(Connectivity::NotConnected),
DetailedConnectivity::Connecting => Some(Connectivity::Connecting),
DetailedConnectivity::Working => Some(Connectivity::Working),
DetailedConnectivity::InterruptingIdle => Some(Connectivity::Connected),
DetailedConnectivity::Connected => Some(Connectivity::Connected),
// Just don't return a connectivity, probably the folder is configured not to be
// watched or there is e.g. no "Sent" folder, so we are not interested in it
DetailedConnectivity::NotConfigured => None,
}
}
fn to_icon(&self) -> String {
match self {
DetailedConnectivity::Error(_)
| DetailedConnectivity::Uninitialized
| DetailedConnectivity::NotConfigured => "<span class=\"red dot\"></span>".to_string(),
DetailedConnectivity::Connecting => "<span class=\"yellow dot\"></span>".to_string(),
DetailedConnectivity::Working
| DetailedConnectivity::InterruptingIdle
| DetailedConnectivity::Connected => "<span class=\"green dot\"></span>".to_string(),
}
}
fn to_string_imap(&self, _context: &Context) -> String {
match self {
DetailedConnectivity::Error(e) => format!("Error: {}", e),
DetailedConnectivity::Uninitialized => "Not started".to_string(),
DetailedConnectivity::Connecting => "Connecting…".to_string(),
DetailedConnectivity::Working => "Getting new messages…".to_string(),
DetailedConnectivity::InterruptingIdle | DetailedConnectivity::Connected => {
"Connected".to_string()
}
DetailedConnectivity::NotConfigured => "Not configured".to_string(),
}
}
fn to_string_smtp(&self, _context: &Context) -> String {
match self {
DetailedConnectivity::Error(e) => format!("Error: {}", e),
DetailedConnectivity::Uninitialized => {
"(You did not try to send a message recently)".to_string()
}
DetailedConnectivity::Connecting => "Connecting…".to_string(),
DetailedConnectivity::Working => "Sending…".to_string(),
// We don't know any more than that the last message was sent successfully;
// since sending the last message, connectivity could have changed, which we don't notice
// until another message is sent
DetailedConnectivity::InterruptingIdle | DetailedConnectivity::Connected => {
"Your last message was sent successfully".to_string()
}
DetailedConnectivity::NotConfigured => "Not configured".to_string(),
}
}
fn all_work_done(&self) -> bool {
match self {
DetailedConnectivity::Error(_) => true,
DetailedConnectivity::Uninitialized => false,
DetailedConnectivity::Connecting => false,
DetailedConnectivity::Working => false,
DetailedConnectivity::InterruptingIdle => false,
DetailedConnectivity::Connected => true,
DetailedConnectivity::NotConfigured => true,
}
}
}
#[derive(Clone, Default)]
pub(crate) struct ConnectivityStore(Arc<Mutex<DetailedConnectivity>>);
impl ConnectivityStore {
async fn set(&self, context: &Context, v: DetailedConnectivity) {
{
*self.0.lock().await = v;
}
context.emit_event(EventType::ConnectivityChanged);
}
pub(crate) async fn set_err(&self, context: &Context, e: impl ToString) {
self.set(context, DetailedConnectivity::Error(e.to_string()))
.await;
}
pub(crate) async fn set_connecting(&self, context: &Context) {
self.set(context, DetailedConnectivity::Connecting).await;
}
pub(crate) async fn set_working(&self, context: &Context) {
self.set(context, DetailedConnectivity::Working).await;
}
pub(crate) async fn set_connected(&self, context: &Context) {
self.set(context, DetailedConnectivity::Connected).await;
}
pub(crate) async fn set_not_configured(&self, context: &Context) {
self.set(context, DetailedConnectivity::NotConfigured).await;
}
async fn get_detailed(&self) -> DetailedConnectivity {
self.0.lock().await.deref().clone()
}
async fn get_basic(&self) -> Option<Connectivity> {
self.0.lock().await.to_basic()
}
async fn get_all_work_done(&self) -> bool {
self.0.lock().await.all_work_done()
}
}
/// Set all folder states to InterruptingIdle in case they were `Connected` before.
/// Called during `dc_maybe_network()` to make sure that `dc_accounts_all_work_done()`
/// returns false immediately after `dc_maybe_network()`.
pub(crate) async fn idle_interrupted(scheduler: RwLockReadGuard<'_, Scheduler>) {
let [inbox, mvbox, sentbox] = match &*scheduler {
Scheduler::Running {
inbox,
mvbox,
sentbox,
..
} => [
inbox.state.connectivity.clone(),
mvbox.state.connectivity.clone(),
sentbox.state.connectivity.clone(),
],
Scheduler::Stopped => return,
};
drop(scheduler);
let mut connectivity_lock = inbox.0.lock().await;
// For the inbox, we also have to set the connectivity to InterruptingIdle if it was
// NotConfigured before: If all folders are NotConfigured, dc_get_connectivity()
// returns Connected. But after dc_maybe_network(), dc_get_connectivity() must not
// return Connected until DC is completely done with fetching folders; this also
// includes scan_folders() which happens on the inbox thread.
if *connectivity_lock == DetailedConnectivity::Connected
|| *connectivity_lock == DetailedConnectivity::NotConfigured
{
*connectivity_lock = DetailedConnectivity::InterruptingIdle;
}
drop(connectivity_lock);
for state in &[&mvbox, &sentbox] {
let mut connectivity_lock = state.0.lock().await;
if *connectivity_lock == DetailedConnectivity::Connected {
*connectivity_lock = DetailedConnectivity::InterruptingIdle;
}
}
// No need to send ConnectivityChanged, the user-facing connectivity doesn't change because
// of what we do here.
}
/// Set the connectivity to "Not connected" after a call to dc_maybe_network_lost().
/// If we did not do this, the connectivity would stay "Connected" for quite a long time
/// after `maybe_network_lost()` was called.
pub(crate) async fn maybe_network_lost(
context: &Context,
scheduler: RwLockReadGuard<'_, Scheduler>,
) {
let stores = match &*scheduler {
Scheduler::Running {
inbox,
mvbox,
sentbox,
..
} => [
inbox.state.connectivity.clone(),
mvbox.state.connectivity.clone(),
sentbox.state.connectivity.clone(),
],
Scheduler::Stopped => return,
};
drop(scheduler);
for store in &stores {
let mut connectivity_lock = store.0.lock().await;
if !matches!(
*connectivity_lock,
DetailedConnectivity::Uninitialized
| DetailedConnectivity::Error(_)
| DetailedConnectivity::NotConfigured,
) {
*connectivity_lock = DetailedConnectivity::Error("Connection lost".to_string());
}
drop(connectivity_lock);
}
context.emit_event(EventType::ConnectivityChanged);
}
impl fmt::Debug for ConnectivityStore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(guard) = self.0.try_lock() {
write!(f, "ConnectivityStore {:?}", &*guard)
} else {
write!(f, "ConnectivityStore [LOCKED]")
}
}
}
impl Context {
/// Get the current connectivity, i.e. whether the device is connected to the IMAP server.
/// One of:
/// - DC_CONNECTIVITY_NOT_CONNECTED (1000-1999): Show e.g. the string "Not connected" or a red dot
/// - DC_CONNECTIVITY_CONNECTING (2000-2999): Show e.g. the string "Connecting…" or a yellow dot
/// - DC_CONNECTIVITY_WORKING (3000-3999): Show e.g. the string "Getting new messages" or a spinning wheel
/// - DC_CONNECTIVITY_CONNECTED (>=4000): Show e.g. the string "Connected" or a green dot
///
/// We don't use exact values but ranges here so that we can split up
/// states into multiple states in the future.
///
/// Meant as a rough overview that can be shown
/// e.g. in the title of the main screen.
///
/// If the connectivity changes, a DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
pub async fn get_connectivity(&self) -> Connectivity {
let lock = self.scheduler.read().await;
let stores: Vec<_> = match &*lock {
Scheduler::Running {
inbox,
mvbox,
sentbox,
..
} => [&inbox.state, &mvbox.state, &sentbox.state]
.iter()
.map(|state| state.connectivity.clone())
.collect(),
Scheduler::Stopped => return Connectivity::NotConnected,
};
drop(lock);
let mut connectivities = Vec::new();
for s in stores {
if let Some(connectivity) = s.get_basic().await {
connectivities.push(connectivity);
}
}
connectivities
.into_iter()
.min()
.unwrap_or(Connectivity::Connected)
}
/// Get an overview of the current connectivity, and possibly more statistics.
/// Meant to give the user more insight about the current status than
/// the basic connectivity info returned by dc_get_connectivity(); show this
/// e.g., if the user taps on said basic connectivity info.
///
/// If this page changes, a DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
///
/// This comes as an HTML from the core so that we can easily improve it
/// and the improvement instantly reaches all UIs.
pub async fn get_connectivity_html(&self) -> Result<String> {
let mut ret = r#"<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="initial-scale=1.0" />
<style>
ul {
list-style-type: none;
padding-left: 1em;
}
.dot {
height: 0.9em; width: 0.9em;
border: 1px solid #888;
border-radius: 50%;
display: inline-block;
position: relative; left: -0.1em; top: 0.1em;
}
.bar {
width: 90%;
border: 1px solid #888;
border-radius: .5em;
margin-top: .2em;
margin-bottom: 1em;
position: relative; left: -0.2em;
}
.progress {
min-width:1.8em;
height: 1em;
border-radius: .45em;
color: white;
text-align: center;
padding-bottom: 2px;
}
.red {
background-color: #f33b2d;
}
.green {
background-color: #34c759;
}
.yellow {
background-color: #fdc625;
}
</style>
</head>
<body>"#
.to_string();
let lock = self.scheduler.read().await;
let (folders_states, smtp) = match &*lock {
Scheduler::Running {
inbox,
mvbox,
sentbox,
smtp,
..
} => (
[
(
Config::ConfiguredInboxFolder,
Config::InboxWatch,
inbox.state.connectivity.clone(),
),
(
Config::ConfiguredMvboxFolder,
Config::MvboxWatch,
mvbox.state.connectivity.clone(),
),
(
Config::ConfiguredSentboxFolder,
Config::SentboxWatch,
sentbox.state.connectivity.clone(),
),
],
smtp.state.connectivity.clone(),
),
Scheduler::Stopped => {
return Err(anyhow!("Not started"));
}
};
drop(lock);
ret += "<h3>Incoming messages</h3><ul>";
for (folder, watch, state) in &folders_states {
let w = self.get_config(*watch).await.ok_or_log(self);
let mut folder_added = false;
if w.flatten() == Some("1".to_string()) {
let f = self.get_config(*folder).await.ok_or_log(self).flatten();
if let Some(foldername) = f {
let detailed = &state.get_detailed().await;
ret += "<li>";
ret += &*detailed.to_icon();
ret += " <b>";
ret += &*escaper::encode_minimal(&foldername);
ret += ":</b> ";
ret += &*escaper::encode_minimal(&*detailed.to_string_imap(self));
ret += "</li>";
folder_added = true;
}
}
if !folder_added && folder == &Config::ConfiguredInboxFolder {
let detailed = &state.get_detailed().await;
if let DetailedConnectivity::Error(_) = detailed {
// On the inbox thread, we also do some other things like scan_folders and run jobs
// so, maybe, the inbox is not watched, but something else went wrong
ret += "<li>";
ret += &*detailed.to_icon();
ret += " ";
ret += &*escaper::encode_minimal(&detailed.to_string_imap(self));
ret += "</li>";
}
}
}
ret += "</ul>";
ret += "<h3>Outgoing messages</h3><ul><li>";
let detailed = smtp.get_detailed().await;
ret += &*detailed.to_icon();
ret += " ";
ret += &*escaper::encode_minimal(&detailed.to_string_smtp(self));
ret += "</li></ul>";
let domain = dc_tools::EmailAddress::new(
&self
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default(),
)?
.domain;
ret += &format!("<h3>Storage on {}</h3><ul>", domain);
let quota = self.quota.read().await;
if let Some(quota) = &*quota {
match &quota.recent {
Ok(quota) => {
let roots_cnt = quota.len();
for (root_name, resources) in quota {
use async_imap::types::QuotaResourceName::*;
for resource in resources {
ret += "<li>";
// root name is empty eg. for gmail and redundant eg. for riseup.
// therefore, use it only if there are really several roots.
if roots_cnt > 1 && !root_name.is_empty() {
ret +=
&format!("<b>{}:</b> ", &*escaper::encode_minimal(root_name));
} else {
info!(self, "connectivity: root name hidden: \"{}\"", root_name);
}
ret += &match &resource.name {
Atom(resource_name) => {
format!(
"<b>{}:</b> {} of {} used",
&*escaper::encode_minimal(resource_name),
resource.usage.to_string(),
resource.limit.to_string(),
)
}
Message => {
format!(
"<b>Messages:</b> {} of {} used",
resource.usage.to_string(),
resource.limit.to_string(),
)
}
Storage => {
// do not use a special title needed for "Storage":
// - it is usually shown directly under the "Storage" headline
// - by the units "1 MB of 10 MB used" there is some difference to eg. "Messages: 1 of 10 used"
// - the string is not longer than the other strings that way (minus title, plus units) -
// additional linebreaks on small displays are unlikely therefore
// - most times, this is the only item anyway
let usage = (resource.usage * 1024)
.file_size(file_size_opts::BINARY)
.unwrap_or_default();
let limit = (resource.limit * 1024)
.file_size(file_size_opts::BINARY)
.unwrap_or_default();
format!("{} of {} used", usage, limit)
}
};
let percent = resource.get_usage_percentage();
let color = if percent >= QUOTA_ERROR_THRESHOLD_PERCENTAGE {
"red"
} else if percent >= QUOTA_WARN_THRESHOLD_PERCENTAGE {
"yellow"
} else {
"green"
};
ret += &format!("<div class=\"bar\"><div class=\"progress {}\" style=\"width: {}%\">{}%</div></div>", color, percent, percent);
ret += "</li>";
}
}
}
Err(e) => {
ret += format!("<li>{}</li>", e).as_str();
}
}
if quota.modified + QUOTA_MAX_AGE_SECONDS < time() {
self.schedule_quota_update().await;
}
} else {
ret += "<li>One moment...</li>";
self.schedule_quota_update().await;
}
ret += "</ul>";
ret += "</body></html>\n";
Ok(ret)
}
pub async fn all_work_done(&self) -> bool {
let lock = self.scheduler.read().await;
let stores: Vec<_> = match &*lock {
Scheduler::Running {
inbox,
mvbox,
sentbox,
smtp,
..
} => [&inbox.state, &mvbox.state, &sentbox.state, &smtp.state]
.iter()
.map(|state| state.connectivity.clone())
.collect(),
Scheduler::Stopped => return false,
};
drop(lock);
for s in &stores {
if !s.get_all_work_done().await {
return false;
}
}
true
}
}

View File

@@ -1,9 +1,9 @@
//! Verified contact protocol implementation as [specified by countermitm project](https://countermitm.readthedocs.io/en/stable/new.html#setup-contact-protocol).
//! Verified contact protocol implementation as [specified by countermitm project](https://countermitm.readthedocs.io/en/stable/new.html#setup-contact-protocol)
use std::convert::TryFrom;
use std::time::{Duration, Instant};
use anyhow::{anyhow, bail, Context as _, Error, Result};
use anyhow::{bail, Context as _, Error, Result};
use async_std::channel::Receiver;
use async_std::sync::Mutex;
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
@@ -14,11 +14,10 @@ use crate::config::Config;
use crate::constants::{Blocked, Viewtype, DC_CONTACT_ID_LAST_SPECIAL};
use crate::contact::{Contact, Origin, VerifiedStatus};
use crate::context::Context;
use crate::dc_tools::time;
use crate::e2ee::ensure_secret_key_exists;
use crate::events::EventType;
use crate::headerdef::HeaderDef;
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
use crate::key::{self, DcKey, Fingerprint, SignedPublicKey};
use crate::message::Message;
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::param::Param;
@@ -254,17 +253,17 @@ async fn get_self_fingerprint(context: &Context) -> Option<Fingerprint> {
#[derive(Debug, thiserror::Error)]
pub enum JoinError {
#[error("Unknown QR-code: {0}")]
#[error("Unknown QR-code")]
QrCode(#[from] QrError),
#[error("A setup-contact/secure-join protocol is already running")]
AlreadyRunning,
#[error("An \"ongoing\" process is already running")]
OngoingRunning,
#[error("Failed to send handshake message: {0}")]
#[error("Failed to send handshake message")]
SendMessage(#[from] SendMsgError),
// Note that this can currently only occur if there is a bug in the QR/Lot code as this
// is supposed to create a contact for us.
#[error("Unknown contact (this is a bug): {0}")]
#[error("Unknown contact (this is a bug)")]
UnknownContact(#[source] anyhow::Error),
// Note that this can only occur if we failed to create the chat correctly.
#[error("Ongoing sender dropped (this is a bug)")]
@@ -328,14 +327,14 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
let start = Instant::now();
let chatid = loop {
{
match chat::get_chat_id_by_grpid(context, &group_id).await? {
Some((chatid, _is_protected, _blocked)) => break chatid,
None => {
match chat::get_chat_id_by_grpid(context, &group_id).await {
Ok((chatid, _is_protected, _blocked)) => break chatid,
Err(err) => {
if start.elapsed() > Duration::from_secs(7) {
context.free_ongoing().await;
return Err(JoinError::Other(anyhow!(
"Ongoing sender dropped (this is a bug)"
)));
return Err(err
.context("Ongoing sender dropped (this is a bug)")
.into());
}
}
}
@@ -356,6 +355,12 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
#[error("Failed sending handshake message")]
pub struct SendMsgError(#[from] anyhow::Error);
impl From<key::Error> for SendMsgError {
fn from(source: key::Error) -> Self {
Self(anyhow::Error::new(source))
}
}
async fn send_handshake_msg(
context: &Context,
contact_chat_id: ChatId,
@@ -484,7 +489,7 @@ pub(crate) async fn handle_securejoin_handshake(
return Err(Error::msg("Can not be called with special contact ID"));
}
let step = mime_message
.get_header(HeaderDef::SecureJoin)
.get(HeaderDef::SecureJoin)
.context("Not a Secure-Join message")?;
info!(
@@ -502,7 +507,7 @@ pub(crate) async fn handle_securejoin_handshake(
)
})?;
if chat.blocked != Blocked::Not {
chat.id.unblock(context).await?;
chat.id.unblock(context).await;
}
chat.id
};
@@ -520,7 +525,7 @@ pub(crate) async fn handle_securejoin_handshake(
// 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 mime_message.get_header(HeaderDef::SecureJoinInvitenumber) {
let invitenumber = match mime_message.get(HeaderDef::SecureJoinInvitenumber) {
Some(n) => n,
None => {
warn!(context, "Secure-join denied (invitenumber missing)");
@@ -576,19 +581,19 @@ pub(crate) async fn handle_securejoin_handshake(
==========================================================*/
// verify that Secure-Join-Fingerprint:-header matches the fingerprint of Bob
let fingerprint: Fingerprint =
match mime_message.get_header(HeaderDef::SecureJoinFingerprint) {
Some(fp) => fp.parse()?,
None => {
could_not_establish_secure_connection(
context,
contact_chat_id,
"Fingerprint not provided.",
)
.await?;
return Ok(HandshakeMessage::Ignore);
}
};
let fingerprint: Fingerprint = match mime_message.get(HeaderDef::SecureJoinFingerprint)
{
Some(fp) => fp.parse()?,
None => {
could_not_establish_secure_connection(
context,
contact_chat_id,
"Fingerprint not provided.",
)
.await?;
return Ok(HandshakeMessage::Ignore);
}
};
if !encrypted_and_signed(context, mime_message, Some(&fingerprint)) {
could_not_establish_secure_connection(
context,
@@ -609,7 +614,7 @@ pub(crate) async fn handle_securejoin_handshake(
}
info!(context, "Fingerprint verified.",);
// verify that the `Secure-Join-Auth:`-header matches the secret written to the QR code
let auth_0 = match mime_message.get_header(HeaderDef::SecureJoinAuth) {
let auth_0 = match mime_message.get(HeaderDef::SecureJoinAuth) {
Some(auth) => auth,
None => {
could_not_establish_secure_connection(
@@ -644,15 +649,15 @@ pub(crate) async fn handle_securejoin_handshake(
// the vg-member-added message is special:
// this is a normal Chat-Group-Member-Added message
// with an additional Secure-Join header
let field_grpid = match mime_message.get_header(HeaderDef::SecureJoinGroup) {
let field_grpid = match mime_message.get(HeaderDef::SecureJoinGroup) {
Some(s) => s.as_str(),
None => {
warn!(context, "Missing Secure-Join-Group header");
return Ok(HandshakeMessage::Ignore);
}
};
match chat::get_chat_id_by_grpid(context, field_grpid).await? {
Some((group_chat_id, _, _)) => {
match chat::get_chat_id_by_grpid(context, field_grpid).await {
Ok((group_chat_id, _, _)) => {
if let Err(err) =
chat::add_contact_to_chat_ex(context, group_chat_id, contact_id, true)
.await
@@ -660,7 +665,12 @@ pub(crate) async fn handle_securejoin_handshake(
error!(context, "failed to add contact: {}", err);
}
}
None => bail!("Chat {} not found", &field_grpid),
Err(err) => {
error!(context, "Chat {} not found: {}", &field_grpid, err);
return Err(
err.context(format!("Chat for group {} not found", &field_grpid))
);
}
}
} else {
// Alice -> Bob
@@ -729,7 +739,7 @@ pub(crate) async fn handle_securejoin_handshake(
inviter_progress!(context, contact_id, 800);
inviter_progress!(context, contact_id, 1000);
let field_grpid = mime_message
.get_header(HeaderDef::SecureJoinGroup)
.get(HeaderDef::SecureJoinGroup)
.map(|s| s.as_str())
.unwrap_or_else(|| "");
if let Err(err) = chat::get_chat_id_by_grpid(context, &field_grpid).await {
@@ -778,7 +788,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
return Err(Error::msg("Can not be called with special contact ID"));
}
let step = mime_message
.get_header(HeaderDef::SecureJoin)
.get(HeaderDef::SecureJoin)
.context("Not a Secure-Join message")?;
info!(context, "observing secure-join message \'{}\'", step);
@@ -792,7 +802,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
)
})?;
if chat.blocked != Blocked::Not {
chat.id.unblock(context).await?;
chat.id.unblock(context).await;
}
chat.id
};
@@ -815,19 +825,19 @@ pub(crate) async fn observe_securejoin_on_other_device(
.await?;
return Ok(HandshakeMessage::Ignore);
}
let fingerprint: Fingerprint =
match mime_message.get_header(HeaderDef::SecureJoinFingerprint) {
Some(fp) => fp.parse()?,
None => {
could_not_establish_secure_connection(
let fingerprint: Fingerprint = match mime_message.get(HeaderDef::SecureJoinFingerprint)
{
Some(fp) => fp.parse()?,
None => {
could_not_establish_secure_connection(
context,
contact_chat_id,
"Fingerprint not provided, please update Delta Chat on all your devices.",
)
.await?;
return Ok(HandshakeMessage::Ignore);
}
};
return Ok(HandshakeMessage::Ignore);
}
};
if mark_peer_as_verified(context, &fingerprint).await.is_err() {
could_not_establish_secure_connection(
context,
@@ -860,7 +870,7 @@ async fn secure_connection_established(
"?"
};
let msg = stock_str::contact_verified(context, addr).await;
chat::add_info_msg(context, contact_chat_id, msg, time()).await;
chat::add_info_msg(context, contact_chat_id, msg).await;
emit_event!(context, EventType::ChatModified(contact_chat_id));
info!(context, "StockMessage::ContactVerified posted to 1:1 chat");
@@ -884,7 +894,7 @@ async fn could_not_establish_secure_connection(
)
.await;
chat::add_info_msg(context, contact_chat_id, &msg, time()).await;
chat::add_info_msg(context, contact_chat_id, &msg).await;
error!(
context,
"StockMessage::ContactNotVerified posted to 1:1 chat ({})", details
@@ -985,8 +995,8 @@ mod tests {
assert_eq!(sent.recipient(), "alice@example.com".parse().unwrap());
let msg = alice.parse_msg(&sent).await;
assert!(!msg.was_encrypted());
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vc-request");
assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some());
assert_eq!(msg.get(HeaderDef::SecureJoin).unwrap(), "vc-request");
assert!(msg.get(HeaderDef::SecureJoinInvitenumber).is_some());
// Step 3: Alice receives vc-request, sends vc-auth-required
alice.recv_msg(&sent).await;
@@ -994,10 +1004,7 @@ mod tests {
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vc-auth-required"
);
assert_eq!(msg.get(HeaderDef::SecureJoin).unwrap(), "vc-auth-required");
// Step 4: Bob receives vc-auth-required, sends vc-request-with-auth
bob.recv_msg(&sent).await;
@@ -1032,16 +1039,16 @@ mod tests {
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
msg.get(HeaderDef::SecureJoin).unwrap(),
"vc-request-with-auth"
);
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
assert!(msg.get(HeaderDef::SecureJoinAuth).is_some());
let bob_fp = SignedPublicKey::load_self(&bob.ctx)
.await
.unwrap()
.fingerprint();
assert_eq!(
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
*msg.get(HeaderDef::SecureJoinFingerprint).unwrap(),
bob_fp.hex()
);
@@ -1090,7 +1097,7 @@ mod tests {
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
msg.get(HeaderDef::SecureJoin).unwrap(),
"vc-contact-confirm"
);
@@ -1139,7 +1146,7 @@ mod tests {
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
msg.get(HeaderDef::SecureJoin).unwrap(),
"vc-contact-confirm-received"
);
}
@@ -1224,16 +1231,16 @@ mod tests {
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
msg.get(HeaderDef::SecureJoin).unwrap(),
"vc-request-with-auth"
);
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
assert!(msg.get(HeaderDef::SecureJoinAuth).is_some());
let bob_fp = SignedPublicKey::load_self(&bob.ctx)
.await
.unwrap()
.fingerprint();
assert_eq!(
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
*msg.get(HeaderDef::SecureJoinFingerprint).unwrap(),
bob_fp.hex()
);
@@ -1265,7 +1272,7 @@ mod tests {
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
msg.get(HeaderDef::SecureJoin).unwrap(),
"vc-contact-confirm"
);
@@ -1294,7 +1301,7 @@ mod tests {
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
msg.get(HeaderDef::SecureJoin).unwrap(),
"vc-contact-confirm-received"
);
}
@@ -1337,8 +1344,8 @@ mod tests {
assert_eq!(sent.recipient(), "alice@example.com".parse().unwrap());
let msg = alice.parse_msg(&sent).await;
assert!(!msg.was_encrypted());
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vg-request");
assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some());
assert_eq!(msg.get(HeaderDef::SecureJoin).unwrap(), "vg-request");
assert!(msg.get(HeaderDef::SecureJoinInvitenumber).is_some());
// Step 3: Alice receives vg-request, sends vg-auth-required
alice.recv_msg(&sent).await;
@@ -1346,10 +1353,7 @@ mod tests {
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vg-auth-required"
);
assert_eq!(msg.get(HeaderDef::SecureJoin).unwrap(), "vg-auth-required");
// Step 4: Bob receives vg-auth-required, sends vg-request-with-auth
bob.recv_msg(&sent).await;
@@ -1384,16 +1388,16 @@ mod tests {
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
msg.get(HeaderDef::SecureJoin).unwrap(),
"vg-request-with-auth"
);
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
assert!(msg.get(HeaderDef::SecureJoinAuth).is_some());
let bob_fp = SignedPublicKey::load_self(&bob.ctx)
.await
.unwrap()
.fingerprint();
assert_eq!(
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
*msg.get(HeaderDef::SecureJoinFingerprint).unwrap(),
bob_fp.hex()
);
@@ -1421,10 +1425,7 @@ mod tests {
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vg-member-added"
);
assert_eq!(msg.get(HeaderDef::SecureJoin).unwrap(), "vg-member-added");
// Bob should not yet have Alice verified
let contact_alice_id =
@@ -1451,7 +1452,7 @@ mod tests {
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
msg.get(HeaderDef::SecureJoin).unwrap(),
"vg-member-added-received"
);

View File

@@ -12,7 +12,7 @@ use anyhow::{Error, Result};
use async_std::sync::MutexGuard;
use crate::chat::{self, ChatId};
use crate::constants::Viewtype;
use crate::constants::{Blocked, Viewtype};
use crate::contact::{Contact, Origin};
use crate::context::Context;
use crate::events::EventType;
@@ -236,7 +236,7 @@ impl BobState {
context: &Context,
mime_message: &MimeMessage,
) -> Result<Option<BobHandshakeStage>> {
let step = match mime_message.get_header(HeaderDef::SecureJoin) {
let step = match mime_message.get(HeaderDef::SecureJoin) {
Some(step) => step,
None => {
warn!(
@@ -336,10 +336,9 @@ impl BobState {
// the very handshake message we're handling now. But
// only after we have returned. It does not impact
// the security invariants of secure-join however.
let is_verified_group = chat::get_chat_id_by_grpid(context, grpid)
.await?
.map_or(false, |(_chat_id, is_protected, _blocked)| is_protected);
let (_, is_verified_group, _) = chat::get_chat_id_by_grpid(context, grpid)
.await
.unwrap_or((ChatId::new(0), false, Blocked::Not));
// when joining a non-verified group
// the vg-member-added message may be unencrypted
// when not all group members have keys or prefer encryption.
@@ -362,7 +361,7 @@ impl BobState {
if let QrInvite::Group { .. } = self.invite {
let member_added = mime_message
.get_header(HeaderDef::ChatGroupMemberAdded)
.get(HeaderDef::ChatGroupMemberAdded)
.map(|s| s.as_str())
.ok_or_else(|| Error::msg("Missing Chat-Group-Member-Added header"))?;
if !context.is_self_addr(member_added).await? {

View File

@@ -9,7 +9,7 @@ use std::convert::TryFrom;
use anyhow::Result;
use crate::key::Fingerprint;
use crate::key::{Fingerprint, FingerprintError};
use crate::lot::{Lot, LotState};
/// Represents the data from a QR-code scan.
@@ -103,6 +103,8 @@ impl TryFrom<Lot> for QrInvite {
pub enum QrError {
#[error("Unsupported protocol in QR-code")]
UnsupportedProtocol,
#[error("Failed to read fingerprint")]
InvalidFingerprint(#[from] FingerprintError),
#[error("Missing fingerprint")]
MissingFingerprint,
#[error("Missing invitenumber")]

View File

@@ -1,5 +1,3 @@
//! # Simplify incoming plaintext.
use itertools::Itertools;
// protect lines starting with `--` against being treated as a footer.
@@ -155,8 +153,8 @@ fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>)
let quoted_text = lines[l_last..first_quoted_line]
.iter()
.map(|s| {
s.strip_prefix('>')
.map_or(*s, |u| u.strip_prefix(' ').unwrap_or(u))
s.strip_prefix(">")
.map_or(*s, |u| u.strip_prefix(" ").unwrap_or(u))
})
.join("\n");
if l_last > 1 && is_empty_line(lines[l_last - 1]) {
@@ -201,8 +199,8 @@ fn remove_top_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>) {
lines[first_quoted_line..last_quoted_line + 1]
.iter()
.map(|s| {
s.strip_prefix('>')
.map_or(*s, |u| u.strip_prefix(' ').unwrap_or(u))
s.strip_prefix(">")
.map_or(*s, |u| u.strip_prefix(" ").unwrap_or(u))
})
.join("\n"),
),
@@ -243,12 +241,14 @@ fn render_message(lines: &[&str], is_cut_at_end: bool) -> String {
ret.replace("\u{200B}", "")
}
/// Returns true if the line contains only whitespace.
/**
* Tools
*/
fn is_empty_line(buf: &str) -> bool {
buf.chars().all(char::is_whitespace)
// for some time, this checked for `char <= ' '`,
// see discussion at: <https://github.com/deltachat/deltachat-core-rust/pull/402#discussion_r317062392>
// and <https://github.com/deltachat/deltachat-core-rust/pull/2104/files#r538973613>
// see discussion at: https://github.com/deltachat/deltachat-core-rust/pull/402#discussion_r317062392
// and https://github.com/deltachat/deltachat-core-rust/pull/2104/files#r538973613
}
fn is_quoted_headline(buf: &str) -> bool {
@@ -396,7 +396,7 @@ mod tests {
assert!(!is_cut);
assert_eq!(footer, None);
// Nonstandard footer sent by <https://siju.es/>
// Nonstandard footer sent by https://siju.es/
let input = "Message text here\n---Desde mi teléfono con SIJÚ\n\nQuote here".to_string();
let (plain, _, is_cut, _, footer) = simplify(input.clone(), false);
assert_eq!(plain, "Message text here [...]");

View File

@@ -1,20 +1,19 @@
//! # SMTP transport module.
//! # SMTP transport module
pub mod send;
use std::time::{Duration, SystemTime};
use async_smtp::smtp::client::net::ClientTlsParameters;
use async_smtp::{error, smtp, EmailAddress, ServerAddress};
use async_smtp::{error, smtp, EmailAddress};
use crate::constants::DC_LP_AUTH_OAUTH2;
use crate::context::Context;
use crate::events::EventType;
use crate::login_param::{
dc_build_tls, CertificateChecks, LoginParam, ServerLoginParam, Socks5Config,
};
use crate::login_param::{dc_build_tls, CertificateChecks, LoginParam, ServerLoginParam};
use crate::oauth2::dc_get_oauth2_access_token;
use crate::provider::Socket;
use crate::{context::Context, scheduler::connectivity::ConnectivityStore};
use crate::stock_str;
/// SMTP write and read timeout in seconds.
const SMTP_TIMEOUT: u64 = 30;
@@ -29,10 +28,12 @@ pub enum Error {
#[source]
error: error::Error,
},
#[error("SMTP failed to connect: {0}")]
#[error("SMTP: failed to connect: {0}")]
ConnectionFailure(#[source] smtp::error::Error),
#[error("SMTP oauth2 error {address}")]
Oauth2 { address: String },
#[error("SMTP: failed to setup connection {0:?}")]
ConnectionSetupFailure(#[source] smtp::error::Error),
#[error("SMTP: oauth2 error {address}")]
Oauth2Error { address: String },
#[error("TLS error {0}")]
Tls(#[from] async_native_tls::Error),
#[error("{0}")]
@@ -52,11 +53,6 @@ pub(crate) struct Smtp {
/// (eg connect or send succeeded). On initialization and disconnect
/// it is set to None.
last_success: Option<SystemTime>,
pub(crate) connectivity: ConnectivityStore,
/// If sending the last message failed, contains the error message.
pub(crate) last_send_error: Option<String>,
}
impl Smtp {
@@ -101,17 +97,27 @@ impl Smtp {
return Ok(());
}
self.connectivity.set_connecting(context).await;
let lp = LoginParam::from_database(context, "configured_").await?;
self.connect(
context,
&lp.smtp,
&lp.socks5_config,
&lp.addr,
lp.server_flags & DC_LP_AUTH_OAUTH2 != 0,
lp.provider.map_or(false, |provider| provider.strict_tls),
)
.await
let res = self
.connect(
context,
&lp.smtp,
&lp.addr,
lp.server_flags & DC_LP_AUTH_OAUTH2 != 0,
lp.provider.map_or(false, |provider| provider.strict_tls),
)
.await;
if let Err(ref err) = res {
let message = stock_str::server_response(
context,
format!("SMTP {}:{}", lp.smtp.server, lp.smtp.port),
err.to_string(),
)
.await;
context.emit_event(EventType::ErrorNetwork(message));
};
res
}
/// Connect using the provided login params.
@@ -119,7 +125,6 @@ impl Smtp {
&mut self,
context: &Context,
lp: &ServerLoginParam,
socks5_config: &Option<Socks5Config>,
addr: &str,
oauth2: bool,
provider_strict_tls: bool,
@@ -158,7 +163,7 @@ impl Smtp {
let send_pw = &lp.password;
let access_token = dc_get_oauth2_access_token(context, addr, send_pw, false).await?;
if access_token.is_none() {
return Err(Error::Oauth2 {
return Err(Error::Oauth2Error {
address: addr.to_string(),
});
}
@@ -189,20 +194,17 @@ impl Smtp {
_ => smtp::ClientSecurity::Wrapper(tls_parameters),
};
let client =
smtp::SmtpClient::with_security(ServerAddress::new(domain.to_string(), port), security);
let client = smtp::SmtpClient::with_security((domain.as_str(), port), security)
.await
.map_err(Error::ConnectionSetupFailure)?;
let mut client = client
let client = client
.smtp_utf8(true)
.credentials(creds)
.authentication_mechanism(mechanism)
.connection_reuse(smtp::ConnectionReuseParameters::ReuseUnlimited)
.timeout(Some(Duration::from_secs(SMTP_TIMEOUT)));
if let Some(socks5_config) = socks5_config {
client = client.use_socks5(socks5_config.to_async_smtp_socks5_config());
}
let mut trans = client.into_transport();
if let Err(err) = trans.connect().await {
return Err(Error::ConnectionFailure(err));

View File

@@ -14,9 +14,9 @@ pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Envelope error: {}", _0)]
Envelope(#[from] async_smtp::error::Error),
EnvelopeError(#[from] async_smtp::error::Error),
#[error("Send error: {}", _0)]
SmtpSend(#[from] async_smtp::smtp::error::Error),
SendError(#[from] async_smtp::smtp::error::Error),
#[error("SMTP has no transport")]
NoTransport,
#[error("{}", _0)]
@@ -46,7 +46,8 @@ impl Smtp {
let recipients = recipients_chunk.to_vec();
let recipients_display = recipients.iter().map(|x| x.to_string()).join(",");
let envelope = Envelope::new(self.from.clone(), recipients).map_err(Error::Envelope)?;
let envelope =
Envelope::new(self.from.clone(), recipients).map_err(Error::EnvelopeError)?;
let mail = SendableEmail::new(
envelope,
format!("{}", job_id), // only used for internal logging
@@ -59,7 +60,7 @@ impl Smtp {
transport
.send_with_timeout(mail, Some(&Duration::from_secs(timeout)))
.await
.map_err(Error::SmtpSend)?;
.map_err(Error::SendError)?;
context.emit_event(EventType::SmtpMessageSent(format!(
"Message len={} was smtp-sent to {}",

View File

@@ -1,4 +1,4 @@
//! # SQLite wrapper.
//! # SQLite wrapper
use async_std::path::Path;
use async_std::sync::RwLock;
@@ -198,7 +198,7 @@ impl Sql {
}
}
info!(context, "Opened database {:?}.", dbfile);
info!(context, "Opened {:?}.", dbfile);
Ok(())
}
@@ -770,7 +770,7 @@ mod test {
/// existed and `PRAGMA` returned non-empty result.
///
/// Statements were not finalized due to a bug in sqlx:
/// <https://github.com/launchbadge/sqlx/issues/1147>
/// https://github.com/launchbadge/sqlx/issues/1147
#[async_std::test]
async fn test_db_reopen() -> Result<()> {
use tempfile::tempdir;
@@ -802,36 +802,4 @@ mod test {
Ok(())
}
#[async_std::test]
async fn test_migration_flags() -> Result<()> {
let t = TestContext::new().await;
t.evtracker.get_info_contains("Opened database").await;
// as migrations::run() was already executed on context creation,
// another call should not result in any action needed.
// this test catches some bugs where dbversion was forgotten to be persisted.
let (recalc_fingerprints, update_icons, disable_server_delete, recode_avatar) =
migrations::run(&t, &t.sql).await?;
assert!(!recalc_fingerprints);
assert!(!update_icons);
assert!(!disable_server_delete);
assert!(!recode_avatar);
info!(&t, "test_migration_flags: XXX");
loop {
if let EventType::Info(info) = t.evtracker.recv().await.unwrap() {
assert!(
!info.contains("[migration]"),
"Migrations were run twice, you probably forgot to update the db version"
);
if info.contains("test_migration_flags: XXX") {
break;
}
}
}
Ok(())
}
}

View File

@@ -1,5 +1,3 @@
//! Migrations module.
use anyhow::Result;
use crate::config::Config;
@@ -169,7 +167,7 @@ CREATE TABLE tokens (
ALTER TABLE acpeerstates ADD COLUMN verified_key;
ALTER TABLE acpeerstates ADD COLUMN verified_key_fingerprint TEXT DEFAULT '';
CREATE INDEX acpeerstates_index5 ON acpeerstates (verified_key_fingerprint);"#,
39,
38,
)
.await?;
}
@@ -456,7 +454,7 @@ paramsv![]
info!(context, "[migration] v75");
sql.execute_migration(
"ALTER TABLE contacts ADD COLUMN status TEXT DEFAULT '';",
75,
74,
)
.await?;
}
@@ -468,14 +466,6 @@ paramsv![]
if dbversion < 77 {
info!(context, "[migration] v77");
recode_avatar = true;
sql.set_db_version(77).await?;
}
if dbversion < 78 {
// move requests to "Archived Chats",
// this way, the app looks familiar after the contact request upgrade.
info!(context, "[migration] v78");
sql.execute_migration("UPDATE chats SET archived=1 WHERE blocked=2;", 78)
.await?;
}
Ok((

View File

@@ -72,10 +72,6 @@ CREATE TABLE msgs (
timestamp_sent INTEGER DEFAULT 0,
timestamp_rcvd INTEGER DEFAULT 0,
hidden INTEGER DEFAULT 0,
-- mime_headers column actually contains BLOBs, i.e. it may
-- contain non-UTF8 MIME messages. TEXT was a bad choice, but
-- thanks to SQLite 3 being dynamically typed, there is no need to
-- change column type.
mime_headers TEXT,
mime_in_reply_to TEXT,
mime_references TEXT,

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